fix(pm): tell a completed clause-② review from a gate that never ran - #14174
Merged
Conversation
`--pair` answered C3 / exit 4 on a legitimately cleared pair: a `Clause-②: yes` declaration is history and stays on the thread, while the label is state that a completed review clears from both carriers by rule, so every pair that completes its review landed in exactly C3's trigger shape. Read literally, the landing check's precondition (2) could only be satisfied between the review PASS and the label clear — the wrong order. C3 now reads the `needs:contract-review` label EVENT STREAM on both carriers and answers four distinguishable states: never hung (the fail-open, unchanged), hung-then-cleared with the head unmoved (the completed state, clean), hung-then-cleared with the head moved since (the re-hang-owed state), and bound on one carrier only (the strip signature). An unreadable stream is UNJUDGED, never clean. No verdict comment is read: the PASS conjunct of the recovery rule stays human. No spelling is relaxed, and the tool still writes nothing. Event streams are fetched only for pairs already in C3's candidate shape, so sweep cost stays bounded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Msg17tAHJ3jVTYFgHydCm2
os-sam
marked this pull request as ready for review
September 1, 2026 10:16
os-sam
enabled auto-merge
September 1, 2026 10:16
Collaborator
|
Ready + auto-merge armed — provenance per the landing discipline.
Generated by Claude Code |
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.
Fixes #14155
check-clause2-carriers.mjs --pairanswered C3 / exit 4 on a legitimately cleared clause-② pair. AClause-②: yesdeclaration is history — it stays on the thread forever. The gate label is state — a completed review clears it from both carriers, by rule. So every clause-② pair that completes its review lands in exactly C3's trigger shape: declaredyes, gate on neither carrier. Read literally, precondition ② of the landing check could only be satisfied in the window between the review PASS and the label clear — the wrong order — and each landed pair kept its C3 row on every later sweep.What changed
C3 (and therefore
--pair) now reads theneeds:contract-reviewlabel event stream on both carriers and answers four distinguishable states:This mechanizes the recovery rule in
references/contract-review.md— 「PASS + 无标 + head 未动 = 已清标非被剥;head 后移或无结论才重挂」 — minus its PASS conjunct, which stays human. Precondition ① is still a person reading the PASS comment on the card.The three standing refusals are intact
Contract review: PASSmarker at 5 of 35 removals against 26 for an "any PASS token" reading — a check that can barely fail — and reading a verdict to clear a gate is the 自查放行 this file exists against. Exit 0 now means "the gate was bound by the discipline and cleared, and nothing has moved since", never "the review passed".Clause-②: yes/Clause-②: noremain the only two readings; the declaration reader is untouched.The exit register keeps its shape — 0/4/3(/2), 4 for pair-adverse, never 0-with-a-message — and the docblock's exit table is updated to match. Exit 2 already existed for
--pair; it now also covers an unreadable event stream.Cost
Event streams are fetched only for pairs already in C3's candidate shape (declared
yes, bare on both carriers), and the head commit only once both carriers read cleared.needsGateHistoryis the single predicate the live reader and the UNJUDGED accounting share, so the set that owes a stream and the set that gets one cannot drift apart. Measured live: full sweep 8s, 9 pairs, 0 UNJUDGED.Two readings that shaped the implementation:
null— UNJUDGED — when it cannot.head_ref_force_pushed) exists only on the timeline endpoint, which caps at 250 events and truncates silently; a capped read backing a clean verdict is the fail-open shape this family refuses everywhere else. The residual hole — a force-push landing a deliberately backdated commit — is named at the reading site, and the human PASS conjunct is what closes it.The gate label constant and the label-event vocabulary are imported from
check-half-states.mjsrather than restated, so this row cannot disagree with H31/H35 about what a gate event is. The repo-wide, windowed form of the one-carrier question stays H35's; this row is the per-pair form and is unbounded in time, and it points at H35 rather than re-judging it.Verification
Reverse-verified in two legs, each mutate → prove on disk → run → restore → prove restored (hash equals the HEAD blob,
git diff HEADempty):--pair 13864→ exit 4 with the C3 row on card The undeclared-field door sits in FRONT of the hooks, so a key a beforeInsert hook writes has no door at all — and the drivers then disagree (memory stores it, SQL throws a raw statement error) #13657. New tool, same pair, same minute → exit 0.2 of 98 case(s) failed, naming the completed-state cases. Restored: 98 pass.Self-test grew 64 → 98 cases, with fixture pairs for all four states replayed from the 2026-09-01 measurement (card #13657 hung 16:55:54Z / cleared 08:54:47Z; PR #13864 hung 15:19:53Z / cleared 08:54:56Z — nine seconds apart; head
9af92aa3dated 08:16:59Z).lint.ymlstill runs the self-test only; the network sweep stays out of CI.Gates at final HEAD
260a96e4d, exit codes captured before any pipe: the full derived union (dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, 15 commands, re-derived at final HEAD and identical) passes, plus repo-widepnpm lint(eslint . --no-inline-config, exit 0, full scan — no narrowing).check-test-completenessis NOT MEASURED rather than green: it grades a savedturbo run testlog that CI tees and no standalone invocation can produce.scripts/pm/**-only diff — publishes nothing from any package, soskip-changesetrather than a changeset file.Generated by Claude Code
Generated by Claude Code