No-Merge Migration Playbook

Last updated: 2025-11-06 (reconciled against src/Concelier 2026-05-30)

Purpose. Retire the legacy advisory Merge service (AdvisoryMergeService) and cut every advisory pipeline over to Link-Not-Merge (LNM) — observations plus linksets — with zero data loss and a rehearsed rollback.

Audience. The BE-Merge, Architecture, DevOps, and Docs guilds coordinating Sprint 110 (Ingestion & Evidence). This playbook feeds CONCELIER-LNM-21-101 / MERGE-LNM-21-001 and downstream DOCS-LNM-22-008.

Reconciliation note (2026-05-30): the master toggle (concelier:features:noMergeEnabled, default true), the gated AddMergeModule DI registration, the merge:reconcile job removal, the allowlist behaviour, the CONCELIER0002 analyzer, and the LNM observation/linkset read APIs are all live in src/Concelier and verified against source. Several operator-facing surfaces in this playbook are not yet implemented and are flagged inline as NOT IMPLEMENTED (e.g. the stella concelier CLI verbs, MergeJobScheduler.PauseAsync, the policy:overlays:requireLinksetEvidence flag) or corrected to the real source names (metric instruments, allowlist values, persistence engine). Do not script against the NOT IMPLEMENTED items until they ship.

0. Scope & objectives

1. Prerequisites checklist

ItemOwnerNotes
Normalized version ranges emitted for all Sprint 110 connectors (Acsc, Cccs, CertBund, CertCc, Cve, Ghsa, Ics.Cisa, Kisa, Ru.Bdu, Ru.Nkcki, Vndr.Apple, Vndr.Cisco, Vndr.Msrc).Connector guildsFollow docs/dev/normalized-rule-recipes.md; update fixtures with UPDATE_*_FIXTURES=1.
Metrics dashboards for linkset coverage available in Grafana/CI snapshots.Observability guildPublish baseline before shadow rollout. The dashboard panel names LinksetVersionCoverage / Normalized version rules missing are NOT IMPLEMENTED as code-emitted instruments; build panels from the live instruments (lnm.cache.hit_total, lnm.cache.write_total, lnm.cache.rebuild_ms, concelier.merge.identity_conflicts) plus the CERT/CC normalized-rule histogram.
Concelier WebService exposes linkset and observation read APIs for policy/CLI consumers.BE-Merge / PlatformVerified in source: GET /v1/lnm/linksets, POST /v1/lnm/linksets/search, GET /v1/lnm/linksets/{advisoryId} (current). GET /concelier/observations (auth policy Concelier.Observations.Read, scope vuln:view = StellaOpsScopes.VulnView) is verified but DEPRECATED with successor /v1/lnm/linksets?includeObservations=true — do not build new integrations on it. Confirm contract parity with Merge outputs.
Export Center / Offline Kit aware of new manifests.Export Center guildProvide beta bundle for QA verification.
Docs guild aligned on public migration messaging.Docs guildUpdate docs/dev, docs/modules/concelier, and release notes once cutover date is locked.

Do not proceed to Phase 1 until all prerequisites are checked or explicitly waived by Architecture guild.

2. Feature flag & configuration plan

ToggleDefaultPurposeNotes
concelier:features:noMergeEnabledtrueMaster switch to disable legacy Merge job scheduling/execution.Verified: ConcelierOptions.FeaturesOptions.NoMergeEnabled defaults true; gates builder.Services.AddMergeModule(...) in Program.cs and removes the merge:reconcile job definition. Env var: CONCELIER_FEATURES__NOMERGEENABLED.
concelier:features:lnmShadowWritestrueEnables dual-write of linksets while Merge remains active.Verified flag binding (FeaturesOptions.LnmShadowWrites, default true). Note: no code path currently reads this flag, so its enforcement is NOT IMPLEMENTED — LNM linksets are always built from observations. Keep enabled through the shadow phase regardless.
concelier:features:mergeJobAllowlist and concelier:jobs:merge:allowlist (two distinct keys, both honoured)[]Explicit allowlist of merge job kinds kept scheduled when noMergeEnabled is false. Two independent code paths gate merge:reconcile, each reading a different key — verify both against source: (1) JobRegistrationExtensions.ConfigureMergeJob reads only the config sections concelier:jobs:merge:allowlist / jobs:merge:allowlist (string arrays via GetSection(...).Get<string[]>()); it does not read FeaturesOptions.MergeJobAllowlist. (2) The Program.cs PostConfigure<JobSchedulerOptions> (~line 830) reads features.MergeJobAllowlist, bound from concelier:features:mergeJobAllowlist and populated by env vars CONCELIER_FEATURES__MERGEJOBALLOWLIST__0..9. The only valid entry in either path is merge:reconcile (env-var array form: CONCELIER_FEATURES__MERGEJOBALLOWLIST__0=merge:reconcile).Set to empty during Phase 2+ to prevent accidental restarts.
policy:overlays:requireLinksetEvidencefalsePolicy engine safety net to require linkset-backed findings.NOT IMPLEMENTED — no such config key or enforcement exists in src/ (no match in Policy or Concelier). Treat as a roadmap/aspirational gate; do not rely on it for cutover safety.

