RESEARCH — Ledger mutation CLI (ID-35.1)
RESEARCH — Ledger mutation CLI (ID-35.1)
Section titled “RESEARCH — Ledger mutation CLI (ID-35.1)”Task: ID-35 — Ledger mutation CLI (bun scripts/ledger-cli.ts).
This Subtask: {35.1} RESEARCH — primitive inventory + patch-server reuse audit + CLI
surface RFC.
Deps: ID-20 (task-view v0.2.0 shipped) + ID-34 (field discipline — this worker’s prior
deliverable).
Replaces the hand-written Python /tmp/claude/*.py ledger-splice scripts the Orchestrator
writes per mutation. All claims below are verified against the INSTALLED task-view repo at
../task-view HEAD 5652135 (tag v0.2.0-task-view) and the KH repo at HEAD 73ffb2b4 —
no fictional APIs (the cmux brief flags that prior S264/S265 specs cited fiction twice).
§1 Mutation-primitive inventory (the 10 subcommands)
Section titled “§1 Mutation-primitive inventory (the 10 subcommands)”| # | Subcommand | Mutation class | task-view primitive (verified) | File:export |
|---|---|---|---|---|
| 1 | show <ledger> <id> | read | — (read + detectSchema + find by id) | detect-schema.ts:detectSchema |
| 2 | flip-task <id> <status> | field edit | applyPatches w/ {fieldPath:["tasks",id,"status"], newValue} | patch-apply.ts:applyPatches |
| 3 | flip-subtask <taskId> <subId> <status> | field edit | applyPatches w/ nested ["tasks",id,"subtasks",subId,"status"] | patch-apply.ts:applyPatches |
| 4 | append-journal <taskId> <subId> <block> | field edit | applyPatches on […,"details"] (read-append-write whole field) | patch-apply.ts:applyPatches |
| 5 | add-subtask <taskId> <json> | field edit (array append) | applyPatches on ["tasks",id,"subtasks"] w/ new array | patch-apply.ts:applyPatches |
| 6 | update-backlog <id> <field> <value> | field edit | applyPatches on backlog ["items",id,field] | patch-apply.ts:applyPatches |
| 7 | open-task <json> | record CREATE | insertRecord | record-mutate.ts:insertRecord |
| 8 | create-backlog <json> | record CREATE | insertRecord | record-mutate.ts:insertRecord |
| 9 | delete-backlog <id> | record DELETE | removeRecord | record-mutate.ts:removeRecord |
| 10 | promote <backlogId> <taskJson> | cross-ledger transaction | promoteTransaction | ledger-transaction.ts:promoteTransaction |
Plus: show doubles as the read/list primitive.
GATE RISK — RESOLVED ✓
Section titled “GATE RISK — RESOLVED ✓”The S62E crossover audit (docs/research/id-35-crossover-audit.md §2) flagged 4 of 10
subcommands (open-task, create-backlog, delete-backlog, promote) as GAPs needing
NEW record-level primitives “gated on ID-20.15”. Verified: task-view v0.2.0 SHIPS all
four:
record-mutate.ts(header: “ID-20.15 record-level CREATE / DELETE primitives”) exportsinsertRecord+removeRecord— both clone the snapshot, mutate the per-kind collection (tasks/themes/items), re-parse the WHOLE document via the vendored Zod schema, and return a discriminated-union result (duplicate-id/record-not-found/schema-error/invalid-body). Covers open-task + create-backlog + delete-backlog.ledger-transaction.ts(header: “ID-20.15 cross-ledger atomic transaction”) exportspromoteTransaction— validate-everything-first, stage-both (durable temps), commit-last (two adjacent renames, ADD side first so a kill yields a benign duplicate not a lost update). Covers promote.
No fiction; no deferral needed. All 10 subcommands are buildable against v0.2.0. The cmux brief’s contingency (“if the 4 need NEW primitives that aren’t shipped → write OQ-pending, scope to supported subcommands”) does not trigger.
§2 Patch-primitive reuse audit (verified signatures)
Section titled “§2 Patch-primitive reuse audit (verified signatures)”| Primitive | Signature (verified) | Reuse |
|---|---|---|
detectSchema(parsed) | → {kind:"task-list"|"roadmap"|"backlog", data} | {kind:"unknown", documentName}. Discriminates by document_name ("Knowledge Hub Task List" / "Knowledge Hub Roadmap" / "Product Backlog" — match KH ledgers exactly). | direct |
applyPatches(detected, patches[]) | patches: {fieldPath:string[], newValue:unknown}[]. → {ok:true,parsed} | walk-error | schema-error | empty-patches | kind-mismatch. Caller clones + serialises + writes. | direct (all field-edit subcommands) |
insertRecord(detected, record) | → {ok:true,detected,recordId} | duplicate-id | schema-error | invalid-body. Re-parses whole doc (runs sibling-dep + unique-id superRefines). | direct (open-task, create-backlog) |
removeRecord(detected, id) | → {ok:true,detected,recordId} | record-not-found | schema-error. | direct (delete-backlog) |
promoteTransaction({taskListPath, backlogPath, *BaseMtime, sourceBacklogId, taskRecord}) | → {ok:true, …mtimes, newTaskId, removedBacklogId, mirrors…} | {ok:false,status,error}. Internally composes insertRecord+removeRecord+stageAtomicWrite+commitStagedWrite. | direct (promote) — see §4 on mirror coupling |
atomicWriteFile(path, content) / stageAtomicWrite / commitStagedWrite / abortStagedWrite | write-to-temp + fsync + POSIX rename. | direct (single-file commits) |
All field-edit subcommands collapse to one path: read file → detectSchema →
structuredClone → applyPatches([{fieldPath, newValue}]) → on ok, JSON.stringify(…, 2)
→ atomicWriteFile. The CLI is genuinely a thin dispatcher over these.
§3 Workspace-dep vs vendor decision — VENDOR
Section titled “§3 Workspace-dep vs vendor decision — VENDOR”Decision: VENDOR the primitives into lib/ledger/, rewired to KH’s existing
@/lib/validation/* schemas. NOT a file:../task-view workspace dep.
Decisive evidence:
- CI cannot see
../task-view. KH CI (ci.yml) clones onlyknowledge-hub. Afile:../task-viewdep inpackage.jsonwould failbun install,tsc, lint, and the CLI’s own tests in CI — every job. The mirror-parity job clones task-view transiently into.cache/at a pinned tag; it is not a persistent dep. - KH already vendors the schemas.
lib/validation/{task-list,roadmap,backlog}-schema.tsare vendored copies of task-view’spackages/schemas/src/*, guarded bytask-view-vendor-drift.yml. They export the exact symbols the primitives import —TaskListSchema/TaskList/TaskSchema/SubtaskSchema/Task/Subtask,RoadmapSchema/Roadmap/RoadmapThemeSchema,BacklogSchema/BacklogDocument/BacklogItemSchema/BacklogItem(verified by grep). So rewiring the primitives’@task-view/schemas/{task-list,roadmap,backlog}imports →@/lib/validation/*is a mechanical, name-for-name swap. - The primitives are small + dependency-light (atomic-write 150 lines; detect-schema 80; patch-apply ~440; record-mutate 223; ledger-transaction 390 — minus the mirror block, §4). Vendoring is cheaper than the CI breakage a workspace dep causes.
Vendor-drift coverage. The task-view-vendor-drift.yml workflow currently watches the
schema files only. TECH (§) recommends extending its watch set (or a header note) to the new
lib/ledger/ primitives so a future task-view release that changes applyPatches/insert/
remove/promote semantics surfaces a re-vendor reminder.
§4 Mirror coupling — CLI mutates JSON; mirrors owned by regen-mirrors.sh
Section titled “§4 Mirror coupling — CLI mutates JSON; mirrors owned by regen-mirrors.sh”Verified KH mirror reality:
- All three ledgers have per-record
.mdmirrors:docs/reference/tasks/(40),docs/reference/roadmap/(11),docs/reference/backlog/(148). - CI job
ledger-mirror-parity(ci.yml:920) gates ongit diff --exit-code -- docs/reference/tasks docs/reference/roadmap docs/reference/backlog, pinned toTASK_VIEW_TAG: v0.2.0-task-view. scripts/regen-mirrors.shis the single idempotent regen command (clones task-view @ the CI tag into.cache/, runstask-view.js --check ×3). It is the canonical, CI-pinned mirror generator.
Implication: any CLI mutation makes mirrors stale → CI red unless regen runs. The CLI
must not generate mirrors itself — duplicating mirror-generator.ts risks byte-divergence
from the CI-pinned task-view release (defeating the parity gate). Instead:
- The CLI mutates canonical JSON only.
- After a successful mutation it prints a mirror-staleness reminder (
run bash scripts/regen-mirrors.sh before committing — CI gates on parity). - An opt-in
--regen-mirrorsflag runs the script as a convenience (off by default — the script re-clones task-view, which is slow + network-bound; the operator/parent controls when). - Consequence:
mirror-generator.tsis NOT vendored.promoteTransaction’s post-commit mirror-regen block (already “best-effort, derived state”) is omitted from the vendoredledger-transaction.ts; the validate/stage/commit atomicity core is preserved verbatim.
§5 Error envelope precedent
Section titled “§5 Error envelope precedent”scripts/ast-dataflow-cli.ts (860 lines, installed) is the KH CLI precedent. RESEARCH
defers the exact shape to PRODUCT/TECH but the target is a structured JSON envelope:
success → stdout {ok:true, …}; error → stderr {ok:false, error, detail} + non-zero exit.
Schema-parse failure is exit 1, never a silent corrupt write (the primitives already
re-parse before any byte is written, so a malformed mutation aborts pre-write).
§6 Open Questions (provisional defaults applied)
Section titled “§6 Open Questions (provisional defaults applied)”-
OQ-35-1 — direct in-process vs HTTP loopback. task-view exposes the primitives both as library functions and via loopback HTTP routes (
PATCH /api/ledger/record/:id, etc.). Default: direct in-process library calls (the vendored functions). No server, no loopback — the CLI isbun scripts/ledger-cli.ts, invoked synchronously by the Orchestrator. Resolved by §3 (vendor). -
OQ-35-2 — mirror regen ownership. Default (§4): CLI mutates JSON + reminds; regen owned by
regen-mirrors.sh;--regen-mirrorsopt-in. Parent may override to auto-regen-by-default if the re-clone cost is acceptable. -
OQ-35-3 — id allocation for
open-task/create-backlog. Who picks the new bare-digit id? Default: the caller supplies the full record JSON includingid; the CLI rejects a duplicate id (insertRecord’sduplicate-id). A convenience--next-idmode (max(existing)+1) is a nice-to-have, deferred. Keeps the CLI deterministic + the Orchestrator in control. -
OQ-35-4 — commit-message convention. Does the CLI commit? Default: no — the CLI mutates files only; committing stays with
commit-commands/ the Orchestrator (matches the worker-branch-only model + keeps the CLI single-responsibility). A--dry-runpreviews the post-mutation document without writing. -
OQ-35-5 — vendor-drift guard extension. Should
task-view-vendor-drift.ymlwatch the newlib/ledger/primitives? Default: yes — add thelib/ledger/*.tspaths to its trigger set (or a header note) so a task-view release bumping primitive semantics surfaces a re-vendor reminder. Specified in TECH. -
OQ-35-6 — Promote mtime args from a CLI.
promoteTransactiontakes client base-mtimes for optimistic-concurrency 409s. A CLI has no “last-seen” mtime. Default: the CLIstats both files immediately before the transaction and passes those as base-mtimes (no concurrent web editor in the orchestration context, so the 409 window is effectively nil; the check is retained for correctness, not contention).
§7 Acceptance for this Subtask ({35.1})
Section titled “§7 Acceptance for this Subtask ({35.1})”- 10-primitive inventory mapped to verified task-view exports (§1) — GATE RISK resolved (all 4 record-level primitives ship in v0.2.0).
- Patch-server reuse audit with verified signatures (§2).
- Workspace-dep-vs-vendor resolved → VENDOR, with the CI-cannot-see-
../task-viewdecisive evidence + schema-symbol compatibility confirmed (§3). - Mirror coupling resolved → CLI mutates JSON, regen owned by
regen-mirrors.sh(§4), grounded in the verified CI parity gate. - ≥5 OQs surfaced with provisional defaults (§6) — 6 surfaced.