Ledger-CLI v2 — fix-wave Subtask breakdown ({35.13}+)
Ledger-CLI v2 — fix-wave Subtask breakdown ({35.13}+)
Section titled “Ledger-CLI v2 — fix-wave Subtask breakdown ({35.13}+)”Status: Plan of record. Authored: {35.12} RESEARCH round (S271, Task Planner) via
planning-and-task-breakdown.
Derived from: RESEARCH.md (the gap+fix spec of record).
For: the orchestrator to write the Subtask records into
docs/reference/task-list.json under Task ID-35.
Parent Task: ID-35 (Ledger mutation CLI). Existing subtasks 1–12 (ids contiguous, max 12) → this wave starts at subId 13. 13 new subtasks (13–25); total 25, at the soft ceiling.
Amendment (S271, parent directive + checker pass). A parallel backlog-integrity audit surfaced a record-set-preservation drop guard (RESEARCH §2.6) — the most severe wrong-shape write (a silently dropped record). It is landed as a distinct foundation write-gate subtask {35.16} (sibling of the budget pre-check {35.17}), and every create/delete/update/backfill command depends on it. This was a separate-subtask (not a fold-into-budget) choice because there was room under the ceiling (13 new subtasks → 25 total, exactly the ceiling) and the guard is a structurally distinct invariant (collection membership) from budget enforcement (field length); conflating their acceptance criteria would weaken both. {35.16}‘s insertion renumbered the former {35.16}–{35.24} to {35.17}–{35.25} and rewired the dependency arrays accordingly. Also folded: the {35.22} backfill id-set assertion (RESEARCH §2.6 applied to the 149-item backlog), the {35.22} discoverability dep correction, and the {35.21} (now {35.22}) auto-id testStrategy property fix (
max+1, not a literal id).
Architecture decisions (full rationale → RESEARCH.md)
Section titled “Architecture decisions (full rationale → RESEARCH.md)”- Prevent-at-source, CLI-layer enforcement. Budget gating is a CLI pre-write check, NOT a schema
.max()(which would reject the live ledger + diverge the vendored schema; RESEARCH §2.3, §7). - deps
number/string: functional-keep. TM mandate, verified empirically; the fix is discoverability, not migration (RESEARCH §3). Cross-Task unification is an Open Question, escalated separately. - Backlog
title: optionalstring≤80, schema + CLI this wave; mirror H1 render is task-view fork (ID-20), OUT OF SCOPE (RESEARCH §6). - Auto-id lives in the CLI (
max+1), not the vendoredinsertRecord(RESEARCH §2.2). - Zero new deps — extend the hand-rolled
parseArgs(RESEARCH §2.4).
Shared-hotspot map (parallel-vs-serialise)
Section titled “Shared-hotspot map (parallel-vs-serialise)”| File | Subtasks touching it | Parallelism |
|---|---|---|
lib/validation/ledger-budgets.ts (+ *-schema.ts helpers) | 13 | foundation, parallel-safe vs CLI |
lib/validation/backlog-schema.ts | 14 | parallel-safe vs CLI (own file) |
scripts/ledger-cli.ts (SHARED HOTSPOT) | 15, 16, 17, 18, 19, 20, 21, 22 | MUST SERIALISE — one writer at a time |
backlog data (product-backlog.json) via CLI | 23 | fan-out across 3–4 executors, disjoint id-ranges |
__tests__/scripts/ledger-cli*, __tests__/lib/ledger/* | 24 | after commands land |
docs (task-list-discipline.md, READMEs, this spec) | 25 | after commands land |
Rule of thumb: anything editing scripts/ledger-cli.ts (15–22) serialises on that file. The
recommended order is 13/14 (parallel foundation) → 15 → 16 (record-set guard) → 17 (budget) →
18 → {19, 20} → 21 → 22 → {23} → {24, 25}. The two write-gate guards {35.16}+{35.17} land before any
new command so every create/delete/update path commits through both. The dependency arrays below
encode the hard constraints; the orchestrator dispatches CLI subtasks one-at-a-time even where deps
would technically allow overlap, because they share the file.
Subtask records (TM-shape)
Section titled “Subtask records (TM-shape)”All ids are integers local to Task 35.
dependenciesare sibling-only integer arrays (other subtasks of Task 35).status: "pending".detailsis the load-bearing dispatch brief; rationale lives in RESEARCH.md (cited, not inlined).testStrategyis a one-line single assertion.
{35.13} — Unified 3-ledger budget registry
Section titled “{35.13} — Unified 3-ledger budget registry”- description: Introduce a single budget registry mapping (ledger, recordKind, field) to a char budget, covering task-list, roadmap and backlog fields, and wire all three parse-with-warnings helpers to read it.
- details: Per RESEARCH §4.1. Create
lib/validation/ledger-budgets.tsexporting a registry keyed by record kind (task / subtask / theme / item) → field → budget. Seed with the existing task-list numbers (taskDescription 1500, taskStatusNote 300, subtaskDescription 250, subtaskTestStrategy 300) and add roadmap (theme description/notes) + backlog (description;titlebudget added by {35.14}) entries. Do NOT budgetsubtask.details(append-only journal). Re-pointparseTaskListWithWarnings(task-list-schema.tsL207FIELD_BUDGETS) and thescripts/ledger-sweep-s269.tsconsumer at the registry (re-exportFIELD_BUDGETSfrom it or superset). AddparseBacklogWithWarnings/ extendparseRoadmapWithWarningsto emit budget warnings from the registry. NO schema.max()— registry is data, not a Zod constraint (RESEARCH §2.3/§7). - testStrategy:
bun run test— registry exports all three ledgers’ field budgets and the three parse-with-warnings helpers emit a budget warning for an over-budget field of each kind. - dependencies:
[]
{35.14} — Backlog title schema field
Section titled “{35.14} — Backlog title schema field”- description: Add an optional
titlefield to BacklogItemSchema and register its 80-char budget, keeping the live 149-item ledger parseable. - details: Per RESEARCH §6.1. In
lib/validation/backlog-schema.tsaddtitle: z.string().min(1).optional()positioned first afterid(heading convention). MUST be optional — all 149 live items lack it.BacklogItemSchemais NOT.strict()so this is non-breaking, andpatch-applypicks it up viaBACKLOG_ITEM_KNOWN_FIELDS(soupdate-backlog <id> titleworks with no walker change — RESEARCH §1.1). Add thetitle≤80 budget to the registry from {35.13}.descriptionstays required (min(1)). Note in a code comment that this trips the non-blockingtask-view-vendor-drift.ymlreminder (expected, RESEARCH §7). - testStrategy:
bun run test— current 149-item product-backlog.json still parses, and an item with atitlefield both parses and exposes a registry budget of 80. - dependencies:
[13]
{35.15} — CLI input layer: --file/stdin, named flags, auto-id, reject-unknown-flags
Section titled “{35.15} — CLI input layer: --file/stdin, named flags, auto-id, reject-unknown-flags”- description: Extend the CLI arg parser with JSON-from-file/stdin input, named value-flags, a max+1 auto-id helper, and unknown-flag rejection, as the shared foundation every new command builds on.
- details: Per RESEARCH §2.2/§2.4/§5.3. In
scripts/ledger-cli.ts: (1) extend hand-rolledparseArgswith a value-flag set (--file,--id,--title,--description,--status,--depends,--priority, …) that consumes the next argv token; reject unknown flags (exit 1, list known). (2) AddreadRecordInput()resolving precedence positional-JSON →--file <path>(-= stdin) → named-flags-object. (3) AddnextId(detected, collectionKey)returningmax(ids)+1as the correct primitive type (string for tasks/themes/items, number for subtasks). ZERO new deps (Node ≥22; no commander). Do not change any command behaviour yet — pure plumbing other subtasks consume. - testStrategy:
bun run test—readRecordInputyields an identical record object from positional JSON,--file, and equivalent named flags; an unknown flag exits non-zero. - dependencies:
[]
{35.16} — Record-set-preservation write gate (the drop guard)
Section titled “{35.16} — Record-set-preservation write gate (the drop guard)”- description: Add a CLI write-gate assertion that the post-write id-set and count equal the pre-write set under the intended delta, rejecting any silent record drop or duplicate before bytes land.
- details: Per RESEARCH §2.6 (highest-severity prevent-at-source guard). In
scripts/ledger-cli.tsaddassertRecordSet(beforeIds, afterIds, expectedDelta)run at the write gate of every mutating path —commitMutation(scoped AND whole-file) +promote(twice, once per ledger).beforeIds= collection id-set atloadLedgertime;afterIds= id-set parsed from the bytes about to be written (parse thescopedSerialise/escapeSerialiseoutput string, NOT the in-memorydetected.data, so a serialise-side drop is caught).expectedDelta: field-edit/journal = ∅; create = +1 (the one new id); delete = −1; promote = task +1 AND backlog −1. Mismatch →{ok:false, error:'record-set-violation', detail:'<ledger>: missing [..] / unexpected [..]'}, exit 1, write nothing. Composes with — does NOT replace — the {35.17} budget gate; both must pass beforeatomicWriteFile/commitStagedWrite. Add to USAGE error list. - testStrategy:
bun run test— a write whose serialised output is missing one pre-existing id is rejectedrecord-set-violation(exit 1, no bytes written), while a normal field-edit (∅ delta) and a normal create (+1) both pass. - dependencies:
[15]
{35.17} — CLI write-time budget pre-check + --force
Section titled “{35.17} — CLI write-time budget pre-check + --force”- description: Gate every mutating command on a pre-write budget check of the single changed
record, rejecting over-budget writes unless
--force, with an inline record-scoped message. - details: Per RESEARCH §2.3 (north star). In
scripts/ledger-cli.ts, after the in-memory mutation but BEFOREatomicWriteFile, check the changed record’s budgeted fields against the registry ({35.13}). Over-budget →{ok:false, error:'budget-exceeded', detail:'<field> is <N> chars (budget <B>) on <ledger> <id>'}, exit 1, write nothing.--forcedowngrades to the existing soft warning + proceeds. Message is scoped to the changed record — NEVER the whole-ledgerparseTaskListWithWarningsdump. Wire intocommitMutation+promote, alongside the {35.16} record-set gate (both run at the same write-gate layer; record-set is the membership invariant, budget is the field-length one).subtask.detailsis exempt. - testStrategy:
bun run test— a 789-char subtask description is rejected (exit 1, no write) without--forceand written with--force. - dependencies:
[13, 15]
{35.18} — Mirror regen default-on + fail-loud
Section titled “{35.18} — Mirror regen default-on + fail-loud”- description: Make mirror regeneration the default after every write, add
--no-regen-mirrorsto opt out, and surface a loud warning if regen fails. - details: Per RESEARCH §2.5. In
scripts/ledger-cli.tsflip the default:commitMutation/promotecallmaybeRegenMirrors(true)unless--no-regen-mirrors. Add the--no-regen-mirrorsflag (parser from {35.15}); remove reliance on--regen-mirrorsopt-in (keep it as a harmless alias for back-compat).regen-mirrors.shnon-zero exit → loud stderr warning (write already committed, so post-write alert, not rollback). UpdateMIRROR_REMINDER/USAGE text accordingly. - testStrategy:
bun run test— a mutating command with no mirror flag invokes regen by default, and--no-regen-mirrorssuppresses it. - dependencies:
[15]
{35.19} — update-subtask command (gap 1, highest value)
Section titled “{35.19} — update-subtask command (gap 1, highest value)”- description: Add
update-subtask <taskId.subId> <field> <value>to edit any subtask field via the keyset-guarded walker with scoped-write and both write-gate guards. - details: Per RESEARCH §2.1. In
scripts/ledger-cli.tsaddupdate-subtaskaccepting a dottedtaskId.subId; build FieldPatch['tasks', taskId, 'subtasks', String(subId), field]; runfieldPatchMutation+commitMutationwith--scopedavailable exactly asflip-subtask. Value parse: field-type-aware coercion driven by the SubtaskSchema field type (sodependenciesparses as number[],descriptionstays a string even if it looks like JSON — RESEARCH §5.3); the keyset guard (SUBTASK_KNOWN_FIELDS) rejects unknown fields. Commits through the {35.16} record-set gate (∅ delta) + {35.17} budget pre-check (description/testStrategy). Add to USAGE. - testStrategy:
bun run test—update-subtask 35.1 status doneflips the field with a scoped 1-line diff and re-parses; an unknown field exits non-zero. - dependencies:
[15, 16, 17]
{35.20} — Task / roadmap field-editors + create-theme
Section titled “{35.20} — Task / roadmap field-editors + create-theme”- description: Add
update-task,update-roadmapfield-editors and acreate-themecommand so every record kind has non-status field editing and roadmap gains create+edit (it has none today). - details: Per RESEARCH §4. In
scripts/ledger-cli.ts: (1)update-task <taskId> <field> <value>(FieldPath['tasks', taskId, field]; e.g. status_note, description, priority — keysetTASK_KNOWN_FIELDS). (2)update-roadmap <themeId> <field> <value>(FieldPath['themes', themeId, field], mirror ofupdate-backlog; keysetROADMAP_THEME_KNOWN_FIELDS). (3)create-theme <json>viainsertRecord(mirror ofcreate-backlog). All use field-type-aware value coercion and commit through the {35.16} record-set gate (∅ for edits, +1 for create-theme) + {35.17} budget pre-check. Add to USAGE. Roadmap has 11 themes (under 12 ceiling). - testStrategy:
bun run test—update-roadmapedits a theme field,update-taskedits a task field, andcreate-themeinserts a theme; each re-parses and rejects an unknown field. - dependencies:
[15, 16, 17]
{35.21} — Auto-id wiring + field-type-aware coercion on create/update-backlog
Section titled “{35.21} — Auto-id wiring + field-type-aware coercion on create/update-backlog”- description: Wire the max+1 auto-id helper into all record-creating commands and replace update-backlog’s silent JSON.parse heuristic with field-type-aware coercion.
- details: Per RESEARCH §2.2/§5.3. In
scripts/ledger-cli.ts, when a record-creating command (add-subtask,open-task,create-backlog,create-theme) omitsid(named-flag path) or--idis absent, injectnextId(...)from {35.15} (subtask → number, others → string).--id <n>forces an explicit id (still duplicate-checked byinsertRecord). Replaceupdate-backlog’sJSON.parse(value)-then-bare-string fallback with type-aware coercion driven by the BacklogItemSchema field type (soupdate-backlog 100 description "123"stays string “123”). Apply the same coercion to the {35.19}/{35.20} update commands. Auto-id-created records still commit through the {35.16} record-set gate (+1 delta). - testStrategy:
bun run test—add-subtask 35 --title Xassignsmax(existing subId)+1andcreate-backlog --title Yassignsmax(existing item id)+1;update-backlog 100 description "123"keeps “123” a string. - dependencies:
[15, 16, 20]
{35.22} — Discoverability: schema, per-subcommand --help, get
Section titled “{35.22} — Discoverability: schema, per-subcommand --help, get”- description: Add a
schema [ledger|kind]subcommand printing each field’s name+type+budget, a per-subcommand--help, and aget <ledger> <id> [field]single-field read. - details: Per RESEARCH §5.1/§5.2 (the “prevent guessing” fix). In
scripts/ledger-cli.ts: (1)schema [ledger|recordKind]prints per record kind every field name + Zod type (sourced fromSchema.shape) + budget (registry {35.13}) + required/optional + enum values (fromlib/validation/work-status.ts), sosubtask.dependencies: number[] (sibling-only)/task.dependencies: string[]/subtask.description: string ≤250/backlog.title: string ≤80are explicit. (2) Per-subcommand--helpprints that command’s flags + its target record’s schema slice (replaces today’s bare-USAGE fall-through). (3)get <ledger> <id> [field]extendsshowwith single-field reads. Add all to USAGE. (Per-subcommand--helpmust enumerate the new commands → depends on them existing.) - testStrategy:
bun run test—schema taskliststask.dependenciesasstring[]andsubtask.dependenciesasnumber[];get backlog 100 statusprints just the status. - dependencies:
[13, 14, 15, 19, 20, 21]
{35.23} — Backlog title backfill (FAN OUT across 3–4 executors)
Section titled “{35.23} — Backlog title backfill (FAN OUT across 3–4 executors)”- description: Backfill a concise ≤80-char title for all 149 backlog items via update-backlog, fanned out across 3–4 executors on disjoint id-ranges, with a pre-commit id-set assertion.
- details: Per RESEARCH §6.4 + §2.6. ORCHESTRATOR FANS OUT across 3–4 parallel executors on
disjoint id-ranges (ids sorted then quartered, ~37–38 items each). Each executor, for each item
in its range, reads the item
descriptionand writes a concise heading-essencetitle(≤80 chars) viaupdate-backlog <id> title "<title>"(budget-enforced by {35.17}, so >80 is rejected at source; record-set-guarded by {35.16}, ∅ delta per edit — dogfoods both gates). Do NOT over-engineer: no LLM batch, no title-generation heuristic — a human-/agent-written short heading per item. Before the orchestrator commits the backfill, assert the backlog id-set + count are IDENTICAL pre/post — onlytitlefields added, ZERO records dropped or reordered-out. Canonical state: 149 records, max id 185, ids non-contiguous (gaps are legitimate promotions / never- allocated pre-migration ids, NOT losses). Orchestrator runsbash scripts/regen-mirrors.shonce after all ranges complete. Mirror H1 still rendersdescriptionuntil the task-view fork ships (ID-20, OUT OF SCOPE — RESEARCH §6.3). - testStrategy:
bun run test(+jq) — after backfill the backlog id-set is identical pre/post (149 records, max id 185, no id added or dropped) and every item carries a non-emptytitle≤80 chars. - dependencies:
[14, 16, 17]
{35.24} — Test coverage for the v2 command surface
Section titled “{35.24} — Test coverage for the v2 command surface”- description: Extend the existing ledger-cli test suites to cover every new command, both write-gate guards, auto-id, and the three input modes.
- details: Per
docs/reference/test-philosophy.md(verify real behaviour, not implementation). Extend__tests__/scripts/ledger-cli.test.ts+ledger-cli-integration.test.ts(and__tests__/lib/ledger/scoped-serialise.test.tsif scoped paths change). Cover:update-subtask/update-task/update-roadmap/create-themehappy + unknown-field paths; record-set- violation reject (exit 1, no write) on a dropped/duplicated id across scoped AND whole-file paths; budget-exceeded reject +--forcewrite; auto-id max+1 for subtask (number) and backlog/theme (string);--file/stdin/named-flag equivalence; reject-unknown-flag; mirror-regen default-on flag wiring;schema/getoutput. Usebun run test(NOTbun test). - testStrategy:
bun run test— the ledger-cli suites pass with new cases covering all v2 commands, both write-gate guards, auto-id, and input modes. - dependencies:
[16, 19, 20, 21, 22]
{35.25} — Docs: discipline + CLI usage + v2 spec close-out
Section titled “{35.25} — Docs: discipline + CLI usage + v2 spec close-out”- description: Update the field-discipline doc, ledger-cli usage/README, and the v2 spec to
document the new command surface, both write-time guards, and the backlog
titlefield. - details: Update
docs/reference/task-list-discipline.md(note CLI now REJECTS over-budget AND record-set-violating writes at write time, not just warns; add backlogtitle≤80 row), thescripts/ledger-cli.tsheader USAGE block +lib/ledger/README.md(new commands, the record-set + budget write gates, input modes, auto-id, regen default-on), and note in this spec dir that the task-view mirror renderstitleas H1 only after a future ID-20 fork release. Reference RESEARCH.md as the spec of record. UK English; no schema.max()claims. - testStrategy:
bun run test—doc-freshness.test.tsand any mirror/doc guard tests pass after the doc updates. - dependencies:
[19, 20, 21, 22]
Sibling-only dependency check (forcing function — PASSED)
Section titled “Sibling-only dependency check (forcing function — PASSED)”Every dependencies[] above references only other subtasks of Task 35 (ids 13–25, all ≤ the
referencing id — no forward deps). No cross-Task subtask dependency surfaced. The two cross-Task
concerns (id-type unification; task-view mirror fork) are handled as scope boundaries / Open
Questions (RESEARCH §3, §6.3), NOT as subtask deps — so the constraint is honoured without bending.
25-subtask ceiling check (PASSED — AT the ceiling)
Section titled “25-subtask ceiling check (PASSED — AT the ceiling)”Task 35 had 12 subtasks; this wave adds 13 (13–25, incl. the {35.16} record-set guard) → 25 total,
exactly at the 25 soft ceiling. No split needed, but there is no further headroom under this
Task. The optional generic set / list polish (RESEARCH §5.1/§5.3) would push past 25 → it MUST
become a follow-on Task, not a 26th subtask here. Any further ID-35 expansion is also a split signal.
Open Questions (for the orchestrator to route — NOT decided here)
Section titled “Open Questions (for the orchestrator to route — NOT decided here)”- id-type unification (cross-Task). RESEARCH §3 recommends functional-keep (TM mandate). If
Liam wants unification anyway, it is a separate Task touching the task-view fork (ID-20),
lib/validation/*,lib/ledger/*walkers, all mirrors, and external consumers — must NOT fold into this wave. - task-view mirror
titleH1 render (cross-Task, ID-20). Rendering backlogtitleas the mirror H1 needs an ID-20 fork release; this wave ships only the schema field + CLI surfacing. - Budget-registry home. RESEARCH §4.1 recommends a new
lib/validation/ledger-budgets.tsover enlarging the vendoredtask-list-schema.tsFIELD_BUDGETSdelta; either is correct — the {35.13} executor confirms. - Generic
set/list(optional polish). Named commands close the gaps; a fully genericset+ alistare optional and would push past the 25 ceiling → defer to a follow-on Task.