2025-11-06: WebService now defaults concelier:features:noMergeEnabled to true, skipping Merge DI registration and removing the merge:reconcile job unless operators set the flag to false and allowlist the job (MERGE-LNM-21-002).

2025-11-06: Analyzer CONCELIER0002 ships with Concelier hosts to block new references to AdvisoryMergeService / AddMergeModule. Suppressions must be paired with an explicit migration note. 2025-11-06: Analyzer coverage validated via unit tests catching object creation, field declarations, typeof, and DI extension invocations; merge assemblies remain exempt for legacy cleanup helpers.

Configuration hygiene: Document the toggle values per environment in ops/devops/configuration/staging.md and ops/devops/configuration/production.md. Air-gapped customers receive defaults through the Offline Kit release notes.

3. Rollout phases

PhaseGoalDurationKey actions
0 – PreparationEnsure readiness2–3 daysFinalise prerequisites, snapshot Merge metrics, dry-run backfill scripts in dev.
1 – Shadow / Dual WriteValidate parity5–7 daysKeep Merge primary. Compare linkset vs merged outputs and fix discrepancies. (The CLI stella concelier diff-merge --snapshot <date> is NOT IMPLEMENTED — no concelier command group or diff-merge verb exists in src/Cli. Use the read APIs in §0 plus a manual/scripted diff until a parity command ships. The legacy merge run itself is triggered by stella db merge.)
2 – CutoverSwitch to LNM1 day (per env)Enable noMergeEnabled, disable Merge job schedules, update Policy/Export configs, run post-cutover smoke tests.
3 – HardenDecommission Merge2–3 daysRemove Merge background services, clean dashboards, notify operators. (The table is vuln.merge_events (plural) with MergeEventRepository / IMergeEventRepository; it is created in 001_initial_schema.sql (monthly-partitioned by created_at), not migration 030030_add_advisory_raw_store.sql creates advisory_raw, and the 006* partition migrations are archived under Migrations/_archived/pre_1.0/. There is no dedicated merge_events retention job in source; if data retention is required, add one explicitly rather than “deleting” a job that isn’t there.)

3.1 Environment sequencing

  1. Dev/Test clusters: Validate all automation. Run full regression suite (dotnet test src/Concelier/...).
  2. Staging: Execute complete backfill (see §4) and collect 24 h of telemetry before sign-off.
  3. Production: Perform cutover during low-ingest window; communicate via Slack/email + status page two days in advance.
  4. Offline kit: Package new Observer snapshots with LNM-only data; ensure instructions cover flag toggles for air-gapped deployments.

3.2 Smoke test matrix

4. Backfill strategy

  1. Freeze Merge writes: Prevent new merge events while snapshots are taken. (MergeJobScheduler.PauseAsync is NOT IMPLEMENTED — no such type/method exists. With noMergeEnabled=true (the default) the merge:reconcile job is never scheduled at all; to freeze a deployment that still has Merge enabled, set noMergeEnabled=true and restart, or remove merge:reconcile from the allowlist.)
  2. Generate linkset baseline: Rebuild linksets from the advisory_raw store (table verified in StellaOps.Concelier.Persistence). (The CLI/host command ... -- linkset backfill --from <date> is NOT IMPLEMENTED — there is no linkset backfill verb on the WebService host or CLI; the only orchestrator backfill is the generic stella orch backfill start <source-id> --from --to, which is a per-source ingestion backfill, not a linkset rebuild. Treat the linkset-rebuild job as roadmap and capture whatever artefacts the chosen mechanism produces.) Capture job output artefacts and attach to the sprint issue.
  3. Validate parity: Compare sample advisories’ linkset vs merged output. (The internal diff tool tools/concelier/compare-linkset-merge.ps1 is NOT IMPLEMENTED — no tools/concelier/ directory or that script exists in the repo. Until a parity tool ships, drive the comparison from the read APIs in §0 plus a manual/scripted diff.) Any diffs must be triaged before production cutover.
  4. Publish evidence: For air-gapped tenants, create a one-off Offline Kit slice and push to staging mirror. (No linkset-backfill export profile exists in source — export profile linkset-backfill is NOT IMPLEMENTED as a named profile; pick a concrete Export Center profile or capture the artefacts manually.)
  5. Tag snapshot: Record the PostgreSQL snapshot/WAL position (Concelier persists to PostgreSQL via StellaOps.Concelier.Persistence, not MongoDB — the original “Mongo oplog” reference is incorrect) and the S3/object-storage manifests (federation bundle export uses S3/RustFS via S3FederationBundlePublisher) in ops/devops/runbooks/concelier/no-merge.md (new section) so rollback knows the safe point.

