Repository navigation
feat(gates): give the closing-keyword parity gate a --body pre-flight mode - #17552
Merged
Merged
Conversation
There was no runnable instrument a PR author could point at a body to prove its closing keywords before the PR exists. The one grammar this repo grades for agreement is spelled three times and every spelling is private to its own file, so the only enforcement that reads a body is the blocking gate that fires after the body is written. This gate already builds the three shipped parsers as real RegExps from the shipped bytes. `--body <FILE|->` runs exactly those over one body and prints every hit with the parser id and its reference scope. No fourth spelling is introduced -- a transcribed grammar is the defect the sweep exists to catch, and it would go red on one. Exit register, documented in the header: 0 when no registered parser binds anything, 2 when at least one binds (the report, not a failure), 1 when the instrument did not run -- no path, an unreadable input, or a parser that could not be extracted. The case it exists for is a sentence written to prevent a close that arms one, because no parser reads negation. Six new self-test cases cover it: the negation body, a `Part of` body, the colon spelling on a qualified reference, and two end-to-end drives of the mode through a real process (a file and stdin) that grade the exit code. Claude-Session: https://claude.ai/code/session_01YKEjmbYNvYWJvWGSWx26zK Co-authored-by: Claude <noreply@anthropic.com>
…cwd's copy The two end-to-end cases spawned `join(root, SELF)`, and `root` comes from `repoRoot()` — the cwd. Launched from a sibling checkout the battery therefore spawned THAT tree's copy of the script and graded a file nobody had edited: measured, it reported `got 3` (the dependency-prerequisite exit of an uninstalled sibling) instead of the exit register under test. A harness that verified the wrong subject has verified nothing. The child is now `fileURLToPath(import.meta.url)`, so the process under test is the process that is running. Claude-Session: https://claude.ai/code/session_01YKEjmbYNvYWJvWGSWx26zK Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Sep 10, 2026
os-litant
marked this pull request as ready for review
September 10, 2026 22:37
This was referenced Sep 10, 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 #17533
scripts/check-closing-keyword-parity.mjsgains a third mode,--body PATH(alone
-reads stdin), beside--listand--self-test. One file, +159/-1.#NNNNN.Pasting the real ones into a PR body would arm exactly the auto-close this
instrument exists to warn about — code fences do not protect a body from
GitHub's parser — so the masking is not tidiness, it is the demonstration.
The gap it addresses
This repository ships one closing-keyword grammar in three spellings and grades
them for agreement. Every spelling is private to its own file: two live inside
actions/github-scriptblocks in workflows that deliberately never check therepo out, and the third is a module-private function in the PM lane's hot
script. So no runnable instrument could read a PR body before the PR
existed — the only enforcement that reads one is the blocking gate that fires
after the body is already written.
This gate, however, already builds all three parsers as real RegExps from the
shipped bytes (
loadParsers→parserFrom). It was one input away from beingthe body reader the card asks for.
Usage and exit register
Why 2 and not 1 for a hit.
1is already this gate's "the parsers disagree/ extraction failed" — #4690's rule, a harness that could not find its subject
has verified nothing. A body that legitimately declares a close must never
report through the same code as a broken instrument, so the hit code is
distinct. Callers must not collapse "non-zero" into one meaning.
It decides nothing and blocks nothing. It is not wired into CI as a body
check and no workflow calls it with
--body; its self-test cases ride theexisting
Closing-keyword parser paritystep inlint.yml, which already runs--self-testand the sweep.Design: one grammar, read, never transcribed
judgeBody(parsers, body)is the only new exported function. It runsbody.matchAll(parser.make())for every parser without aproblem— i.e. itruns whatever
loadParsers()returns and adds nothing. No new regex literallands anywhere in this diff. A fourth spelling transcribed into a body checker
is precisely the defect this gate's own sweep exists to catch, and the sweep
would go red on one; the sweep verdict below shows it did not have to.
The answer is per parser and deliberately not collapsed. The three differ on
purpose in reference scope (qualified-only / same-repo / bare-only — the
header's REFERENCE SCOPE paragraph), so which parser binds is the answer an
author needs, not a boolean.
Zone-2 readings (measured on this branch)
:487-499before the edit)process.argv[2]:--list,--self-test, elserun()— as quoted^export function/^functionin the parity scriptrepoRoot,workflowScript,parserFrom,loadParsers,sweep,judge,list,run,selfTestgrep -n "^export.*closingKeywordRe" scripts/pm/check-half-states.mjsgrep -c closingKeywordRe scripts/pm/check-half-states.mjs:1786plus six call sites and comment references); the export count is the 0.partOfRelikewise: 7 occurrences, 0 exports. The card's claim ("module-private") holds; only the arithmetic in the dispatch prompt was off24 assertions→30 assertions, exit 0 both timesOK (3 parsers agree … sweep found 5 file(s) … all registered), exit 0 both times — identical, so the edit registered as no fourth parser.github/workflows/lint.ymlstepClosing-keyword parser parityruns--self-testand the sweep.git grep -n closing-keyword-parity -- package.jsonreturns nothing, and the workflow's own comment says thenodeinvocation is deliberate (GATE INVOCATION IDIOM). ⇒ ⛔ no second wiring added;package.jsonandlint.ymlare untouchedThe scope demonstration
A body built from the card's own examples, run through the new mode (numbers
masked, per the warning at the top):
The body's three lines were a qualified colon reference, the sentence
Merging this must not VERB #NNNNN(VERB standing in for a real closing verb), andPart of #NNNNN. All threepredictions hold: the qualified reference binds in the two qualified-capable
parsers; the negation sentence binds in the same-repo and bare parsers and is
invisible to the cross-repo closer;
Part ofbinds nowhere. No parser readsnegation — that is the whole finding, and the mode shows it before the PR
exists rather than after.
All three exit legs, measured:
--body demo-body.md--body clean-body.md(onlyPart ofand a bare reference with no verb)printf … | --body ---bodyNo body was read, so nothing was verified.)--body missing.mdcannot read … ENOENT)[duplicate-fix-guard] … no job \duplicate-fix-guard` with steps -- renamed? The parser cannot be located.`Ablation (two-legged, from the committed state)
Mutation:
bodyModereads an empty string instead of the input — deliberatelyjudge()-independent, so every parser-vs-parser assertion stays green and onlythe new cases can catch it.
The mutation script carried an EXIT/INT/TERM trap that restores the file, with absolute
paths, the substitution was proved on disk by grep counts of both the deleted
anchor and the injected marker (not by the editor's exit code), and the restore
was proved by blob-hash equality plus an empty
git diff HEAD— not by an exitcode. Nothing permanent was added: the ablation lives only in this transcript.
Note the deliberate asymmetry: case X12 (a body that declares no close must
exit 0) stays green under this mutation, because an empty body legitimately
exits 0. That is correct — X11 is the leg that can fail, which is why the
two-legged proof needs the hit case and not only the clean one.
The ablation found a real defect in the new self-test itself, which the
second commit repairs. The two end-to-end cases originally spawned
join(root, SELF), androotcomes fromrepoRoot()— the cwd. Launched froma sibling checkout the battery spawned that tree's copy of the script and
graded a file nobody had edited, reporting
got 3(the dependency-prerequisiteexit of an uninstalled sibling) instead of the exit register under test. The
child is now
fileURLToPath(import.meta.url), so the process under test is theprocess that is running. Measured both ways: from a sibling checkout's cwd the
battery reported
got 3before the repair and✓ … 30 assertionsafter.Self-test cases added (+6 assertions, 24 → 30)
Driven from the existing
REFS/KEYWORDSfixtures rather than inventedspellings, so a case here cannot drift away from what the same file asserts
about the same three parsers:
Part ofbody binds nothing (judge()'s rule 4b asserts the same string from the parser side)The one judgement, on the four axes
The card offered three routes: export
closingKeywordRe()(andpartOfRe()),give this gate a body mode, or write a standalone body-sweep script. Real
business need is measured, not speculative: the negation trap fired on a real
PR in this repo and armed a genuine auto-close, and the dispatch loop was
already issuing an instruction naming an instrument that could not read a body —
the demand existed before the capability. Long-term soundness favours the
body mode: this gate already owns the extraction of all three parsers, so
reading them is free and the grammar stays single, whereas exporting one
private function publishes one of three scopes — an answer narrower than the
question — from the lane's hottest script, and a standalone script is a third
file to keep in parity. Not letting AI write it wrong is the axis that
decides it: a body mode leaves nowhere to put a fourth regex and the gate's own
sweep goes red if anyone tries, while an exported grammar is the lenient-consumer
shape — N call sites free to wrap it in their own tolerance — and the mode's
refusal to collapse the per-parser scope split keeps the author reading the
truth instead of a boolean. Startup-stage focus is satisfied in the cheapest
form available: one exported function, one argv branch, no new file, no new
package script, no new CI wiring, no published surface. The axes do not
conflict here; all four point the same way. The one cost worth naming is that a
gate whose stated job was parser-vs-parser agreement now answers a second
question — mitigated because both answers come from the same loaded parsers, so
there is no second source of truth to keep in sync.
Verification
Gate families derived from the FINAL diff by
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths — the script derives its own change set), every family run, every exit
code captured before any pipe, reconciled with
--ran:All 31 exited 0. Verdict lines from the load-bearing ones:
node scripts/check-closing-keyword-parity.mjsOK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 8334 tracked file(s), all registered).node scripts/check-closing-keyword-parity.mjs --self-test✓ … 30 assertions, 5 mutations of the shipped parsers each driven to red.pnpm check:pm-dispatch-gates✓ dispatch-gates self-test: 1678 cases pass.node scripts/check-self-test-wired.mjs✓ … every one of the 203 script(s) CI runs that ship a --self-test has that self-test run by CI.node scripts/check-scripts-symbol-anchors.mjs✅ … 3168 anchors across 246 scripts resolve …pnpm check:cross-package-test-inputsOK: 28 package(s) read outside themselves, all declared …pnpm check:nul-bytescheck-nul-bytes: OK (scanned 8327 text file(s) … no raw ASCII control bytes).Beyond the derived set:
npx eslint . --no-inline-config --format json→ exit 0, 6589 files linted, 0 errors, 0 warnings, taken at25a74a35. No scope claim is being made, because none was needed.node scripts/pm/check-governed-merges.mjs --branch claude/issue-17533-closing-keyword-body-mode→ exit 0,✅ NOT governed — ordinary queue landing applies to a PR with exactly this file list.grep -naPover the ASCII control range returned nothing (exit 1).dispatch-gatesreports as applying "once this card's changeset exists" do not apply — see below.Changeset: none,
skip-changesetapplied. Nothing published moves. The rootpackage is
private: true, and no published package'sfiles[]escapes its owndirectory, so a repo-root
scripts/file cannot be shipped by any package;git grep -l judgeBodyreturns exactly one tracked file, the script itself.Acceptance notes
join(repoRoot(), SELF)rather than the running file is a general hazardfor any gate whose self-test re-invokes itself; this PR corrects only the
instance it introduced, in the file it introduced it in. A tree-wide sweep for
the pattern has no carrier PR and no owner today — successor: none — and it is
an observation rather than a reproducible defect elsewhere, so it is noted
here and not filed.
docs/audits/2026-09-self-test-shape-census.mdstillrecords this script's self-test dispatch as
NONE / DEFEATED (exit 0, printed 0 byte(s)). TheselfTestReachedVerdicthandshake in the entrypoint has sinceclosed that; the audit is a dated census, not a live ledger, so a stale row in
it is neither a defect nor a contract violation. Not filed.
CI as a body check would be a policy decision (what a hit should do to a
PR), and this PR deliberately takes none.
This body, run through the mode it adds
The last step before opening this PR was to point the new mode at this body.
Measured before this section was appended (the number is masked here per the
policy at the top — unmasking it inside a transcript would plant a second copy
of the same declaration in the body):
Exactly one distinct declaration — the
Fixesline at the top of this body,seen by the two parsers scoped to the bare form and invisible to the cross-repo
closer, which is correct for a same-repo reference. Every other card number in
this body binds nowhere. Re-run on the final text with this section appended,
the reading is unchanged — still 2 hits and one distinct card number, because
the masked quotation carries no digits for a parser to bind.
Clause-②: no
Generated by Claude Code