Skip to content

feat: add decision coverage ledger - #2233

Open
coreldh wants to merge 10 commits into
kunchenguid:mainfrom
coreldh:fm/c0806-fm-decision-ledger
Open

feat: add decision coverage ledger#2233
coreldh wants to merge 10 commits into
kunchenguid:mainfrom
coreldh:fm/c0806-fm-decision-ledger

Conversation

@coreldh

@coreldh coreldh commented Aug 12, 2026

Copy link
Copy Markdown

Intent

Make a captain decision that was answered and then archived out of the live backlog count as closed, so a finished investigation lane can be cleaned up instead of being blocked forever, while a key that is genuinely unaccounted for still refuses; and make it structurally impossible to report raw status events as open captain decisions by requiring every open-decision figure to be backed by an enumerated list of live keys, each carrying a disposition.

What Changed

  • Added a read-only decision coverage ledger that distinguishes status transition events from live decision keys and enumerates every open key with a disposition.
  • Integrated the ledger into Bearings and hardened captain-hold lifecycle handling so resolved archived answers are closed while missing or malformed holds remain unaccounted for.
  • Added regression coverage and CI wiring for the ledger, aligned transition classification with the authoritative fold, and moved large snapshot inputs off command arguments.

Risk Assessment

✅ Low: The final bounded canonical-inventory change keeps generic captain gates out of the keyed decision union while preserving separately enumerated hold figures and coverage invariants.

Testing

Inspected the target diff, ran the complete focused decision-hold lifecycle test, and ran all focused decision-ledger assertions in executor-sized batches. Evidence shows the CLI/state behavior directly; no rendered UI applies because this is a shell/JSON ledger change.

Evidence: End-to-end captain-decision lifecycle evidence
ok - report-only unresolved decision is reproduced and completion refuses before loss
ok - an answered-and-archived key is closed while an unaccounted one still refuses
ok - an archived captain answer cannot be reopened and stays identity-checked
ok - non-forced scout teardown always requires durable inventory verification
ok - captain holds are idempotent, distinct, teardown-safe, Bearings-visible, and durably routed before close
ok - completion and verification validate origins before constructing paths
ok - ended visual review follows the same decision-hold completion owner
ok - resolved findings and decision-like prose do not create false holds
ok - terminal single-owner stale status decisions do not block empty inventory
ok - main-home and secondmate-home captain holds remain correctly routed
ok - resolve matches first/middle/last in quoted blocked_by and rejects a genuinely absent id
Evidence: Decision-ledger contract evidence
ok - only the lines the authoritative fold acts on become ledger figures
ok - raw decision events stay distinct and can never stand in for the open-key count
ok - every distinct live key carries its own disposition and closed keys stay out
ok - an active captain hold remains an origin-labelled ledger row after status closure
ok - an active captain hold without a status key remains an origin-labelled ledger row
ok - a blocked captain hold stays out of the actionable current-source set

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (7) ✅
  • 🚨 bin/fm-decision-ledger.sh:124 - Intent requires “every open-decision figure” to be backed by enumerated live keys with dispositions, but the new ledger reads only scan_open_decisions status rows. After the required complete flow writes captain-held to close that status copy, an active captain hold remains in Bearings’ decisions_open yet is absent from open_decision_keys/decision_keys; the ledger can therefore report zero open decisions while one still awaits the captain. Build the ledger from the shared union of actionable captain holds and folded status keys, deduplicated by hold identity, before publishing its count.

🔧 Fix: Include captain holds in decision ledger
1 error still open:

  • 🚨 bin/fm-decision-ledger.sh:284 - The binding requires the four labelled figures to stay derivable from the origin-labelled union rows. But a transferred decision is explicitly emitted with status_open_decision_keys == 0 while its sole row still carries origins: ["captain-hold","folded-status-key"]: line 284 assigns that folded origin from the historical OPENED_ROWS set, not the current fold. A consumer therefore cannot derive the folded-status figure from the advertised origin breakdown. Preserve lineage separately and add a current-source/origin field whose folded-status membership comes only from OPEN; derive the separate figures from that field.

