Skip to content

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
os-steve merged 2 commits into
mainfrom
claude/issue-10947-h21-gate-promotion
Sep 7, 2026
Merged

feat(lint): refuse a body that binds a closing keyword to a card its own sentence says it is not closing (H21 promotion)#16444
os-steve merged 2 commits into
mainfrom
claude/issue-10947-h21-gate-promotion

Conversation

@claude

@claude claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

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.mjs against 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 sweep runs on main since 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, and
the reason is a route refusal rather than expiry
: the run-logs endpoint answers
302 to a results host the egress proxy denies (connect_rejected, organization
policy) — 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:

corpus value
PRs read (open at some point in the window) 2,285
bodies non-empty 2,285
created_at range 2026-08-20T13:02:51Z … 2026-09-07T00:27:44Z
H21 flags 3
false positives (a sentence that was in fact a correct close) 0

The three flags, each classified

PR the flagged sentence classification
#11746 One adjacent finding filed rather than fixed: #11745 specimen — true positive, and it cost a card
#14524 ... is walked deliberately, not as an afterthought: a scan omitting it reports 0 cycles on the pre-fix #13513 true negation
#15738 The person still cannot register a login, because per the 2026-09-02 ruling recorded on closed #14349 true negation

None 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 completed two seconds after the merge
(15:33:58Z merge, 15:34:00Z close, 2026-08-24). That card is bug +
domain:spec, still carries pm:queue, and describes a real unfixed defect — it
has 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 the
existing 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.

  • The predicate is imported, not re-spelled: h21NegatedClosingKeyword was
    already exported, so no import-side change was needed and
    scripts/pm/check-half-states.mjs is byte-identical to origin/main. A
    fourth copy of the closing-keyword grammar is exactly what the parity guard
    exists to prevent.
  • RULE 3 has its own remedy string, which keeps the three safe rewordings and
    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.
  • Disjointness is free: a number already declared Part of is RULE 1's row and
    the predicate skips it, so one card number can never be reported twice.
  • The clean verdict line now says RULE 3 was judged too. A run that judged one
    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.yml is untouched: the gate
    needs 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 of is 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 → 0 occurrences, injected text 1, blob hash
03724543a3feedd0bb22 — and the script carried a trap restoring an
absolute path.

ABLATED  EXIT=1   ✗ check-partof-closing-keyword self-test: 9 of 83 case(s) failed.
           the two specimens, the no-forked-rule check, and all six remedy pins red
RESTORED EXIT=0   ✓ check-partof-closing-keyword self-test: 83 cases pass.
           blob back to 03724543a3 · `git diff HEAD` empty for that 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:

PR #11746 (flagged, real body)   -> EXIT=1, and the output names the card,
                                    quotes the sentence, and offers the three
                                    rewordings plus the backtick escape
PR #16429 (control, real body)   -> EXIT=0, "carries no Part-of/closing-keyword
                                    contradiction and no closing keyword bound
                                    to a card its own sentence says it is not
                                    closing"

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 set
re-derived from the merge base at 90c2b5dacf. Every exit captured before
any pipe.

  • 31 derived commands, all exit 0. Reconciled: --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 pass
  • node scripts/pm/check-governed-merges.mjs --test on 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 for
    raw control bytes, grep_exit=1, no matches.
  • Whole-repo pnpm lint (eslint . --no-inline-config) through
    scripts/pm/os-verify-lock.sh, slot issue-10947, acquired once and held:
    VERDICT command-exit 0 · held the lock 86s · waited 0s. No narrowing was
    needed, so none is claimed.

skip-changeset: scripts/** is internal tooling that publishes nothing.

验收备注

noted, not filed — under the finding admission threshold, neither is a
reproducible defect, a contract violation, or a metadata-authoring trap:


🤖 Generated with Claude Code

https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox


Generated by Claude Code

…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
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 7, 2026
@github-actions github-actions Bot added the size/m label Sep 7, 2026
@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Verdict: ACCEPT

Implemented-by: os-dev flight U (opus), branch claude/issue-10947-h21-gate-promotion, head 90c2b5dacf
Reviewed-by: pm-dispatch skills seat (contract-tier read in a detached compare worktree at 90c2b5dacf, merge-base ac76425f0b)

What was checked

  • Premise first, as ruled, and it holds: the live sweep reads zero H21 rows; the recorded runs' logs are unreadable through this container's egress (a route refusal, stated as such, not padded), so the dev enumerated the strictly wider population instead — every PR open at any point since the row landed, 2,285 bodies through the shipped predicate: 3 flagged, 0 false positives under the card's own definition. One of the three is a second specimen of the original incident (PR feat(lint): give validate-translation-references a flows leg #11746 closed i18n: translateFlow walks flow.nodes flat — 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 two seconds after its merge, a real unfixed defect reading as finished; the PR lived 41 minutes between two sweeps). That is the measured case for a PR-time gate rather than a patrol row.
  • Gate side only: one file, scripts/check-partof-closing-keyword.mjs, +171/−6. h21NegatedClosingKeyword is imported (the export already existed; scripts/pm/check-half-states.mjs is byte-identical to origin/main, zero diff lines) and judged as RULE 3 under the existing contradiction exit, disjoint from RULE 1 by construction (a Part of number is RULE 1's and H21 skips it), with its own remedy string keeping the three rewordings and the backtick escape and saying that backticks are not the escape on the commit surface. The clean line now names RULE 3 as judged. The workflow file is untouched because the gate needs no new input.
  • Self-test 66 → 83 in one registered battery (17 cases: both real specimens byte-for-byte, the control, the near-miss register, the window boundaries, the disjointness pair, the no-forked-rule check, six remedy pins), battery floor 15 → 16; ablation of the one call reds exactly the predicted nine and the restore is proved by blob state. Live: the gate refuses PR feat(lint): give validate-translation-references a flows leg #11746's real body and passes PR docs(lint): security-anchor-high-privilege states its scope — declared everyone suggestions, not guest bindings #16429's; this PR's own body passes it.
  • Gates in the compare worktree: check:partof-closing-keyword 83 pass; check-governed-merges --test exit 0, NOT governed. CI on the head: 23 success / 11 skipped / 0 failure.

Rulings

  • Open question 1 (a closing keyword that is the tail of a hyphenated compound, one body in 2,285): A — the predicate stays as it is; the shape is a hard red with a one-backtick remedy, in the loud direction. No card for the throwaway-PR measurement: it is below the finding admission threshold, and it rides as a 验收备注 for the next flight that opens H21's register in check-half-states.mjs.
  • Open question 2 (which reading of "the specimen class is not refused"): A, as shipped — both readings pinned; neither narrows the gate.
  • Out-of-scope finding on i18n: translateFlow walks flow.nodes flat — 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: the seat reopens the card with the evidence (a state set by the auto-close, not by any actor's judgement); its labels stay for the spec lane to triage.

验收备注 (noted, not filed)

  • H21's word boundary admits pre-fix-style compounds; whether GitHub's parser fires there is unmeasured. Measure it with a throwaway PR bound to a deliberately open throwaway card when H21's register is next opened; do not narrow on a guess.
  • The recorded sweep logs are unreachable from a dev container (the run-logs endpoint redirects to a host the egress policy refuses; GraphQL answers 403 to the shared identity). A platform reading for the ledger, folded into the next readings increment rather than filed.

Landing

Not governed, ordinary queue landing: the seat flips the draft to ready and arms auto-merge (SQUASH). On merge the seat strips pm:dispatched and clears the assignee on #10947.


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

Development

Successfully merging this pull request may close these issues.

Promote H21 (negation-window closing-keyword detection) from report-only patrol row to the blocking PR gate, once the patrol baseline is long enough

2 participants