Lesson 177: Governance Metric Dictionary Minor-Increment Migration Without Breaking the Lesson 164 Bind Contract (2026)

Why this matters now

Lessons 173–176 added real analytics surface area: deficiency recurrence histograms, signer fatigue heat-maps, WORM archive pointers, and reply-packet tuple manifests. Every one of those artifacts eventually resolves to rows in the governance metric dictionary that Lesson 164 already promised would stay bit-for-bit aligned between the leadership dashboard and the partner SLA annex.

In 2026, two independent pressures hit the same table:

  1. Unity 6.x and the 2022.2 / 2026.2 BI exporter cadence renamed half a dozen canonical field labels your ingestion scripts keyed off (queue_depthqueueDepth, breach_countbreachCount, owner_route_id nested under route). The renames are correct for new Unity Analytics export JSON — they are fatal for a dictionary row that still exposes the old column name to the partner annex SQL view.
  2. Partner readbacks now diff annex totals against leadership rollups using the same rollup mismatch help workflow you already ship for Quest governance. A minor semver bump that adds columns for Lessons 173–176 is allowed under the Lesson 164 bind contract only if the dual-write window, exporter allow-list, and rollback trigger in this lesson are in place first.

This lesson is the migration lane — not the analytics lane. It tells you how to add dictionary columns without becoming the team that ships a green weekly reconciliation in Lesson 166 while the partner annex still reads yesterday's column names.

Lesson objectives

By the end of this lesson you will have:

  • A dictionary_migration_state table with explicit from_semver, to_semver, phase (prepare / dual_write / cutover / rollback), and opened_at_dt_utc / closed_at_dt_utc timestamps tied to a single migration UUID.
  • A dictionary_column_dual_write view exposing both legacy and modern column names during the dual-write window so exporters, SQL views, and Lesson 170 FAQ bind checks can reference either name deterministically.
  • An exporter_allowlist_diff artifact (JSON + SHA-256) checked into CI that fail-closes any Unity / BI export path that emits a column not present in the allow-list for the active migration phase.
  • A reconciliation_rollback_trigger wired to Lesson 166's weekly job: if the post-migration reconciliation delta exceeds the configured epsilon for two consecutive weeks, the migration auto-reverts to from_semver and pages the governance owner from Lesson 174 routing tables.

Prerequisites from earlier lessons

  • Lesson 164 — leadership–partner SLA dashboard sync; the bind contract this lesson must not break.
  • Lesson 166 — weekly SLA snapshot reconciliation job; consumer of the rollback trigger.
  • Lesson 170 — FAQ rows keyed on dictionary_metric_id; migration must not orphan active FAQ bindings.
  • Lesson 171publish_tuple_hash semantics; dictionary rows feed tuple manifests in Lessons 175–176.
  • Lessons 173–176 — concrete new columns or manifest keys you are migrating into the dictionary.

If Lesson 166 is not green for three consecutive weeks, pause this lesson's cutover — migrating on top of a broken reconciliation baseline produces rollback noise you cannot distinguish from migration failure.

The dictionary_migration_state table

CREATE TABLE dictionary_migration_state (
  migration_id            TEXT PRIMARY KEY,
  from_semver             TEXT NOT NULL,
  to_semver               TEXT NOT NULL,
  phase                   TEXT NOT NULL
                          CHECK (phase IN ('prepare','dual_write','cutover','rollback')),
  opened_at_dt_utc        TIMESTAMPTZ NOT NULL,
  closed_at_dt_utc        TIMESTAMPTZ,
  opened_by               TEXT NOT NULL,
  rollback_reason         TEXT,
  UNIQUE (from_semver, to_semver, phase)
);

Rule: Only one row may sit in dual_write or cutover at a time. A second migration attempt while the first is unresolved raises the same red-banner pattern as an open publish block in Lesson 171.

Dual-write window mechanics

During dual_write, every exporter and SQL transform must emit both the legacy column and the modern column for each migrated metric:

Legacy column (164-era) Modern column (173–176-era) Population rule
breach_count breachCount Same integer copied to both until cutover
owner_route_id route.ownerRouteId Flatten nested JSON in ETL, mirror flat legacy

The dictionary_column_dual_write view is not magic — it is documentation the BI team cannot ignore:

CREATE VIEW dictionary_column_dual_write AS
SELECT
  dictionary_metric_id,
  breach_count       AS breach_count_legacy,
  breachCount        AS breach_count_modern,
  CASE WHEN breach_count IS DISTINCT FROM breachCount THEN 'MISMATCH' ELSE 'OK' END AS dual_write_health
FROM governance_metric_dictionary_current;