🔧 Fix: Separate current decision sources
3 issues (2 errors, 1 warning) still open:

  • 🚨 bin/fm-decision-ledger.sh:321 - The binding instruction says “no single merged open-decision number may be exposed or made the easy thing to read,” but this hunk still emits the merged union scalar open_decision_keys; Bearings publishes it as a figure. Keep only the separately labelled current-source figures, or obtain authorization to retain this merged count.
  • 🚨 bin/fm-decision-ledger.sh:308 - The binding requires raw events and stale/superseded keys, as well as folded keys and holds, to be derivable from enumerated current-source row sets. current_sources only marks live union rows; raw event counts, keys_opened_distinct, and keys_superseded remain standalone aggregates from historical status input, with no enumerated rows or refusal invariants for them. The required per-figure derivability therefore remains absent.
  • ⚠️ bin/fm-decision-ledger.sh:134 - The ledger’s documented local-only/no-network read now invokes the full fleet snapshot with its default secondmate aggregation. A remote or failing registered secondmate can trigger network work or make the local ledger unavailable, and Bearings then performs that expensive snapshot a second time. Request only the local main-home inventory here (for example FM_SNAPSHOT_SECONDMATES=0) because the ledger consumes only .backlog.records.

🔧 Fix: Read captain holds locally
2 errors still open:

  • 🚨 bin/fm-decision-ledger.sh:185 - If data/backlog.md is unreadable or missing, the suppressed awk failure yields an empty HOLD_TSV and the ledger emits zero captain holds rather than refusing. That can silently omit a live captain decision, violating the no-hidden-remainder invariant. Fail closed on an unreadable/local backlog before parsing it.
  • 🚨 bin/fm-decision-ledger.sh:143 - This local parser calls a queued captain row actionable without requiring (hold: ...), unlike the canonical actionability rule. A malformed queued -decision- row with kind: captain and hold-kind: captain is therefore counted as a captain-hold source here but is not actionable in Bearings, producing contradictory open-decision figures. Parse and require the hold reason (and preserve the canonical supported row grammar) before adding it to HOLD_TSV.

🔧 Fix: Fail closed on malformed captain holds
2 errors still open:

  • 🚨 bin/fm-decision-ledger.sh:154 - The local hold scanner only recognizes checkbox rows, while the canonical backlog parser also supports - **<id>** - ... structured rows. An actionable captain hold in that supported bold form is silently skipped here, so its live decision is omitted from the union even though Bearings treats it as actionable. Consume the canonical parser or make this parser fail closed / handle every canonical structured-row form; pin a bold captain-hold fixture against the canonical summary.
  • 🚨 bin/fm-decision-ledger.sh:142 - Blocker readiness diverges for duplicate structured IDs: the canonical parser considers an ID resolved only when every occurrence is Done, whereas done[id] = (section == "Done") overwrites the earlier state. A queued hold blocked by an ID that appears once non-Done and later Done is incorrectly counted actionable here. Accumulate resolution with logical-AND, matching the canonical resolved_ids rule.

