Service-consolidation recipe (canonical, granular)
Status: binding execution checklist for every ADR-039 D14 merge program (SPRINT_20260722_010–016) and any future consolidation. The Vulnerabilities program (003–008) is the reference implementation of these stages at full scale. Agents execute from the program sprint + this recipe; a stage without its evidence is not done.
Method baseline (ADR-039 D2): rebuild from scratch by default; transfer code only where the transformation is purely mechanical (tool/script-based namespace + file renames) and the executing agent judges it genuinely cheaper; never port a layer the merge exists to fix. Each sprint’s S4 task records the explicit carry-list vs re-author-list.
Stages
S0 — Immersion + target design
Verify the family brief (service-consolidation-review.md §2.1) against src/ and real flows. Inventory with no gaps: every deployable and its process seat; every persona-facing surface; every table (purpose → P13 retention class → carry/redesign/drop); every API route + consumer; every UI page/typed client touching the family; every cross-service seam (register X-entries + undiscovered ones); every compose key/env var/volume. Output: target design doc (service shape, roles, API surface, schema layout, event contracts). Evidence: design doc reviewed; table/route/consumer inventories complete with dispositions.
S1 — Decision record
Merge / keep / defer with revisit trigger, named approver; ADR-039 amendment only on deviation. Evidence: decision recorded in the sprint + ADR link.
S2 — New service skeleton
src/<Module>/ layout: StellaOps.<Service>.WebService (+ .Worker role where the design says so), solution file, module AGENTS.md, plugin hosting if applicable. Namespaces StellaOps.<Service>.* from birth. Evidence: solution builds; module AGENTS.md present; conformance walker recognizes the service.
S3 — Persistence baseline
StellaOps.<Service>.Persistence (P2/D12 naming), fresh baseline migrations 001_* (no legacy migration history), P13 retention-class header on every table, embedded resources, wired via AddStartupMigrations against STELLAOPS_POSTGRES_<SERVICE>_CONNECTION only, fail-closed. Traps: embedded resource names follow the assembly name (rebuild the owning lib); archived migrations must not re-embed; CREATE INDEX idempotent. DC-26: remove the module’s IMigrationModulePlugin entry from Platform.Database/MigrationModulePlugins.cs in the SAME change — otherwise platform-web re-creates the schema in the shared DB and races this migrator. Evidence: fresh-DB convergence test green; §2.11 no-demo-rows guard green; no migration targets the shared platform DB; the module’s central-migrator plugin is gone.
S4 — Domain refactor
Re-author domain/application layers into the new home per the sprint’s carry/re-author lists. Writers follow P6/P13: content-guarded upserts, no unconditional DO UPDATE, declared rebuild path for every derived table. Evidence: adjacent specs green via tools/scripts/test-targeted-xunit.ps1 with ran-vs-suite counts; carry-list vs re-author-list recorded in the sprint log.
S5 — API surface + gateway
Consolidated endpoints under the service’s route group; gateway route config (src/Router/StellaOps.Gateway.WebService/appsettings.json) updated; OpenApiAggregator sources updated; src/Api governance regenerated. Scope catalog unchanged (scopes name capabilities, not services); route-config tests added/updated. Trap: verify Bearer/identity-envelope forwarding on new/regex routes. Route changes follow DC-19: one routing change per program execution window, serialized by the board. Evidence: route tests green; scratch-stack forcing call through the gateway succeeds authorized and fails unauthorized.
S6 — Console/UI
Typed API clients repointed via the API base map (never per-component edits); affected pages migrated; removed features’ tiles/strings deleted; honest empty/degraded states where data windows exist (no-mocks rule). Evidence: per-page vitest specs green (Rule A) and ng build green; layout guards clean; four-persona live gate (tools/scripts/run-console-four-role-live.ps1) where the family has UI-visible surfaces.
S7 — Doctor plugin + conformance + tests
Host the doctor plugin (P11) with the standard checks (db-size budget, migration status, connection self-check) and register with the Platform capability registry (SPRINT_20260722_009 contract). Update DatabaseOwnershipConformanceTests (only the owner connection resolved; no STELLAOPS_POSTGRES_CONNECTION reference) and the startup-migrations conformance walker. Role-grant integration test: cross-owner writes rejected. Evidence: conformance suites green; doctor checks visible via the registry on a scratch stack.
S8 — DevOps
Compose: new service key(s), image names, env (STELLAOPS_POSTGRES_<SERVICE>_CONNECTION in .env
- compose), healthchecks, init jobs, volumes;
devops/docker/build-service-publish.shkey;devops/etc/**config files; cluster placement per matrix. Connection env vars follow DC-20 (one generateddatabases.envblock; vars declared exactly once). Deploy verification per the live-deploy playbook (same-tag images need--force-recreate; HELLO re-send after restart). Evidence: scratch-stack compose up converges healthy from a fresh DB; deploy runbook entry written.
S9 — Data move + verify
Create stellaops_<service> + owner role on the target cluster; maintenance window: copy the (MB-scale) data; row-count + spot-check parity; repoint fail-closed; revoke old-schema grants; soak on revocation (hidden readers surface as permission errors, cheaply); live forcing function for the service’s core behavior post-move. Evidence: parity numbers recorded; soak clean; forcing function passes; matrix row flipped.
S10 — Obsoletization + docs rewrite
Delete: old projects/solutions, old namespaces (no compat shims unless explicitly logged with a sunset), old compose keys/images/env, old route entries, dead UI strings. Docs: new module dossier docs/modules/<service>/architecture.md; old module dirs become stub READMEs pointing at the new dossier (§1.5 convention); AGENTS.md §1.4 map + module-index.md playbook + ownership matrix updated; verify-knowledge-base green. The old schema drop in the shared platform DB is a separate destructive approval (ADR-004 posture) — grants are revoked in S9, the drop is its own window. Finally: sprint completion per §2.1 archive discipline. Evidence: repo grep shows zero old-namespace references; knowledge-base check green; destructive approval recorded (or explicitly deferred) for the old schemas.
Cross-cutting rules
- Web/worker of the merged service are replica roles of ONE deployable family — one DB (CoC §8.2).
- Every stage that touches runtime (S8 deploys, S9 windows, S10 drops) needs its named approval; everything before is repo work.
- If S0 finds the family brief wrong, stop at S1 with a keep/defer decision — a merge that no longer simplifies a persona seam is cancelled, not pushed through.
Part B — Database-move recipe (M0–M6) for same-service moves
For matrix §5.3 rows: the service does not change shape — it only gains its own database (and, where deviating, the D12 DAL rename). Programs: SPRINT_20260722_017 (Scanner), 018 (ReleaseOrchestrator), 019 (Signer + shared-lib pattern), 020 (Wave-3 batch), 021 (Platform endgame). Same discipline: a stage without its evidence is not done.
M0 — Inventory + retro-audit
Enumerate every connection key the service reads (services often have more than one — Scanner has SCANNER_SCANNER__STORAGE__DSN and ScannerStorage__Postgres__ConnectionString), every schema/table (assign P13 retention classes retroactively — each move is the chance to fix bloat patterns), every reader/writer of the schema from outside the service (any hit = a register entry or a blocker), data size, and the D12 rename need. Evidence: inventory recorded in the sprint; foreign readers dispositioned.
M1 — DAL rename (D12) + owner connection
Rename the persistence project to StellaOps.<Service>.Persistence where deviating (embedded Migrations/** resource names change with the assembly — rebuild the owning lib, re-verify fresh-DB convergence). Add STELLAOPS_POSTGRES_<SERVICE>_CONNECTION, fail-closed; remove every generic fallback. DC-26: delete the module’s IMigrationModulePlugin from Platform.Database/MigrationModulePlugins.cs in the same change. Evidence: fresh-DB convergence green under the new assembly name; no generic-connection reference.
M2 — Doctor plugin + conformance
Host the doctor plugin (standard checks incl. db-size budget) + registry registration; update DatabaseOwnershipConformanceTests + compose lint for the new database. Evidence: conformance green; doctor checks visible.
M3 — DevOps
.env + compose env, healthchecks unchanged, cluster placement per matrix; scratch-stack compose-up converges healthy from a fresh DB. Connection env vars follow DC-20. Evidence: scratch stack healthy from empty.
M4 — Data move window
Create stellaops_<service> + owner role; copy (MB-scale); row-count + spot-check parity; repoint; revoke old-schema grants; soak on revocation; live forcing function for the service’s core behavior. Evidence: parity numbers; clean soak; forcing function passes.
M5 — Obsoletization + docs
Matrix row flipped; module dossier updated (topology section); verify-knowledge-base green; old schema drop in the shared DB = separate destructive approval. Evidence: docs green; drop approval recorded or deferred.
M6 — (Batch programs only) per-service sign-off row
In batch sprints each service gets its own tracker row; no blanket ticks — every service’s M0–M5 evidence is individually linked.
