Skip to content

Rank the half-state anchor trim by row family, and make every family legible - #14071

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-13947-patrol-family-visibility
Sep 1, 2026
Merged

Rank the half-state anchor trim by row family, and make every family legible#14071
os-sam merged 3 commits into
mainfrom
claude/issue-13947-patrol-family-visibility

Conversation

@claude

@claude claude Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Fixes #13947

Verified at HEAD 0e42227db (one file: scripts/pm/check-half-states.mjs, +574 / −6).

The defect, in triage's words

「族内按重要性裁,族外按运气裁」 — this file already carries a whole rank of per-family ceilings (BLOCKING_DEPENDENT_LIST_CAP, H19_TARGET_LIST_CAP, H20_BRANCH_LIST_CAP, H36_SAMPLE_PATHS, H37_MEMBER_LIST_CAP, H17_INDEX_ROW_CAP, H40_ROW_CAP), and every one of them answers which of this family's rows matter most. The body trim answered a different question — which rows happened to be laid out first — and dropped 157 of 231 findings on the measured run.

Premise re-checked on origin/main before writing a line, and it holds. The flat positional trim is still the live behaviour (renderMarkdown, the for loop over rows breaking on MARKDOWN_BODY_BUDGET). #13964's H40 arrived as a reserved section precisely to route around this, and its own header says so — that reservation and its unconditional summary clause are untouched here, and the #13947 cases it left behind still pass verbatim.

What lands

1. A row-family registry and an explicit priority tableHALF_STATE_FAMILY_BANDS (the five bands, each carrying the question it answers) and HALF_STATE_FAMILY_BAND (code to band), sitting together above the renderer. renderMarkdown's sort gains one key between the existing bands and the card number:

