Skip to content

fix(tooling): check:i18n-walk-parity refuses with the prerequisite code, not a finding's - #16970

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-16558-i18n-walk-parity-exit-code
Sep 8, 2026
Merged

fix(tooling): check:i18n-walk-parity refuses with the prerequisite code, not a finding's#16970
baozhoutao merged 1 commit into
mainfrom
claude/issue-16558-i18n-walk-parity-exit-code

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Part of #16558

Clause-②: no

The defect

scripts/check-i18n-walk-parity.mjs refused correctly when its inputs were not built, printed the right words — NOTHING was measured — and then returned exit 1, the code a real finding uses. The card measured the cost directly: on one worktree sweep three gates refused for the same reason and returned 3, 1, 3. Two are legible as NOT MEASURED from the number alone; this one read as a false RED against whatever landed most recently. The number, not the stderr, is what a sweep, a runner script or a CI step condition reads.

What changed

One file: scripts/check-i18n-walk-parity.mjs.

  • Imports EXIT_PREREQUISITE_NOT_MET and EXIT_FINDINGS from scripts/import-prerequisite.mjs — the repo's one home for these two numbers, whose header states the reason in as many words. Four siblings already import it, two of them in this same i18n family (check-i18n-bundles.mjs, check-i18n-coverage.mjs). Neither number is re-spelled here.
  • reportPrerequisiteNotMet exits EXIT_PREREQUISITE_NOT_MET, and its text is split out into a pure prerequisiteNotMetText so the self-test can assert on the advisory as a VALUE rather than on a comment about it. That split is the shape check-i18n-coverage.mjs and check-i18n-bundles.mjs already carry.
  • The three finding returns in main() are spelled EXIT_FINDINGS — same value, check-dts-closure.mjs's shape.
  • The refusal's existing text is byte-for-byte unchanged (545 bytes, verified against the pre-change run). It gains the shared "capture the code BEFORE any pipe" advisory, which INTERPOLATES both numbers rather than spelling either.

scripts/import-prerequisite.mjs was read, never written.

Judgement on the second bare process.exit(1) — it stays 1

The claim brief flagged a second bare process.exit(1) that does not go through the helper (line 725 before this change, 766 after). It is a real finding and exit 1 is correct. Left alone, deliberately.

It is the --self-test dispatch's completion guard:

const code = selfTest();
if (!selfTestReachedVerdict) {
  console.error('...selfTest() returned without reaching its verdict...');
  process.exit(1);
}

Three reasons it is not a prerequisite:

  1. It has no prerequisite to be unmet. This gate's own header says --self-test "needs neither" built side — it drives the pure classifiers against recorded samples. Nothing about the environment can make that path refuse.
  2. It fires on a defect in this file's own source, not on an unbuilt or uninstalled tree: an early return added above the verdict line. pnpm install and turbo build clear nothing here; the remedy is to fix the source. That is the definition of a finding.
  3. Exit 3 would silence it. A sweep reads 3 as NOT MEASURED, automatically and correctly — so a self-test that stopped running would be swept under "not measured" and nobody would look. That is the same silence the guard was added to break, one level up.

Corroborating precedent, not inference: check-i18n-coverage.mjs carries the identical guard with the identical process.exit(1), and the PR that moved that file's refusal numbers from 1 to 3 left it at 1.

The five reportPrerequisiteNotMet call sites were re-read and all five are genuine "this gate cannot read its input" refusals — inputs not built, inputs unloadable, the built schema exposing no shape, the built walker not exporting the walker, and an empty population. Their classification is unchanged by this PR.

Callers: nothing changes behaviour, checked

  • .github/workflows/lint.yml:5902run: pnpm check:i18n-walk-parity, a bare run step. Any non-zero fails it; 3 is non-zero.
  • package.json — the script itself.
  • scripts/check-ratchet-remedy-authority.mjs — reads this file's remedy TEXT, not its exit code. Unchanged, and the gate is green below.
  • Repo-wide grep for a consumer branching on this gate's exit code by value: none. The only exit-3 branch in the repo is half-state-patrol.yml on the pm/ gates, unrelated.
  • Measured, not assumed: pnpm check:i18n-walk-parity propagates the new code — pnpm reports Command failed with exit code 3 and exits 3 — so what the CI step sees is 3, and its verdict (fail) is unchanged.

The pin, and why it is two-sided

A test that only checked the prerequisite path would pass on a file that returned 3 for everything — a worse defect than the one being fixed. So a second self-test battery drives all three verdicts through the exit code a shell actually sees, in a throwaway root holding this gate, the four modules it imports, and whichever built inputs the case wants it to see. The child runs in production mode, so it can never spawn a child of its own, and the spawn is bounded by a timeout.

driven case expected observed
the card's repro: built inputs absent 3 3, with the same headline
FIRING CONTROL: a declared group nothing emits, both sides readable 1 1, reached by measuring — no refusal in its output
NONSENSE CONTROL: nothing to find 0 0

Plus structural cases over the FUNCTION BODIES, because the regression that costs something is not a mistyped constant — it is a process.exit(1) written back in by an author who never thought about exit codes, or a number typed into the advisory instead of interpolated. Each of those predicates has a NEGATIVE CONTROL that must SEE a function doing the forbidden thing, so a typo in either regex cannot pass forever.

Verification

Exit codes captured BEFORE any pipe, cmd > file 2>&1; EXIT=$?.

Before / after, the card's repro (worktree installed, dependency closure not built):

BEFORE  pnpm check:i18n-walk-parity   EXIT=1   "PREREQUISITE NOT MET: the workspace spec package is not built"
AFTER   node scripts/check-i18n-walk-parity.mjs   EXIT=3   same headline, byte-identical for 545 bytes
AFTER   pnpm check:i18n-walk-parity   EXIT=3   "ELIFECYCLE Command failed with exit code 3."

On the built tree (turbo run build --filter=@objectstack/cli --filter=@objectstack/spec, 57/57 tasks successful):

pnpm check:i18n-walk-parity   EXIT=0
  self-test: 43 cases pass
  check-i18n-walk-parity: 11 declared group(s), 8 walked, 3 exempted

Ablations — each mutation proved to have reached disk by grep count in both directions, each restored from HEAD and verified by blob hash, git diff HEAD empty at the end. These were run from the committed implementation, so the restore leg had something to restore to.

  1. Put the defect backprocess.exit(EXIT_PREREQUISITE_NOT_MET) back to process.exit(1) (injected count 3, removed count 0). Self-test EXIT=1 with exactly the two cases that should see it:
    refusal class: the refusal exits through the named constant, never a literal
    end to end: an unbuilt tree refuses with the PREREQUISITE code: got 1, want 3
    
  2. The worse defect — the finding path made to return the prerequisite code too. Self-test EXIT=1:
    FIRING CONTROL: a REAL finding in the same harness still exits 1: got 3, want 1
    
    This is the leg that proves the pin is not one-sided.
  3. A real finding on the real built tree — a deliberately stale ledger entry planted in KNOWN_NO_EXTRACTOR_FACE. Production path, built tree, EXIT=1, printing the ledger-ratchet finding. Restored; the unmutated tree exits 0 in the same harness.

Gate family — derived from the merge base rather than hand-listed, then reconciled:

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.txt
Run reconciliation — 31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN.

All 31 exited 0, including check:ratchet-remedy-authority, check:pm-dispatch-gates, check:self-test-wired, check:scripts-symbol-anchors, check:declaration-mirrors, check:nul-bytes and check:parse-guard.

Linteslint --no-inline-config over the changed file: 1 file, 0 errors, 0 warnings. The narrowing is a measurement, not a skip: this repo runs one eslint.config.mjs which, in its own words, "never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file", so a one-file diff cannot move the verdict on any untouched file. The repo-wide pnpm lint run is CI's.

Control charactersgrep -naP for the C0/DEL class over the changed file: no matches.

Changeset