🔧 Fix: Canonicalize captain-hold inventory
3 errors still open:

  • 🚨 bin/fm-decision-ledger.sh:157 - jq -e exits unsuccessfully when its filter produces no value. Thus a valid backlog with zero actionable captain holds makes this selection emit nothing and the ledger refuses as “inventory unavailable.” Remove -e here (the explicit error(...) checks still fail malformed inventory) or emit an intermediate empty array.
  • 🚨 bin/fm-decision-ledger.sh:162 - The canonical actionability predicate does not require Origin: or Decision key: body lines, but either missing field makes this array expression produce no TSV row. If another valid hold exists, the malformed actionable hold is silently omitted rather than reaching the existing undetermined-disposition path. This contradicts the required “every open-decision figure [be] backed by an enumerated list of live keys, each carrying a disposition.” Collect each body field as optional ([...][0] // "") so the row is emitted as undetermined with its reason.
  • 🚨 .github/workflows/ci.yml:369 - The ledger test script invokes eleven pass cases, but CI rejects any successful run whose output has anything other than five ok - lines. Consequently this job fails even when every ledger test passes. Update the expected count (or remove the brittle count assertion).

🔧 Fix: Disclose malformed captain-hold lineage
1 error still open:

  • 🚨 bin/fm-decision-ledger.sh:161 - The canonical predicate accepts generic actionable captain rows, but the ledger contract labels this figure as rows with -decision- hold identities. A queued captain-run-style row is therefore selected here, becomes an open_decisions row, and increases captain_holds_active/open_decision_keys despite not being a keyed decision hold. Filter the canonical inventory to the durable -decision- identity namespace before constructing ledger rows.

🔧 Fix: Separate captain gates from decision holds
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • git diff --find-renames --find-copies b5d430d6fdcd961ce9b681bf196f365c1825c284 e4c0df5b559979ca2cc5d3837eeb28de09d52bb7
  • bash tests/fm-decision-hold-lifecycle.test.sh
  • Focused tests/fm-decision-ledger.test.sh assertions, run in short batches: raw-event separation, enumerated/disposition-bearing keys, hold-source handling, malformed/unavailable disclosure, and rejection of unbacked Bearings counts.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

coreldh added 10 commits August 11, 2026 20:33
…n key

Two enforcement gaps let a reporting error look like a real backlog.

An answered captain decision whose backlog row had been archived out by Done
retention made its inventory key an impossible permanent blocker: the gate that
teardown enforces refused because the identity was no longer in the live
backlog, so the lane could never be cleaned up. A hold identity now carries one
durable state - held, resolved, archived, invalid, or absent - and the archived
answer satisfies the gate, because an inventory key proves a decision was
inventoried rather than that it is still open. The archived state counts only
when the archived record still carries the complete resolution record, so a row
that was collapsed into another, edited down, or deleted is still unaccounted
for and still refuses. The archived identity also cannot be reopened as a fresh
hold, and answering it again stays identity-checked.

Raw decision events could also be reported as open decisions. bin/fm-decision-ledger.sh
now owns every decision figure and its definition: it folds the open set through
the authoritative status_open_decisions rather than counting events, classifies
each key through the hold classifier, and refuses to emit unless the
open-decision figure equals the length of an enumerated row list in which every
live key carries a disposition. An open-decision count is therefore never a
free-standing number, and no key can go silently unaccounted for. Bearings
publishes that record and fails rather than print a count no enumerated key
backs.

The canonical snapshot passed whole-fleet inventories to jq in argv, so at a few
hundred backlog rows the entire bearings surface died with "Argument list too
long". The three fleet-scaled documents now arrive on stdin, with an explicit
refusal if an input is missing.
The ledger's raw pass read the decision-key grammar itself to decide which
status lines were decision events. That was a second reading of a rule the
fold owns, and the fold has since grown one the counter never inherited: a
reserved key namespace only transitions when the note speaks its own
vocabulary. A line the fold ignores was still counted as a decision opened,
and then reported as superseded - a decision that never existed on either
side of the ledger.

Give the fold's own gates one public accessor, status_line_decision_transition,
which answers with the same three predicates the fold gates on: the key
grammar, the reserved-namespace rule, and a shared verb class that is now the
single statement of which verbs move a decision at all. The ledger counts
through that accessor and applies no rule of its own, so a gate added to the
fold cannot reach the fold and miss the counter.

The accessor replaces status_line_decision_key, which had no other consumer.

Tests pin both directions: a reserved-namespace line the fold ignores moves no
ledger figure, the same key's recognized form opens exactly one, and a
table-driven case derives the fold's verdict from folded output alone and
requires the accessor to match it line for line.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant