fix(pm): a legal exit for a card whose sibling carries the clause-② declaration - #16832
Conversation
… whose sibling carries the clause-② declaration `--pair` derived one pair per card a PR delivers and demanded a `Clause-②:` line from EVERY one of them. Measured on PR #16243 (`Fixes #15542` / `Closes #15854`): the first pair read `Clause-②: yes` from #15542's claim, and the second exited 4 naming a remedy no legitimate act could supply — #15854 by its own maintainer ruling closes when that PR lands, is never separately dispatched, never receives a claim comment, and carried `pm:retriage`, where a `Claim:` is a dispatch act the PM protocol forbids. GitHub closed #15854 one second after the merge on the same relation, so the pairing was right; the place the declaration was demanded from was not. Direction 3 of the filing, as the triage seat scoped it: the predicate is untouched and the limb gets a FOURTH READING — a card delivered by a PR whose SIBLING card carries the declaration. It prints a row naming the card, the PR, the sibling and the value read from that sibling's claim, and it rides on EXIT_OK: `references/contract-review.md` states the landing check as 「0 = 双肢一致…4 = 任一不成立」, so any new non-zero code would re-block the legal workflow while saying nothing the row does not. ⛔ No spelling is relaxed: the sibling is read by the same `cardDeclaration` over the same imported `CLAIM_COMMENT_MARKER`, and only a `declared` state on its own claim comment counts. ⛔ `prDeliversCard` is not narrowed by one card. ⛔ The #13914 absence cannot reach this reading: it fires only on `absent` (no claim comment at all — the state of a card that was never separately dispatched), so a dispatched card missing its line reads `missing` and keeps its C2 row and its exit 4; and #13914's own shape is one card and one PR, with no sibling to read. Both halves are pinned as controls in the self-test. The sweep counts the reading apart from the two owings, for the same reason `absent` and `missing` were split: a card that owes nothing must not be counted beside cards that owe a claim comment. Self-test 260 → 299 cases; new battery of 39, floor raised by exactly the one battery added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P58euzUXCVJNwmhuPC9DXY
…ause2-sibling-declaration
|
Queue-entry record under ruling C (#16502, director record 5565958378, maintainer 「同意」) — skills seat, session The check
It is not a required context (the ruling's own reading of this same gate on PR #16470). Residual risk of landing: under today's Generated by Claude Code |
Fixes #16304
check-clause2-carriers --pairderives one pair per card a PR delivers and demanded aClause-②:line from every one of them. On a PR that delivers two cards, the second card's row reported a missing reading that no legitimate act could supply — so exit 4 was unreachable-by-construction on a fully compliant workflow.This is direction 3 of the filing, as triage scoped it: the predicate is untouched, and the declaration limb gets a fourth reading — a card delivered by a PR whose sibling card carries the declaration. One file changed, 346 insertions.
The measurement, restated from the card
PR #16243 carried two delivery lines in its body: a
Fixesnaming card 15542, and on a separate line a closing keyword naming card 15854.--pair 16243therefore built two pairs, readClause-②: yesfrom #15542's claim comment on the first, and exited 4 on the second.Card #15854 has no claim comment and by maintainer ruling
5557098503never gets one — 「both halves are onedomain:specPR, and this card closes when that PR lands」. It also carriedpm:retriage, where aClaim:is a dispatch act the PM protocol forbids outright. And this tool's own ⛔ forbids filling the line in on the claiming seat's behalf. Three independent rules, one unreachable remedy.prDeliversCardreads. The predicate asks the right cards; it asked one of them in a place a legitimate workflow cannot fill. A where, not a whether.Why direction 3 and not direction 1 — the four axes
实际业务需求. Measured, not speculative: one live blocking exit 4 on a compliant PR (#16243), and the filing seat correctly refused to clear it. Direction 1 (substitute the sibling's declaration for the card's) and direction 3 (report what was actually seen) both unblock that PR. What separates them is what a later reader is told. Direction 1 makes the reading invisible — card #15854's row would simply disappear, and the run would say the same thing about it that it says about a card that declared for itself. Direction 3 prints a row that names the card, the PR, the sibling, and the value read from that sibling's claim. C2's whole value is that its rows are trustworthy; a row that says where a judgement lives is worth more than a silence that implies it lives here.
项目长远合理性. The declaration limb was keyed on the card while the judgement it reads is a property of the PR's contract increment. Those coincide for one-card-one-PR and come apart at two cards. Direction 3 states that fact in the instrument instead of papering it: the row asserts a location, never a substitution, and says so in as many words. That is contract-first — the tool reports the shape of the thing rather than smoothing it — and it leaves the real protocol question (whether a second card ought ever to be separately declarable) open for the maintainer rather than answering it by fiat, which is exactly the call
RANKING_UNSETTLEDalready makes one row up in this file.防 AI 写代码犯错. This is the axis that decides it, and it is the triage seat's own question: how does the change avoid reading a real #13914-class absence as declared? Direction 1 answers by argument; direction 3 answers by construction, with two structural guards that are pinned as controls:
absent— no comment on the thread begins a lineClaim:. That is precisely the state of a card that was never separately dispatched. A card that was dispatched carries a claim comment by protocol, so a dispatched card whose declaration line is missing readsmissing, keeps its C2 row, and keeps its exit 4.malformedandmisplacedare likewise untouched — in each of those a seat did read the card and owes it something a legitimate act can supply.Clause-②: yes | nomachine spelling is missing from the claim comment on 2 of 3 measured cards — the enqueue gate's predicate reads it there, and it is not there #13914's own shape is one card, one PR. It has no sibling, so it cannot reach this row at all.⛔ And no spelling is relaxed anywhere: the sibling is read by the same
cardDeclarationover the same importedCLAIM_COMMENT_MARKER, and only adeclaredstate on the sibling's own claim comment counts. Prose on the sibling is prose; a malformed value is malformed; a misplaced line is misplaced; an unreadable thread is not a carrier. #12409's boundary is exactly where it was — the gap was in which carrier is asked, never in what counts as an answer.创业阶段不扩散需求. No new flag, no new config, no new surface a seat has to learn. One reading, one row code, no capability that is declared and not enforced.
prDeliversCardis narrowed by not one card (direction 2, which the landing measurement already excluded).The exit table — why this rides on
EXIT_OKThe decisive fact is the consumer.
references/contract-review.mdstates the landing check's precondition ② as 「该命令 0 = 双肢一致且无放宽 tell,4 = 任一不成立,3 = 环境答不了」. A seat reads$?and proceeds on 0. Any new non-zero code would be the same refusal wearing a different number — it would satisfy "distinguishable" while failing this card's acceptance, which is that exit 4 has a legal exit.The existing meanings all hold, and none is stretched:
--pair(on stderr, beside the rows, marked as a note) and in the sweep, and the ✓ line changes to say the declaration is readable on a sibling card rather than on this card. A seat reading only$?can still find out from the run why this pair answered 0 without a declaration of its own.The rule this table already had is the row says which, the exit says only the class. This follows it rather than carving an exception.
Readings — specimen and controls, before and after
Offline through
--pair-json, built from the card's quoted body (aFixesnaming 15542, a closing keyword naming 15854; #15542's thread carrying the claim whose line isClause-②: yes; #15854's thread carrying only its ruling comment).C2-SIBLINGnote naming card #15854, PR #16243, sibling card #15542 and its valueyesClause-②:linemissing)Control C is the #13914 guard proper: the same declaring sibling, the same PR, and the reading does not fire, because the subject card carries a claim comment. Byte-identity was measured by diffing the two runs' output with only the read-path provenance line stripped (it names the script path).
Tests
check-clause2-carriers --pairdemands aClaim:comment on EVERY card a PR delivers — including a card that by its own ruling is never separately dispatched, so exit 4 is unreachable-by-construction #16304: the fourth reading — one PR, two cards, and the controls that keep exit 4", floor 39.SELF_TEST_BATTERY_FLOORraised 14 → 15, i.e. by exactly the one battery added, so the roster's existing slack is preserved rather than tightened or loosened as a side effect.c2DeclarationUnreadable(pair)— the pre-change function, called directly — so a future edit that changes that row cannot pass silently.5f43696c→bddcd976), then self-test reds with 20 failures, all in the new battery; the specimen returns to exit 4; and all three controls stay exit 4, byte-identical to HEAD under the ablation too. Restore proven by blob hash back to5f43696c7d717bf962ea45173ffa57ec64aa9a68, marker count 0,git diff HEADempty andgit status --porcelainempty.\Q…\Ecancelled the manual escapes in the anchor — and the script's own on-disk landing check caught it and refused to report a reading; it is recorded here rather than quietly retried.Gates
Derived in the worktree after the commit,
--repo objectstack-ai/objectstack, no paths: 34 families from the one changed path. Ran the union of those and the dispatch list (37 commands) — all exit 0. Exit codes captured by redirect before any pipe.dispatch-gates --ranreconciles: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. Three commands on the dispatch list are outside this card's derivation and were run anyway — they were derived whencheck-half-states.mjswas expected in the change set, and it is read-only here.pnpm check:pm-clause2-carriers,pnpm check:pm-half-statesandpnpm check:pm-dispatch-gates(1561 cases) are green.pnpm exec eslinton the changed file: clean.check-governed-merges --teston the file: exit 0, NOT governed — ordinary queue landing; no.mdwas added.skip-changeset:scripts/pm/**is not published — nothing in any package'sfiles[]moves.验收备注
Observations for the reviewing seat. None is filed, and none is folded into this PR.
completed, so no card state can change — but if the queue squashes commit messages into the merge commit, the seat may want to trim those two spans out of the squash body. The PR body above deliberately never places a closing keyword next to a foreign card number, so the PR itself derives no stray pair.references/contract-review.mddescribes precondition ② as 「0 = 双肢一致」. That instruction stays operationally correct (0 still means proceed), but its description of 0 is now one reading short. The file is.claude/skills/pm-dispatch/**, outside this card's declared file surface, and adding an.mdwould flip this PR onto the governed surface — so it is left to the seat that owns it, which is the seat reviewing this PR.renderSweepnow emits anotesarray in--jsonalongsiderows. Additive; no existing key changed shape.Generated by Claude Code