skip-changeset, measured rather than asserted. The changed file is a repo-root gate script; the root package is private. Searching every published package's declared files[] roots — 76 roots across 23 published packages — for check-i18n-walk-parity returns 0 hits, with a positive control (TranslationDataSchema) returning 32 in the same sweep. Nothing published moves.

The neighbour sweep the card asked for — it is NOT a class

The card named check-doc-frontmatter.mjs as an unverified neighbour and the triage asked for the branch to be measured before assuming anything, with the instruction that a third or fourth site would make it a class to handle in one pass. Measured: there is no second site.

Every file in scripts/ and packages/lint/scripts/ that prints PREREQUISITE NOT MET was enumerated and each one's refusal branch read:

  • check-doc-frontmatter.mjs, check-dts-closure.mjs, check-i18n-bundles.mjs, check-i18n-coverage.mjs, check-published-readme-exports.mjs, check-regen-pending.mjs, check-release-section-coverage.mjs, check-sourcemap-no-sources-content.mjs, check-test-completeness.mjs, check-type-check-coverage.mjs, check-vendor-export-contract.mjs — all import EXIT_PREREQUISITE_NOT_MET and already exit 3.
  • check-dual-build-cjs-loads.mjs, check-manifest-repository-directory.mjs, docs-audit/check-docs-transcript-drift.mjs — refuse with a locally named EXIT_PREREQ = 3. Correct number; the shared module's own header lists that spelling as prior art rather than as a violation.
  • objectui-changeset-digest.mjs refuses with a documented exit 2 of its own, pinned by its own self-test. A different declared contract, not this one.

Every bare process.exit(1) sitting near one of those refusal texts — eight of them, check-doc-frontmatter.mjs:1805, check-dts-closure.mjs:581, check-dual-build-cjs-loads.mjs:1599, check-manifest-repository-directory.mjs:1258, objectui-changeset-digest.mjs:1193 among them — is the same self-test completion guard judged above, and 1 is correct in every one. So the answer the card wanted is: check-i18n-walk-parity.mjs really was the only site, and this PR closes it.

验收备注

Noted while reading, not filed:

  • Three gates spell EXIT_PREREQ = 3 locally rather than importing the shared constant. They return the right number and each pins it in its own self-test, so this is neither a reproducible defect nor a contract breach — the shared module's header names that spelling as the prior art the constant was extracted from. Whoever next edits one of those three files is the one who would touch it; no PR is heading there today, so: no taker.

Generated by Claude Code

…de, not a finding's

`reportPrerequisiteNotMet` said "NOTHING was measured" and then returned exit 1
— the code a real finding uses. On one worktree sweep three gates refused for
the same reason and returned 3, 1, 3: two are legible as NOT MEASURED from the
number alone, and this one read as a false RED against whatever landed most
recently. The number, not the stderr, is what a sweep, a runner script or a CI
step condition reads.

Route the refusal through the repo's one answer: import EXIT_PREREQUISITE_NOT_MET
and EXIT_FINDINGS from `scripts/import-prerequisite.mjs` rather than re-spelling
either, the way `check-dts-closure.mjs` does and the two sibling gates in this
same i18n family already do. The refusal's words are unchanged byte for byte;
it gains the shared "capture the code BEFORE any pipe" advisory, which names
both numbers by interpolating them.

Pinned by a second self-test battery that DRIVES all three verdicts through the
exit code a shell actually sees — a throwaway root holding this gate, the modules
it imports and whichever built inputs the case wants it to see:

  * the card's repro (no built inputs)          -> 3, with the same headline
  * FIRING CONTROL: a declared group nothing
    emits, both sides readable                  -> 1, reached by measuring
  * NONSENSE CONTROL: nothing to find           -> 0

A pin that only checked the prerequisite path would pass on a file that returned
3 for everything, which is a worse defect than the one being fixed. Structural
cases pin the same thing over the function bodies — no literal exit call, no
spelled-out code in the advisory — each with a negative control that must SEE a
function doing the forbidden thing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 8, 2026
@github-actions github-actions Bot added the size/m label Sep 8, 2026
@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT in substance — PR #16970, reviewed against the tree, ⛔ not against the report

