feat: add decision coverage ledger - #2233
Open
coreldh wants to merge 10 commits into
Open
Conversation
…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.
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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
Evidence: Decision-ledger contract evidence
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 onlyscan_open_decisionsstatus rows. After the requiredcompleteflow writescaptain-heldto close that status copy, an active captain hold remains in Bearings’decisions_openyet is absent fromopen_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 withstatus_open_decision_keys == 0while its sole row still carriesorigins: ["captain-hold","folded-status-key"]: line 284 assigns that folded origin from the historicalOPENED_ROWSset, 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 fromOPEN; 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 scalaropen_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_sourcesonly marks live union rows; raw event counts,keys_opened_distinct, andkeys_supersededremain 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 exampleFM_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- Ifdata/backlog.mdis unreadable or missing, the suppressedawkfailure yields an emptyHOLD_TSVand 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 withkind: captainandhold-kind: captainis 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 toHOLD_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, whereasdone[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 canonicalresolved_idsrule.🔧 Fix: Canonicalize captain-hold inventory
3 errors still open:
bin/fm-decision-ledger.sh:157-jq -eexits 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-ehere (the expliciterror(...)checks still fail malformed inventory) or emit an intermediate empty array.bin/fm-decision-ledger.sh:162- The canonical actionability predicate does not requireOrigin:orDecision 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 elevenpasscases, but CI rejects any successful run whose output has anything other than fiveok -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 queuedcaptain-run-style row is therefore selected here, becomes anopen_decisionsrow, and increasescaptain_holds_active/open_decision_keysdespite 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 e4c0df5b559979ca2cc5d3837eeb28de09d52bb7bash tests/fm-decision-hold-lifecycle.test.shFocusedtests/fm-decision-ledger.test.shassertions, 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.