Roadmap-Backlog Consolidation + Roadmap Rethink (Linear-style) — TECH.md
Roadmap-Backlog Consolidation + Roadmap Rethink (Linear-style) — TECH.md
Section titled “Roadmap-Backlog Consolidation + Roadmap Rethink (Linear-style) — TECH.md”Spec slug: roadmap-backlog-consolidation
Version: v1.0 (draft for ratification at S64)
Authored by: task-planner (fresh instance per Q-PLANNER-2 — independent review pass against PRODUCT.md) under write-tech-spec for Subtask ID-30.3.
Status: PENDING-RATIFICATION (Liam to ratify; Checker promotes 30.3 → done).
Linked PRODUCT: docs/specs/id-30-roadmap-backlog-consolidation/PRODUCT.md (15 invariants across 5 clusters, ratified at S63 W1).
Last verified: 22/05/2026 against production-readiness HEAD e8213bdb.
§0 How to read this spec
Section titled “§0 How to read this spec”The TECH spec mirrors the PRODUCT cluster shape (A → consolidated Backlog, B → Shape A Roadmap, C → task-view 20.14 extension, D → curator skill rewrites + migration sequencing, E → cross-cutting hygiene). Each PRODUCT invariant gets a 1-to-1 row in §2 Proposed changes; every cluster gets its own §-block in §4 Skill rewrites / §5 Per-Subtask file ownership / §6 Testing.
Per write-tech-spec: PRODUCT is the source of truth for behaviour; this doc translates behaviour into implementation. Where PRODUCT says “the curator skill maintains discipline”, TECH names the lines of SKILL.md to rewrite. Where PRODUCT says “the schema rejects malformed entries”, TECH names the Zod refinement to add.
§1 Current state
Section titled “§1 Current state”§1.1 Schema surface (the canonical Zod modules)
Section titled “§1.1 Schema surface (the canonical Zod modules)”lib/validation/roadmap-schema.ts (260 lines) — .strict() Zod, RoadmapSchema root with sections: RoadmapSectionSchema[], each section carrying items: RoadmapItemSchema[]. Status enum (line 50-57) pending | blocked | spec_needed | in_progress | deferred | imp_deferred. The RoadmapItemSchema carries the full Phase-B provenance triple (session_refs, commit_refs, cross_doc_links). forward_looking_only: z.literal(true) (line 249) is a strict-doctrine guard.
lib/validation/backlog-schema.ts (181 lines) — BacklogSchema root with flat items: BacklogItemSchema[], .superRefine() enforces id-uniqueness (line 163-178). BacklogItemSchema has 13 fields today plus optional details + testStrategy (lines 131-137 — added at S50 Wave A.1 per PRODUCT inv 38 for promotion-compatibility). Status drawn from BacklogStatus via WorkStatus.exclude(...) (5 values: spec_needed | needs_research | parked | ready | blocked).
lib/validation/task-list-schema.ts (255 lines) — TaskListSchema root, TaskSchema .strict() .superRefine(...) for sibling-only dep enforcement (line 132-151), 7 KH-extension fields (lines 116-130: effort_estimate, owner, priority_note, status_note, cross_doc_links, session_refs, commit_refs). TaskListStatus 8-value enum. The last_updated field has 4 refinements (max 200 chars, session-id prefix, single-line, exactly-one-session-id — lines 178-196).
lib/validation/work-status.ts (103 lines) — WorkStatus master enum, per-surface subsets via .exclude(...). Priority master enum (line 93-102, 8 values: must | should | could | future | high | medium | low | trigger).
lib/validation/schemas.ts line 50 — export const BARE_ID_REGEX = /^\d+$/; (the canonical bare-digit-string id pattern; consumed by both BacklogSchema and TaskSchema).
§1.2 Live content (counts as of 22/05/2026)
Section titled “§1.2 Live content (counts as of 22/05/2026)”docs/reference/product-roadmap.json — 16 sections, 61 items (matches PRODUCT inv 6 numbers). Section IDs 1 through 12.x plus narrative-only sections. Two .strict() round-trip guards: __tests__/docs/roadmap-roundtrip.test.ts (MD ↔ JSON) and schema-parse on every bun run roadmap:render.
docs/reference/product-backlog.json — 54 items in items[] (PRODUCT.md says 55; live count is 54 — surfaced as T-OQ-1 below). Tracks observed: ai-integration, ast-dataflow-tooling, authentication, bid-management, browse, canonical-pipeline-followup, change-reporting, cloud-run, content-quality, database, documentation, ingestion, mobile, onboarding, production-readiness, qa-pairs, search-quality, testing (18 distinct tracks). Round-trip guard at __tests__/docs/backlog-schema-roundtrip.test.ts.
docs/reference/task-list.json — Task ID-30 in_progress with 4 subtasks (30.1 done, 30.2 in_progress, 30.3 pending, 30.4 pending). Task ID-20 in_progress with Subtasks 1-13 (no 20.14 yet — must be added at 30.4 PLAN authoring time).
§1.3 Curator skill surface
Section titled “§1.3 Curator skill surface”.claude/skills/triage-finding/SKILL.md (306 lines) — Branch A binary in-scope rule (file-path predicate OR axis predicate). Branches B/C/D for OUT-OF-SCOPE findings. Branch B = strategic/cross-cutting → roadmap; Branch C = tactical/single-feature → backlog. Step 1 grep-existing-coverage gate; Step 3 structured-decision output. Examples 1-4 illustrate the decision tree.
.claude/skills/update-roadmap-backlog/SKILL.md (449 lines) — Four operation modes: Create (default), Update, Delete, Promote. Step 1-7 Create flow; Update / Delete / Promote each have their own flow + inputs. last_updated field-discipline rule (lines 51-95) enforces ≤200 chars + single-session-id format.
§1.4 Render pipeline
Section titled “§1.4 Render pipeline”scripts/roadmap-from-json.ts — read RoadmapSchema → render MD. Triggered by bun run roadmap:render (per package.json). The MD ↔ JSON round-trip test runs this on every CI run.
No equivalent render pipeline for the backlog — docs/reference/product-backlog.json is read directly by consumers (per update-roadmap-backlog Step 6).
No structured-patch API endpoint exists yet — per-task-mirror (Task ID-20) PRODUCT inv 29 / 33 / 36 specifies the contract but Task 20’s implementation Subtasks have not yet landed the API surface. The rank-edit + bulk-reorder UX (PRODUCT inv 10) extends but does not invent the patch path — it presumes Task 20 has shipped (or ships in parallel within PR-A) the structured-patch convention. This dependency is recorded as T-OQ-2.
§2 Proposed changes — 1-to-1 against PRODUCT invariants
Section titled “§2 Proposed changes — 1-to-1 against PRODUCT invariants”Each row maps one PRODUCT invariant to a concrete code change. Reference the source under “Mechanism” — line numbers are the post-change targets. “Test verification” cites the test path that proves the invariant.
Cluster A — Consolidated Backlog (invariants 1-5)
Section titled “Cluster A — Consolidated Backlog (invariants 1-5)”| Inv | Proposed change | Mechanism | Test verification |
|---|---|---|---|
| 1 | Backlog grows from current 54 (live count; PRODUCT says 55 — see T-OQ-1) to 108-109 items via PR-B bulk migration. No new field on BacklogSchema for this invariant alone; the growth is content-only. | PR-B migration script scripts/migrate-backlog-from-roadmap.ts (new file) reads RESEARCH §3.2 JSON snippet (orchestrator-direct-splice-ready, IDs 78-131) and appends 54 net-new entries to docs/reference/product-backlog.json items[]. Script is idempotent (skips ids already present). | __tests__/scripts/migrate-backlog-from-roadmap.test.ts (new): assert (a) item count delta = +54 on first run, +0 on second run (idempotency); (b) all 54 new ids 78-131 present; (c) BacklogSchema.parse() passes against the resulting JSON; (d) the existing backlog-schema-roundtrip.test.ts continues to pass. |
| 2 | Each migrated entry carries the full Phase-B schema shape including notes with the canonical "Migrated from roadmap §X.Y. ..." marker for queryability. No schema change required — BacklogItemSchema already has every required field. The 6 PARTIAL_OVERLAP clusters (RESEARCH §5.4) get an additional notes line documenting the thematic adjacency. | The RESEARCH §3.2 JSON snippet already encodes the migration shape per Phase-B (verified by spot-check during this TECH authoring). Migration script copies the snippet verbatim into items[]; PARTIAL_OVERLAP notes additions are coded inline in the script for each of the 4 PARTIAL clusters identified in PRODUCT inv 5 (ingest-UI: ID-61+62+79+100, doc-triage: ID-64+82, prompt-suggestion: ID-49+91, portal-automation: ID-53+101). | Migration test asserts every new entry’s notes field contains the "Migrated from roadmap §" substring. Separate test asserts the 4 PARTIAL_OVERLAP cluster id-lists are all present in the post-migration backlog and that each cluster member has a notes line citing every other member of its cluster. |
| 3 | Add rank: z.number().int().nullable().optional() to BacklogItemSchema (lib/validation/backlog-schema.ts line 122, between notes and details). No .superRefine() constraint on uniqueness or contiguity (PRODUCT says discipline lives in curator skill, not schema). | One-line schema addition; export type re-derives via z.infer<typeof BacklogItemSchema>. Pre-migration items keep no rank (field is .optional() — absent is valid); post-migration items may set it explicitly. Migration sequencing: the schema add lands in PR-A BEFORE PR-B writes any new items, so PR-B can choose to set rank on any new entry without a schema failure. | Extend __tests__/lib/validation/ with a new backlog-schema-rank-field.test.ts: (a) item with rank: null parses; (b) item with rank: 10 parses; (c) item with rank: -5 parses (no schema-level positive constraint — discipline only); (d) item with rank: "10" fails (non-integer); (e) rank: 1.5 fails (non-integer); (f) item omitting rank parses (field is optional). Existing backlog-schema-roundtrip.test.ts continues to pass against the live (rank-less) content. |
| 4 | Sort algorithm: priority-then-rank-then-id, null rank last within tier. Implement as a pure helper function in lib/backlog/sort.ts (new file): export function sortBacklogItems(items: BacklogItem[]): BacklogItem[]. Pure, deterministic, no side effects. Consumed by all surfaces (per-task-mirror Backlog index page, future MD renderer if added, curator skill listings). | The function uses Priority enum ordinal mapping: must=0, should=1, could=2, future=3, high=4, medium=5, low=6, trigger=7 (matches the canonical lib/validation/work-status.ts Priority enum order). Within priority, sort by rank ?? Number.MAX_SAFE_INTEGER, then by id (lexicographic on bare-digit strings — but pad-numeric-compare via parseInt so "10" sorts after "9"). | __tests__/lib/backlog/sort.test.ts (new): 8-case fixture asserting sort stability across priority changes, rank null vs set, id collision tiebreaker. Test philosophy: pure behaviour-verifying; no Supabase fixture; no React render. |
| 5 | Migration verification — no net-new entry duplicates an in-flight Task. Implement as a build-time check in the migration script: before commit, the script reads docs/reference/task-list.json and asserts no new backlog description substring-matches any active Task title (cosine-similarity is overkill; substring match per RESEARCH §5.2 method is sufficient for the 54 new entries). | Migration script Step N: load TaskListSchema.parse(); iterate new backlog entries; for each, lowercase + tokenise both new description and every active Task title; assert no 4-word-window overlap. PARTIAL_OVERLAPs from inv 2 are pre-filtered (they’re acknowledged thematic adjacency, not duplicates). | Migration test asserts on a fixture with a fake “duplicate” entry the script aborts with a clear error; on the real RESEARCH §3.2 payload the script succeeds. |
Cluster B — Shape A Roadmap (invariants 6-9)
Section titled “Cluster B — Shape A Roadmap (invariants 6-9)”| Inv | Proposed change | Mechanism | Test verification |
|---|---|---|---|
| 6 | Reshape RoadmapSchema to drop sections: RoadmapSectionSchema[] and add themes: RoadmapThemeSchema[] (flat array, per P-OQ-2 default — see §3 below). Delete RoadmapSectionSchema from lib/validation/roadmap-schema.ts (lines 186-226). Delete RoadmapItemSchema (lines 106-180) — no roadmap-item shape exists under Shape A; the items either migrated to backlog (PR-B) or got absorbed into a theme as linked_tasks / linked_backlog. The RoadmapSchema keeps document_name, document_purpose, date, status, forward_looking_only, related_documents, last_updated, plus the new themes[]. | This is a breaking schema change — every RoadmapSchema consumer (the MD renderer at scripts/roadmap-from-json.ts; the round-trip test) must update concurrently in PR-C. Sequence: PR-A adds the new RoadmapThemeSchema alongside the existing RoadmapSectionSchema (additive — both shapes valid in PR-A; root accepts either sections[] OR themes[] via z.union(...)). PR-C migrates content and drops sections[] from the schema. The renderer is updated in PR-C to emit theme-heading-per-theme MD. | __tests__/lib/validation/roadmap-schema-shape-a.test.ts (new): (a) document with themes[] only parses; (b) document with sections[] only parses (transitional in PR-A); (c) document with both fails with a clear union-discriminator error (PR-A); (d) document with neither fails (PR-A). In PR-C: (a) document with themes[] parses; (b) document with sections[] fails (no fallback after PR-C ships). Round-trip test updated in PR-C to render the new theme shape. |
| 7 | Add RoadmapThemeSchema to lib/validation/roadmap-schema.ts with these fields: id: z.string().regex(BARE_ID_REGEX), title: z.string().min(1), description: z.string().min(1), time_horizon: z.enum(['now', 'next', 'later']), status: z.enum([...P-OQ-1 values...]), linked_tasks: z.array(z.string()), linked_backlog: z.array(z.string()), session_refs: z.array(z.string()), commit_refs: z.array(z.string()), cross_doc_links: z.array(DocLinkSchema), notes: z.string().nullable(). .strict(). | Mirrors BacklogItemSchema shape conventions (Phase-B provenance triple, DocLinkSchema array, .strict() for additive-field hygiene). linked_tasks[] + linked_backlog[] are forward-link arrays; no .superRefine() on referential integrity (broken links render with the same missing-target marker per per-task-mirror PRODUCT inv 12 — handled at render time, not schema time, to avoid hard-failing the schema on a stale link). Theme status enum is per P-OQ-1 default (3 values: `pending | in_progress |
| 8 | Theme soft cap of 12 is NOT enforced in RoadmapSchema.superRefine. Implementation: extend the existing parseTaskListWithWarnings pattern (lib/validation/task-list-schema.ts line 235-254) to roadmap with parseRoadmapWithWarnings(input): { value: Roadmap; warnings: RoadmapWarning[] }. A theme count > 12 emits one warning; consumers (the rewritten update-roadmap-backlog skill, future Astro docs page) choose whether to surface it. | The warning shape mirrors TaskListWarning so future curator UIs can render warnings consistently. No CI-blocking; pure planning signal. The rewritten update-roadmap-backlog Create-mode (see §4 below) surfaces the warning in its YAML report when crossing the threshold. | __tests__/lib/validation/roadmap-schema-soft-cap.test.ts (new): (a) 12 themes — no warning; (b) 13 themes — one warning entry naming the count; (c) 30 themes — still one warning (per-document, not per-excess-theme); (d) hard schema parse succeeds for all three (the warning is non-blocking). |
| 9 | Add capability_theme: z.string().nullable().optional() to TaskSchema (lib/validation/task-list-schema.ts line 130, after the array-fields block, before the .strict()). Optional + nullable per OQ-6 ratification — the field is a convenience back-link, not the authoritative source-of-truth (which is the theme’s forward linked_tasks[] per inv 7). | One-line schema addition. The existing TaskSchema.superRefine() (sibling-only dep enforcement, lines 132-151) does not validate capability_theme against the live Roadmap themes — same render-time stale-link tolerance as inv 7. Migration sequencing: the schema add lands in PR-A (additive; absent field is valid; existing tasks unaffected). PR-C backfills capability_theme on Tasks where the theme→task mapping is clear (this is curator-skill work, not a one-shot script — see §4 below). | __tests__/lib/validation/task-list-schema-capability-theme.test.ts (new): (a) task with capability_theme: null parses; (b) task with capability_theme: "1" parses; (c) task without capability_theme parses (optional); (d) task with capability_theme: 123 fails (must be string-or-null). Existing task-list.json continues to parse cleanly post-add. |
Cluster C — Task-view (ID-20.14) extension (invariants 10-11)
Section titled “Cluster C — Task-view (ID-20.14) extension (invariants 10-11)”| Inv | Proposed change | Mechanism | Test verification |
|---|---|---|---|
| 10 | Subtask 20.14 (NEW — to be created by 30.4 PLAN authoring) extends the per-task-mirror Backlog index page (built by Task 20’s existing Subtasks per PRODUCT.md per-task-mirror inv 20-25) with: (a) rank integer-input field per item, edit-in-place via the same affordance pattern as per-task-mirror inv 30 (structured-enum dropdown), (b) “(unset)” option to clear rank to null, (c) drag-and-drop bulk-reorder UX within a priority tier, keyboard-operable (arrow keys + Enter). Sort order on the index becomes priority → rank (nulls last) → id per inv 4. | UI implementation lives in components/task-mirror/backlog-index-page.tsx (new file under per-task-mirror surface — exact path/naming aligned with Task 20’s existing component conventions, to be confirmed when 30.4 PLAN authors and Task 20 implementation matures). Drag handle uses @dnd-kit/core if available (check package.json) or react-dnd — TECH defers UX library choice to implementation Subtask 20.14, only constrains: keyboard-operable per WCAG 2.1 AA, semantic tokens per Warm Meridian, atomic patch transaction (one multi-field patch per drag-drop, with server-side rollback on validation failure). Integer-input uses <input type="number"> with min undefined (negative ranks valid per inv 3); save via structured-patch path per per-task-mirror inv 33. | E2E spec e2e/tests/task-mirror-backlog-rank.spec.ts (new): (a) navigate to Backlog index; (b) click rank affordance on item X; (c) type 5; (d) save; (e) assert item X moves above item Y (which has higher rank) within the same priority tier. Drag test: (a) drag item A above item B; (b) save; (c) assert both ranks updated atomically; (d) refresh page; (e) assert new order persists. Accessibility test using axe-core per playwright-best-practices skill: keyboard-navigate to the drag handle, focus visible, arrow-keys move, Enter commits. |
| 11 | No Promote-to-task-list affordance in v1. No code change required — the per-task-mirror Backlog index page does not surface a Promote button. The existing “Promotion-ready” badge (per-task-mirror inv 24) remains. Re-evaluation deferred to v1.1 per OQ-5. | Documented as a non-change in TECH so the absence is auditable. The rewritten update-roadmap-backlog Promote mode (see §4 below) remains the canonical path. | Negative-assertion test in the E2E spec from inv 10: assert no element with text “Promote” / data-testid="promote-button" exists on the Backlog index or per-item page. |
Cluster D — Curator skill rewrites + migration sequencing (invariants 12-13)
Section titled “Cluster D — Curator skill rewrites + migration sequencing (invariants 12-13)”| Inv | Proposed change | Mechanism | Test verification |
|---|---|---|---|
| 12 | 3-PR sequencing: PR-A (schema + skills + 20.14 UI), PR-B (bulk migration content), PR-C (Roadmap reshape to Shape A + capability_theme back-fill). Each PR has an explicit acceptance gate per PRODUCT inv 12. Implementation contract: each PR opens against production-readiness; PR-B blocked by PR-A merge (gh pr view PR-A --json mergedAt); PR-C blocked by PR-B merge. | No code mechanism — process discipline. The 30.4 PLAN.md decomposition organises implementation Subtasks into 3 sequential waves; the Orchestrator gates wave-N+1 dispatch on wave-N merge (per workflow-orchestration §wave-sequencing). The PRs themselves are conventional GitHub PRs against production-readiness per CLAUDE.md track convention. | Per-PR CI gate: each PR’s acceptance gate from PRODUCT inv 12 is a test in __tests__/integration/. PR-A gate: schema tests pass on live (rank-less, section-shape) JSON. PR-B gate: backlog item count = 108-109 (T-OQ-1); roadmap item count = 53. PR-C gate: theme count ≤ 12; every theme has ≥ 1 linked Task or Backlog id; every theme has a time_horizon. The gates run on every PR push so a regression on a later PR’s prerequisites surfaces immediately. |
| 13 | Curator skill rewrites land in PR-A as a single standalone Subtask (per OQ-8 ALT — load-bearing, not folded into the schema-extension Subtask). The Subtask dispatches a sub-agent invoking /update-skill against both .claude/skills/triage-finding/SKILL.md and .claude/skills/update-roadmap-backlog/SKILL.md. See §4 for the surgery details. | Sub-agent invocation pattern matches the existing /update-skill skill convention (already loaded in .claude/skills/update-skill/). Sub-agent reads PRODUCT inv 13 (a-e) directly and produces the diffs. Output: two updated SKILL.md files. | Smoke test invokes Create against Backlog under the rewritten skill (mock payload simulating Create-mode Branch C decision from rewritten triage-finding) and asserts: (a) no schema error; (b) Phase-B provenance round-trips; (c) the rank field is settable on Create per inv 13 (c). Test path: __tests__/skills/curator-rewrites-smoke.test.ts (new) — note this is a node-level invocation of the skill’s documented contract, not the literal Skill tool (skill execution happens in the orchestrator’s wave loop). |
Cluster E — Cross-cutting hygiene (invariants 14-15)
Section titled “Cluster E — Cross-cutting hygiene (invariants 14-15)”| Inv | Proposed change | Mechanism | Test verification |
|---|---|---|---|
| 14 | UK English + Warm Meridian discipline applies throughout all authored prose + UI. No new schema; no new mechanism. The existing project-wide conventions (CLAUDE.md “UK English throughout”; docs/design/warm-meridian-implementation-spec.md) apply. The “(unset)” rank option uses the same visual treatment as per-task-mirror inv 30’s “(unset)” Roadmap status / priority options. | Convention adherence is verified at code-review time (Checker), not by automated tests. The existing pre-commit hooks (bun run format, bun run lint) and design-system review (.claude/checks/ directory) cover the UI surface. | E2E spec from inv 10 includes a Warm Meridian semantic-token smoke check: assert no raw Tailwind colour classes appear in the rank-edit affordance markup (regex against the rendered DOM via Playwright). UK English in PRODUCT/TECH/PLAN/skill prose: spot-checked at PR review, no test. |
| 15 | All migration writes pass through validated schemas; no silent failures. The migration script uses standard Node fs.writeFileSync for the local JSON edits (no Supabase) — but every write is preceded by BacklogSchema.parse() against the new state. On parse failure, the script aborts BEFORE writing and emits the Zod error. Idempotency: re-running the migration produces the same final JSON (script detects already-migrated ids by checking items[].id against the IDs 78-131 set). | Pattern: read-merge-validate-write. The script reads the current backlog, merges in the new entries (deduplicating by id), validates the merged state via BacklogSchema.parse(), and only writes on parse success. The supabase-failure-prevention guards (sb(), tryQuery()) do not apply (no Supabase calls) but the silent-failure spirit is honoured by Zod-fail-loud-not-quiet. | __tests__/scripts/migrate-backlog-from-roadmap.test.ts (already cited under inv 1) extends with: (a) run migration twice; assert identical final JSON state; (b) run migration on a fixture with one id deliberately collision-pre-populated; assert the script reports the collision and does NOT write; (c) corrupt one new entry’s track field to an empty string in the fixture; assert Zod parse failure aborts the write. |
§3 Schema migrations
Section titled “§3 Schema migrations”§3.1 PR-A schema additions
Section titled “§3.1 PR-A schema additions”Three Zod modules touched in PR-A:
lib/validation/backlog-schema.ts (1 line addition):
// At line ~122 inside BacklogItemSchema, between `notes` and `details`:/** * Within-priority deterministic ordering. Lower integer = higher rank within tier. * Default at insert is null; pre-existing items omit the field. Schema does NOT * enforce uniqueness or contiguity within tier (per PRODUCT inv 3). Curator skill * maintains discipline (per PRODUCT inv 13 c and P-OQ-3 default — auto-shift on collision). */rank: z.number().int().nullable().optional(),No new exports; type re-derives via z.infer<typeof BacklogItemSchema>.
lib/validation/roadmap-schema.ts (additive in PR-A; breaking reshape in PR-C):
PR-A: introduce RoadmapThemeSchema (new export) and extend RoadmapSchema to accept either sections[] OR themes[] via z.union(...) at the root. Both paths valid in PR-A. The existing RoadmapSectionSchema + RoadmapItemSchema exports remain.
// New export:export const RoadmapThemeSchema = z .object({ id: z.string().regex(BARE_ID_REGEX, 'Theme id must be a bare digit string'), title: z.string().min(1), description: z.string().min(1), time_horizon: z.enum(['now', 'next', 'later']), status: z.enum(['pending', 'in_progress', 'done']), // P-OQ-1 default linked_tasks: z.array(z.string()), linked_backlog: z.array(z.string()), session_refs: z.array(z.string()), commit_refs: z.array(z.string()), cross_doc_links: z.array(DocLinkSchema), notes: z.string().nullable(), }) .strict();export type RoadmapTheme = z.infer<typeof RoadmapThemeSchema>;
// RoadmapSchema reshape (PR-A: union of legacy + new):export const RoadmapSchema = z .object({ document_name: z.literal('Knowledge Hub Roadmap'), document_purpose: z.string().min(1), date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), status: z.literal('Active'), forward_looking_only: z.literal(true), related_documents: z.array(z.string()), last_updated: z.string().min(1), // PR-A union — PR-C drops `sections`: sections: z.array(RoadmapSectionSchema).optional(), themes: z.array(RoadmapThemeSchema).optional(), }) .strict() .superRefine((doc, ctx) => { const hasSections = doc.sections !== undefined; const hasThemes = doc.themes !== undefined; if (hasSections === hasThemes) { ctx.addIssue({ code: 'custom', path: ['sections', 'themes'], message: 'Exactly one of `sections` or `themes` must be present (transitional in PR-A; `themes` only in PR-C).', }); } });PR-C migration: drop RoadmapSectionSchema, RoadmapItemSchema, and the sections field from RoadmapSchema. The superRefine becomes superfluous (only themes valid). parseRoadmapWithWarnings helper (new — mirrors parseTaskListWithWarnings) emits a warning when themes.length > 12.
lib/validation/task-list-schema.ts (1 line addition):
// At line ~130, after the array-fields block (cross_doc_links / session_refs / commit_refs), before .strict():/** * Optional back-link to a Roadmap theme (per OQ-6 ratification). Authoritative * direction is theme.linked_tasks[]; capability_theme is a convenience back-link * the curator skill maintains in sync. Absent means unaffiliated. */capability_theme: z.string().nullable().optional(),§3.2 Migration ordering + idempotency
Section titled “§3.2 Migration ordering + idempotency”| Order | Action | Idempotent? | Validation gate |
|---|---|---|---|
| 1 (PR-A) | BacklogItemSchema.rank field added | Trivially — adding an optional field is a no-op against existing content | bun run test __tests__/lib/validation/ passes; backlog-schema-roundtrip.test.ts continues to pass |
| 2 (PR-A) | TaskSchema.capability_theme field added | Same — optional field | bun run test __tests__/lib/validation/task-list-schema*.test.ts passes |
| 3 (PR-A) | RoadmapThemeSchema introduced; RoadmapSchema accepts union | Yes — existing roadmap content (sections-shape) continues to validate via the union branch | Roadmap round-trip test passes; new schema tests for both paths |
| 4 (PR-A) | Curator skill rewrites land (via /update-skill sub-agent) | Skill rewrites are file edits — idempotent in the sense that re-running the rewrite produces the same edited file | Smoke test invokes Create against both ledgers under the new field set |
| 5 (PR-A) | per-task-mirror Subtask 20.14 lands (rank-edit + drag-reorder UX) | Code change — idempotent build artefact | E2E spec from inv 10 |
| 6 (PR-B) | Migration script scripts/migrate-backlog-from-roadmap.ts runs once | Required per PRODUCT inv 15 — script checks if any of IDs 78-131 already present; if all present, no-op exit code 0 | Migration test asserts second run produces identical JSON |
| 7 (PR-B) | 8 REMOVE_REDUNDANT roadmap items deleted | Same script handles both adds and deletes; deletes are conditional on item.id presence | Roadmap item count drops from 61 to 53; round-trip test passes |
| 8 (PR-C) | Curator-authored content reshape — author 10-12 themes; populate linked_tasks / linked_backlog; set capability_theme back-links on Tasks where clear | Hand-authored content; not idempotent at the curator level, but each individual edit is idempotent (overwriting a theme with identical content = no change) | PR-C acceptance gate: theme count ≤ 12; every theme has ≥ 1 link; every theme has time_horizon |
| 9 (PR-C) | RoadmapSchema reshape — drop sections + RoadmapSectionSchema + RoadmapItemSchema; remove union; require themes[] | One-way breaking change; pre-PR-C JSON content INVALID after PR-C ships | Full test suite must pass; bun run roadmap:render updated to emit theme-headings-per-theme MD |
No schema cycle. BacklogItemSchema depends on DocLinkSchema (re-exported from roadmap-schema.ts). RoadmapThemeSchema depends on DocLinkSchema (same module). TaskSchema depends on DocLinkSchema (same module). capability_theme on TaskSchema is a string ref (not a Zod schema ref) so no cycle. linked_tasks[] / linked_backlog[] on RoadmapThemeSchema are string[] refs (same — no cycle).
§3.3 Test additions (per-schema-touch)
Section titled “§3.3 Test additions (per-schema-touch)”| New test file | Verifies invariant(s) | Pattern |
|---|---|---|
__tests__/lib/validation/backlog-schema-rank-field.test.ts | inv 3 | Pure schema parse — BacklogItemSchema.parse(fixture); 6 cases |
__tests__/lib/validation/roadmap-schema-shape-a.test.ts | inv 6, 7 | PR-A union acceptance + PR-C rejection of legacy shape |
__tests__/lib/validation/roadmap-schema-soft-cap.test.ts | inv 8 | parseRoadmapWithWarnings() emits exactly one warning at >12 themes |
__tests__/lib/validation/task-list-schema-capability-theme.test.ts | inv 9 | Optional + nullable; 4 cases |
__tests__/lib/backlog/sort.test.ts | inv 4 | Pure helper — 8-case fixture covers all sort edges |
__tests__/scripts/migrate-backlog-from-roadmap.test.ts | inv 1, 2, 5, 15 | Migration idempotency + collision detection + Zod-fail-on-corrupt-fixture |
__tests__/skills/curator-rewrites-smoke.test.ts | inv 13 | Smoke invocation of rewritten skill contracts |
e2e/tests/task-mirror-backlog-rank.spec.ts | inv 10, 11, 14 | UX behaviour + Promote-button absent + Warm Meridian semantic-token regex |
§4 Skill rewrites (Inv-13 OQ-8-ALT)
Section titled “§4 Skill rewrites (Inv-13 OQ-8-ALT)”Per OQ-8 RATIFIED-ALT, the rewrites of triage-finding and update-roadmap-backlog are a standalone Subtask in PR-A, dispatched to a sub-agent invoking the /update-skill skill. This §4 outlines the surgery; the sub-agent reads PRODUCT inv 13 (a-e) directly and produces the file diffs.
§4.1 triage-finding rewrites
Section titled “§4.1 triage-finding rewrites”File: .claude/skills/triage-finding/SKILL.md (306 lines).
Section to rewrite: lines 110-134 (Branch B — “Is it strategic / cross-cutting?”) and lines 136-165 (Branch C — “Is it tactical, single-feature?”).
Branch B (semantic rewrite — “capability theme promotion”):
The current Branch B criteria (cross-cuts features / strategic / multi-month / new top-level capability / unblocks downstream features) read as a 5-condition OR-gate routing to the section structure of the current roadmap. Under Shape A, Branch B routes to the theme structure.
Rewrite the trigger: a finding reaches Branch B when it surfaces a new capability theme not already on the Roadmap — i.e. a multi-month-scoped capability area (e.g. “support multi-tenant deployments”, “ship sales-proposal as a sibling application”). The 5-condition list collapses to two: (1) the finding describes a capability not covered by any existing theme’s linked_tasks[] or linked_backlog[] chain; (2) the capability is multi-month or cross-cutting (single-feature/weeks-scope items stay in Branch C).
Rewrite the output: instead of roadmap_target_section, produce roadmap_proposed_theme with title, description, time_horizon (default later — the curator may revise), initial_linked_tasks[] (the finding’s source task id if relevant), initial_linked_backlog[] (empty by default).
Branch C (semantic rewrite — “active work item promotion”):
The current Branch C criteria (single-feature / weeks-or-smaller / not-blocking-active-Task / not-touching-current-Subtask) are largely correct under Shape A — but the output gets new fields. The rewrite adds priority AND rank to the output (PRODUCT inv 13 c). Rank default is null; the curator skill may set it explicitly if the finding’s evidence carries an obvious within-tier ordering signal.
Keep the existing 4 examples; add a fifth illustrating a Branch B routing under Shape A semantics (a finding that surfaces a new capability theme).
Branch A and Branch D semantics unchanged per PRODUCT inv 13 a-b.
Failure-modes section (lines 237-245): add a new bullet “Routing a tactical item to Branch B — it belongs on Backlog. Branch B = new capability theme only; a single-feature finding routes to Branch C even if it touches a theme’s linked_backlog area.”
Rewrite size: ≤200 words of new prose per Branch (B + C); examples follow the existing format. Total SKILL.md churn ~80 lines.
§4.2 update-roadmap-backlog rewrites
Section titled “§4.2 update-roadmap-backlog rewrites”File: .claude/skills/update-roadmap-backlog/SKILL.md (449 lines).
Sections to rewrite: Create flow (Steps 1-7, lines 99-258), Update mode (lines 261-308), Promote mode (lines 364-415).
Create mode — Roadmap target:
The Step 3 “Compose the new entry” section (lines 137-184) needs a complete rewrite for the Roadmap path because the schema shape changes. The new field set per PR-A schema (RoadmapThemeSchema):
| Field | How to populate |
|---|---|
id | Next free bare-digit id; current free starting at “11” once PR-C reshape completes |
title | Short capability name from the triage_payload’s roadmap_proposed_theme.title |
description | Multi-paragraph Markdown — why this capability matters; outcome shape |
time_horizon | From triage payload; default later |
status | Default pending (P-OQ-1 default) |
linked_tasks | From triage payload’s initial_linked_tasks[] |
linked_backlog | From triage payload’s initial_linked_backlog[] |
session_refs | [provenance.session_counter] at minimum |
commit_refs | [provenance.source_commit_sha] if available |
cross_doc_links | If the finding cites a spec, populate {path, anchor, raw} |
notes | Free text; default null |
The existing Roadmap Step 3 table (5 fields including section_id, phase_label, severity) gets removed wholesale — none of those exist under Shape A.
Create mode — Backlog target:
The Step 3 backlog field table (lines 167-184) gains a new row for rank:
| Field | How to populate |
|---|---|
rank | null by default. If the triage payload’s backlog_slot carries an explicit rank, use it. If the priority tier already has items ranked, the curator may choose to insert at next free integer (e.g. tier high has items ranked 1-5; new entry gets null or 6 at curator’s discretion). |
Update mode — rank editing:
Add rank to the allowed field_edits map (line 271). Validation: integer or null. Collision behaviour: per P-OQ-3 default — the curator skill auto-shifts existing items in the same priority tier (inserting at rank: N pushes all items in that tier with rank ≥ N to rank + 1). The auto-shift is implemented in the skill’s Update flow Step 4 (not in the schema). Pseudocode:
if field_edits.rank is set AND target tier has items with rank >= new_rank: for each item in tier with rank >= new_rank, increment item.rank by 1 apply field_edits.rank emit warning: "auto-shifted N items in tier T to accommodate insert at rank=K"Update mode — time_horizon editing for themes:
Add time_horizon to the allowed field_edits map for the roadmap path. Validation: must be one of now | next | later.
Promote mode — capability_theme copy-through:
Per PRODUCT inv 13 d + P-OQ-4 default: the Promote flow Step 2 (“Compose destination entry”) gains a new lookup. Pseudocode:
1. Read roadmap.json themes[].2. Find themes where source_backlog_id in linked_backlog[].3. If exactly one match: set destination Task.capability_theme = that theme's id.4. If zero matches: leave capability_theme unset.5. If two or more matches: leave capability_theme unset AND emit a warning in the Promote report's YAML: "warning: source_backlog_id={id} linked from {N} themes ({list}); capability_theme left unset for explicit curator decision".Failure-modes section (lines 432-440): add two new bullets:
- “Creating a 13th theme without first checking whether two existing themes should merge per inv 8 soft cap —
parseRoadmapWithWarnings()will emit a warning at write-time; the curator should consider the merge first.” - “Forgetting to set
rankon Create when the priority tier is otherwise empty — leaving it null is fine, but inserting a single ranked item without a numeric rank loses the within-tier ordering benefit.”
Rewrite size: Roadmap-path field table is a full replacement (~30 lines). Update + Promote additions are ~40 lines combined. Failure-modes additions ~15 lines. Total SKILL.md churn ~150 lines.
§4.3 Sub-agent dispatch contract
Section titled “§4.3 Sub-agent dispatch contract”The PR-A Subtask invoking /update-skill gets this brief:
SKILL REWRITE BRIEF — Subtask {30.X} (TBD by 30.4 PLAN)
Inputs:- docs/specs/id-30-roadmap-backlog-consolidation/PRODUCT.md inv 13 (a-e) verbatim.- docs/specs/id-30-roadmap-backlog-consolidation/TECH.md §4.1 + §4.2 verbatim.- Current SKILL.md files at .claude/skills/triage-finding/SKILL.md and .claude/skills/update-roadmap-backlog/SKILL.md.
Task: rewrite both SKILL.md files per the surgery outlined in TECH §4.1 and §4.2.
Output: two modified SKILL.md files. Commit message: "feat(s64-w?-?): rewritetriage-finding + update-roadmap-backlog under Shape A".
Verification: run `__tests__/skills/curator-rewrites-smoke.test.ts` (createdin the same PR — see TECH §6 for the smoke test contract).§5 Per-Subtask file ownership
Section titled “§5 Per-Subtask file ownership”The 30.4 PLAN.md decomposition will produce implementation Subtasks {30.5} through {30.M}. This §5 names the file boundaries and the suggested Subtask grouping; the 30.4 Planner finalises Subtask IDs and dependencies.
§5.1 PR-A Subtask candidates
Section titled “§5.1 PR-A Subtask candidates”| Suggested Subtask | File ownership | Sibling deps | Estimated effort |
|---|---|---|---|
| A1: Schema field additions | lib/validation/backlog-schema.ts, lib/validation/task-list-schema.ts, lib/validation/roadmap-schema.ts (additive only — RoadmapThemeSchema + union root) | none (PR-A entry point) | ~1-1.5h |
| A2: Schema tests + sort helper | __tests__/lib/validation/backlog-schema-rank-field.test.ts, __tests__/lib/validation/roadmap-schema-shape-a.test.ts, __tests__/lib/validation/roadmap-schema-soft-cap.test.ts, __tests__/lib/validation/task-list-schema-capability-theme.test.ts, lib/backlog/sort.ts, __tests__/lib/backlog/sort.test.ts | A1 | ~1.5-2h |
| A3: Curator skill rewrites (load-bearing per OQ-8 ALT — dedicated sub-agent invoking /update-skill) | .claude/skills/triage-finding/SKILL.md, .claude/skills/update-roadmap-backlog/SKILL.md, __tests__/skills/curator-rewrites-smoke.test.ts | A1, A2 | ~2-3h |
| A4: per-task-mirror 20.14 extension (rank-edit + drag-reorder UX) | components/task-mirror/backlog-index-page.tsx (path TBD by Task 20 conventions), e2e/tests/task-mirror-backlog-rank.spec.ts, integration with existing per-task-mirror patch path | A1 | ~2-3h |
PR-A boundary: A1 + A2 + A3 + A4 ship together. CI green = PR-A merge-ready.
§5.2 PR-B Subtask candidates
Section titled “§5.2 PR-B Subtask candidates”| Suggested Subtask | File ownership | Sibling deps | Estimated effort |
|---|---|---|---|
| B1: Migration script + test | scripts/migrate-backlog-from-roadmap.ts, __tests__/scripts/migrate-backlog-from-roadmap.test.ts | A1, A3 (PR-A merge in remote — sibling-only-within-Task constraint maps to PR-A SUBTASKS being marked done first, which is local to Task 30) | ~2-3h |
| B2: Bulk content migration execution + commit | docs/reference/product-backlog.json (54 entries appended), docs/reference/product-roadmap.json (8 items removed) | B1 | ~1h (run script, verify, commit) |
PR-B boundary: B1 + B2 ship together. CI green + PR-B acceptance gate (count delta) = merge-ready.
§5.3 PR-C Subtask candidates
Section titled “§5.3 PR-C Subtask candidates”| Suggested Subtask | File ownership | Sibling deps | Estimated effort |
|---|---|---|---|
| C1: Content reshape — author themes | docs/reference/product-roadmap.json (full reshape to themes[]) | B2 | ~3-4h (curator-authored content; ~10-12 themes with full descriptions) |
| C2: Schema final reshape | lib/validation/roadmap-schema.ts (drop sections / RoadmapSectionSchema / RoadmapItemSchema; remove union; require themes only); parseRoadmapWithWarnings() helper | C1 | ~1.5h |
| C3: Renderer update + capability_theme backfill | scripts/roadmap-from-json.ts (theme-headings emit), docs/reference/task-list.json (set capability_theme on Tasks where clear), __tests__/docs/roadmap-roundtrip.test.ts (updated to new shape) | C2 | ~2-3h |
PR-C boundary: C1 + C2 + C3 ship together. CI green + PR-C acceptance gate (theme count ≤ 12; all themes linked + horizoned) = merge-ready.
§5.4 Sibling-only dep verification
Section titled “§5.4 Sibling-only dep verification”All Subtask deps above stay within Task 30’s sibling space. Cross-Task deps with Task 20 are at the TASK level (Task 30 depends on Task 20 for the per-task-mirror viewer to exist — A4 builds on Task 20’s surface). If A4 needs Task 20 not-yet-shipped affordances, escalate at 30.4 PLAN authoring: either re-scope A4, or split Task 20 to surface the dep at Task level. The 30.4 Planner verifies this when finalising decomposition.
§6 Testing and validation
Section titled “§6 Testing and validation”§6.1 Per-invariant test mapping (cross-reference with §2 columns)
Section titled “§6.1 Per-invariant test mapping (cross-reference with §2 columns)”| Inv | Test path | Test kind |
|---|---|---|
| 1 | __tests__/scripts/migrate-backlog-from-roadmap.test.ts | Migration unit test (idempotency + count delta) |
| 2 | __tests__/scripts/migrate-backlog-from-roadmap.test.ts | Migration unit test (notes-field marker) |
| 3 | __tests__/lib/validation/backlog-schema-rank-field.test.ts | Pure Zod schema test (6 cases) |
| 4 | __tests__/lib/backlog/sort.test.ts | Pure helper test (8-case fixture) |
| 5 | __tests__/scripts/migrate-backlog-from-roadmap.test.ts | Migration unit test (no-duplicate-against-task-list assertion) |
| 6 | __tests__/lib/validation/roadmap-schema-shape-a.test.ts | Pure Zod schema test (union acceptance PR-A; rejection PR-C) |
| 7 | __tests__/lib/validation/roadmap-schema-shape-a.test.ts | Pure Zod schema test (RoadmapThemeSchema field validation) |
| 8 | __tests__/lib/validation/roadmap-schema-soft-cap.test.ts | Pure helper test (parseRoadmapWithWarnings) |
| 9 | __tests__/lib/validation/task-list-schema-capability-theme.test.ts | Pure Zod schema test (optional + nullable) |
| 10 | e2e/tests/task-mirror-backlog-rank.spec.ts | E2E Playwright (rank-edit + drag-reorder + axe-core a11y) |
| 11 | e2e/tests/task-mirror-backlog-rank.spec.ts | E2E negative assertion (no Promote button) |
| 12 | Per-PR CI gate via existing CI matrix; no new test file — gates are run-this-test-suite + acceptance-prose | Integration |
| 13 | __tests__/skills/curator-rewrites-smoke.test.ts | Node-level smoke (skill contract round-trip) |
| 14 | E2E spec spot-check (semantic-token regex against rendered DOM); manual UK English review at PR | E2E + manual |
| 15 | __tests__/scripts/migrate-backlog-from-roadmap.test.ts | Migration unit test (Zod-fail-loud-not-quiet) |
§6.2 Existing tests that must continue to pass
Section titled “§6.2 Existing tests that must continue to pass”__tests__/docs/backlog-schema-roundtrip.test.ts— pre-migration JSON content continues to parse cleanly across PR-A (rank field absent is valid).__tests__/docs/roadmap-roundtrip.test.ts— MD ↔ JSON in PR-A (existing section shape valid via union); updated in PR-C for theme shape.- All existing per-task-mirror tests for Task 20 continue to pass — the 20.14 extension is additive.
__tests__/lib/validation/— full existing surface continues to pass.
§6.3 Test philosophy compliance
Section titled “§6.3 Test philosophy compliance”Per docs/reference/test-philosophy.md — every new test above verifies real behaviour, not implementation:
- Schema tests parse against canonical Zod exports (not internal helpers).
- Migration test asserts file state + JSON content (not internal script structure).
- Sort helper test asserts return value (not internal comparator).
- E2E test asserts user-observable behaviour (rank affordance visible; drag changes order; refresh persists).
- Smoke test asserts skill contract output (not skill internal phrasing).
No mock-Supabase fixtures (no Supabase calls in this Task). No mock-Anthropic fixtures.
§6.4 CI matrix impact
Section titled “§6.4 CI matrix impact”New test files add to the quality-test 4-shard Vitest matrix automatically (no vitest.config.ts change needed — directory globbing picks them up). E2E spec adds to e2e-smoke matrix (verify Playwright spec naming matches the matrix glob — e2e/tests/*.spec.ts).
Expected delta: ~6-8 new test files; +50-100 new test cases; runtime impact <30s in the worst-case shard.
§7 Risks + mitigations
Section titled “§7 Risks + mitigations”| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
PR-C breaking-change cascades — when the union shape collapses to themes-only in PR-C, any consumer of RoadmapSectionSchema / RoadmapItemSchema that wasn’t migrated breaks at runtime | Medium | High — broken render pipeline, broken docs surface | Pre-PR-C: grep -rn "RoadmapSectionSchema|RoadmapItemSchema" --include="*.ts" to enumerate every consumer; migrate or delete each one in PR-C. The grep result is included in PR-C’s PR description as a checklist. |
| Rank-field collision policy bugs in curator skill — auto-shift logic on insert may misfire if the priority tier has gaps in rank numbering | Medium | Medium — curator skill emits silent ordering bug | The auto-shift pseudocode in §4.2 is conservative (only shifts items at exactly rank >= new_rank, doesn’t compact). The smoke test for curator rewrites includes a tier-with-gaps fixture asserting auto-shift correctness. |
Migration script not finding live RESEARCH §3.2 payload — the snippet lives in docs/research/roadmap-backlog-consolidation-and-roadmap-rethink.md lines 308-1101 as a code fence; the script needs to extract or be passed it | Low | Low — script load failure, no data loss | The B1 Subtask brief specifies: extract the JSON snippet from the RESEARCH doc by line-range substring (lines 308-1101) AND/OR copy the JSON snippet into a sibling fixture file scripts/fixtures/backlog-migration-payload.json. The fixture file is committed in B1; the script reads the fixture. |
| Backlog count discrepancy — PRODUCT says 55 → 109; live count is 54; T-OQ-1 captures this. Need to reconcile pre-PR-B | Low | Low — off-by-one in acceptance gate prose | Pre-PR-B: re-count live docs/reference/product-backlog.json items; update PRODUCT inv 1 (one-line text edit per spec-current discipline) AND PR-B acceptance gate to 54 → 108 (or whatever the live count + 54 yields). |
Theme status enum default vs ALT — P-OQ-1 default is 3 values; if Liam ratifies ALT (8-value TaskListStatus mirror), schema needs revision in §3.1 | Low | Low — schema 1-line change | Default proposal in P-OQ-1 is conservative + matches the curator’s “slowly-changing strategic surface” framing. If ratified ALT, swap the 3-value enum for TaskListStatus import; 1-line change. |
| per-task-mirror patch API not landed when 20.14 dispatched — A4 Subtask assumes Task 20’s patch-path infra is operational | Medium | High — A4 blocked; PR-A merge slips | T-OQ-2 below: pre-30.4 PLAN authoring, verify Task 20’s structured-patch endpoint is implemented (or in PR-merge-pending state). If not, escalate to Orchestrator: either re-sequence A4 after Task 20 ships, or surface as a cross-Task dependency on Task 30 itself (Task 30.dependencies += "20" at Task level). |
| The 6 PARTIAL_OVERLAP clusters’ adjacency notes drift from reality — if Liam re-scopes any of ID-49 / 53 / 61 / 62 / 64 / 79 / 82 / 91 / 100 / 101 between TECH ratification and PR-B execution, the migration script’s hard-coded note text becomes stale | Low | Low — informational notes, not data integrity | The migration script encodes the adjacency-note text as a per-cluster const at the top of the file (easy to update in a follow-up commit). Pre-PR-B sanity check: re-read RESEARCH §5.4 against live docs/reference/product-backlog.json to confirm cluster membership unchanged. |
| Skill rewrite tests false-positive on curator decision-tree changes — the smoke test invokes Create against a mocked triage payload; if the mock drifts from the rewritten skill’s actual input contract, the test passes vacuously | Low | Medium — silent skill regression | The smoke test’s mock payload is verified at write-time by hand-walking the rewritten triage-finding Step 3 output schema; any output-field name change in the skill rewrite triggers a test-input update. The A3 Subtask brief calls this out explicitly. |
bun run roadmap:render infinite-loop / non-determinism on theme-shape output — the existing renderer assumes section structure; reshape to theme-headings may surface latent bugs | Low | Medium — CI hang or non-determinism | The renderer rewrite in C3 Subtask is paired with a render-twice-assert-equal test (idempotency check) added to the round-trip test suite. |
§8 Estimated effort
Section titled “§8 Estimated effort”Per Subtask suggestion in §5 (Planner-confirmed at 30.4):
| Subtask | Estimated effort |
|---|---|
| A1: Schema field additions | 1-1.5h |
| A2: Schema tests + sort helper | 1.5-2h |
| A3: Curator skill rewrites | 2-3h |
| A4: per-task-mirror 20.14 extension | 2-3h |
| PR-A total | 6.5-9.5h |
| B1: Migration script + test | 2-3h |
| B2: Bulk content migration execution + commit | 1h |
| PR-B total | 3-4h |
| C1: Content reshape — author themes | 3-4h |
| C2: Schema final reshape | 1.5h |
| C3: Renderer update + capability_theme backfill | 2-3h |
| PR-C total | 6.5-8.5h |
| Task ID-30 grand total | 16-22h (≈ 4-6 working sessions at 4h cap) |
This is above the 2h per-Subtask soft cap from CLAUDE.md, which is the expected shape for a 3-PR cross-cutting feature. The 30.4 PLAN authoring will confirm the per-Subtask sizes against the Planner’s “max 2h per agent” rule from CLAUDE.md — if any single Subtask above exceeds 2h, the Planner splits it (e.g. A4 may split into “rank integer-input affordance” + “drag-reorder UX” if needed).
§9 T-OQs (Tech-level Open Questions)
Section titled “§9 T-OQs (Tech-level Open Questions)”Five T-OQs surfaced during TECH authoring that PRODUCT did not settle. Each carries a recommended default; ratification by Liam (or Orchestrator) before relevant Subtask dispatch.
T-OQ-1. Live backlog item count vs PRODUCT stated count
Section titled “T-OQ-1. Live backlog item count vs PRODUCT stated count”Question. PRODUCT.md inv 1 + §3 PR-B acceptance gate cite “55 → 109” but live docs/reference/product-backlog.json items count is 54 (verified by jq '.items | length' at TECH authoring time). Which is correct?
Recommended default. Update PRODUCT.md inv 1 + PR-B acceptance gate to 54 → 108 (one-line text edits per “keep the spec current” discipline) and update PR-B acceptance gate to assert delta of +54, not absolute target. The delta is the load-bearing assertion; the absolute count is informational.
Action. Either confirm the +54 delta is correct AND amend PRODUCT/TECH counts; OR identify the 1-item discrepancy (one item may have been added post-PRODUCT-author or removed) and reconcile before PR-B opens.
T-OQ-2. Task 20 patch-API readiness for A4 Subtask
Section titled “T-OQ-2. Task 20 patch-API readiness for A4 Subtask”Question. A4 (per-task-mirror 20.14 extension) assumes Task 20’s structured-patch API endpoint (per per-task-mirror PRODUCT inv 29 / 33 / 36) is operational. As of TECH authoring, grep -rn "structured patch" lib/ returned no implementation. Has Task 20’s patch path landed, or is it scheduled in parallel with PR-A?
Recommended default. Pre-30.4 PLAN authoring: verify Task 20’s patch-API status (which Subtask ships it; is it merged on production-readiness?). If not landed, two options: (a) re-sequence A4 to depend on Task 20 patch-API Subtask at Task level (Task 30.dependencies += "20.X"), OR (b) implement a minimal patch-API stub in A4 itself (Subtask A4 ships the rank-edit endpoint as a one-off, scoped to backlog rank field only). Option (a) preferred to avoid double-implementation.
Action. 30.4 Planner verifies Task 20 patch-API status during decomposition; escalates to Orchestrator if blocking.
T-OQ-3. PR-C capability_theme back-fill cardinality
Section titled “T-OQ-3. PR-C capability_theme back-fill cardinality”Question. PR-C Subtask C3 includes “set capability_theme on Tasks where clear”. How many of the existing ~36 Tasks in task-list.json need a capability_theme set, and is this a tractable manual exercise or does it warrant a follow-up Subtask?
Recommended default. Author the 10-12 themes in C1 first; for each theme, the linked_tasks[] array names which Tasks contribute; the reverse-map gives the capability_theme value for those Tasks. C3 iterates tasks[].id and sets capability_theme from the reverse-map (a small Node script, ~30 LOC). Tasks not in any theme’s linked_tasks[] stay capability_theme: null (which is the schema default). Tractable in C3 without follow-up.
Action. If the reverse-map cardinality exceeds 30 Tasks at C1 authoring time, escalate to Orchestrator for split (C3a = back-fill script + C3b = renderer update).
T-OQ-4. RoadmapSchema union vs discriminated-union in PR-A
Section titled “T-OQ-4. RoadmapSchema union vs discriminated-union in PR-A”Question. §3.1 PR-A schema uses z.object({ sections: ..., themes: ... }).superRefine(...) to enforce “exactly one of”. Should this be a Zod discriminatedUnion instead for clearer type narrowing?
Recommended default. Stay with superRefine for PR-A. discriminatedUnion requires a literal discriminator field (shape: "legacy" | "shape_a") which would force a content edit on every existing roadmap.json read in PR-A — an unnecessary churn for a transitional shape that lives ~1 PR cycle (PR-A → PR-C). The superRefine approach lets both shapes coexist without a discriminator field. PR-C drops both branches and replaces with themes[] directly, so the discriminator never has long-term value.
Action. No action — recommended default applies.
T-OQ-5. Test philosophy compliance for skill smoke test
Section titled “T-OQ-5. Test philosophy compliance for skill smoke test”Question. __tests__/skills/curator-rewrites-smoke.test.ts invokes the curator skill’s documented contract via a mock payload, not via the literal Skill tool execution. Is this an acceptable test pattern for skill rewrites, or does the test need to invoke the Skill tool directly (which is awkward in Vitest)?
Recommended default. Mock-payload + contract-output assertion is the right pattern. The test verifies the skill’s input → output contract (the YAML structure produced by the rewritten skill); it doesn’t need to verify the LLM produces that output (that’s the agent harness’s job). This matches the pattern used elsewhere in KH for skill-adjacent tests (e.g. the update-docs skill tests at __tests__/scripts/). The smoke test does not exercise the agent loop; it asserts the skill’s documented contract is consumable.
Action. No action — recommended default applies; A3 Subtask brief codifies this pattern.
End of TECH.md. Awaits Liam ratification of T-OQ-1 (backlog count) and T-OQ-2 (Task 20 patch-API readiness) at minimum before 30.4 PLAN authoring. T-OQ-3/4/5 carry recommended defaults that the 30.4 Planner can adopt without re-ratification.
Cross-references:
- PRODUCT.md:
docs/specs/id-30-roadmap-backlog-consolidation/PRODUCT.md(15 invariants, 4 P-OQs) - RESEARCH source:
docs/research/roadmap-backlog-consolidation-and-roadmap-rethink.md(1648 lines) - Schema modules:
lib/validation/{backlog,roadmap,task-list,work-status}-schema.ts - Curator skills:
.claude/skills/{triage-finding,update-roadmap-backlog}/SKILL.md - Render pipeline:
scripts/roadmap-from-json.ts - Round-trip guards:
__tests__/docs/{backlog,roadmap}-{schema-,}roundtrip.test.ts