Flip + arm withheld until the four running legs report.

⭐ The exit-site map, old head → new head, read by me

origin/main this head verdict
:324 process.exit(1)the refusal :336 process.exit(EXIT_PREREQUISITE_NOT_MET) ✅ the defect, fixed
:725 process.exit(1) — the self-test completion guard :916 process.exit(1) unchanged, exactly as judged
:727 process.exit(code) :918 unchanged
:729 process.exit(await main(…)) :920 unchanged
:722 const controlHardcodedExit = () => { process.exit(1); }; new: a negative control

⇒ Exactly one exit site moved, and it is the one the card is about. Nothing else in the file changed number.

probe want got
imports from ./import-prerequisite.mjs 1 1
EXIT_PREREQUISITE_NOT_MET / EXIT_FINDINGS referenced >0 10 / 11
prerequisiteNotMetText extracted as a value >0 6
firing control — "a real finding still exits 1" asserted >0 1
the self-test completion guard still present >0 4
staleness control — the bare process.exit(1) was in the refusal on main >0 2
nonsense control 0 0

scripts/import-prerequisite.mjs is not in the diff — read, never written, as the claim required.

⭐ The :725 judgement is right, and it is corroborated rather than argued

I asked you to judge the second bare process.exit(1) the card never mentions. It stays 1, and the three reasons hold:

  1. --self-test has no prerequisite at all — this gate's own header says it needs neither built side.
  2. It fires on a defect in this file's own source (an early return above the verdict line), which no install or build clears.
  3. Exit 3 there would make a sweep read a self-test that stopped running as NOT MEASURED — the same silence the guard exists to break. That is the sharpest of the three: the guard and the constant point in opposite directions, and telling them apart is the whole card.

And it is corroborated, not inferred: the identical guard sits at eight sites — including inside the very files whose refusals were moved to 3. ⇒ the repo already treats the two as different questions; this file now does too.

⭐ The pin is two-sided, which is what makes it worth having

Ablation 2 is the leg I most wanted: making the finding path return the prerequisite code too fails on
'FIRING CONTROL: a REAL finding in the same harness still exits 1: got 3, want 1'.

⇒ ⛔ A test that only checked the prerequisite path would have passed on a file that returned 3 for everything — a worse defect than the one being fixed. This one cannot.

⭐ And :722's controlHardcodedExit is the same discipline one level up: the structural assertion ("no literal exit call in the refusal") is given a function that does the forbidden thing, so it can fail. A structural test with no such control is decoration.

The caller check, which the brief demanded and which was actually done

lint.yml:5902 is a bare run: pnpm check:i18n-walk-parity — any non-zero fails it, unchanged. check-ratchet-remedy-authority.mjs reads this gate's remedy text, not its exit code. A repo-wide grep found no consumer branching on the value. ⭐ And the one that could have bitten was measured, not assumed: pnpm propagates the new code (ELIFECYCLE Command failed with exit code 3), so what the CI step sees is 3.

The neighbour sweep — and the answer is that this is not a class

Every scripts/** file printing PREREQUISITE NOT MET was enumerated: eleven already import the shared constant, three declare a local EXIT_PREREQ = 3, one documents its own exit 2. check-i18n-walk-parity.mjs was the only site returning 1 for a refusal. ⇒ ⭐ this was a singleton, not a family, and the right report was to say so rather than open five more cards.

⛔ Correctly not filed: the three local-EXIT_PREREQ sites. The number is right, each pins it in its own self-test, and import-prerequisite.mjs's header names that spelling as the prior art the constant was extracted from ⇒ neither a reproducible defect nor a breached contract.

Message preservation

The refusal's existing text is byte-for-byte unchanged for its first 545 bytes, verified programmatically against the pre-change run, with only the shared capture-the-code-before-any-pipe advisory appended — and that advisory interpolates both numbers rather than spelling either. ⇒ ⛔ no second home for the constant was created.


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.

2 participants