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, defaulttrue), the gatedAddMergeModuleDI registration, themerge:reconcilejob removal, the allowlist behaviour, theCONCELIER0002analyzer, and the LNM observation/linkset read APIs are all live insrc/Concelierand verified against source. Several operator-facing surfaces in this playbook are not yet implemented and are flagged inline asNOT IMPLEMENTED(e.g. thestella concelierCLI verbs,MergeJobScheduler.PauseAsync, thepolicy:overlays:requireLinksetEvidenceflag) or corrected to the real source names (metric instruments, allowlist values, persistence engine). Do not script against theNOT IMPLEMENTEDitems until they ship.
0. Scope & objectives
- Primary goal: cut over all advisory pipelines to Link-Not-Merge with no residual dependencies on
AdvisoryMergeService. - Secondary goals: maintain deterministic evidence, zero data loss, and reversible deployment across online and offline tenants.
- Success criteria:
- All connectors emit observation
affected.versions[]with provenance and pass LNM guardrails. - Linkset coverage dashboards show no advisories missing normalized version ranges. (Note: the metric names
missing_version_entries_total/version_entries_totaland aNormalized version rules missing…log warning are NOT IMPLEMENTED as named instruments. The CERT/CC connector emits a per-source histogramcertcc.map.normalized_versions.countdescribing “normalized version rules emitted per CERT/CC advisory”; the LNM cache emitslnm.cache.hit_total,lnm.cache.write_total, andlnm.cache.rebuild_msfromStellaOps.Concelier.WebService/Telemetry/LinksetCacheTelemetry.cs. Treat the names below as placeholders pending an observability spike.) - Policy, Export Center, and CLI consumers operate solely on observations/linksets. The current LNM read surface is
GET /v1/lnm/linksets,POST /v1/lnm/linksets/search, andGET /v1/lnm/linksets/{advisoryId}(registered inProgram.cs; tenant authorization enforced in-handler viaEnsureTenantAuthorized).GET /concelier/observations(auth policyConcelier.Observations.Read, scopevuln:view) still exists but is itself DEPRECATED —DeprecationMiddleware/DeprecationHeaders.LegacyConcelierObservationsmark it with successor/v1/lnm/linksets?includeObservations=true. New consumers must target the/v1/lnm/linksets*endpoints; the legacy merged-document path is gated off by default. - Rollback playbook validated and rehearsed in staging.
- All connectors emit observation
1. Prerequisites checklist
| Item | Owner | Notes |
|---|---|---|
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 guilds | Follow docs/dev/normalized-rule-recipes.md; update fixtures with UPDATE_*_FIXTURES=1. |
| Metrics dashboards for linkset coverage available in Grafana/CI snapshots. | Observability guild | Publish 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 / Platform | Verified 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 guild | Provide beta bundle for QA verification. |
| Docs guild aligned on public migration messaging. | Docs guild | Update 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
| Toggle | Default | Purpose | Notes |
|---|---|---|---|
concelier:features:noMergeEnabled | true | Master 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:lnmShadowWrites | true | Enables 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:requireLinksetEvidence | false | Policy 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:noMergeEnabledtotrue, skipping Merge DI registration and removing themerge:reconcilejob unless operators set the flag tofalseand allowlist the job (MERGE-LNM-21-002).2025-11-06: Analyzer
CONCELIER0002ships with Concelier hosts to block new references toAdvisoryMergeService/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.mdandops/devops/configuration/production.md. Air-gapped customers receive defaults through the Offline Kit release notes.
3. Rollout phases
| Phase | Goal | Duration | Key actions |
|---|---|---|---|
| 0 – Preparation | Ensure readiness | 2–3 days | Finalise prerequisites, snapshot Merge metrics, dry-run backfill scripts in dev. |
| 1 – Shadow / Dual Write | Validate parity | 5–7 days | Keep 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 – Cutover | Switch to LNM | 1 day (per env) | Enable noMergeEnabled, disable Merge job schedules, update Policy/Export configs, run post-cutover smoke tests. |
| 3 – Harden | Decommission Merge | 2–3 days | Remove 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 030 — 030_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
- Dev/Test clusters: Validate all automation. Run full regression suite (
dotnet test src/Concelier/...). - Staging: Execute complete backfill (see §4) and collect 24 h of telemetry before sign-off.
- Production: Perform cutover during low-ingest window; communicate via Slack/email + status page two days in advance.
- Offline kit: Package new Observer snapshots with LNM-only data; ensure instructions cover flag toggles for air-gapped deployments.
3.2 Smoke test matrix
- Confirm the deployment reports healthy and shows zero Merge workers. (
stella concelier status --include linksetis NOT IMPLEMENTED — there is noconcelierCLI group; check the WebService health endpoint and confirm the scheduler has nomerge:reconciledefinition, which is the default behaviour whennoMergeEnabled=true.) - Run a policy evaluation against sample tenants and confirm identical findings pre/post cutover. (The exact verb
stella policy evaluateis NOT IMPLEMENTED, and there is nopolicy checkorpolicy listverb either — those do not exist inCommandFactory.cs. ThepolicyCLI group’s actual verbs includesimulate,lint,compile,activate,edit,test,new,history,explain,init,version,submit,review,publish,rollback,sign,verify-signature,lattice,verdicts, andpromote. For a parity-style evaluation usestella policy simulate <policy-id>or the Policy WebService evaluate endpoint.) - Export Center bundle diff shows only expected metadata changes (manifest ID, timestamps).
- Grafana dashboards: LNM cache instruments (
lnm.cache.write_total,lnm.cache.rebuild_ms) steady andconcelier.merge.identity_conflictsflatlined after cutover. (The earlier nameslinkset_insert_duration_ms/merge.identity.conflictsare NOT the emitted instrument names — see §5.)
4. Backfill strategy
- Freeze Merge writes: Prevent new merge events while snapshots are taken. (
MergeJobScheduler.PauseAsyncis NOT IMPLEMENTED — no such type/method exists. WithnoMergeEnabled=true(the default) themerge:reconcilejob is never scheduled at all; to freeze a deployment that still has Merge enabled, setnoMergeEnabled=trueand restart, or removemerge:reconcilefrom the allowlist.) - Generate linkset baseline: Rebuild linksets from the
advisory_rawstore (table verified inStellaOps.Concelier.Persistence). (The CLI/host command... -- linkset backfill --from <date>is NOT IMPLEMENTED — there is nolinkset backfillverb on the WebService host or CLI; the only orchestrator backfill is the genericstella 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. - Validate parity: Compare sample advisories’ linkset vs merged output. (The internal diff tool
tools/concelier/compare-linkset-merge.ps1is NOT IMPLEMENTED — notools/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. - Publish evidence: For air-gapped tenants, create a one-off Offline Kit slice and push to staging mirror. (No
linkset-backfillexport profile exists in source —export profile linkset-backfillis NOT IMPLEMENTED as a named profile; pick a concrete Export Center profile or capture the artefacts manually.) - Tag snapshot: Record the PostgreSQL snapshot/WAL position (Concelier persists to PostgreSQL via
StellaOps.Concelier.Persistence, not MongoDB — the original “Mongooplog” reference is incorrect) and the S3/object-storage manifests (federation bundle export uses S3/RustFS viaS3FederationBundlePublisher) inops/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-determinismflag where available and archive the checksum report underartifacts/lnm-backfill/<date>/.
5. Validation gates
- Metrics (actual instrument names — verified in source):
lnm.cache.hit_total,lnm.cache.write_total,lnm.cache.rebuild_ms(meterStellaOps.Concelier.Linksets, fromLinksetCacheTelemetry.cs) andconcelier.merge.identity_conflicts(meterStellaOps.Concelier.Merge, fromAdvisoryMergeService.cs). The nameslinkset_insert_duration_ms,linkset_documents_total, andnormalized_version_rules_missingare NOT IMPLEMENTED as emitted instruments; a “normalized rules emitted” signal exists only as a per-source histogram in the CERT/CC connector —certcc.map.normalized_versions.count(unitrules, “Distribution of normalized version rules emitted per CERT/CC advisory”,CertCcDiagnostics.cs).- Gate (revised): confirm linkset cache writes (
lnm.cache.write_total) are flowing andconcelier.merge.identity_conflictsstays flat before relying onnoMergeEnabled; the originalnormalized_version_rules_missing == 0gate is NOT IMPLEMENTED (substitute the CERT/CC histogram or an explicit coverage check). Original window: 48 h before enablingnoMergeEnabled.
- Gate (revised): confirm linkset cache writes (
- Logs: Ensure no merge-fallback log lines after cutover. (The literal string
Fallbacking to merge servicedoes NOT exist in source; instead confirm the startup warning “Legacy merge module disabled via concelier:features:noMergeEnabled; Link-Not-Merge mode active.” is present, perProgram.cs, and thatmerge:reconcilejob logs are absent.) - Change streams: Policy and Scheduler should observe only
advisory.linkset.updated@1events (contract name verified inAdvisoryLinksetUpdatedEvent.cs); monitor for stragglers referencing merge IDs. - QA: The
StellaOps.Concelier.Merge.Testsproject exists; the off-by-default behaviour is asserted byWebServiceEndpointsTests(MergeModuleDisabledByDefault,MergeModuleReenabledWhenFeatureFlagCleared,MergeJobRemovedWhenAllowlistExcludes,MergeJobRemainsWhenAllowlisted) and theCONCELIER0002analyzer is covered byStellaOps.Concelier.Merge.Analyzers.Tests. Add integration tests verifying LNM-only exports.
Capture validation evidence in the sprint journal (attach Grafana screenshots + CLI output).
6. Rollback plan
- Toggle sequence:
- Set
concelier:features:noMergeEnabled=false. - Re-enable the Merge job by allowlisting it:
concelier:features:mergeJobAllowlist=["merge:reconcile"](the legacy sectionconcelier:jobs:merge:allowlistis also honoured). The job kind ismerge:reconcile, notmerge:default—merge:defaultdoes not exist anywhere in source and will not re-enable anything. (WithnoMergeEnabled=falseand no allowlist, the job is also re-added automatically.) - The
policy:overlays:requireLinksetEvidencestep is moot: that flag is NOT IMPLEMENTED (no enforcement to disable).
- Set
- 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
rollbacktag published before cutover.
- Verification:
- Confirm Merge workers are active: check that the scheduler now contains the
merge:reconcilejob definition and theAdvisoryMergeServiceis DI-registered. (stella concelier statusis NOT IMPLEMENTED — there is noconcelierCLI group; use the WebService health/scheduler state instead.) - Monitor
concelier.merge.identity_conflicts(notmerge.identity.conflicts) for spikes; if present, roll forward and re-open incident with Architecture guild.
- Confirm Merge workers are active: check that the scheduler now contains the
- 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
- Update
docs/modules/concelier/architecture.mdappendix to mark Merge deprecated and link back to this playbook. - Coordinate with Docs guild to publish operator-facing guidance under
docs/releases/(a dedicatedrelease-notes-no-merge.md) and update CLI help text. - Notify product/CS teams with a short FAQ covering timelines, customer impact, and steps for self-hosted installations.
8. Responsibilities matrix
| Area | Lead guild(s) | Supporting |
|---|---|---|
| Feature flags & config | BE-Merge | DevOps |
| Backfill scripting | BE-Merge | Tools |
| Observability dashboards | Observability | QA |
| Offline kit packaging | Export Center | AirGap |
| Customer comms | Docs | Product, Support |
9. Deliverables & artefacts
- Config diff per environment (stored in GitOps repo).
- Backfill checksum report (
artifacts/lnm-backfill/<date>/checksums.json). - Grafana export (PDF) showing validation metrics.
- QA test run attesting to LNM-only regressions passing.
- Updated runbook entry in
ops/devops/runbooks/concelier/.
10. Migration readiness checklist
| Item | Primary owner | Status 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.
