Skip to content

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)”
#SubcommandMutation classtask-view primitive (verified)File:export
1show <ledger> <id>read— (read + detectSchema + find by id)detect-schema.ts:detectSchema
2flip-task <id> <status>field editapplyPatches w/ {fieldPath:["tasks",id,"status"], newValue}patch-apply.ts:applyPatches
3flip-subtask <taskId> <subId> <status>field editapplyPatches w/ nested ["tasks",id,"subtasks",subId,"status"]patch-apply.ts:applyPatches
4append-journal <taskId> <subId> <block>field editapplyPatches on […,"details"] (read-append-write whole field)patch-apply.ts:applyPatches
5add-subtask <taskId> <json>field edit (array append)applyPatches on ["tasks",id,"subtasks"] w/ new arraypatch-apply.ts:applyPatches
6update-backlog <id> <field> <value>field editapplyPatches on backlog ["items",id,field]patch-apply.ts:applyPatches
7open-task <json>record CREATEinsertRecordrecord-mutate.ts:insertRecord
8create-backlog <json>record CREATEinsertRecordrecord-mutate.ts:insertRecord
9delete-backlog <id>record DELETEremoveRecordrecord-mutate.ts:removeRecord
10promote <backlogId> <taskJson>cross-ledger transactionpromoteTransactionledger-transaction.ts:promoteTransaction

Plus: show doubles as the read/list primitive.

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”) exports insertRecord + 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”) exports promoteTransaction — 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)”
PrimitiveSignature (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 / abortStagedWritewrite-to-temp + fsync + POSIX rename.direct (single-file commits)

All field-edit subcommands collapse to one path: read file → detectSchemastructuredCloneapplyPatches([{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:

  1. CI cannot see ../task-view. KH CI (ci.yml) clones only knowledge-hub. A file:../task-view dep in package.json would fail bun 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.
  2. KH already vendors the schemas. lib/validation/{task-list,roadmap,backlog}-schema.ts are vendored copies of task-view’s packages/schemas/src/*, guarded by task-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.
  3. 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 .md mirrors: docs/reference/tasks/ (40), docs/reference/roadmap/ (11), docs/reference/backlog/ (148).
  • CI job ledger-mirror-parity (ci.yml:920) gates on git diff --exit-code -- docs/reference/tasks docs/reference/roadmap docs/reference/backlog, pinned to TASK_VIEW_TAG: v0.2.0-task-view.
  • scripts/regen-mirrors.sh is the single idempotent regen command (clones task-view @ the CI tag into .cache/, runs task-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-mirrors flag 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.ts is NOT vendored. promoteTransaction’s post-commit mirror-regen block (already “best-effort, derived state”) is omitted from the vendored ledger-transaction.ts; the validate/stage/commit atomicity core is preserved verbatim.

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)”
  1. 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 is bun scripts/ledger-cli.ts, invoked synchronously by the Orchestrator. Resolved by §3 (vendor).

  2. OQ-35-2 — mirror regen ownership. Default (§4): CLI mutates JSON + reminds; regen owned by regen-mirrors.sh; --regen-mirrors opt-in. Parent may override to auto-regen-by-default if the re-clone cost is acceptable.

  3. 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 including id; the CLI rejects a duplicate id (insertRecord’s duplicate-id). A convenience --next-id mode (max(existing)+1) is a nice-to-have, deferred. Keeps the CLI deterministic + the Orchestrator in control.

  4. 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-run previews the post-mutation document without writing.

  5. OQ-35-5 — vendor-drift guard extension. Should task-view-vendor-drift.yml watch the new lib/ledger/ primitives? Default: yes — add the lib/ledger/*.ts paths to its trigger set (or a header note) so a task-view release bumping primitive semantics surfaces a re-vendor reminder. Specified in TECH.

  6. OQ-35-6 — Promote mtime args from a CLI. promoteTransaction takes client base-mtimes for optimistic-concurrency 409s. A CLI has no “last-seen” mtime. Default: the CLI stats 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).


  • 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-view decisive 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.