fix(pm): check-half-states answers --help and refuses an unknown option before any board read - #18430
Merged
os-elon-musk merged 3 commits intoSep 16, 2026
Merged
Conversation
…on before any board read
argv was never validated. The only membership tests were
`includes('--self-test')` / `includes('--probe')`, and `parseOutputOptions`
matched two prefixes and dropped every other token silently — so `--help` and a
flag that certainly does not exist behaved identically: both fell through into
the full sweep, a multi-page, rate-limit-spending read of a live board, with no
usage string anywhere in the file to print instead.
The expensive half is the mistyped REAL flag: `--format markdown` with a space,
or a `--repo=…` this tool has never had, was ignored and the sweep then ran with
the default format against the default board — a wrong-target reading
indistinguishable from the one the caller asked for.
One roster (`CLI_FLAGS` / `CLI_VALUE_OPTIONS`) feeds both the refusal and the
printed `USAGE`, so the two cannot drift. `--help`/`-h` is answered before the
`SWEEP_REPO` / `CLOSED_FLOOR` guards, before the proxy re-exec and before any
request; an unrecognised token is refused by name in `git-history.mjs`'s
spelling, at this file's own bad-usage exit code 2. The valid-invocation body is
byte-identical: the whole diff outside the new block is `if (isMain) {` becoming
`} else if (isMain) {`.
Pinned by 35 new self-test cases — the pure refusals and roster/usage parity,
plus four spawned entry runs under a `globalThis.fetch` trap that exits 97, with
the default sweep as the control that proves the trap is armed.
Claude-Session: https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt
Co-authored-by: Claude <noreply@anthropic.com>
…entry guard
Two gates judged the first shape and both were right.
`check:entry-guard` refused `if (CLI_HELP) {} else if (…) {} else if (isMain) {}`:
a scripts/** file that exports bindings must put its top-level dispatch behind a
literal `isEntrypoint(import.meta.url)` guard, and mine was reachable only
through a variable. Restored to the untouched `if (isMain) {`, with the two new
branches as early exits inside it — so the valid-invocation body is byte-
identical and the whole diff outside the new block is one import line.
`check-closing-keyword-parity` went red naming the REGISTRY ("remove it, or fix
the signature"). The registry is correct; the cause is size. Its sweep skips any
tracked file over 2 MiB (`statSync(...).size > 2 * 1024 * 1024`), this file was
2,082,119 bytes — 15,033 under — and the first draft added 15,213. Measured, not
inferred: appending 260 lines of pure comment to the unmodified file reproduces
the identical failure, and restoring it byte-for-byte clears it. The block is
now 13,380 bytes, leaving 1,653 bytes of headroom, and the cliff is reported as
a finding: the failure text prescribes deleting a live registry row.
`--help` writes through `writeSync(1, …)` rather than `console.log`, because the
next statement is `process.exit` and stdout to a pipe is asynchronous on some
platforms — the self-test reads it through one.
Self-test 4,270 -> 4,303 cases, all green, 1.5s.
Claude-Session: https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt
Co-authored-by: Claude <noreply@anthropic.com>
…eck-half-states-help-argv
This was referenced Sep 16, 2026
os-elon-musk
marked this pull request as ready for review
September 16, 2026 10:50
This was referenced Sep 16, 2026
os-elon-musk
deleted the
claude/issue-18369-check-half-states-help-argv
branch
September 16, 2026 11:12
This was referenced Sep 16, 2026
This was referenced Sep 17, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…objectstack-ai#18449) Fixes objectstack-ai#18435 `scripts/check-closing-keyword-parity.mjs` sweeps the whole tracked tree hunting for a fourth, unregistered spelling of GitHub's closing-keyword grammar. Its sweep skipped any file over 2 MiB **before reading it** — and the registry check downstream had no way to tell "this file stopped carrying the grammar" from "the sweep never opened this file", so it reported the second as the first. ## Before / after, on the real blocker `scripts/pm/check-half-states.mjs` is a `PARSERS`-registered file and the repo's largest script. PR objectstack-ai#18430 landed while this branch was in flight, which puts it at **2,095,499 B — 1,653 bytes under the 2,097,152 B cutoff**. Appending 40 lines of plain comment (2,000 B, the size of a paragraph of prose, no grammar touched) takes it to 2,097,499 B. Both readings below are on this branch's base `b6fe311a`, same padded file, only the gate swapped. **Before** — the gate exactly as it ships on `origin/main` (exit 1): ``` check-closing-keyword-parity: 1 failure(s) • [sweep] scripts/pm/check-half-states.mjs is registered as carrying the closing-keyword grammar but no longer matches the sweep. Stale registry entries verify nothing -- remove it, or fix the signature. ``` The prescribed remedy — *remove it* — deletes a correct registry row and silently retires a live parity check on H7. The actual cause, the file's size, is named nowhere in the text. **After** — this PR's gate, same 2,097,499 B file (exit 0): ``` check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 8733 tracked file(s), all registered). 1 UNREGISTERED file(s) were not read, so the hunt for a fourth parser did not cover them (every registered file WAS read -- the cutoff does not apply to those): • packages/spec/CHANGELOG.md -- 6077484 bytes exceeds the sweep's 2097152-byte cutoff for UNREGISTERED files ``` The padding was a transient measurement: restored byte-for-byte afterwards, proven by `git hash-object` back to the HEAD blob and an empty `git diff HEAD`. `scripts/pm/check-half-states.mjs` carries **no change in this diff** — three other PRs own its regions. ## The rule Registration is the statement *this file carries the grammar*. A sweep that skips a registered file does not refute that statement; it simply fails to look. So: - **A registered file is read unconditionally.** `REGISTERED` is hoisted out of `judge()` to above `sweep()`, and the cutoff now reads `size > SIZE_CUTOFF && !REGISTERED.has(f)`. A few megabytes of `readFileSync` is by a wide margin the cheaper half of that trade. - **An unregistered file over the cutoff is still skipped** — the sweep's job on those is a hunt, and a multi-megabyte blob is overwhelmingly a fixture or a generated corpus — **but the skip is now RECORDED.** `sweep()` returns `skipped` (file to cause, in words) beside `hits`. - **The registry check reads that cause.** A registered file missing from `hits` *and* present in `skipped` now fails with the cause named and the opposite prescription: the entry is NOT stale, do not remove it. A registered file missing from `hits` and NOT skipped still gets the original stale-registry text, unchanged. - **The green line says what it did not cover.** A sweep reporting "across 8733 tracked file(s)" while having silently read fewer overstates itself. That hole was already there and nobody could see it: `packages/spec/CHANGELOG.md` at 6,077,484 B has been outside this sweep the whole time. It is a changelog, not a fourth parser — but the gate now says so out loud instead of implying full coverage. `skipped` is part of the sweep result rather than an optional extra, and the one hand-built sweep object in the self-test was updated to carry it. A default in `judge()` would have silently restored exactly the misdirecting message this PR removes. ## New self-test cases, each shown red under a control `--self-test` goes from 30 assertions to 40. **X14 / X15 — the cutoff, on a real tree.** `extraFiles` hands the sweep text directly and therefore never reaches the `statSync` branch at all, so a case built that way would pin nothing about the cutoff. These build a real git-tracked temp tree with two real oversized files (3,690,082 B each), one registered and one not, and grade both arms: the registered one is read, the unregistered one is skipped *and* the skip is recorded with its cause. Control — restore the unconditional skip (`size > SIZE_CUTOFF` alone), self-test exit 1: ``` • X14: an oversized REGISTERED parser is still SCANNED and found (3690082 bytes), got hits: nothing • X14: and is not recorded as skipped, got: 3690082 bytes exceeds the sweep's 2097152-byte cutoff for UNREGISTERED files ``` **X13 — unread is not stale.** Control: delete the new diagnostic branch, self-test exit 1, and the red prints the exact defect text this PR exists to remove: ``` • X13: a registered file the sweep never read is reported with its CAUSE, got: scripts/pm/check-half-states.mjs is registered as carrying the closing-keyword grammar but no longer matches the sweep. Stale registry entries verify nothing -- remove it, or fix the signature. • X13: and never with the stale-registry remedy, which would delete a correct registry row ``` **X7 keeps reading as stale.** The genuinely-stale case is unchanged and now also asserts the *negative*: it must NOT be reported as unread. An arm that swallowed the other would retire the check X7 exists to be. Each control arm was applied to the committed file, proven on disk by grep counts before the run, restored with `git checkout HEAD -- PATH`, and the restore proven by blob hash and an empty `git diff HEAD`. The restored tree is green again (exit 0, 40 assertions). ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from this worktree derived 30 commands for this diff; every one was run in the foreground. Results are in the report comment on the card. ## Changeset None — `skip-changeset`. AGENTS.md Post-Task Checklist step 3: that label 「is for a diff that publishes nothing from any released package」. This diff is one repo-root script. The root package is `private: true`, no package's `files[]` ships repo-root `scripts/`, and no package imports this file — `git grep` finds it referenced only from `.github/workflows/`, `docs/audits/`, and comments in three sibling scripts. ## Acceptance notes - **Scope.** `scripts/check-closing-keyword-parity.mjs` only. The fix is in the gate, not in the file that trips it — `scripts/pm/check-half-states.mjs` carries no change here. - **The cutoff is kept, not raised.** The alternative of raising or deleting it would have made the whole-tree read unconditionally expensive and would not have fixed the diagnostic, which was the part that prescribed the wrong remedy. - **Noted, not filed:** `packages/spec/CHANGELOG.md` (6,077,484 B) has been outside this sweep since the cutoff was introduced. It is release-compiled prose, not a parser, so there is nothing to register — the new green-line disclosure is the whole remedy. No other gate was audited for a similar silent size skip; the card records that census as not run. - **Not measured:** whether any other whole-tree gate carries the same silent skip. --- _Generated by [Claude Code](https://claude.ai/code/session_01HPfcjvF23QBoBj7P47DDxs)_ Co-authored-by: Claude <noreply@anthropic.com>
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 #18369
node scripts/pm/check-half-states.mjs --helpprinted nothing and started the fullboard sweep. So did
--totally-bogus-flag-xyz. argv was read here but nevervalidated: the only membership tests were
includes('--self-test')/includes('--probe'), andparseOutputOptionsmatched two prefixes and droppedevery other token silently.
The unanswered
--helpis the cheap half. The expensive half is the mistypedreal flag —
--format markdownwith a space, or a--repo=…this tool hasnever had — which was ignored, so the sweep ran with the default format
against the default board and produced a wrong-target reading that is
indistinguishable from the one the caller asked for.
What changed
One roster (
CLI_FLAGS/CLI_VALUE_OPTIONS) feeds both the refusal and theprinted
USAGE, so the two cannot drift. Inside the existingif (isMain)entry guard, before theSWEEP_REPO/CLOSED_FLOORguards,before the proxy re-exec and before any request:
--help/-hwritesUSAGEto stdout and exits 0.The refusal wording mirrors
git-history.mjs(check-half-states: unknown option --foo).The exit code does not: this file's exit vocabulary is pinned by its own
header at 0 / 2 / 3, and all three of its pre-existing bad-usage exits are 2, so a
fourth code would make "bad usage" two numbers inside one tool. That is a
deliberate deviation from the card's "matching git-history.mjs's existing
spelling", which the dispatch order anticipated.
--helpwrites throughwriteSync(1, …)rather thanconsole.logbecause thenext statement is
process.exit, and stdout to a pipe is asynchronous on someplatforms — the self-test reads it through one.
The roster, measured from the file rather than the docs
Enumerated by grepping
process.argv,parseOutputOptions,rearmThroughProxyand every double-dash literal in the file, then cross-checked against every
spelled invocation in
.github/,scripts/,package.jsonand.claude/.--self-testincludes()--probeincludes()--format=FMTparseOutputOptions--provenance=TEXTparseOutputOptions--help,-hNothing was dropped. Three tokens the dispatch order listed as needing to keep
working do not exist in this file and never did —
--repo=,--closed-floorand
--use-env-proxyas a script argument:(
resolveSweepReporeadsPM_SWEEP_REPOthenGITHUB_REPOSITORY;resolveClosureFloorreadsPM_SWEEP_CLOSED_FLOOR). A--repo=on the commandline was silently dropped and the default board swept — the card's own
wrong-target scenario, now refused by name and pinned.
--use-env-proxyis node's flag, read at process start, so it belongs inNODE_OPTIONSor before the script path. Every invocation in the repo spells itthat way. After the script path it did nothing; it is now refused, and
USAGEcarries the spelling that works. The
rearmThroughProxyre-exec is untouchedand still passes the flag as an execArgv.
There are no positional arguments and never were, so a bare
owner/nameon thecommand line is refused with a sentence naming where the board actually comes from.
Measurements (pinned to 6bd81fa, the final commit)
Wall time is not the acceptance criterion — "zero HTTP requests" is — so both legs
were run with
globalThis.fetchreplaced by a trap that exits 97 on anyattempt, under
env -u HTTPS_PROXY -u https_proxy -u NODE_OPTIONS -u GITHUB_TOKEN -u GH_TOKEN(the proxy is cleared so no re-exec can escape the trap).
--help--totally-bogus-flag-xyz--helpWithout the trap, in this container's real environment:
Leg 3 — a valid invocation is untouched, and the evidence is structural
rather than behavioural: outside the new block the entire diff is one line, the
node:fsimport gainingwriteSync.if (isMain) {is unchanged and the wholebody under it is byte-identical (
git diff -U0against the merge base showsexactly one deleted line). Behaviourally,
--probestill exits 0 after itsproxy re-exec, and a live
--format=markdown --provenance=…run still completedand rendered the markdown body, exit 0, report-only.
Self-test: 4,270 cases before, 4,303 after (+33), all green, 1.5 s.
Twenty pure cases on
refuseUnknownArgsand roster/usage parity, plus thirteenspawned entry runs under the fetch trap — including the default-sweep control.
Ablation, from the committed state: forcing the
--helpbranch false turns fivecases red and no others (
--help exits 0 (got 97, want 0),--help issues ZERO requests (got true, want false), the stdout pin, the-hpin, and--help answers even when the sweep target is malformed (got 2, want 0)— themutant falls into the
SWEEP_REPOguard, which is the ordering that case existsto pin). Restored byte-identically (
git hash-objectmatchedHEAD), and theself-test re-run green. No
dist/is in the resolution path — the self-testspawns
SELF_PATHdirectly — so there is no build step to invalidate the reading.check-closing-keyword-parity's sweep skips any tracked file over 2 MiB(
statSync(...).size > 2 * 1024 * 1024). This file was 2,082,119 bytes —15,033 under. The first draft of this change added 15,213 and the gate went red
with:
The registry entry is correct and the signature is untouched; the file had simply
become invisible to the sweep. Measured, not inferred: appending 260 lines of
pure comment to the otherwise unmodified file reproduces that exact failure, and
restoring it byte-for-byte clears it. The prescribed remedy would delete a live
registry row and silently retire a real parity check.
This change was refitted to 13,380 bytes, leaving 1,653 bytes of headroom.
Anyone else editing this file should know the wall is there. Filed as a finding.
Changeset
skip-changeset:scripts/pm/**is repo tooling under the root workspace, whichis
private: trueand ships nofiles[]. Nothing published moves.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 40 commands from the real change set. All 40 were run on 6bd81fa and
reconciled:
Every one exited 0, including the two that judged the first draft
(
check-closing-keyword-parityandcheck:entry-guard). The derived list wasidentical at dispatch time, after the refit, and after merging
main.origin/mainwas merged before this PR opened (25 files, none of them this one,no deferred regeneration recorded). No package source moved, so no package
test/typecheckis owed;scripts/**carries notypecheckscript.Lint is a full-population reading, not a narrowed one:
pnpm exec eslint . --no-inline-config --format json— 6,792 files linted fromeslint's own config, 0 errors, 0 warnings, exit 0, 85 s.
Acceptance notes
--self-testis COUNT-shaped — one printed total, noSELF_TEST_BATTERIESroster and no per-battery floor, which AGENTS.md's"Writing a
--self-test" section requires of a new one. It is already recordedas
COUNT | HELDindocs/audits/2026-09-self-test-shape-census.md. The newcases were added in the shape the file already has; converting a 4,300-case
battery to the roster shape is not this card, and would not fit the byte budget
above. Noted, not filed — the census page is who picks this up.
lint.yml's comment on this step still says "no network, ~0.05 s"; theself-test measured 1.13 s before this change and 1.53 s after. Prose drift in a
file outside this card's surface. Noted, not filed — whoever next edits that
step picks it up.
post-stamped.mjsuses 1,label-write.mjsuses 2,git-history.mjsexits 1 fromusage(). This changefollows the file it is in. Observation, not a defect. Noted, not filed — nobody picks this up; there is nothing to pick up.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt
Generated by Claude Code