Health row turns red on any MISMATCH during dual_write. That row blocks cutover promotion in CI until resolved — same mechanical discipline as tuple drift.

exporter_allowlist_diff and CI fail-closed gate

  1. Export a golden JSON sample from Unity / BI for each active platform before changing dictionary semver.
  2. Compute sorted key list K_before.
  3. After code changes that add columns, compute K_after.
  4. Store exporter_allowlist_diff.json with three arrays: added_keys, removed_keys, type_changed_keys.
  5. CI step: if removed_keys is non-empty while phase = dual_write, fail — dual-write forbids removal; only additive keys are legal until cutover completes.

This gate is what keeps Unity 6.x field renames from slipping into production as silent deletions.

Cutover checklist (single maintenance hour)

  1. Freeze dictionary edits in the authoring UI (banner + Slack #gov-ops lock).
  2. Run synthetic replay diff gate from Lesson 167 against frozen CSV snapshots at from_semver and to_semver — both must pass before flipping phase.
  3. Flip phase to cutover, deploy exporter that writes only modern columns.
  4. Run Lesson 166 reconciliation immediately (not next Monday — now).
  5. If reconciliation delta within epsilon, close migration with closed_at_dt_utc and archive allow-list diff under migrations/{migration_id}/.

reconciliation_rollback_trigger coupling to Lesson 166

Extend the Lesson 166 job with a post-step query:

SELECT migration_id
FROM dictionary_migration_state
WHERE phase = 'cutover'
  AND migration_id IN (
    SELECT migration_id FROM weekly_reconciliation_summary
    WHERE week_over_week_delta_ratio > v_epsilon_threshold
      AND consecutive_high_weeks >= 2
  );

For every migration_id returned, insert a phase = 'rollback' row, redeploy exporters to from_semver, and emit a partner-visible notice in the Lesson 170 FAQ export stating which migration rolled back and why (epsilon breach, not operator whim).

Common mistakes to avoid

  • Skipping dual-write because "we only added one nullable column." Nullable columns still shift CSV column order in some exporters; partners diff by position in fragile spreadsheets.
  • Allowing removed_keys during dual-write. That is a breaking change masquerading as a minor bump — semver must jump major, which is explicitly out of scope for this lesson.
  • Running cutover during cert week. Schedule migrations in the same Tuesday maintenance window you already use for dictionary advances in Lesson 164.
  • Letting FAQ bind checks read only modern keys while dual-write is active. Lesson 170 must query the dual-write view until cutover closes.

Verification checklist

  • [ ] dictionary_migration_state exists with phase CHECK and single-active-migration invariant enforced in application code.
  • [ ] dictionary_column_dual_write view returns zero MISMATCH rows for 72 consecutive hours before cutover approval.
  • [ ] exporter_allowlist_diff.json committed with SHA-256 matching CI artifact.
  • [ ] Lesson 166 reconciliation runbook updated with rollback trigger query and pager routing.
  • [ ] Partner annex SQL view version bumped in lockstep with to_semver and recorded in readback footer per Lesson 165.

Mini exercise (60 minutes)

Pick one nullable numeric column you think is "safe." Add it only to the staging dictionary, run the dual-write view, and intentionally populate mismatched values for five metric IDs. Watch CI fail-closed. Then fix the ETL bug and re-run until green. That muscle memory is cheaper than a January 2027 partner reopen.

Prepare phase: inventory and bind-contract proof (week one)

Before dual_write opens, the team produces three artifacts that partner reviewers in 2026 increasingly ask for by name:

  1. dictionary_bind_proof_before.pdf — a one-page join diagram showing every dictionary_metric_id referenced by the leadership rollup SQL and the partner annex SQL for the same from_semver. This is the Lesson 164 contract expressed as a picture.
  2. dictionary_bind_proof_after.sql — the exact same joins rewritten against the proposed to_semver column layout while still reading legacy columns through compatibility views. No metric may disappear from the join result set; row counts must match within zero tolerance for static dimensions (identity keys), and within the Lesson 167 epsilon policy for noisy numeric measures.
  3. migration_impact_matrix.csv — rows are dashboards (leadership, partner_annex, mock_audit_export, reply_manifest_validator), columns are migration risks (column_rename, nested_json, nullable_add, type_widen). Each cell is green, amber, or red. Any red blocks prepare → dual_write promotion.

Cadence discipline: treat prepare as a full calendar week even if the schema change looks tiny. The failure mode in late 2026 is not "we missed a column" — it is "we missed a consumer that only runs on Friday reconciliation" and the first signal is a partner email, not CI.

Dual-write operations runbook (week two)

Daily job #1 — dual_write_health_scan: materialise counts of MISMATCH rows, grouped by dictionary_metric_id and owning squad. Post the top five IDs to the same morning channel you already use for Lesson 173's trend board. If the same ID appears three days in a row, open a deficiency ticket against the ETL owner route, not against the migration itself.

Daily job #2 — exporter_key_fingerprint: hash the sorted key list of every JSON export landed in the staging bucket. Compare to the committed allow-list artifact. Drift without a merged PR is an automatic page — that pattern catches "helpful" manual edits in the Unity exporter preset that never went through Git.

Daily job #3 — faq_bind_probe: for every faq_rows entry in active status, run the bind query from Lesson 170 against dictionary_column_dual_write. Zero rows may return bind_miss. A single miss means FAQ answers could diverge from annex language even though rollups still match — that is still a governance stop because partner reviewers quote FAQ text in email threads.

Cutover expanded: seven micro-steps inside the maintenance hour

  1. Announce lock — 15 minutes before, flip the red banner in the authoring UI and set migration_banner_id in the footer stamp per Lesson 165 so downstream parsers know a migration window is live.
  2. Snapshot exports — pull golden JSON from both leadership and partner pipelines at from_semver and store under migrations/{migration_id}/golden_before/.
  3. Flip exporter profiles — deploy the profile that emits only modern keys. Keep legacy compatibility views in SQL for seven days read-only for forensics, not for production writers.
  4. Invalidate edge caches — purge CDN paths that cache annex CSV fragments; stale fragments are the fastest way to pass CI yet fail human diff in a partner Zoom.
  5. Run Lesson 166 reconciliation immediately — do not wait for cron; the first reconciliation after cutover is your earliest honest signal.
  6. Emit migration_complete_event — append-only row with signer ack from the same route class as dictionary advances in Lesson 164.
  7. Post-mortem note in Lesson 170 export — one paragraph, plain language: what changed, which partner-visible totals were expected to move, and where to roll back if needed.

Epsilon policy for rollback (ties Lesson 166 to this lesson)

Pick two thresholds, not one:

  • epsilon_soft — first week after cutover may exceed baseline noise slightly; triggers amber Slack, not rollback.
  • epsilon_hard — if week_over_week_delta_ratio exceeds this value or epsilon_soft is breached twice consecutively, the reconciliation_rollback_trigger fires.

Document both numbers in the same README row as your Lesson 167 epsilon table so teams cannot debate which epsilon "was meant" during a 2 a.m. incident.

Troubleshooting

Symptom Likely cause Fix
Leadership totals match, partner annex low by 2% Cached fragment or stale CDN path Purge annex caches; re-export partner bundle
Dual-write health OK but FAQ bind misses View join used modern key while FAQ still pointed at legacy alias Extend compatibility view with both aliases
Rollback fires weekly Epsilon thresholds too tight for seasonal cert noise Re-tune using last four quarters' variance, not last week
CI allow-list passes but humans see wrong column Sorted key hash collision on duplicate keys Fail export on duplicate keys at source

Pro tips

  • Tip — Migration as code review: require two human approvals on exporter_allowlist_diff.json just like application code — bots cannot judge semantic renames.
  • Tip — Staging tenant: run the entire migration twice on a staging Snowflake/BigQuery tenant that mirrors production row cardinality but redacts PII; cardinality-only mismatches catch 80% of join bugs.
  • Tip — Pair with Lesson 176: when reply_tuple_manifest_json references a metric ID, add that ID to an explicit protected_ids array in the migration JSON so renames cannot orphan replies mid-flight.

Next lesson teaser

The next lesson (Lesson 178: Carved-Back Deficiency Quorum Timer and Evidence Rebind Workflow (2026)) covers what happens when Lesson 172 deficiencies move from carved_out back to open — quorum, timer, and evidence rebind to a new cert_window_id. Lesson 177 keeps the dictionary stable while analytics expand; Lesson 178 keeps carve-backs from becoming silent reopen bombs in Q1 2027 intake checklists.

Continuity

  • Paired Unity guide chapter (next Guide-Create pass will author): Unity 6.6 LTS OpenXR governance metric dictionary minor-increment migration preflight — editor-side Governance/MetricDictionaryMigration ScriptableObject with fromSemver / toSemver / phase enum, CI hook that ingests exporter_allowlist_diff.json, and one-button rollback that restores legacy exporter profile.
  • Help article: OpenXR Governance Partner SLA Snapshot vs Leadership Dashboard Rollup Mismatch (Quest) Fix — when annex totals disagree after migration, this is still the first triage article.
  • Lesson 176 — reply tuple manifests consume dictionary IDs; migration must preserve every referenced ID.
  • Lesson 175 — archive rows pin dictionary_semver; migration bumps must write new archive metadata rows, not mutate cold WORM pointers.
  • Lesson 174 — rollback pages route through fatigue-aware on-call owners.
  • Lesson 173 — new failure_mode_tag columns enter dictionary during dual_write first.
  • Lesson 172 — mock audit deficiencies reference dictionary metrics; carve-out status in Lesson 178 depends on stable IDs from this lesson.
  • Lesson 171 — publish pipeline must refuse cutover if migration state machine has open MISMATCH rows.
  • Lesson 170 — FAQ bind contract reads dictionary through dictionary_column_dual_write during migration.
  • Lesson 167 — synthetic replay diff gate is the hard gate before cutover.
  • Lesson 166 — reconciliation epsilon defines rollback trigger thresholds.
  • Lesson 165 — footer semver pins must include active dictionary_semver after migration closes.
  • Lesson 164 — bind contract is the non-negotiable success criterion.

exporter_allowlist_diff.json shape (commit this, diff this, hash this)

Treat the allow-list artifact as data that ships governance, not as a Unity-side convenience file. A minimal schema teams can adopt without inventing a new format:

{
  "migration_id": "mig-2026-05-14-dict-minor-173-176",
  "phase": "dual_write",
  "from_semver": "2.4.0",
  "to_semver": "2.4.1",
  "allowed_top_level_keys": [
    "breach_count",
    "breachCount",
    "owner_route_id",
    "route"
  ],
  "nested_paths_allowed": [
    "route.owner_route_id",
    "route.queueDepth"
  ],
  "forbidden_exact_keys": [
    "queue_depth"
  ],
  "protected_metric_ids": [
    "MET-0144",
    "MET-0171"
  ]
}

CI contract: the job that ingests Unity / BI JSON must (1) flatten nested objects to dotted paths, (2) sort keys lexicographically, (3) reject duplicate keys after flattening, (4) compute SHA-256 over the canonical JSON bytes, (5) compare to the committed hash in migrations/{migration_id}/allowlist.sha256. Any exporter build that adds a key not in allowed_top_level_keys or nested_paths_allowed fails the pull request before anyone merges a green dashboard screenshot.

Human review contract: the governance owner and the analytics owner both approve the diff when forbidden_exact_keys grows — that list is your explicit admission that a legacy key must die during cutover, and partner-facing SQL must stop selecting it the same hour the exporter stops emitting it.

Governance review gates (who signs what, and when)

Gate Owner Artifact Blocks promotion to…
G0 — impact matrix Analytics lead migration_impact_matrix.csv prepare
G1 — bind proof Data / BI engineer dictionary_bind_proof_before.pdf + dictionary_bind_proof_after.sql dual_write
G2 — allow-list Unity exporter maintainer exporter_allowlist_diff.json + hash merge to main exporter profile
G3 — FAQ probe FAQ editor from Lesson 170 faq_bind_probe log cutover
G4 — replay SRE / data Lesson 167 synthetic replay green cutover
G5 — reconciliation baseline On-call from Lesson 174 routing three consecutive Lesson 166 greens opening dual_write

Skipping G1 is the fastest way to pass every automated test while still failing the partner annex on the first row of their diff script — bind proof is the only gate that proves semantic parity, not just syntactic JSON health.

Week-three narrative: what you tell leadership in one slide

Title: "Dictionary 2.4.1 — additive columns only; no rollup contract change."
Bullets: dual-write window dates, count of MISMATCH rows (target zero), exporter hash before/after, explicit sentence that Lesson 164 leadership–partner bind is unchanged because every new column is nullable until cutover and every rename is served through dictionary_column_dual_write.
Risk line: rollback trigger fires on two consecutive reconciliation breaches above epsilon_hard — same language as Lesson 166 weekly email so executives do not learn a new vocabulary during a crisis week.

That slide is not bureaucracy; it is the artifact that stops someone from "helpfully" asking engineering to skip dual-write because the cert window is loud.

FAQ

Can we bundle two unrelated column families in one migration?
No. One migration UUID per semantic family (fatigue columns vs archive columns). Bundling complicates rollback forensics.

What if Unity ships another exporter rename mid-dual-write?
Open a new migration row chaining to_semver forward; never edit an in-flight migration_id in place.

Does this replace semantic versioning for footer schema?
No. Footer semver from Lesson 165 and dictionary semver are orthogonal but must be cross-linked in migration notes so replay parsers know which pair they are reading.


Minor dictionary bumps are how small teams keep pace with 2026 engine and BI churn without breaking the Lesson 164 promise. Dual-write is boring; boring is what keeps partner annexes aligned when everything else in the stack is moving.