Skip to content

fix(pm): a legal exit for a card whose sibling carries the clause-② declaration - #16832

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-16304-clause2-sibling-declaration
Sep 8, 2026
Merged

fix(pm): a legal exit for a card whose sibling carries the clause-② declaration#16832
hotlong merged 2 commits into
mainfrom
claude/issue-16304-clause2-sibling-declaration

Conversation

@hotlong

@hotlong hotlong commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #16304

check-clause2-carriers --pair derives one pair per card a PR delivers and demanded a Clause-②: 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 readinga 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 Fixes naming card 15542, and on a separate line a closing keyword naming card 15854. --pair 16243 therefore built two pairs, read Clause-②: yes from #15542's claim comment on the first, and exited 4 on the second.

Card #15854 has no claim comment and by maintainer ruling 5557098503 never gets one — 「both halves are one domain:spec PR, and this card closes when that PR lands」. It also carried pm:retriage, where a Claim: 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.

⚠️ The pairing was correct and that is what narrows the question: GitHub itself auto-closed card 15854 one second after the merge, on the same relation prDeliversCard reads. 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_UNSETTLED already 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:

  1. The reading fires only on absent — no comment on the thread begins a line Claim:. 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 reads missing, keeps its C2 row, and keeps its exit 4. malformed and misplaced are likewise untouched — in each of those a seat did read the card and owes it something a legitimate act can supply.
  2. The Clause-②: yes | no machine 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 cardDeclaration over the same imported CLAIM_COMMENT_MARKER, and only a declared state 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. prDeliversCard is narrowed by not one card (direction 2, which the landing measurement already excluded).

The exit table — why this rides on EXIT_OK

The decisive fact is the consumer. references/contract-review.md states 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:

  • 2 (UNJUDGED) keeps meaning could not read. Nothing here is unread: the sibling's claim comment was read, in the fixed spelling, and the row quotes the line.
  • 4 (adverse) keeps meaning what it means today. This reading is not adverse — the limb is legible, on a carrier the PR itself designates by delivering it. Byte-identical exit-4 controls are pinned below.
  • 0 already carries a message today (the ✓ line, plus the widening clause). The file's ⛔ against "0-with-a-message" is about rendering an adverse verdict as 0; the docblock now says so explicitly at both entries, so a later reader does not have to re-derive the boundary.
  • Not silent. The row prints in --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 (a Fixes naming 15542, a closing keyword naming 15854; #15542's thread carrying the claim whose line is Clause-②: yes; #15854's thread carrying only its ruling comment).

run before after
specimen — PR #16243, both pairs exit 4 on the second pair, C2 row: "the CLAIM COMMENT is what is missing" exit 0, one C2-SIBLING note naming card #15854, PR #16243, sibling card #15542 and its value yes
control A — a PR delivering exactly ONE card, no claim on it (the #13914 shape) exit 4 exit 4, byte-identical
control B — two cards, neither carrying a claim with a Clause-②: line exit 4 on both exit 4, byte-identical
control C — sibling declares, but the subject card was dispatched (claim comment present, no line ⇒ missing) exit 4 exit 4, byte-identical

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

  • Self-test: 260 → 299 cases, exit 0. New battery "[finding] check-clause2-carriers --pair demands a Claim: 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_FLOOR raised 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.
  • Among the 39: the C2 row under the control is asserted byte-identical to c2DeclarationUnreadable(pair) — the pre-change function, called directly — so a future edit that changes that row cannot pass silently.
  • Ablation. Deleting the sibling lookup (an early return before the loop that finds it): mutation proven on disk (marker grep 0 → 1, blob 5f43696cbddcd976), 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 to 5f43696c7d717bf962ea45173ffa57ec64aa9a68, marker count 0, git diff HEAD empty and git status --porcelain empty. ⚠️ The first mutation attempt was a no-op — Perl \Q…\E cancelled 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 --ran reconciles: 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 when check-half-states.mjs was expected in the change set, and it is read-only here.

pnpm check:pm-clause2-carriers, pnpm check:pm-half-states and pnpm check:pm-dispatch-gates (1561 cases) are green. pnpm exec eslint on the changed file: clean. check-governed-merges --test on the file: exit 0, NOT governed — ordinary queue landing; no .md was added.

skip-changeset: scripts/pm/** is not published — nothing in any package's files[] moves.

验收备注

Observations for the reviewing seat. None is filed, and none is folded into this PR.

  1. ⚠️ The commit message quotes the two delivery lines literally, so it contains a closing keyword adjacent to cards 15542 and 15854. Force-push is forbidden, so it could not be amended after the push. Both cards are already closed as 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.
  2. references/contract-review.md describes 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 .md would flip this PR onto the governed surface — so it is left to the seat that owns it, which is the seat reviewing this PR.
  3. The separable UNJUDGED / label-events reading at the foot of the card is not folded in here, per the triage ruling. See the report's out-of-scope section for its status.
  4. renderSweep now emits a notes array in --json alongside rows. Additive; no existing key changed shape.

Generated by Claude Code

… 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

hotlong commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Queue-entry record under ruling C (#16502, director record 5565958378, maintainer 「同意」) — skills seat, session session_01P58euzUXCVJNwmhuPC9DXY, 2026-09-08T09:24Z.

The check Part-of PR must not also close its card (workflow run 34209395257) is red on head ddade65c5 because the branch's commit message quotes PR #16243's two delivery lines literally — a closing keyword beside cards 15542 and 15854, both already closed as completed (the author flagged it as item 1 of the PR's 验收备注; history rewrites are forbidden, so no author action clears it). Ruling C's three conditions, each read now rather than recalled:

  • (a) the gate's own source names this red as by-design on a pushed branch — RULE 2 of scripts/check-partof-closing-keyword.mjs: its output's BRANCH ALREADY PUSHED — paragraph says no author action clears this red and that this is expected, and its header (RULE 2) says the same.
  • (b) the gate does not run on merge_group.github/workflows/partof-closing-keyword-guard.yml on origin/main f89812e4d triggers on pull_request with types: [opened, edited, reopened, synchronize] only; its header states the missing merge_group: trigger is deliberate.
  • (c) this comment records which check and why.

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 squash_merge_commit_message = COMMIT_MESSAGES (measured 09:02Z on the #16802 squash) the squash body will carry the two quoted spans; both cards are closed, so no card state can change — the same residue the ruling accepted on #16470, and the one setting D (PR_BODY) removes once the maintainer applies it. Every other check run on ddade65c5 must read success/skipped before the seat flips this PR ready and arms auto-merge; ⛔ nothing else is admitted under this record.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants