PRODUCT — Ledger mutation CLI (ID-35.2)
PRODUCT — Ledger mutation CLI (ID-35.2)
Section titled “PRODUCT — Ledger mutation CLI (ID-35.2)”Task: ID-35. This Subtask: {35.2} PRODUCT — Behavior invariants + per-subcommand acceptance criteria. Predecessor: RESEARCH.md (10-primitive inventory verified against task-view v0.2.0; VENDOR decision; mirror coupling).
Defines the observable behaviour of bun scripts/ledger-cli.ts. Each invariant is numbered
and testable; {35.3} TECH maps a Proposed change to each.
§1 Scope
Section titled “§1 Scope”In scope: a headless CLI (bun scripts/ledger-cli.ts <subcommand> [args]) with the 10
subcommands from RESEARCH §1, mutating the three KH workflow ledgers via vendored task-view
v0.2.0 primitives; a structured JSON output envelope; a dry-run preview; a mirror-staleness
reminder.
Out of scope: mirror generation (owned by regen-mirrors.sh, RESEARCH §4); git commit
(owned by the Orchestrator / commit-commands); a long-running server or HTTP loopback; id
auto-allocation beyond the deferred --next-id nicety; the ID-34 retroactive sweep.
§2 Command surface
Section titled “§2 Command surface”bun scripts/ledger-cli.ts <subcommand> [args] [--flags]
show <ledger> <id> read one record (ledger: task|roadmap|backlog) flip-task <taskId> <status> set Task.status flip-subtask <taskId> <subId> <status> set Subtask.status append-journal<taskId> <subId> <text> append an <info added on …> block to Subtask.details add-subtask <taskId> <subtaskJson> append a Subtask to a Task update-backlog<itemId> <fieldPath> <value> set a field on a backlog item open-task <taskJson> insert a new Task into task-list create-backlog<itemJson> insert a new backlog item delete-backlog<itemId> remove a backlog item promote <backlogId> <taskJson> move backlog item → new Task (atomic)
Global flags: --dry-run print the post-mutation document to stdout; write nothing --pretty human-readable output instead of JSON envelope --regen-mirrors run scripts/regen-mirrors.sh after a successful write --ledger-dir <path> override the ledger directory (default docs/reference)§3 Behavior invariants
Section titled “§3 Behavior invariants”Output & safety
Section titled “Output & safety”-
Structured JSON envelope by default. Success → stdout
{ok:true, subcommand, …result}with exit 0. Error → stderr{ok:false, error:<machine-code>, detail}with exit 1. The envelope is parseable (the Orchestrator may consume it programmatically).--prettyswaps to human-readable lines but the exit codes are identical. Acceptance: every subcommand prints a valid JSON envelope to the correct stream with the correct exit code. -
No silent corrupt write. A mutation is re-parsed against the vendored Zod schema before any byte is written. A schema-parse failure aborts with exit 1 and the original file is byte-unchanged. Acceptance: feeding
add-subtaska Subtask that violates the schema leaves the ledger unchanged + exits 1 witherror:"schema-error"and the Zod issues indetail. -
Atomic single-file writes. Every write goes through write-to-temp + fsync + POSIX rename (the vendored
atomicWriteFile); a crash mid-write never leaves a truncated ledger. Acceptance: the write path callsatomicWriteFile; noBun.write/writeFiledirectly onto a canonical path. -
Unknown ledger / unknown subcommand / missing args fail loudly. A
document_namethat is not one of the three known values, an unrecognised subcommand, or missing positional args exit 1 with a usage-bearing error — never a partial mutation. Acceptance:detectSchema → kind:"unknown"maps to exit 1; an unknown subcommand prints usage + exits 1. -
--dry-runwrites nothing. It runs the full mutation in-memory (including the Zod re-parse) and prints the resulting document (or a diff summary) to stdout; the canonical file mtime is unchanged. Acceptance: after--dry-run, the target file’s mtime + bytes are identical.
Per-subcommand correctness
Section titled “Per-subcommand correctness”-
flip-task/flip-subtaskset status via a field patch + re-parse. Status is validated against the schema enum (TaskListStatus/SubtaskStatus); an invalid status exits 1. Acceptance:flip-subtask 35 1 donesetstasks[id=35].subtasks[id=1].status="done";flip-task 35 not-a-statusexits 1 with schema-error. -
append-journalappends an<info added on …>block, preserving prior content. It reads the currentdetails, appends a timestamped block per the field-discipline journal shape (ID-34), and writes the whole field back. Append-only — never truncates. Acceptance: priordetailscontent is intact and the new block is present with a valid ISO timestamp wrapper. -
add-subtaskappends tosubtasks[]with the sibling-dep superRefine enforced. A new Subtask whosedependencies[]reference a non-sibling is rejected (the schema’s superRefine runs on re-parse). Acceptance: a valid Subtask appends + re-parses; a cross-Task dep exits 1 with the superRefine message. -
open-task/create-backloginsert a whole record; duplicate id is rejected. UsesinsertRecord; a record id already present exits 1 witherror:"duplicate-id". Acceptance: inserting a fresh id succeeds; re-inserting it exits 1 duplicate-id; the document re-parses (unique-id / sibling-dep superRefines run). -
delete-backlogremoves by id; absent id is a clean not-found. UsesremoveRecord; a missing id exits 1 witherror:"record-not-found"and writes nothing. Acceptance: deleting an existing item removes exactly it; deleting an absent id exits 1 not-found, file unchanged. -
promoteis atomic across two ledgers. Uses the vendoredpromoteTransactionsemantics (validate-everything-first → stage-both → commit-last, ADD side first). On any pre-commit failure BOTH files are left in their pre-transaction state. The backlog item is removed AND the Task inserted, or neither. Acceptance: a successful promote removes the backlog id + inserts the Task; a schema-invalidtaskJsonleaves BOTH ledgers unchanged + exits 1. -
showis read-only. Prints the named record (orerror:"record-not-found"); never writes. Acceptance:show task 35prints the ID-35 record; the file mtime is unchanged.
Discipline & mirror integration
Section titled “Discipline & mirror integration”-
Mutating commands surface field-discipline warnings. After a successful mutation the CLI runs
parseTaskListWithWarnings(task-list) / the roadmap+backlog equivalents and prints anywarnings[]to stderr (non-fatal) — so ID-34 drift surfaces to the operator at write time. (Per ID-34 TECH §3, this is the “ID-34 produces inputs the CLI consumes” contract.) Acceptance: mutating a Task whose description is over budget prints the discipline warning to stderr but still exits 0. -
Mutating commands print a mirror-staleness reminder. After a successful write the CLI reminds: mirrors are now stale; run
bash scripts/regen-mirrors.shbefore committing (CI gates on parity).--regen-mirrorsruns it automatically. Acceptance: a successful write prints the reminder;--regen-mirrorsinvokes the script. -
The CLI does not commit and does not generate mirrors. It mutates canonical JSON only. Acceptance: no
gitinvocation in the write path; no mirror.mdwritten by the CLI itself (only by the opt-inregen-mirrors.sh).
Vendoring integrity (parent-ratified)
Section titled “Vendoring integrity (parent-ratified)”- Vendored primitives are guarded against silent drift. The
lib/ledger/vendored copies are added to.github/workflows/task-view-vendor-drift.yml’s watch set alongside the existing schema files, so a task-view release that changes primitive semantics surfaces a re-vendor reminder. (Parent ratification of OQ-35-5, S267.) Acceptance: the workflow’s path filter / watch list includeslib/ledger/.
§4 Self-bootstrap acceptance (first dogfood)
Section titled “§4 Self-bootstrap acceptance (first dogfood)”Per the cmux brief + ID-35 description: the first real use of the CLI is to open ID-35 itself via Promote — but ID-35 already exists as a Task (not a backlog item), so the literal Promote dogfood is re-framed as: the CLI’s own integration test exercises the full round-trip (open-task → flip-subtask → append-journal → show) against a temp-copied ledger fixture, proving the primitives wire correctly end-to-end. The ledger status updates for ID-35’s own subtasks are performed via the CLI where practical (dogfood), demonstrating the tool on its own Task record.
§5 Validation summary
Section titled “§5 Validation summary”| Inv | Assertion | Verified by |
|---|---|---|
| 1,4 | JSON envelope + exit codes + loud failure | per-subcommand unit tests |
| 2 | no silent corrupt write | schema-invalid mutation leaves file unchanged |
| 3,15 | atomic write; no commit/mirror in write path | code inspection + integration test |
| 5 | dry-run writes nothing | mtime/bytes unchanged |
| 6–12 | per-subcommand correctness | unit + integration tests on temp fixtures |
| 13 | discipline warnings surfaced | over-budget mutation prints warning, exits 0 |
| 14 | mirror reminder | stdout/stderr content check |
| 16 | vendor-drift guard extended | workflow path-filter grep |
≥12 numbered invariants (16 here), each with a Checker-verifiable acceptance criterion; honours all RESEARCH ratifications + the parent’s S267 vendor-drift add-on.