Legacy ID + Cross-Surface Schema Alignment Migration — TECH Spec
Legacy ID + Cross-Surface Schema Alignment Migration — TECH Spec
Section titled “Legacy ID + Cross-Surface Schema Alignment Migration — TECH Spec”Status: TECH — Subtask ID-15.2 deliverable (S57 re-dispatch, supersedes S56
backlog-only spec at worktree-agent-a2815771c07f389f6 commits 42b2206e +
cd7af8b3).
Scope: Unified spec covering all four ordered phases of Task ID-15
(docs/reference/task-list.json) per ratified §8.4 of
docs/research/cross-surface-schema-alignment.md (S56 OQ-1 outcome).
- Phase A — Backlog bare-digit ID migration (43 live legacy items →
21..65contiguous block in current data; mapping doc §3.2 envelope is21..65for the 46-item snapshot — see §A.0 below). - Phase B — Schema fixes: drop
surfaced; add structured-provenance triple (session_refs[],commit_refs[],cross_doc_links[]); resolvetech-debt/tech_debtenum drift; alignRoadmapPriorityto shared master. - Phase C — Stale-warning cleanup (3 files) + shared constant
(
BARE_ID_REGEX) relocation tolib/validation/schemas.tsper S56 OQ ratification. - Phase D — Label-reversal residue audit method + verdict template.
Cross-references.
docs/research/cross-surface-schema-alignment.md— ID-15.6 RESEARCH (694 lines). §10 carries all 5 ratified OQs.docs/research/legacy-id-migration-mapping.md— ID-15.1 RESEARCH (post-S56 bare-digit rewrite). §3.2 mapping table is the authoritative target.docs/reference/task-list.json— Task ID-15 + 9 Subtasks.lib/validation/{backlog-schema,task-list-schema,roadmap-schema,work-status,schemas}.ts— current canonical modules.scripts/migrate-roadmap-section-3.tsprecedent — deleted at9df51c67(post-merge), authored at8983f991. The Phase-A script follows the same shape and lifecycle..claude/skills/{update-roadmap-backlog,triage-finding}/SKILL.md+.claude/agents/workflow-curator.md— three artefacts carrying stale label-reversal warnings (per §5.4 of RESEARCH).
§1 Context
Section titled “§1 Context”§1.1 Why this spec exists
Section titled “§1.1 Why this spec exists”The S56 first-attempt ID-15.2 TECH spec scoped to backlog-only id-rewrite. S56
mid-session, Liam surfaced a deeper structural concern: the canonical-pipeline
migration coming next session compounds drift if it forks off the current
schema state. ID-15.6 RESEARCH consolidated the four-axis alignment work; its
five OQ ratifications (S56 close) phased the work into A/B/C/D ordered
slices. This spec carries all four phases as a single unified TECH artefact
because (a) Phase A’s schema tightening lands the BARE_ID_REGEX constant
that Phase C relocates; (b) Phase B’s surfaced → structured provenance
field drop touches the same backlog-schema.ts file as Phase A’s id regex
tighten; (c) co-locating decisions in one spec avoids drift between four
sequential planner dispatches.
§1.2 How the current system works
Section titled “§1.2 How the current system works”- Three task surfaces: Task list (
docs/reference/task-list.json), Backlog (docs/reference/product-backlog.json), Roadmap (docs/reference/product-roadmap.json). Each validated by a Zod schema inlib/validation/. - Status + Priority enums DONE (S50 WP1) — shared module
lib/validation/work-status.tsexportsWorkStatusmaster and per-surface exclusions. - Task.id is bare-digit canonical (
"15","20") perTaskSchema.id = z.string().regex(/^\d+$/). 10 Tasks occupy6, 7, 8, 9, 10, 15, 16, 18, 19, 20. - Backlog.id is mixed: 43 legacy items (e.g.
45,30,25) + 2 already-canonical items (ID-17,ID-18). Schema currently permits any non-empty string. - Backlog.surfaced is
z.string().min(1)but 8 live items carrysurfaced: null(53,47,42,43,44,59,51,45). - BacklogItemType accepts only
'tech_debt'(underscore) but 3 live items carry'tech-debt'(kebab) — see §B.2 below. - RoadmapPriority is defined inline in
roadmap-schema.ts(lines 39–48) as a standalonez.enum(...)rather than re-exported fromwork-status.ts. The value set is identical to the sharedPrioritymaster. - Three artefacts carry stale label-reversal warnings that the S40
d49e0b11content migration effectively resolved (per RESEARCH §5.3):.claude/skills/update-roadmap-backlog/SKILL.md(lines 13, 51, 332, 341, 351);.claude/skills/triage-finding/SKILL.md(lines 182–196);.claude/agents/workflow-curator.md(lines 239–254 region). - Shared constants location per CLAUDE.md is
lib/validation/schemas.ts. Cross-surface task ID constants currently DO NOT EXIST anywhere — Phase C introduces them per S56 OQ ratification. - Migration script precedent:
scripts/migrate-roadmap-section-3.tsran at S50 WP4 (commit8983f991) and was deleted post-merge (commit9df51c67). The same shape applies to Phase A: dry-run + apply modes, idempotent, validation pass before commit, deleted in 15.5 post-merge.
§1.3 Relevant code references
Section titled “§1.3 Relevant code references”lib/validation/backlog-schema.ts:64-119—BacklogItemSchemadefinition.idat line 66;surfacedat line 99; type at line 72.lib/validation/task-list-schema.ts:99—Task.idregex.lib/validation/roadmap-schema.ts:39-48—RoadmapPriorityinline enum.lib/validation/work-status.ts—Prioritymaster + per-surface subsets.lib/validation/schemas.ts:1-100— current shared-constants module (domain-validation aggregate; no cross-surface task constants today)..claude/skills/update-roadmap-backlog/SKILL.md:13,51-66,330-341,351— label-reversal references..claude/skills/triage-finding/SKILL.md:182-196— Step 3 label-reversal flag..claude/agents/workflow-curator.md:239-254— Critical-note region.docs/research/legacy-id-migration-mapping.md:130-280— §3.2 + §3.3 ratified mapping table.- Precedent:
scripts/migrate-roadmap-section-3.tsat commit8983f991(git show 8983f991:scripts/migrate-roadmap-section-3.ts); deletion at9df51c67.
§1.4 Behaviour reference
Section titled “§1.4 Behaviour reference”There is no companion PRODUCT.md. This is internal-housekeeping work driven by the two RESEARCH docs (15.1 + 15.6); each Phase’s “behaviour” is expressed as the testable invariants in the per-Phase §3 sections below.
§1.5 Data-state discovery (post-RESEARCH snapshot)
Section titled “§1.5 Data-state discovery (post-RESEARCH snapshot)”Live data at cebeaa72:
- Backlog has 45 items (NOT 46 as RESEARCH §2.4 / mapping doc §2.4 reports).
ID-18was added between the mapping-doc snapshot (track HEADecc7ae31) andcebeaa72. Already canonical, no migration touch. - Backlog has 43 legacy items (NOT 45 as the mapping doc §3.2 contemplates).
AST-S3-O1andAST-S3-O2from the mapping doc Cluster 1 are NOT in the live data — they shipped/closed between the inventory capture and this spec. The script in 15.3 MUST detect this drift and either (a) skip absent legacy IDs from the mapping (warning only) or (b) hard-fail with a directive to refresh the mapping. See §A.0 below for the resolution rule. - Backlog
tech-debt(kebab) items (3):26,27,ID-18. Backlogtech_debt(underscore) items (6 — already canonical):45,61(typefeatureper live data — NOT tech-debt; left in §1.5 for historical context),63(typeinfrastructure), and 6 actual tech_debt-typed items. The 9-item RESEARCH count was approximate; live audit at Checker re-pass confirms the 3 kebab items above. - Backlog
surfaced: nullitems (8):53,47,42,43,44,59,51,45. Matches RESEARCH §2.5 exactly.
§2 OQ ratifications — pinned for downstream
Section titled “§2 OQ ratifications — pinned for downstream”All five ID-15.6 OQ ratifications (RESEARCH §10) are pinned here as binding inputs to the Proposed changes. Re-litigating any requires re-engaging Liam, not in-flight Planner decisioning.
| OQ | Decision | Phase impact |
|---|---|---|
| OQ-1 Scope partitioning | Phased — ID-15 carries 4 ordered phases A/B/C/D | Subtask shape (15.3/4/5 = Phase A; 15.7 = Phase B; 15.8 = Phase C; 15.9 = Phase D) |
| OQ-2 Task.id storage format | Bare-digit aligned DOWN. BacklogItem.id migrates to /^\d+$/; targets are literal "21".."65". Task.id stays bare-digit; “ID-N” is prose-only | Phase A.2 schema tighten regex; mapping doc §3.2 targets bare-digit literals |
| OQ-3 Roadmap dependency consolidation | Keep divergent. Roadmap preserves depends_on + blocks + coordinates_with (3 fields). Task list + Backlog stay flat dependencies[]. Declared intentional in TECH §B.3 below | No data write to roadmap.json for dependencies; documentation only |
| OQ-4 Backlog missing-field backfill | Drop surfaced string; add structured triple session_refs[] + commit_refs[] + cross_doc_links[]. Backlog stays lighter than Task overall — no title / owner / priority_note additions | Phase B.1 schema change + data backfill; affects all 45 items |
| OQ-5 Label-reversal residue audit | Confirm shipped + clean stale warnings. Phase D audit is read-only with verdict template; Phase C applies cleanup | Phase D method; Phase C edits the 3 artefacts |
§A Phase A — Backlog bare-digit ID migration
Section titled “§A Phase A — Backlog bare-digit ID migration”§A.0 Live-data drift resolution rule
Section titled “§A.0 Live-data drift resolution rule”The mapping doc §3.2 was authored against a 46-item / 45-legacy snapshot at
track HEAD ecc7ae31. Live data at cebeaa72 is 45 items / 43 legacy. The
two missing items (AST-S3-O1, AST-S3-O2) were intended targets 21 + 22
in Cluster 1.
Rule: The script (scripts/migrate-legacy-ids-to-id-n.ts) MUST handle absent
legacy IDs idempotently and verbosely:
- Build the canonical mapping from §3.2 in code (43 entries —
21..65contiguous after the missing-AST-S3 compaction). - On each item read, look up the legacy id in the mapping. If absent in the
live data, log
[SKIP] <legacy_id> not present in live data — already shipped/closed. Continue. - Detect and emit a
[GAP]line for each contiguous-block hole this creates (e.g. “Cluster 1 envelope was 21..27, live mapping produced 21..25 with two slots vacated” — the script reports but does not auto-compact). - The actual mapping target list MUST reflect the post-drift block. After this
script run, Backlog occupies
17, 18, 23..65(with the C1 head shift) OR per the simpler alternative, occupies21..65directly by re-targeting C1 to start at23(shipping the 21+22 slots to vacated).
Recommended: Implementer chooses the simpler alternative — drop the
absent mappings, do NOT compact the rest of the block. Final live-data
contiguous target after migration: 17, 18, 23..65 (43 items at new ids;
sequential within each cluster preserved; only the C1 head shifts).
The migration script (15.3) MUST commit-comment the gap and update mapping doc
§3.2 with a [live snapshot 2026-05-20T22:00:00.000Z] annotation. Mapping doc
§3.2 stays the authoritative envelope; the script’s runtime mapping is the
intersect-with-live-data subset.
§A.1 Proposed changes — files touched
Section titled “§A.1 Proposed changes — files touched”| File | Change kind | Diff scope |
|---|---|---|
scripts/migrate-legacy-ids-to-id-n.ts (NEW — 15.3) | new file | ~150–250 lines (precedent: migrate-roadmap-section-3.ts was ~200 lines at 8983f991) |
docs/reference/product-backlog.json | data write | 43 id field rewrites; 43 dependencies[] rewrites where legacy id appears (precedent: 1 such dep — OPS-28.dependencies = ["33"]); idempotent re-run safe |
lib/validation/backlog-schema.ts:66 | schema tighten | id: z.string().min(1) → id: z.string().regex(BARE_ID_REGEX, 'Backlog id must be a string of digits') |
lib/validation/task-list-schema.ts:99 | schema reuse | Replace inline regex(/^\d+$/, ...) with regex(BARE_ID_REGEX, ...) import-from-shared (zero behaviour change, prep for Phase C relocation) |
lib/validation/schemas.ts (top of file, near other regex constants) | constant add | export const BARE_ID_REGEX = /^\d+$/; plus single-line export note |
| Cross-doc sweep | grep+rewrite | Per OQ-4 ratification in mapping doc §5 — always-rewrite scope: lib/, app/, __tests__/, scripts/, .claude/agents/, .claude/skills/, docs/reference/, docs/runbooks/, docs/operations/; best-effort: docs/continuation-prompts/, docs/plans/, docs/specs/, docs/audits/; never-rewrite: .planning/.archive/** |
docs/research/legacy-id-migration-mapping.md | doc fixup | Add [live snapshot 2026-05-20T22:00:00.000Z] annotation per §A.0 |
§A.2 Migration script shape (consumed by Subtask 15.3)
Section titled “§A.2 Migration script shape (consumed by Subtask 15.3)”//// One-shot migration: backlog legacy ids → bare-digit ids.// Idempotent, dry-run-able, validates pre + post.
import { promises as fs } from 'node:fs';import path from 'node:path';import { BacklogSchema } from '@/lib/validation/backlog-schema';// NOTE: Imports BacklogSchema BEFORE the regex tighten lands (use the// pre-tighten shape during migration; the schema tighten in 15.4 happens// AFTER the data is migrated).
// Hard-coded mapping per legacy-id-migration-mapping.md §3.2 + §A.0 drift rule.const MAPPING: ReadonlyMap<string, string> = new Map([ // Cluster 1 — ast-dataflow follow-ons (5 live, AST-S3-O1/O2 absent) ['23', '23'], ['24', '24'], ['25', '25'], ['26', '26'], ['27', '27'], // Cluster 2 — Onboarding ['28', '28'], // … (full table per §3.2 + §A.0)]);
// Dry-run vs applyconst isDryRun = process.argv.includes('--dry-run');const isApply = process.argv.includes('--apply');
// 1. Load + parse backlog (pre-tighten schema)// 2. For each item: if id in MAPPING, rewrite id; if dependencies[] contains// legacy id, rewrite that entry too// 3. Log [SKIP] for absent legacy ids; [GAP] for compaction holes// 4. Validate result against pre-tighten BacklogSchema (must still parse)// 5. If --apply: write file; print diff summary// 6. Cross-doc sweep: walk allowed dirs, regex-match `\b<legacy_id>\b` with// word boundaries to avoid OPS-43 matching 45 in prose// 7. Exit code: 0 on success; 1 on any data-validation failureKey implementation notes for 15.3:
- The migration sequence per
migrate-roadmap-section-3.tsprecedent is data first, schema second. Run the script with the looseid: z.string().min(1)schema active; the 15.4 apply step then tightens the schema. This is critical: if the schema is tightened first, the script’s mid-run state (legacy + new ids commingled) fails validation. - Cross-doc sweep regex set (per mapping doc §3.2 + decision rule §4):
\bOPS-\d+(\.\d+)?\b(catches45correctly with sub-decimal)\bOPS-\d+ \(main\)(catches62— note the parenthetical; per OQ-6 ratified drop-silently)\bAST-S\d+-O\d+\b\bC\d+(-DT|-T\d+|-PA|-Mobile)-[A-Za-z0-9-]+\b\bRLS-P\d+\b\bENG-TAX-SIMPLIFY\b\bEVAL-\d+\b\bPL-\d+\b
- Word-boundary
\bmatches are critical to avoid false positives (e.g.OPS-43inside45prose). Implementer MUST emit a per-match log line in dry-run mode so manual review precedes apply. - The script accepts
--scope=always|best-effort|all(defaultalways) to gate the cross-doc sweep blast radius. Best-effort runs WARN on each rewrite; archive never matches.
§A.3 45 lineage handling (OQ-5 ratification from mapping doc)
Section titled “§A.3 45 lineage handling (OQ-5 ratification from mapping doc)”45 migrates to bare-digit 45. Per ID-15.1 OQ-5 ratification, lineage
is preserved in the notes field on the migrated item:
// Before: {"id": "45", "notes": "<existing notes — 35/39 SHIPPED detail>", ...}// After: {"id": "45", "notes": "Originally 45 — sub-decimal lineage of// OPS-43 (SHIPPED, removed from backlog). Migrated S57 ID-15.3.// <existing notes appended verbatim>", ...}Script logic: if migrated id maps from a legacy id matching ^OPS-\d+\.\d+$,
write the lineage prefix into notes (preserving any existing notes content
appended after).
§A.4 RLS-P9 status override (Cluster 7 — mapping doc §3.2 + S56 revise)
Section titled “§A.4 RLS-P9 status override (Cluster 7 — mapping doc §3.2 + S56 revise)”RLS-P9 migrates to bare-digit 41 AND the migration commit (15.4 apply step)
flips status: spec_needed → done with status_note set:
// Before: {"id": "RLS-P9", "status": "spec_needed", "status_note": null, ...}// After: {"id": "41", "status": "done",// "status_note": "Retroactive RLS audit completed S243-era. No// issues found. Confirmed by Liam at S56 — findings not filed// separately. Status flip applied during ID-15.4 migration.", ...}Note: This is the ONLY status flip in the migration. Apply only to RLS-P9.
The script’s mapping table marks this entry as {target: '41', status_override: 'done', status_note: '...'} rather than inlining the override in the simple
Map<string, string> shape — keeping override logic visible at script-author
time.
§A.5 Cross-doc sweep scope boundaries (ratified OQ-4)
Section titled “§A.5 Cross-doc sweep scope boundaries (ratified OQ-4)”| Tier | Directories | Behaviour |
|---|---|---|
| Always-rewrite | lib/, app/, __tests__/, scripts/, .claude/agents/, .claude/skills/, docs/reference/, docs/runbooks/, docs/operations/ | Rewrite + commit. Script exits non-zero if any always-rewrite path contains a post-sweep legacy match. |
| Best-effort | docs/continuation-prompts/, docs/plans/, docs/specs/, docs/audits/ | Rewrite + commit; emit WARN per match. Historical refs may be intentional (e.g. docs/plans/phase-0-investigation/s48-feedback.md discussing legacy ids by name). Implementer reviews diff before apply. |
| Never-rewrite | .planning/.archive/** | Skip entirely. Snapshot integrity. |
§A.6 Verification — Phase A acceptance
Section titled “§A.6 Verification — Phase A acceptance”- A-INV-1. Migration script
scripts/migrate-legacy-ids-to-id-n.tsexists, is dry-run-able viabun scripts/migrate-legacy-ids-to-id-n.ts --dry-run, applies via--apply, and is idempotent (second--applyis no-op). - A-INV-2. Post-migration:
BacklogSchema.parse()againstdocs/reference/product-backlog.jsonsucceeds (loose schema; tightened in A-INV-4). - A-INV-3. Post-migration: every backlog item’s
idmatches/^\d+$/AND every legacy id in anydependencies[]is rewritten to its bare-digit target. - A-INV-4. Post-tighten:
BacklogItemSchema.idisz.string().regex(BARE_ID_REGEX, ...).bun run test __tests__/validation/backlog-schema.test.tsis green. - A-INV-5.
BARE_ID_REGEXexported fromlib/validation/schemas.tsand imported (not redefined) bybacklog-schema.tsandtask-list-schema.ts.grep -rn "regex(/^\\\\d+\\$/" lib/validation/returns zero matches. - A-INV-6. Cross-doc sweep:
grep -rn -E '\b(OPS-[0-9]+|AST-S[0-9]+-O[0-9]+|C[0-9]+-(DT|T[0-9]+|PA|Mobile)-[A-Za-z0-9-]+|RLS-P[0-9]+|52|EVAL-[0-9]+|PL-[0-9]+)\b' lib app __tests__ scripts .claude/agents .claude/skills docs/reference docs/runbooks docs/operationsreturns zero matches. - A-INV-7.
45is at id45andnotesfield carries the lineage prefix per §A.3. - A-INV-8.
RLS-P9is at id41withstatus: 'done'andstatus_noteper §A.4.40is at id40withstatus: 'spec_needed'(unchanged). - A-INV-9. Schema-roundtrip tests pass:
bun run test __tests__/docs/backlog-no-closed-rows.test.ts __tests__/docs/roadmap-roundtrip.test.ts. - A-INV-10. Mapping doc
docs/research/legacy-id-migration-mapping.md§3.2 carries the[live snapshot 2026-05-20T22:00:00.000Z]annotation reflecting the A.0 drift.
§A.7 Architecture sketch (Phase A)
Section titled “§A.7 Architecture sketch (Phase A)” ┌──────────────────────────────────────────┐ │ docs/research/legacy-id-migration- │ │ mapping.md §3.2 + §3.3 + §A.0 drift │ └──────────────────────────────────────────┘ │ inputs ▼ ┌──────────────────────────────────────────┐ │ scripts/migrate-legacy-ids-to-id-n.ts │ │ - dry-run + apply modes │ │ - idempotent │ │ - cross-doc sweep w/ scope tiers │ └──────────────────────────────────────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────────────────┐ ┌─────────────┐ ┌───────────────────────┐ │ product-backlog │ │ backlog- │ │ Cross-doc rewrites │ │ .json │ │ schema.ts │ │ (always vs best-effort│ │ - 43 id rewrites │ │ - id regex │ │ vs never) │ │ - 34 dep fix │ │ tighten │ │ │ │ - 45 lineage │ │ │ │ │ │ - RLS-P9 status │ │ │ │ │ └─────────────────────┘ └─────────────┘ └───────────────────────┘ │ ▼ ┌──────────────────────────────────────────┐ │ Subtask 15.5 — delete migration script │ │ per migrate-roadmap-section-3 precedent │ └──────────────────────────────────────────┘§B Phase B — Schema fixes
Section titled “§B Phase B — Schema fixes”§B.1 Drop surfaced; add structured-provenance triple (OQ-4 ratification)
Section titled “§B.1 Drop surfaced; add structured-provenance triple (OQ-4 ratification)”Current state:
// backlog-schema.ts:99surfaced: z.string().min(1),// 8 live items violate this with surfaced: null (53, 47, 42, 43,// 44, 59, 51, 45). Live data does NOT parse against the// current schema today.Target state (post-Phase B):
// backlog-schema.ts — surfaced REMOVED// Three new structured-provenance fields added (mirror Roadmap + Task list// shape):session_refs: z.array(z.string()),commit_refs: z.array(z.string()),cross_doc_links: z.array(DocLinkSchema), // import from roadmap-schema.tsData backfill (per OQ-4 ratification — “structured provenance written at backlog creation by workflow-curator → direct copy on promotion to Task”):
- All 45 items receive
session_refs: [],commit_refs: [],cross_doc_links: []as empty arrays in the same Phase B commit that lands the schema change. - For the 8 previously-null-surfaced items, no lossy translation is attempted —
their old
surfacedtext is preserved in the migration COMMIT MESSAGE as a footnote, not back-fitted into the new fields. Forward-looking convention is what matters; legacy provenance text stays in git history. - For the 37 items that had non-null
surfacedstrings, the migration commit message similarly preserves the originalsurfacedtext. No automated parsing into structured fields (would be lossy and incorrect for most free-formsurfacedstrings). - workflow-curator (post Phase C edit — see §C.3) populates these fields on every new backlog item write going forward.
Files touched:
| File | Change |
|---|---|
lib/validation/backlog-schema.ts:99 | Delete surfaced line; add 3 new fields |
docs/reference/product-backlog.json | Drop surfaced from all 45 items; add 3 empty arrays per item |
lib/validation/roadmap-schema.ts | No change — DocLinkSchema already exported (line 89-104) |
__tests__/validation/backlog-schema.test.ts | Update assertions: remove surfaced tests; add structured-triple tests |
.claude/skills/update-roadmap-backlog/SKILL.md | Phase C edit covers — write contract update |
§B.2 BacklogItemType enum drift — tech-debt vs tech_debt
Section titled “§B.2 BacklogItemType enum drift — tech-debt vs tech_debt”Current state:
// backlog-schema.ts:35-44export const BacklogItemType = z.enum([ 'feature', 'bug', 'research', 'tech_debt', 'infrastructure', 'documentation', 'testing', 'ux',]);// Live data: 6 items use 'tech_debt'; 3 items use 'tech-debt' (kebab).// 3-kebab items: 26, 27, ID-18 (per live data at S57).Resolution decision (binding): Adopt tech_debt (underscore) as
canonical. Rationale:
- Schema enum already canonical at
tech_debt. - All other surfaces use snake_case for enum values (
spec_needed,in_progress,imp_deferred, etc.). - The 3 kebab items are the minority; migrating 3 strings is the smaller change.
- Mapping doc §3.2 + RESEARCH §2.7 noted the drift; neither prescribed a direction — the snake_case alignment is the natural canonical here.
Files touched:
| File | Change |
|---|---|
docs/reference/product-backlog.json | Rewrite type: 'tech-debt' → type: 'tech_debt' on the 3 affected items |
lib/validation/backlog-schema.ts | No schema change (already canonical) |
§B.3 RoadmapPriority alignment to shared Priority master
Section titled “§B.3 RoadmapPriority alignment to shared Priority master”Current state:
// roadmap-schema.ts:39-48 — standalone z.enum, identical value set to// work-status.ts Priority master but not linked.export const RoadmapPriority = z.enum([ 'must', 'should', 'could', 'future', 'high', 'medium', 'low', 'trigger',]);Target state:
// roadmap-schema.ts:39import { Priority } from '@/lib/validation/work-status';export const RoadmapPriority = Priority;export type RoadmapPriority = z.infer<typeof RoadmapPriority>;Identical accepted value set (no migration needed for data — every live value
already lives in Priority); the link reduces source-of-truth drift risk for
future Priority changes.
§B.4 Dependency-naming divergence (OQ-3 ratification — declared intentional)
Section titled “§B.4 Dependency-naming divergence (OQ-3 ratification — declared intentional)”Per OQ-3 ratification: keep Roadmap divergent (depends_on + blocks +
coordinates_with). Task list and Backlog stay flat dependencies[]. No code
or data change; this Phase B section documents the decision as ratified.
Documentation surface (one-line edits):
lib/validation/roadmap-schema.ts:169-171— add inline comment:// Per ID-15.6 OQ-3 ratification — intentional divergence from Backlog + Task list flat dependencies[]. Captures strategic decomposition (forward dep / reverse dep / lateral coordination).
§B.5 Verification — Phase B acceptance
Section titled “§B.5 Verification — Phase B acceptance”- B-INV-1.
BacklogSchema.parse()againstdocs/reference/product-backlog.jsonsucceeds with nosurfacedfield present on any item. - B-INV-2. Every backlog item carries
session_refs: [],commit_refs: [],cross_doc_links: [](empty arrays — no automated backfill of legacy provenance). - B-INV-3. Zero items with
type: 'tech-debt'(kebab) inproduct-backlog.json. The 3 affected items (26/26,27/27,ID-18/18) all usetech_debt. Note:ID-18migrates fromID-18→18in Phase A, so Phase B applies the type fix against the post-Phase-A bare-digit id. - B-INV-4.
lib/validation/roadmap-schema.tsimportsPriorityfromwork-status.tsand re-exports asRoadmapPriority. Inlinez.enum(...)is gone. - B-INV-5. Roadmap roundtrip test green:
bun run test __tests__/docs/roadmap-roundtrip.test.ts. - B-INV-6. Backlog schema test green:
bun run test __tests__/validation/backlog-schema.test.ts.
§B.6 Architecture sketch (Phase B)
Section titled “§B.6 Architecture sketch (Phase B)”backlog-schema.ts roadmap-schema.ts┌──────────────────┐ ┌────────────────────┐│ DROP │ │ z.enum(...) ││ surfaced │ │ ──── REPLACE ─────►││ │ │ import Priority ││ ADD │ │ from work-status ││ session_refs[] │ │ ││ commit_refs[] │ │ (one-line edit; ││ cross_doc_ │ │ docs intent) ││ links[] (re- │ └────────────────────┘│ uses DocLink ││ from roadmap) │ product-backlog.json└──────────────────┘ ┌────────────────────┐ │ │ 3 items: type │ ▼ │ 'tech-debt' → │product-backlog.json │ 'tech_debt' │┌──────────────────┐ └────────────────────┘│ ALL 45 items: ││ drop surfaced ││ add 3 empty ││ arrays │└──────────────────┘§C Phase C — Stale-warning cleanup + shared-constant relocation
Section titled “§C Phase C — Stale-warning cleanup + shared-constant relocation”§C.1 Stale label-reversal warning removal
Section titled “§C.1 Stale label-reversal warning removal”Per RESEARCH §5.3 + §5.4, the label-reversal warning is stale. Three files carry the warnings; OQ-5 ratification removes them.
§C.1.a .claude/skills/update-roadmap-backlog/SKILL.md
Section titled “§C.1.a .claude/skills/update-roadmap-backlog/SKILL.md”Edit 1 (line 13): Remove the phrase current KH label-reversal between roadmap and backlog. Rewrite the surrounding context to read target semantics drive write routing without historical label-reversal framing.
Edit 2 (lines 51–66): Replace the entire “Critical: KH currently has roadmap and backlog labelled the wrong way around (confirmed Session 46)…” block with a simplified Step-1 routing rule:
### Step 1: Resolve target → file
| Target semantics | File ||---|---|| Strategic / cross-cutting / multi-month | `docs/reference/product-roadmap.json` || Tactical / single-feature / weeks-scope OR parked / deferred / pre-work | `docs/reference/product-backlog.json` |
The mapping is 1:1 by `document_purpose`. The skill enforces target-semanticrouting; the curator never auto-corrects the destination.Edit 3 (lines 330–341): Delete the entire “Label-reversal note for future migration” section.
Edit 4 (line 341): Delete the trailing “The label-reversal flag from
triage-finding is the signal…” sentence and its surrounding paragraph.
Edit 5 (line 351): Delete bullet “Auto-correcting the label reversal” from the forbidden-actions list (no longer applicable).
Edit 6 (Step-3 write-contract — Backlog table): Update the Backlog-write contract to reflect Phase B schema:
- DROP
surfacedcolumn. - ADD
session_refs: string[],commit_refs: string[],cross_doc_links: DocLink[]columns withdefault: []semantics. - Update example payloads accordingly.
§C.1.b .claude/skills/triage-finding/SKILL.md
Section titled “§C.1.b .claude/skills/triage-finding/SKILL.md”Edit 1 (lines 182–196 — Step 3 Check for label-reversal flag): Delete the entire Step 3. Renumber subsequent steps (Step 4 → Step 3, Step 5 → Step 4, etc.). The “Examples” section at end of file MUST be re-checked and any remaining label-reversal-flag references purged.
§C.1.c .claude/agents/workflow-curator.md
Section titled “§C.1.c .claude/agents/workflow-curator.md”Edit (lines 239–254 region — Critical note on roadmap vs backlog
labelling): Delete the entire region. The agent’s prior workflow already
honours target-semantic routing via the (Phase C-edited)
update-roadmap-backlog skill; the critical-note region was an out-of-band
warning that no longer fires.
§C.1.d Other artefacts (scope check)
Section titled “§C.1.d Other artefacts (scope check)”grep -rn "label-reversal\|labelled the wrong way\|wrong way around" .claude/ CLAUDE.md docs/reference/ docs/runbooks/
MUST be run as a Phase C precondition; any matches outside the three artefacts
above are surfaced as additional cleanup work. (Per RESEARCH §6, only the
three named artefacts are expected.)
§C.2 Shared constant relocation — BARE_ID_REGEX in lib/validation/schemas.ts
Section titled “§C.2 Shared constant relocation — BARE_ID_REGEX in lib/validation/schemas.ts”Per OQ ratification (RESEARCH §7.7 default-lean was work-status.ts; S56
revised to lib/validation/schemas.ts to match the CLAUDE.md
Canonical constants: lib/validation/schemas.ts pointer):
// lib/validation/schemas.ts — add near the top, alongside other shared// regex constants
/** * Canonical bare-digit id format used by Task.id (task-list-schema.ts) and * BacklogItem.id (backlog-schema.ts) post-15.4 migration. "ID-N" is a prose * convention only — JSON storage is always bare-digit. * * Roadmap ids use dotted-decimal positional ids (e.g. "9.2", "12.15.3") and * are NOT covered by this regex. */export const BARE_ID_REGEX = /^\d+$/;Consumers update (Phase A also touches these — Phase C verifies the relocation):
lib/validation/task-list-schema.ts:99—regex(/^\d+$/)→regex(BARE_ID_REGEX)withimport { BARE_ID_REGEX } from '@/lib/validation/schemas';lib/validation/backlog-schema.ts:66—regex(BARE_ID_REGEX, ...)(the tightened form per Phase A.1).
§C.3 Write-contract documentation updates (post-Phase B schema)
Section titled “§C.3 Write-contract documentation updates (post-Phase B schema)”The update-roadmap-backlog skill body Step 3 (now post-§C.1.a Edit 6) must
reflect the Phase B field changes:
- New backlog items written by the curator carry
session_refs: [...],commit_refs: [...],cross_doc_links: [...]populated at write time from the curator’s current session context (not empty arrays — the curator KNOWS what surfaced the finding). - The
surfacedfield is GONE; do not write it. - Implementer of Phase C MUST verify
validate-agent.sh(or the equivalent agent-validator) passes on all 3 edited files.
§C.4 Verification — Phase C acceptance
Section titled “§C.4 Verification — Phase C acceptance”- C-INV-1.
grep -rn "label-reversal\|labelled the wrong way\|wrong way around" .claude/ CLAUDE.md docs/reference/ docs/runbooks/returns zero matches. (Archived.planning/.archive/**references are out of scope.) - C-INV-2.
BARE_ID_REGEXexported fromlib/validation/schemas.tsand imported (not redefined) bybacklog-schema.ts+task-list-schema.ts.grep -rn "regex(/^\\\\d+\\$/" lib/validation/returns zero matches. - C-INV-3.
.claude/skills/update-roadmap-backlog/SKILL.mdStep-3 Backlog-write contract enumeratessession_refs[],commit_refs[],cross_doc_links[]; does NOT mentionsurfaced. - C-INV-4.
.claude/skills/triage-finding/SKILL.mdhas no “Step 3 label-reversal” section; subsequent steps renumbered consistently. - C-INV-5.
.claude/agents/workflow-curator.mdhas no “Critical note on roadmap vs backlog labelling” region. - C-INV-6.
validate-agent.sh(or equivalent agent-body validator) passes on all three edited files (the existing CI guard for skill/agent body shape). - C-INV-7.
bun run testgreen — confirmsBARE_ID_REGEXrelocation doesn’t break any schema test.
§D Phase D — Label-reversal residue audit
Section titled “§D Phase D — Label-reversal residue audit”§D.1 Audit method
Section titled “§D.1 Audit method”Per RESEARCH §5.3 conclusion: label-reversal effectively shipped at S40
(d49e0b11). Phase D produces a read-only verdict + audit report
(docs/research/label-reversal-residue-audit.md).
Method (per RESEARCH §5.4 + Subtask 15.9 description):
- Roadmap walk — for each of the 61 items + 16 sections, classify against
the target-semantic criteria (
triage-findingskill Step 2 Branch B):- Strategic AND cross-cutting AND multi-month → CORRECT in Roadmap.
- Single-feature OR weeks-scope OR pre-work → POTENTIAL RESIDUE (should be in Backlog).
- Backlog walk — for each of the 45 (post-15.4: 17, 18, 23..65) items,
classify:
- Tactical OR single-feature OR weeks-scope OR pre-work → CORRECT in Backlog.
- Multi-month strategic cross-cutting → POTENTIAL RESIDUE (should be in Roadmap).
- For each POTENTIAL RESIDUE item, the auditor inspects more closely:
- Is the categorisation borderline or unambiguous?
- Does the item’s history support the current placement
(
session_refs[]/commit_refs[]from Phase B will be empty at audit time — that’s expected; audit relies ondescriptioncontent)? - Verdict: KEEP / MIGRATE / FLAG-AMBIGUOUS.
- Compile audit report with: counts examined, verdict summary, list of MIGRATE items (if any) + list of FLAG-AMBIGUOUS items (if any) + verdict sentence.
§D.2 Verdict template
Section titled “§D.2 Verdict template”# Label-Reversal Residue Audit — Phase D Verdict
**Status:** AUDIT COMPLETE. Subtask ID-15.9 deliverable.**Audited HEAD:** `<commit-sha-of-audit>`**Auditor:** Task Executor (15.9) — read-only audit.
## §1 Method
Per `docs/specs/id-15-legacy-id-migration/TECH.md` §D.1.
## §2 Results
| Surface | Items examined | Verdict CORRECT | Verdict MIGRATE | Verdict FLAG-AMBIGUOUS ||---|---|---|---|---|| Roadmap | 61 items + 16 sections | NN | NN | NN || Backlog | 43 items (17, 18, 23..65) | NN | NN | NN |
## §3 Migrations executed
(If MIGRATE count > 0, list each: source surface, source id, destinationsurface, destination id, rationale. If count == 0, "None — no migrationsneeded.")
## §4 Flagged-ambiguous items
(If FLAG-AMBIGUOUS count > 0, list each with one-sentence rationale + decisiondeferred. If count == 0, "None — no ambiguity surfaced.")
## §5 Verdict sentence (one of)
- **CLEAN.** No residue found. Label-reversal claim closed. All three alignment-encoding artefacts (cleaned in Phase C) accurately describe the live state.- **RESIDUE FOUND.** N items migrated via `update-roadmap-backlog` skill Delete + Create flow. Re-audit after migration. (Update task-list.json Subtask 15.9 details with migration journal.)- **AMBIGUOUS.** N items flagged for Liam ratification. Audit blocks; subtask status stays `in_progress` until ratification.§D.3 If residue is found — migration via update-roadmap-backlog
Section titled “§D.3 If residue is found — migration via update-roadmap-backlog”Per OQ-5 default (Option (a) ratified): no residue expected. If the auditor
encounters MIGRATE items, the migration runs through the existing
update-roadmap-backlog skill CRUD (Delete from source surface + Create on
destination surface). NO new tooling. The skill body — post-Phase C edit —
correctly routes per target semantics.
For each migration:
- Auditor opens the skill (
/update-roadmap-backlog) and supplies--operation=move --source-id=<src> --dest-target=<roadmap|backlog> --rationale="<one-liner>". - The skill validates source/dest schemas accept the payload; performs the Delete + Create as two writes; commits each separately.
- Audit report §3 records each commit SHA.
§D.4 Verification — Phase D acceptance
Section titled “§D.4 Verification — Phase D acceptance”- D-INV-1.
docs/research/label-reversal-residue-audit.mdexists with all five sections (§1 Method / §2 Results / §3 Migrations / §4 Flagged / §5 Verdict). - D-INV-2. Verdict sentence is exactly one of CLEAN / RESIDUE FOUND / AMBIGUOUS.
- D-INV-3. If RESIDUE FOUND: every item listed in §3 has a corresponding
commit on the production-readiness branch;
update-roadmap-backlogskill journal entries exist; bothBacklogSchema.parse()andRoadmapSchema.parse()pass on the post-migration files. - D-INV-4. If AMBIGUOUS: subtask 15.9 status stays
in_progresswith a journal block detailing the ambiguity; orchestrator escalates to Liam. - D-INV-5.
bun run test __tests__/docs/backlog-no-closed-rows.test.ts __tests__/docs/roadmap-roundtrip.test.tsgreen.
§3 Cross-phase risks and mitigations
Section titled “§3 Cross-phase risks and mitigations”| Risk | Phase | Likelihood | Impact | Mitigation |
|---|---|---|---|---|
| R1. Live-data drift makes mapping doc §3.2 absent-entry stale (AST-S3-O1/O2 case) | A | Confirmed | Low | §A.0 drift rule — skip absent ids verbosely; do not auto-compact. Implementer of 15.3 confirms drift at script-author time. |
R2. Cross-doc sweep over-matches (legacy id colliding with unrelated prose, e.g. OPS-43 matching inside 45) | A | Low | Medium | Word-boundary regex \b; per-match log line in dry-run mode; manual review of grep results before apply. Precedent: ID-15.1 mapping doc §4 + commit c813431a. |
| R3. Schema tighten lands before data migration completes → mid-run state fails validation | A | Medium if sequenced wrong | High | §A.2 implementation note — data first, schema second. The 15.3 script runs against the loose schema; the 15.4 apply commit lands the schema tighten as a SEPARATE step AFTER data write. |
R4. Cross-doc sweep accidentally rewrites legitimate historical references in docs/plans/ or docs/specs/ | A | Medium | Medium | Best-effort tier emits WARN per match; implementer reviews diff before apply. Never-rewrite tier protects .planning/.archive/**. |
R5. Backlog provenance backfill lossy — old surfaced strings drop without translation into structured fields | B | Confirmed (deliberate) | Low | OQ-4 ratification accepts this: legacy provenance lives in git history (Phase B commit message footnote); forward writes by workflow-curator populate the structured fields properly. |
R6. RoadmapPriority import-from-shared change breaks any test that imports RoadmapPriority directly | B | Low (zero behaviour change — identical value set) | Low | Re-run bun run test after the one-line schema change. Type alias export type RoadmapPriority = z.infer<typeof RoadmapPriority>; preserves type-name continuity. |
| R7. Tech-debt enum migration writes the wrong direction (kebab vs underscore) | B | Low | Medium | Direction decision ratified §B.2 — underscore canonical (matches schema, matches sibling enums). The 3 affected ids per live data at S57 are 26/26, 27/27, ID-18/18 — listed in §B.2 + B-INV-3. Implementer of 15.7 re-confirms at apply time. |
| R8. Stale-warning cleanup leaves dangling references elsewhere | C | Medium | Low | C-INV-1 grep gate catches dangles. Implementer of 15.8 widens grep to docs/specs/, docs/audits/ if any matches surface. |
R9. BARE_ID_REGEX relocation breaks downstream consumers | C | Low | Low | C-INV-2 grep + bun run test confirm. Two known consumers (task-list-schema.ts, backlog-schema.ts) are touched in-spec. |
| R10. Phase D audit surfaces ambiguous residue → blocks closure | D | Medium per OQ-5 default-lean | Low | D-INV-4 keeps subtask in_progress with ambiguity journal; orchestrator escalates. No silent residue accepted. |
| R11. Phased delivery — Phase A lands but B/C/D pause mid-stream; canonical-pipeline migration starts on partial schema state | A→B/C/D | Medium | Medium | Phase A is the unblock for canonical-pipeline. B/C/D close out at their own pace; no Phase B/C/D dependency on canonical-pipeline migration’s progress. Phase A schema (BacklogItem.id bare-digit, BARE_ID_REGEX shared) is sufficient for canonical-pipeline to begin. |
| R12. Mapping doc §3.2 stale-data annotation conflicts with script-author’s read | A | Low | Low | §A.0 final paragraph — the implementer of 15.3 updates the mapping doc with the live-snapshot annotation as part of the same commit. |
§4 Phased delivery and Subtask dispatch order
Section titled “§4 Phased delivery and Subtask dispatch order”Per OQ-1 (Phased) + the current Subtask shape in
docs/reference/task-list.json Task ID-15:
| Subtask | Phase | Title | Depends on | Effort estimate |
|---|---|---|---|---|
| 15.1 | RESEARCH | Legacy ID inventory + mapping | (done — S56) | — |
| 15.6 | RESEARCH | Cross-surface alignment | (done — S56) | — |
| 15.2 | TECH | This unified TECH spec | 15.1, 15.6 | ~3h (this dispatch) |
| 15.3 | A | Author migration script | 15.2, 15.6 | ~2h |
| 15.4 | A | Apply migration + cross-doc sweep + schema tighten | 15.3 | ~2h |
| 15.5 | A | Delete migration script | 15.4 | ~0.5h |
| 15.7 | B | Schema fixes (drop surfaced + add provenance + enum drift + RoadmapPriority) | 15.4, 15.6 | ~3h |
| 15.8 | C | Stale-warning cleanup + BARE_ID_REGEX relocation + write-contract updates | 15.7 | ~3h |
| 15.9 | D | Label-reversal residue audit | 15.8 | ~1h |
Sibling-only dependency constraint honoured — every dependencies[] entry on
Subtasks 15.3..15.9 references another Subtask of Task 15 (not of any other
Task). Per §3.4 / A7: 9 Subtasks ≪ 25-soft-ceiling. Acceptable.
Phase boundaries provide ratification gates:
- Phase A close (15.5 done) — canonical-pipeline migration unblocks IMMEDIATELY. Phase B/C/D close-out can pause if next-session work demands attention.
- Phase B close (15.7 done) — backlog schema fully aligned with Task list + Roadmap conventions.
- Phase C close (15.8 done) — three artefacts truthful about live state; shared constant relocated.
- Phase D close (15.9 done) — alignment work fully closed-out.
§5 Open questions surfaced during TECH authoring
Section titled “§5 Open questions surfaced during TECH authoring”Per the dispatch brief: ≤ 3 new OQs (5 from 15.6 already ratified). Three surfaced; none requires Liam ratification before Phase A dispatch — the spec proposes defaults and notes the open-question status for the implementer’s awareness.
OQ-A. Mapping doc compaction on AST-S3 drop
Section titled “OQ-A. Mapping doc compaction on AST-S3 drop”Two slots in Cluster 1 vacated (21, 22). §A.0 proposes the simpler
alternative: do NOT compact — leave the vacated slots as gaps in the C1 head.
This means live-data backlog after migration occupies 17, 18, 23..65 (not
17, 18, 21..65).
Default proposal: non-compaction. Rationale: the gap is invisible after
the migration (no one reads id=21 and notices it’s missing); compaction
would require renumbering the rest of Cluster 1 (preserving semantic continuity
across S10/S11 outputs is mapping doc §3.2 intent). Compacting breaks that
intent.
Implementer of 15.3 may flip if Liam disagrees — neither path is hostile to subsequent phases. The mapping doc annotation per §A.0 captures whichever choice lands.
OQ-B. Backlog provenance backfill for legacy items — script-assisted or
Section titled “OQ-B. Backlog provenance backfill for legacy items — script-assisted or”manual?
Phase B §B.1 proposes empty arrays for all 45 items at the schema-change commit; workflow-curator populates structured fields on new writes going forward. Legacy items’ historical provenance lives in git history (commit message footnote).
Alternative: Script-assisted backfill — parse the old surfaced strings
into cross_doc_links[] entries where the string contains a recognisable doc
path. Costlier (regex authoring + per-item review) and lossy for free-form
surfaced strings.
Default proposal: empty arrays. The OQ-4 ratification language (“structured provenance written at backlog creation by workflow-curator → direct copy on promotion to Task”) implies no retroactive backfill is required.
OQ-C. Phase C edit to triage-finding “Examples” section
Section titled “OQ-C. Phase C edit to triage-finding “Examples” section”§C.1.b notes that the “Examples” section at end of triage-finding/SKILL.md
MUST be re-checked for residual label-reversal-flag references. The Planner
did not exhaustively grep that section while authoring this spec.
Default proposal: Implementer of 15.8 grep + clean as part of the
§C.1.b edit. If extensive rewriting needed (more than a paragraph), the
implementer escalates and the Subtask status stays in_progress until Liam
ratifies the rewrite shape.
§6 Verification gate summary (cross-phase)
Section titled “§6 Verification gate summary (cross-phase)”Listed by Subtask for the Checker’s acceptance pass:
| Subtask | Acceptance invariants |
|---|---|
| 15.3 (Phase A script) | A-INV-1, A-INV-2 (dry-run output) |
| 15.4 (Phase A apply) | A-INV-3, A-INV-4, A-INV-5, A-INV-6, A-INV-7, A-INV-8, A-INV-9, A-INV-10 |
| 15.5 (Phase A delete) | Script file scripts/migrate-legacy-ids-to-id-n.ts removed; no other artefact disturbed |
| 15.7 (Phase B) | B-INV-1, B-INV-2, B-INV-3, B-INV-4, B-INV-5, B-INV-6 |
| 15.8 (Phase C) | C-INV-1, C-INV-2, C-INV-3, C-INV-4, C-INV-5, C-INV-6, C-INV-7 |
| 15.9 (Phase D) | D-INV-1, D-INV-2, D-INV-3, D-INV-4, D-INV-5 |
bun run test MUST be green at the close of each Subtask; bun lint MUST be
green where source files are touched (lib/, scripts/).
End of TECH spec.