Skip to content

Commit 1e5b5e0

Browse files
claude[bot]claude
andauthored
fix(pm): report a claim that parses to ZERO branches instead of discarding it (#17766)
Fixes #17149 A `Claim:` comment that parses to ZERO branches is now reported as a MALFORMED claim instead of being discarded. The deliverable is triage 5620657752's, verbatim: 「⛔ do not fix the third spelling. **A parser that silently yields zero must say so** — an unparsed claim is an *unclassified* result, ⛔ never a *no*.」 Option 1 on the card (widening `claimedBranches` for the inline spelling) is **not** done here, and the branch reader's accept set is byte-identical — pinned as a case, because that is what keeps the NEXT unrecognised spelling loud on its first occurrence. ## The mechanism `governingClaim` picked the newest claim comment from which at least one branch parses, and `if (branches.length === 0) return;` threw the rest away. A discard is indistinguishable from an absence, so governance fell back to an older claim and said nothing. `claimGovernance(commentRows)` is now the two-part reading: - `governing` — what `governingClaim` has always returned. `governingClaim` is a one-line wrapper over it, so the ~18 readers that legitimately need only branches are untouched (proved below: the self-test's 3616 pre-existing cases pass unchanged on the refactor alone). - `malformed` — the NEWEST claim-shaped comment when it parses to zero branches, carrying its comment id, its timestamp, and the `created_at` of whatever governance fell back to. Recency is written once and both halves call it, so the two readings can never disagree about which comment is current. ## Every reader says so | reader | before | after | |---|---|---| | H20 / H27 dispatch liveness | probed the superseded branch, or (nothing parses) went silent as "no claim" | **H60**, a new `state`-band row, fires for every open `pm:dispatched` card, naming the comment id, what governance did instead, and the remedy | | `check-clause2-carriers` declaration limb | read the superseded claim's `Clause-②` line as if current | `cardDeclaration` returns `claim-branch-unparsed` **before** any line is read, from any comment (including the #17366 correction path) | | `--pair N` | exit 4 with a verdict taken from the wrong comment | **exit 2 (UNJUDGED)**, with the whole reading printed | | the fold/lane rosters (H37/H38) | built on `governing` | unchanged — they need branches only, and H60 is the row that says the roster may rest on a superseded claim | **The exit code is the file's own, not a new one.** Its table already reads 2 as UNJUDGED — "an unread carrier is NOT a bare carrier and an unread thread is NOT an absent declaration (#4690)" — and an unresolvable carrier is exactly that. Rendering it as 4 would make an unclassified result an adverse verdict, which is the reading the card refuses by name. Rendering it as 0-with-a-message is what the whole file exists against. ## The three questions the card asked ### 1. Where does the guard belong — carriers, half-states, or both? **Both, from one source.** The state is produced once in `check-half-states.mjs` (which owns `CLAIM_COMMENT_MARKER` and `claimedBranches`) and consumed in both files. A second detector in the carriers file is the drift that file's own docblocks refuse by name ("imported rather than restated ... so the two readers cannot drift"). ### 2. Which end? Option 2 only. See the four-axis reading below. ### 3. Is the fleet's claim template the cause? — MEASURED on the live board Read 2026-09-12T03:4xZ, REST, repo-scoped, over every open `pm:dispatched` card: - **28** open `pm:dispatched` cards. - **5** of them (18%) have a newest claim-shaped comment that parses to ZERO branches: **#16310, #16268, #16251, #16175, #15234**. - 22 parse; 1 (#13597) carries no claim-shaped comment at all. Four of the five claims were posted by ONE session (`session_012GKcPZbMoGq7WPzKLfRBTU`, the `domain:devx` execution PM seat) within **three seconds** of each other, 02:53:05Z–02:53:08Z, all in the same shape: ``` Claim: session_012GKcPZbMoGq7WPzKLfRBTU · claude/issue-16310-orphan-locale-key-gateable Clause-②: yes ``` That is not a run of typos; it is a template emitting a carrier no reader accepts. **#16175 is the silent-fallback shape, live today**: its newest claim (5642984850, 2026-09-12) parses to zero, so governance falls back to its 2026-09-06 claim (5557414924) — which names a **different** branch, `claude/issue-16175-regen-sibling-stale-rules` against the current `claude/issue-16175-staleness-mtime-false-refusal`. Every downstream reader is probing the wrong ref, and the two `Clause-②` values happen to AGREE, which is the #16589 near-miss recorded on #16322 reproducing itself. ⛔ No `.claude/**` edit is made from this card, per the dispatch. The template finding is handed to the skills-lane seat; after this PR lands, those five cards are visible rather than silent, which is the point. ## Four-axis reading of option 1 vs option 2 - **实际业务需求** — measured, not speculative: 5 live carriers today, plus the #16322 cost (two rounds, a director re-review, a re-issued claim) and its near-miss sibling. Option 1 addresses the one spelling in front of us; option 2 addresses the population that produces them. - **项目长远合理性** — option 1 is the treadmill the card names and #16170 already bought once; each widening buys one spelling and leaves the next silent. Contract-first says the defect is at the reader's CONTRACT (a two-valued answer where three states exist), not in its accept set. - **防 AI 写代码犯错** — decisive here. Option 1 is consumer-side tolerance — the exact shape the axis forbids, and its failure mode is the silent one. Option 2 is a loud refusal at read time: a claim written in a shape the protocol does not accept is refused with the remedy named, and the seat cannot declare a claim the tooling does not honour. - **创业阶段不扩散需求** — option 2 adds no capability surface: one new reading of data already in hand, no new request, no new exit code, no new label written. Option 1 would grow the accept set permanently for a spelling the standing rule already forbids. ⇒ Option 2, on all four. Option 1 is deliberately NOT also done: doing both would let the accept set absorb the measured spelling and leave the new state unexercised on the live board, which is the one way to ship this fix and still not know whether it works. ## Verification All exit codes captured before any pipe (`cmd > log 2>&1; EXIT=$?`). | command | verdict line | exit | |---|---|---| | `pnpm check:pm-half-states` | `✓ check-half-states self-test: 3656 cases pass.` (was 3616) | 0 | | `pnpm check:pm-clause2-carriers` | `✓ check-clause2-carriers self-test: 493 cases pass (...)` (was 465) | 0 | | `pnpm check:pm-dispatch-gates` | `✓ dispatch-gates self-test: 1678 cases pass.` | 0 | | the derived union, 41 commands | `✓ dispatch-gates --ran: 41 derived famil(ies) accounted for — 41 run, 0 NOT-MEASURED (a DERIVED zero — all 41 recorded an exit code and none of them is 3).` | all 0 | The union was re-derived and re-run **after** the final commit, on head `b2b55c2b4` (`git rev-parse --short HEAD`), with `--repo objectstack-ai/objectstack` asserted. The first derivation printed a STALE TREE clause naming `check-skill-line-ratchet.mjs` and `check-widening-tells.mjs` (PR #17760 had landed); `origin/main` was merged in, the list re-derived byte-identically at 41, and every command re-run on the merged head. Reconciliation was fed `command :: exit N` lines so the zero is derived, not claimed. ⚠️ NOT MEASURED, and named rather than implied: the 47 artifact-roster families, the 11 declared-wide-population families, the 4 families taking a value from the workflow, and the 1 path-scheduled CI job are each outside the derived total — CI's, not this run's. ### Ablation — the new state can actually fail Committed first, then mutated on disk, then restored; the mutation and the restore are both proved by `git hash-object` against the HEAD blob rather than by an exit code. - Mutation: the pre-fix silent discard restored (`malformed` never populated). Anchor occurrences 1 → 0, file hash `f9f869cd` → `013b485b`. - Result: **20 of 3656** half-states cases and **14** carriers cases turn RED, every one of them in the new batteries. Direction: turns red, as pre-registered. - Restore: `git checkout HEAD -- scripts/pm/check-half-states.mjs`; `git diff HEAD` empty, hash back to `f9f869cd`. The script carries `trap ... EXIT INT TERM`. ## Live verification — the fix, run against the real board Both sweeps were run on the merged head, 2026-09-12T04:0xZ. **`check-half-states.mjs` (sweep, exit 0)** emits exactly **5** H60 rows — the same five cards the independent REST census found, arrived at through the fix's own code path: #15234, #16175, #16251, #16268, #16310. Both sentence variants fire live: #15234 gets the "NOTHING governs this card" reading, #16175 gets "governance SILENTLY FELL BACK to an OLDER claim (2026-09-06...)". **`check-clause2-carriers.mjs` (sweep, exit 2)** reports **5 of 18 card/PR pairs UNJUDGED** — and two of them (#15627 via PR #17776, #16565 via PR #17310) are cards the `pm:dispatched` census could not see, because they are not in that population. Before this change the sweep exited 0 on all of them. **The before/after, measured rather than reasoned** — the base tree at `813f8e9f` materialised with `git archive` and its `cardDeclaration` run against the same live comment rows: | card | BEFORE | AFTER | |---|---|---| | #16565 | `declared` `no` (`"Clause-②: no"`) | `claim-branch-unparsed` | | #16175 | `declared` `no` (`"Clause-②: no"`) | `claim-branch-unparsed` | | #15627 | `missing` | `claim-branch-unparsed` | Two of the three read as a confident `Clause-②: no` taken off a comment whose governance was never established — the #16589 near-miss shape, twice, on today's board. The third read as `missing`, which sends the seat looking for a declaration line that is already written. Neither was a reading anybody could have found without opening the card by hand. ## Fixtures The measured bodies are quoted, never paraphrased, so a future widening cannot make these batteries pass by accident: - #16322's two claims (5593513389 `Clause-②: no`, 5594909614 `Clause-②: yes`) — the inline spelling, and the two values DISAGREE, which is why reading the wrong one was a wrong answer and not merely an unlucky one. ⚠️ Re-measured: the OLDER claim is branchless too, so on that card nothing parsed at all and `cardDeclaration`'s `pool = claimRows` fallback read the FIRST claim in thread order — a second silent fallback the card did not name, and the same fix reaches it. - The live 2026-09-12 specimen on #16175, with its different-branch fallback. - #16170's bulleted directive still parsing (its pin stays green, untouched at the H20 branch battery). - Controls: a well-formed newest claim still governs; an older branchless claim beside a well-formed newest one raises nothing; no claim comment at all still reads `absent`; an unreadable thread still reads `unreadable`. - The negative pins that keep this a STATE: the inline spelling still parses to ZERO, and `CLAIM_COMMENT_MARKER` still matches it. ## Acceptance notes - `skip-changeset` applies: `scripts/pm/**` ships in no package (fast-track path, no measurement owed). - `Clause-②: no`, as the claim declares — nothing published moves. - Scope held: no H22 docblock prose (#17626 owns it), no widening tells (#17618), no `.md`, no `.claude/**`. - H60 takes the `state` band, beside H34 — H34 reads a claim whose SEPARATOR the marker refuses, this one reads a claim whose BRANCH line the directive reader refuses; both are a live card contradicting itself, repaired on the board. ⛔ Not `stall`: the row does not claim the card is stopped, and the dev may be working perfectly well — what is broken is the READING. - ⛔ No age gate on H60, unlike H20's 60 minutes: a branchless claim is wrong at the instant it is posted and no later sweep frees it, because the protocol forbids a second `Claim:`. Pinned in both directions. - noted, not filed: `cardDeclaration`'s `pool = claimRows` path (used when nothing on the thread parses) picks the FIRST claim comment in thread order rather than the newest. This PR makes that path unreachable for the branchless case, so the residue is a recency question on a path no live shape now reaches. Successor: whoever next touches `cardDeclaration` — #17098 is open against that function's neighbourhood. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6548118 commit 1e5b5e0

2 files changed

Lines changed: 527 additions & 33 deletions

File tree

0 commit comments

Comments
 (0)