Determinism: rerunning the backfill with identical inputs must produce byte-identical linkset documents. Use the --verify-determinism flag where available and archive the checksum report under artifacts/lnm-backfill/<date>/.

5. Validation gates

Capture validation evidence in the sprint journal (attach Grafana screenshots + CLI output).

6. Rollback plan

  1. Toggle sequence:
    • Set concelier:features:noMergeEnabled=false.
    • Re-enable the Merge job by allowlisting it: concelier:features:mergeJobAllowlist=["merge:reconcile"] (the legacy section concelier:jobs:merge:allowlist is also honoured). The job kind is merge:reconcile, not merge:defaultmerge:default does not exist anywhere in source and will not re-enable anything. (With noMergeEnabled=false and no allowlist, the job is also re-added automatically.)
    • The policy:overlays:requireLinksetEvidence step is moot: that flag is NOT IMPLEMENTED (no enforcement to disable).
  2. Data considerations:
    • Linkset writes continue, so no data is lost; ensure Policy consumers ignore linkset-only fields during rollback window.
    • If Merge pipeline was fully removed (Phase 3 complete), redeploy the Merge service container image from the rollback tag published before cutover.
  3. Verification:
    • Confirm Merge workers are active: check that the scheduler now contains the merge:reconcile job definition and the AdvisoryMergeService is DI-registered. (stella concelier status is NOT IMPLEMENTED — there is no concelier CLI group; use the WebService health/scheduler state instead.)
    • Monitor concelier.merge.identity_conflicts (not merge.identity.conflicts) for spikes; if present, roll forward and re-open incident with Architecture guild.
  4. Communication:
    • Post incident note in #release-infra and customer status page.
    • Log rollback reason, window, and configs in ops/devops/incidents/<yyyy-mm-dd>-no-merge.md.

Rollback window should not exceed 4 hours; beyond that, plan to roll forward with a hotfix rather than reintroducing Merge.

7. Documentation & communications

8. Responsibilities matrix

AreaLead guild(s)Supporting
Feature flags & configBE-MergeDevOps
Backfill scriptingBE-MergeTools
Observability dashboardsObservabilityQA
Offline kit packagingExport CenterAirGap
Customer commsDocsProduct, Support

9. Deliverables & artefacts


10. Migration readiness checklist

ItemPrimary ownerStatus notes
Capture Linkset coverage baselines and archive Grafana export. (Metric names version_entries_total / missing_version_entries_total are NOT IMPLEMENTED; baseline from lnm.cache.* instruments + the CERT/CC normalized-rule histogram instead.)Observability Guild[ ] Pending
Stage and verify a linkset backfill, then store the checksum report under artifacts/lnm-backfill/<date>/. (A dedicated linkset backfill job is NOT IMPLEMENTED — pick a concrete mechanism; the generic stella orch backfill start is per-source ingestion, not a linkset rebuild.)BE-Merge, DevOps Guild[ ] Pending
Confirm feature flags per environment (concelier:features:noMergeEnabled, concelier:features:lnmShadowWrites, concelier:features:mergeJobAllowlist) match the Phase 0–3 plan. (policy:overlays:requireLinksetEvidence is NOT IMPLEMENTED and is intentionally dropped from this row.)DevOps Guild[ ] Pending
Publish operator comms (status page, Slack/email) with cutover + rollback windows.Docs Guild, Product[ ] Pending
Execute rollback rehearsal in staging and log results in ops/devops/incidents/<date>-no-merge.md.DevOps Guild, Architecture Guild[ ] Pending

Update the checklist as each item completes; completion of every row is required before moving to Phase 2 (Cutover).


With this playbook completed, proceed to MERGE-LNM-21-002 to remove the Merge service code paths and enforce compile-time analyzers that block new merge dependencies.