loud (P0-SUSPECT)  ->  UNJUDGED (#11218)  ->  family band (new)  ->  card number

The two existing reservations are untouched by design: an UNJUDGED row is a gap in what was read, and #11218's reservation must survive this change rather than be re-litigated by it. Within one family the issue-number order is preserved, so the list is still stable run to run and diffable in the anchor's edit history.

band families what the band answers
gate H31 H35 the row's subject is a GATE that may have been stripped or split — its absence reads as a green light
unregistered (none today) no band was declared for this code — severity unknown, so protected and flagged rather than trimmed
stall H4 H12 H16 H19 H20 H26 H27 H28 H32 H33 H36 H38 a card or PR whose forward motion is STOPPED or misrouted, and which nothing else will move
state H1 H2 H3 H7 H8 H9 H10 H13 H18 H21 H23 H24 H25 H29 H30 H34 H37 contradictory or half-written label/claim state on a live card — readable and repairable from the board
inventory H5 H6 H11 H14 H15 H22 a census, a cache or residue — an inventory of a population rather than an alarm about one card

⚠️ The band assignment is a semantic judgment this file did not previously hold. It is written as one greppable table beside the codes it ranks — not scattered through the predicates — precisely so the seat can argue with it; moving a code is a one-line edit plus a self-test case. The three anchors: gate is H31's own header (「闸门被剥不是红灯是放行」 — 「被剥」 and 「从未挂过」 are indistinguishable in the evidence, and H35 reads the event behind that state); inventory is triage's own examples of the class gate rows outrank (H14 / H22 / H5); stall is the class where no later sweep frees the card.

2. An unconditional per-family ledgerfamilyLedger + renderFamilyLedger, a first-class section rendered below the findings and reserved out of the budget exactly as the H17 index, the H39 census and the H40 section are. Ordering alone cannot fix this card: it moves which rows are lost, never whether their loss is readable. The load-bearing half is the closing sentence, which resolves the H31 acceptance test in words:

  • a family with rows is in the table, with computed and rendered, whether or not the trim left any of it in the body;
  • a family that computed nothing is named in the Computed 0 row(s) this sweep line;
  • so zero rendered H31 rows is never ambiguous between the two readings.

An unregistered code is flagged in the table, called out beside it, and sorted just below the gate band — an unknown severity is protected from the trim rather than sacrificed to it (#4690's direction), and the flag makes that protection temporary by construction.

3. The trim keeps announcing itself. This is a legibility fix, not a silent-truncation fix. The omission notice, its wording, its run-log pointer and the three self-tests protecting it are unchanged; new cases assert the notice still fires on the severity-ordered body and beside the ledger.

4. A registry-coverage gate that cannot go stale. familyRegistryCoverage reads this file's own source for the codes the sweep actually pushes and compares them with the registry — the AGENTS.md source-scan discipline (a detector with no dependencies cannot itself fail to resolve). A family added without a band fails --self-test, rather than inheriting one silently.

Body-size arithmetic

The ledger's declared ceiling is FAMILY_LEDGER_WORST_CASE_BYTES = 6000:

component worst case
heading + intro prose ~1.1 KB, fixed
table rows FAMILY_LEDGER_ROW_CAP (64) × ~48 B ≈ 3.1 KB
trim callout FAMILY_LEDGER_CALLOUT_CAP (12) × ~26 B + prose
unregistered callout FAMILY_LEDGER_CALLOUT_CAP (12) × ~8 B + prose
computed-0 line registry size (37) × ~8 B + prose

Built and measured, not claimed: 2,285 B with all 37 registered families carrying 999 rows each; 3,754 B with enough unregistered codes on top to reach the row cap. Both are pinned by self-test cases that construct the worst case. 6,000 is under an eighth of MARKDOWN_BODY_BUDGET (60,000), and because the ledger is reserved out of that budget the 5,536-byte headroom up to ISSUE_BODY_LIMIT (65,536) is untouched by it.

⚠️ The ceiling is what a reviewer can check by hand; it is not the guarantee. familyLedgerReservation computes the run-in-hand upper bound — rendered = 0 (longest callout) plus the exact digit slack, since 0 ≤ rendered ≤ computed bounds the column's width — and renderMarkdown subtracts it before laying out a single finding row. A self-test drives every shown value from 0 to the row count and asserts the render fits the bound; on a 58-row fixture the bound is 4 bytes above the observed worst case.

Before / after on the live board

Measured by running the real sweep from this container (node scripts/pm/check-half-states.mjs, exit 0, 2026-09-01), then rendering that identical 247-finding corpus through origin/main's renderer and this branch's:

origin/main this branch
findings computed 247 247
rows rendered 73 47
rows omitted (announced) 174 200
families with rows in the body 17 12
families a reader can account for 17 of 22 22 of 22
gate-band rows rendered (H35, computed 2) 0 of 2 2 of 2
body bytes 59,394 59,611

On origin/main five computed families — H1 (1), H13 (2), H23 (1), H35 (2), H36 (5) — rendered zero rows and were, from the page, indistinguishable from families that found nothing. H35 is a gate family: a gate label removed with no matching review-chain evidence, twice, invisible. On this branch every gate and stall family renders in full (H35 2/2, H4 5/5, H19 13/13, H20 2/2, H26 12/12, H28 1/1, H36 5/5, H38 2/2) and all 22 computed families carry an exact computed/rendered pair.

The honest trade, stated rather than buried: fewer rows fit (47 vs 73, because the surviving high-band rows are longer and the ledger costs ~2.5 KB), so the announced omission count rises. That is the card's own framing — the defect was never "too few rows", it was 「说了丢多少、没说丢的是谁」. What changes is that no finding is now lost unaccountably, and the rows that do survive are the ones whose absence reads as a green light.

Sequencing

Comment 5482734531's constraint is met: sections and families are first-class in the renderer. renderFamilyLedger is a section renderer of the same shape as renderDanglingReferences / renderTriggerIndex / renderClosedResidueCensus, composed into the same reserved block, so the clause-② live-sweep wiring can take its own section without inheriting the flat trim. That wiring is not in this PR (#13922 and #13944 are not addressed here).

Verification

Self-test: 1,951 cases before, 2,017 after (+66), node scripts/pm/check-half-states.mjs --self-test exit 0. Every pre-existing case passes unchanged.

Ablation — three legs, each proving the new cases can fail. Mutation confirmed on disk by occurrence count before each run; restore proved by blob hash equal to the HEAD blob and an empty git diff HEAD, with an absolute-path trap on EXIT/INT/TERM:

leg mutation occurrences result
A delete the family-rank sort key from renderMarkdown 2 → 1 4 cases red
B delete the ledger reservation from the budget accounting 1 → 0 6 cases red (3 of them pre-existing budget cases)
C drop H31 from the registry 1 → 0 6 cases red

Leg A also caught a real vacuity in a case I had written. Asserting that the gate row's indexOf is less than the inventory row's passes when the row was trimmed away entirely, because indexOf answers minus one and minus one is less than everything — so the case stayed green on exactly the regression it existed to catch. Repaired in 0e42227db by requiring the index to be positive as well, with the reason recorded beside it.

Gate union — derived at HEAD with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (17 path-derived + 2 convention-triggered, because this diff edits a gate script). Exit codes captured by redirect before any pipe:

check:agent-test-spelling            0    check:pnpm-filter-targets            0
check:bash32-floor                   0    check:watch-hint-literal             0
check:cli-command-ids                0    check-ci-filter-parity.mjs           0
check:cross-package-test-inputs      0    check-closing-keyword-parity.mjs     0
check:entry-guard                    0    check-cross-package-test-inputs.mjs  0
check:parse-guard                    0    check-shard-attestation.mjs          0
check:partof-closing-keyword         0    bare-root-worklist.mjs --self-test   0
check:pm-half-states                 0    check:pm-dispatch-gates              0
check-nul-bytes.mjs                  0    check:pm-clause2-carriers            0
check:stall-guard-headroom           0    check-half-states.mjs (live sweep)   0

Two gates in that family are NOT MEASURED locally, by their own printed verdicts, not by my reading:

  • node scripts/check-partof-closing-keyword.mjs exits 2 with NOT WIRED — neither PR_BODY nor PR_NUMBER is set … This is a wiring or usage failure, NOT a verdict. Its measurable half, pnpm check:partof-closing-keyword (28 cases), is green.
  • node scripts/check-test-completeness.mjs exits 3 with the local reading for this gate is NOT MEASURED. ⛔ It is not a red, and there is nothing here to fix.

Repo-wide ESLint ran in full — no narrowing to declare. pnpm lint (eslint . --no-inline-config) exit 0, 70 s under the shared verify lock.

check:pm-clause2-carriers (64 cases) and check:stall-guard-headroom (32 assertions) are run explicitly because they import from the edited file and CI schedules them on every PR.

Not in scope

  • No changeset: scripts-only diff, publishes nothing from any package — skip-changeset applied additively and read back.
  • No workflow edit. .github/workflows/half-state-patrol.yml is untouched; the renderer change did not require it.
  • No per-family floor (guaranteeing every computed family at least one rendered row). It would strengthen legibility further, but it is a third mechanism the ruling did not ask for, and the ledger already resolves the acceptance test. Recorded here for the seat rather than smuggled in.

Generated by Claude Code

os-sam commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Provenance — flipped ready and armed auto-merge by the dispatching seat (skills-lane PM, session session_01Msg17tAHJ3jVTYFgHydCm2), citing the in-seat review PASS of record: the ACCEPT on this PR's card, comment 5488076261. Preconditions verified at arm time on head 0e42227db: all 33 checks completed with zero failures (Lint & Repo Gates, TypeScript Type Check, and the real Live half-state sweep among them — every check, not the required subset); non-governed pure-code surface (scripts/pm/** only), so the in-seat landing path applies. Armed ≠ landed: the merge fires on GitHub's side; the card's label hygiene follows the MERGED event.


Generated by Claude Code

@os-sam
os-sam added this pull request to the merge queue Sep 1, 2026
Merged via the queue into main with commit afbf271 Sep 1, 2026
35 checks passed
@os-sam
os-sam deleted the claude/issue-13947-patrol-family-visibility branch September 1, 2026 03:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants