feat(lint): refuse a body that binds a closing keyword to a card its own sentence says it is not closing (H21 promotion) - #16444
Merged
Conversation
…own sentence says it is not closing Promotes the half-state sweep's negation-window predicate from a report-only patrol row to this blocking PR gate, as a new finding kind under the existing contradiction exit. The predicate is imported, not re-spelled: the gate already delegates its Part-of rule to the same module, and a second copy of the grammar is the thing the parity guard exists to prevent. The gate side is the whole change. The predicate, its corpus and its markers are untouched, and the wiring workflow needs no new input -- it already hands over the body this rule reads. Seventeen self-test cases land with it, in their own registered battery: two measured specimens quoted byte-for-byte, the same-register bodies that must stay green, the sentence-window bounds, the backtick escape, and the disjointness that keeps one card number from being reported by two rules at once. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Two batteries carried the same case name, so a red on either printed a line that did not say which rule stopped working -- the exact ambiguity the battery roster exists to remove. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Contributor
Author
Verdict: ACCEPTImplemented-by: os-dev flight U (opus), branch What was checked
Rulings
验收备注 (noted, not filed)
LandingNot governed, ordinary queue landing: the seat flips the draft to ready and arms auto-merge (SQUASH). On merge the seat strips Generated by Claude Code |
os-steve
marked this pull request as ready for review
September 7, 2026 02:18
os-steve
enabled auto-merge
September 7, 2026 02:18
This was referenced Sep 7, 2026
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 #10947
Stage 2 of the two-stage order: the negation-window predicate H21 moves from the
report-only patrol row to the blocking PR gate. Gate side only — the predicate,
its markers and its corpus are untouched, and the wiring workflow needed no new
input because it already hands over the body this rule reads.
The premise came first, and it decides the rest
The card's restart condition is: after 2026-09-04, the patrol runs since H21
landed report zero H21 false positives — where a false positive is a flagged PR
whose sentence was in fact a correct close, and the specimen class is not one.
Measured two ways before a line was edited.
(a) The live sweep, read now
node scripts/pm/check-half-states.mjsagainst this repo, read-only, exit 0 —306 half-states over 439 cards and 30 open PRs, and zero H21 rows. The
newest recorded run agrees in the form that names the family explicitly: the
patrol anchor's body, written by run 34055667490 (swept 2026-09-06T19:46:09Z),
lists H21 among the families that "were EVALUATED and found nothing".
(b) The recorded runs — what could be read, and what could not
64
Live half-state sweepruns onmainsince the row landed(2026-08-21T20:09:43Z), from 2026-08-22T02:05:12Z to 2026-09-06T19:41:09Z, every
one
completed / success. Their logs cannot be read from this container, andthe reason is a route refusal rather than expiry: the run-logs endpoint answers
302 to a results host the egress proxy denies (
connect_rejected, organizationpolicy) — measured identically on the oldest, the middle and the newest run.
GraphQL, which would serve the anchor's body edit history (the archive the
patrol workflow itself names), answers 403 to this seat. Two channels, both shut;
neither was expiry, and neither is padded here with recall.
So the recorded-run leg was replaced with a stated substitute that is strictly
wider than the thing it replaces. H21 reads the OPEN pull request listing at
sweep time, so anything the 64 runs could have flagged was a PR open at some
point in the window. That population was enumerated in full — every currently
open PR, plus every PR closed with activity after the row landed — and every
body was run through the shipped predicate, imported, not re-implemented:
created_atrangeThe three flags, each classified
One adjacent finding filed rather than fixed: #11745... is walked deliberately, not as an afterthought: a scan omitting it reports 0 cycles on the pre-fix #13513The person still cannot register a login, because per the 2026-09-02 ruling recorded on closed #14349None of the three is a correct close that H21 refused, so the false-positive
count is zero and the premise holds.
#11746 is a second specimen of the original incident, and it is the strongest
argument in this PR. Its body said an adjacent finding had been filed rather
than repaired, bound a closing keyword to that card in the same sentence, and
card #11745 went to
completedtwo seconds after the merge(15:33:58Z merge, 15:34:00Z close, 2026-08-24). That card is
bug+domain:spec, still carriespm:queue, and describes a real unfixed defect — ithas read as finished ever since. ⛔ Nothing here touches its state: a state this
flight did not set belongs to another actor, and it is reported for the seat to
judge, not corrected.
The patrol could not have caught it, and no schedule fixes that. PR #11746
was open for 41 minutes — 14:52:47Z to 15:33:58Z — entirely between the
2026-08-24T13:56Z and 19:48Z sweeps. A six-hourly report-only row is
structurally blind to a short-lived pull request. A PR-time gate is not. That is
the asymmetry the stage-2 decision was waiting on, and it now has a measured
case rather than an argument.
The other two are true negations rather than harms: both bind a keyword to a
card the sentence is merely referring to, and in both the named card was
already closed before the merge — so the parser had nothing left to close. That
was luck, not correctness, and the remedy in both cases is one pair of backticks.
What changed
One file,
scripts/check-partof-closing-keyword.mjs. RULE 3 is added beside theexisting rules as a new finding kind under the existing contradiction exit —
a body that negates the relation it also states is the class this gate already
refuses, so it is not a new exit code.
h21NegatedClosingKeywordwasalready exported, so no import-side change was needed and
scripts/pm/check-half-states.mjsis byte-identical toorigin/main. Afourth copy of the closing-keyword grammar is exactly what the parity guard
exists to prevent.
the backtick advice, and says in the same breath that backticks are not the
escape in a commit message — that surface is RULE 2's and is not markdown.
Part ofis RULE 1's row andthe predicate skips it, so one card number can never be reported twice.
surface must never present itself as one that judged both — this file's own
doctrine, applied to the rule it just gained.
.github/workflows/partof-closing-keyword-guard.ymlis untouched: the gateneeds no input the workflow does not already pass.
Self-test: 66 → 83 cases, in a battery of its own
Seventeen cases in one registered battery (roster floor 15 → 16). Two measured
specimens quoted byte-for-byte, the control, the same-register bodies that must
stay green, the window bounds, the backtick escape, the disjointness, and the
remedy text pinned phrase by phrase.
The ruling's "the specimen class is NOT refused" admits two readings, so both
are pinned rather than chosen between: the near-miss register that looks like the
specimen but binds no keyword to a number stays clean, and a number already
declared
Part ofis never double-reported by RULE 3.Reverse verification
Ablation. Committed first, so the restore leg had an implementation to point
at. The one call was replaced with a constant; the mutation was proved on disk
before the run — removed text
1 → 0occurrences, injected text1, blob hash03724543a3→feedd0bb22— and the script carried atraprestoring anabsolute path.
The restore leg is proved by state, not by an exit code:
git checkout HEAD --against an absolute path, then the blob hash and an empty diff.
Against real pull request bodies, both taken from the H21 reading above:
Had this gate existed on 2026-08-24, PR #11746 would have gone red and card
#11745 would still be open.
Gates
Union derived after the final commit —
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no path arguments, change setre-derived from the merge base at
90c2b5dacf. Every exit captured beforeany pipe.
--ran→✓ dispatch-gates --ran: 31 derived famil(ies) accounted for — 31 run, 0 NOT-MEASURED.pnpm check:partof-closing-keyword→✓ check-partof-closing-keyword self-test: 83 cases pass.node scripts/pm/check-half-states.mjs --self-test→✓ check-half-states self-test: 2636 cases pass.— unchanged, on a file this PR does not touch.pnpm check:pm-clause2-carriers→✓ check-clause2-carriers self-test: 190 cases passnode scripts/pm/check-governed-merges.mjs --teston the final file list →exit 0,
✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.pnpm check:nul-bytes→ exit 0; the edited file was also scanned directly forraw control bytes,
grep_exit=1, no matches.pnpm lint(eslint . --no-inline-config) throughscripts/pm/os-verify-lock.sh, slotissue-10947, acquired once and held:VERDICT command-exit 0 · held the lock 86s · waited 0s. No narrowing wasneeded, so none is claimed.
skip-changeset:scripts/**is internal tooling that publishes nothing.验收备注
noted, not filed— under the finding admission threshold, neither is areproducible defect, a contract violation, or a metadata-authoring trap:
translateFlowwalksflow.nodesflat — a screen node inside an ADR-0031 region is never overlaid, in the fourth pass of a class three earlier cards each fixed once #11745 currently reads as finished and is not. Closedcompletedbythe auto-close described above while still carrying
bug,domain:specandpm:queue. It is outside every patrol window now (H22's closed-card horizon is3 days), so nothing on the board will surface it again. The seat's call —
⛔ no state was touched here.
the match sits inside
pre-fix, because-is a word boundary. WhetherGitHub's own parser fires there could not be measured: the named card had been
closed a day before that merge, so there is no natural experiment, this shape
occurs exactly once in 2,285 bodies, and the field that would answer it is
GraphQL-only and 403 here. If GitHub does not fire on it, H21 is marginally
wider than the parser it models and a body like that one would be redded for a
risk it does not carry. Raised as an open question rather than fixed: the
predicate is out of this card's surface by ruling.
🤖 Generated with Claude Code
https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Generated by Claude Code