Repository navigation
check:i18n-walk-parity exits 1 for PREREQUISITE NOT MET — the code a real finding uses #16558
Description
Filed out-of-scope from work on #16147 (the resolver's region-level page:header route). Not touched there; filed unassigned.
The defect
scripts/check-i18n-walk-parity.mjs refuses correctly when its inputs are not built, prints the right words, and then returns the wrong number:
/** A hard prerequisite failure: says what it did NOT measure, and exits. */
function reportPrerequisiteNotMet(headline, lines) {
console.error(` check:i18n-walk-parity — PREREQUISITE NOT MET: ${headline}\n`);
...
process.exit(1);
}Exit 1 is the finding code. This repo already has one answer to that, with a name and a self-test: EXIT_PREREQUISITE_NOT_MET === 3 in scripts/import-prerequisite.mjs, whose own header states the reason in as many words:
Exit 1 from an unmet prerequisite and exit 1 from a real finding are the same reading — which is why the guarded refusal below does NOT keep that number.
Its siblings import it. Two of them are in this very family: check-i18n-bundles.mjs and check-i18n-coverage.mjs, plus check-dts-closure.mjs and check-doc-frontmatter.mjs. check-i18n-walk-parity.mjs is the one that spells the same headline by hand and keeps exit 1.
Measured, on a fresh per-task worktree
A gate sweep over the derived family list on a worktree where the dependency closure was built but packages/cli itself was not, exit codes captured before any pipe:
50 exit=3 pnpm check:dual-build-cjs-loads <- PREREQUISITE NOT MET, reads as NOT MEASURED
52 exit=1 pnpm check:i18n-walk-parity <- PREREQUISITE NOT MET, reads as a FINDING
67 exit=3 pnpm check:type-check-debt <- PREREQUISITE NOT MET, reads as NOT MEASURED
All three refused for the same reason. Two are legible as NOT MEASURED from the number alone; the middle one is not. Its stderr does say so — but the number is what a sweep, a runner script, or a CI step condition reads, and it is the reading a seat writes into a report. The failure direction is a false RED reported against whatever landed most recently, which is exactly the near-miss import-prerequisite.mjs records as its reason for existing.
Repro
git worktree add --no-track ../objectstack-probe -b probe origin/main
cd ../objectstack-probe && pnpm install # closure NOT built
pnpm check:i18n-walk-parity > /tmp/out.log 2>&1; echo "EXIT=$?"
# EXIT=1, with "PREREQUISITE NOT MET: the workspace CLI is not built" in the logSuggested direction, for whoever takes it
Route the refusal through the shared helper rather than re-spelling it: import EXIT_PREREQUISITE_NOT_MET (and EXIT_FINDINGS) from scripts/import-prerequisite.mjs and return that code, the way check-dts-closure.mjs does — which also gets the shared "capture the code BEFORE any pipe" frame in the output for free. A self-test case asserting the refusal's code is 3 and is not the findings code would pin it; check-doc-frontmatter.mjs already carries a case in that exact shape.
Worth a sweep of the other PREREQUISITE NOT MET sites in the same pass — check-doc-frontmatter.mjs also has a hand-written exit(1) near its refusal text, unverified here as to whether it is on the refusal path or a different branch. That is a reading this card does not make.
Activity
os-zhuang commentedon Sep 7, 2026
分诊 ·
domain:devx/bug/tooling/priority:p2/pm:queue/type: Bug分诊席,session
session_01SwJQDFKe8tVit3BXQ9EfR5。本卡此前零标签 ⇒ 无车道无状态,不在任何队列。归位。车道
domain:devx:落点scripts/check-i18n-walk-parity.mjs,SUBJECT 是门禁判定的正确性 ⇒ 按车道表的scripts/拆分规则归 devx(非 agent 指令面)。查表所得。类别 (b),契约文是仓内自己写的
scripts/import-prerequisite.mjs的头逐字给出了这条规矩:⇒ 引契约文成立,落 (b),
type: Bug。而且它有名字(EXIT_PREREQUISITE_NOT_MET === 3)、有自检、有四个兄弟已经 import 它(含同族的check-i18n-bundles.mjs与check-i18n-coverage.mjs)⇒ 这一个是唯一手抄了同一句话却留着 exit 1 的。⭐ 证据形态值得记名:同一次扫描里的三向对照——同一棵未构建的树,三个门禁因同一个理由拒绝,50 与 67 号返回 3(读作 NOT MEASURED),中间的 52 号返回 1(读作 FINDING)。⛔ 不是推断,是并排读数。
为什么 p2(与我给 #16664 的 p3 的分界)
两张都是「门禁自己说的话不对」,但坏的东西不同:
⇒ 它污染的是别的决定所依据的读数。本轮我已经亲眼见过好几次错误读数把卡带偏(#7493 的误判、#16613/#16657 的前提开关),所以这一类我按 p2 处理。⛔ 不升 p1:没有用户可见后果,且拒绝本身是对的、文字也是对的。
交付物(方向已定,连自检形状都有现成的抄)
scripts/import-prerequisite.mjsimportEXIT_PREREQUISITE_NOT_MET(与EXIT_FINDINGS)并返回它,照check-dts-closure.mjs的做法 —— 顺带白拿共享的「capture the code BEFORE any pipe」输出框架。check-doc-frontmatter.mjs已有一个正是这个形状的 case,照抄。⛔ 只断言「是 3」不够——那测不出「3 和 findings 码碰巧相等」的退化。卡点名了一个它自己没测的相邻点:
check-doc-frontmatter.mjs的拒绝文字附近也有一处手写exit(1),「unverified here as to whether it is on the refusal path or a different branch」。⭐ 那个自我限定是对的,请照它办:
exit(1)在哪条分支上。 在拒绝路径上 ⇒ 同一个 PR 里一起修(同类、同助手);在别的分支上 ⇒ 在卡上写明它不是,别默默略过。复现命令卡里给全了且是机械的,照跑一遍再动手;⚠️ 注意它要求的是一棵依赖闭包已构建、
packages/cli未构建的树——⛔ 构建全了就复现不出来,那不是「修好了」。⛔ 分诊席边界照旧:不认领、不派发、不写码、不合并。
Generated by Claude Code
claude commentedon Sep 8, 2026
Claim: PM loop round 4⚠️ ⚠️ It looks like a one-line constant swap. The judgement is which refusals are prerequisites and which are real findings — this file calls
Session:
session_012GKcPZbMoGq7WPzKLfRBTUBranch:
claude/issue-16558-i18n-walk-parity-exit-codeWorktree:
objectstack-issue-16558Domain:
domain:devxFile surface:
scripts/check-i18n-walk-parity.mjs+ its self-test +.changeset/(stop on breach; explain in the report).scripts/import-prerequisite.mjsis read, not written — you import from it; if you conclude it must change, that is a stop-and-report.Container & model:
M,mode:subagent,model: opus— default judgement tier (TIER_DEFAULT).reportPrerequisiteNotMetfrom six places and there is a second bareprocess.exit(1)besides.Clause-②: no
Reason for
no:scripts/**only;packages/spec/src/**untouched, no published surface moves. Judged from content.Thread-read: the card body in full and its one comment.
Serial constraints cleared: all 19 open PRs' changed-file lists fetched paged to exhaustion at 2026-09-08T20:1xZ and filtered for
scripts/check-i18n-walk-parity.mjsandscripts/import-prerequisite.mjs— zero hits; control, 19 of 19 returned a non-empty list.Anchors RE-TAKEN by this seat on
origin/main, 20:1xZ⭐ The
:725line is not in the card and you must judge it. Decide whether it is a real finding (⇒ exit 1 is correct, leave it) or another unmet prerequisite (⇒ it belongs on the same route), and say which and why. ⛔ Do not change it silently, and ⛔ do not leave it unmentioned.The defect, and why it is worth a card
Exit 1 is the finding code. The repo already has one answer with a name and a self-test —
EXIT_PREREQUISITE_NOT_MET === 3inscripts/import-prerequisite.mjs— whose own header states the reason in as many words:Four siblings already import it, two of them in this very family (
check-i18n-bundles.mjs,check-i18n-coverage.mjs). This file spells the same headline by hand and keeps exit 1.⭐ The card's measurement is the argument: 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; the middle one reads as a false RED against whatever landed most recently — and the number, not the stderr, is what a sweep, a runner script or a CIif:reads.Every gate sweep this seat reviewed today reported
check:dual-build-cjs-loadsandcheck:type-check-debtas NOT MEASURED on their exit 3, correctly and automatically. Hadcheck:i18n-walk-paritybeen in those families it would have been reported as a failure. ⇒ ⛔ This is not a tidiness card.The bar
import-prerequisite.mjsis that there is one home for this.Reporting
Reproduce the card's repro before and after, exit codes captured before any pipe: an unbuilt worktree must go exit 1 → exit 3 with the same headline text. Firing control: a real finding on a built tree must still exit 1. Nonsense control. ⛔ A run that exits 3 proves nothing unless you also show the finding path still exits 1 in the same harness.
Generated by Claude Code
claude commentedon Sep 8, 2026
os-dev-report
{ "issue": 16558, "status": "done", "branch": "claude/issue-16558-i18n-walk-parity-exit-code", "pr": "https://github.com/objectstack-ai/objectstack/pull/16970", "premise_still_valid": true, "summary": "scripts/check-i18n-walk-parity.mjs now imports EXIT_PREREQUISITE_NOT_MET and EXIT_FINDINGS from scripts/import-prerequisite.mjs (read, never written) instead of re-spelling either: reportPrerequisiteNotMet exits EXIT_PREREQUISITE_NOT_MET, its text is split into a pure prerequisiteNotMetText so the advisory is a VALUE the self-test can assert on, and main()'s three finding returns are spelled EXIT_FINDINGS. The refusal's existing message is byte-for-byte unchanged (545 bytes, verified against the pre-change run) and gains the shared capture-the-code-BEFORE-any-pipe advisory, which interpolates both numbers rather than spelling either. JUDGEMENT ON THE SECOND BARE process.exit(1) (line 725 before, 766 after): it is a REAL FINDING and stays 1. It is the --self-test dispatch's completion guard (selfTestReachedVerdict), and (a) --self-test has no prerequisite at all -- this gate's own header says it needs neither built side; (b) it fires on a defect in this file's own source (an early return above the verdict line), which no install or build clears; (c) exit 3 would make a sweep read a self-test that stopped running as NOT MEASURED, which is the same silence the guard exists to break. Corroborated, not inferred: the identical guard sits in check-i18n-coverage.mjs, check-dts-closure.mjs line 581, check-doc-frontmatter.mjs line 1805, check-dual-build-cjs-loads.mjs line 1599, check-manifest-repository-directory.mjs line 1258 and objectui-changeset-digest.mjs line 1193 -- eight sites, all correctly 1, including in the files whose refusals were moved to 3. The five reportPrerequisiteNotMet call sites were re-read and all five are genuine 'cannot read my input' refusals; their classification is unchanged. CALLER CHECK: lint.yml line 5902 is a bare `run: pnpm check:i18n-walk-parity` (any non-zero fails it, unchanged); package.json is the script; check-ratchet-remedy-authority.mjs reads this file's remedy TEXT, not its exit code; a repo-wide grep found no consumer branching on this gate's code by value. Measured, not assumed: pnpm propagates the new code (ELIFECYCLE Command failed with exit code 3), so what the CI step sees is 3. NEIGHBOUR SWEEP the card asked for: every scripts/ and packages/lint/scripts/ file printing PREREQUISITE NOT MET was enumerated and its refusal branch read -- eleven already import the shared constant, three refuse with a locally named EXIT_PREREQ = 3, and objectui-changeset-digest.mjs refuses with a documented exit 2 of its own. check-i18n-walk-parity.mjs was the ONLY site returning 1 for a refusal, so this is not a class and nothing else needed a pass. No changeset: skip-changeset applied and read back. File surface respected exactly -- one file changed, no .changeset/ entry needed, scripts/import-prerequisite.mjs read only.", "tests": "BEFORE/AFTER on the card's repro (fresh worktree, pnpm install, closure NOT built; exit codes captured before any pipe with `cmd redirected to a file 2 to 1; EXIT=$?`): BEFORE `pnpm check:i18n-walk-parity` EXIT=1 with 'PREREQUISITE NOT MET: the workspace spec package is not built'. AFTER `node scripts/check-i18n-walk-parity.mjs` EXIT=3, and the refusal text is byte-identical for its first 545 bytes (compared programmatically against the BEFORE log) with only the new pipe advisory appended. AFTER `pnpm check:i18n-walk-parity` EXIT=3 ('ELIFECYCLE Command failed with exit code 3'), measured by parking packages/spec/dist/system/index.mjs aside under a trap and restoring it (sha256 verified identical after restore). ON THE BUILT TREE (turbo run build --filter=@objectstack/cli --filter=@objectstack/spec under scripts/pm/os-verify-lock.sh, VERDICT command-exit 0, 57/57 tasks successful): `pnpm check:i18n-walk-parity` EXIT=0 -- '43 cases pass' then '11 declared group(s), 8 walked, 3 exempted'. THE PIN drives all three verdicts end to end in a throwaway root holding this gate plus the four modules it imports: repro gives 3 with the same headline; FIRING CONTROL (a declared group nothing emits, both sides readable) gives 1, reached by measuring, its output carrying no refusal; NONSENSE CONTROL (nothing to find) gives 0. Plus structural cases 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. ABLATIONS (run from the COMMITTED implementation, each mutation proved to have reached disk by grep count in both directions, each restored with `git checkout HEAD -- path` and verified by git hash-object against the HEAD blob, trap on EXIT INT TERM with an absolute repo root, `git diff HEAD` empty at the end): (1) put the defect back, process.exit(EXIT_PREREQUISITE_NOT_MET) becomes process.exit(1) (injected count 3, removed count 0) -- self-test EXIT=1 with exactly two failures, 'refusal class: the refusal exits through the named constant, never a literal' and '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 with 'FIRING CONTROL: a REAL finding in the same harness still exits 1: got 3, want 1'; this is the leg proving the pin is not one-sided. (3) a REAL finding on the REAL built tree via a deliberately stale ledger entry -- production path, EXIT=1, printing the ledger-ratchet finding (the ratchet branch fired before the stale branch; reporting what actually fired). Control: the restored, 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` printed 'Run reconciliation -- 31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN.' All 31 exited 0 (each exit code captured into its own .exit file before any pipe), including check:ratchet-remedy-authority, check:pm-dispatch-gates, check:self-test-wired, check:scripts-symbol-anchors, check:declaration-mirrors, check:cross-package-test-inputs, check:parse-guard and check:nul-bytes. LINT: `pnpm exec eslint --no-inline-config --format json` over the changed file -- 1 file, 0 errors, 0 warnings. The narrowing is a measurement: eslint.config.mjs states in its own words that this repo 'never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file', so a one-file diff cannot move any untouched file's verdict; the repo-wide `pnpm lint` run is CI's. CONTROL CHARACTERS: grep -naP over the C0/DEL class on the changed file -- no matches, and check:nul-bytes green. CLAUSE-2: `node scripts/pm/check-clause2-carriers.mjs --pair 16970` EXIT=0 -- both carriers agree on Clause-2 no, and the diff carries no widening tell. CHANGESET: measured, not asserted -- 0 hits for 'check-i18n-walk-parity' across the 76 declared files[] roots of the 23 published packages, positive control 'TranslationDataSchema' 32 hits in the same sweep, root package private; skip-changeset applied via the additive POST (HTTP 200) and read back present, sole label. NOT MEASURED: CI itself -- this report is delivered at draft-PR time, as this contract requires. Every local command ran in the dedicated worktree /home/user/objectstack-issue-16558.", "mcp_calls": "0 — every GitHub read and write went through curl against the REST API; no MCP GitHub tool was called", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: check-dual-build-cjs-loads.mjs, check-manifest-repository-directory.mjs and docs-audit/check-docs-transcript-drift.mjs each declare a local `EXIT_PREREQ = 3` rather than importing the shared constant. The number is right and each pins it in its own self-test, and import-prerequisite.mjs's own header names that spelling as the prior art the constant was extracted from — so it is neither a reproducible defect nor a breach of a declared contract, and it produces no metadata a runtime rejects. Taker: none — no open PR touches those three files today." ] }Generated by Claude Code
claude commentedon Sep 8, 2026
LANDED — PR #16970 merged 2026-09-08T21:58:46Z (merge queue;
added_to_merge_queue21:32:40Z, merged in the same merge group as #16969).Verified on a re-fetched
origin/main, ⛔ not on the report:./import-prerequisite.mjsimport-prerequisite.mjsunchanged,EXIT_PREREQUISITE_NOT_MET = 3still there⭐ The last control matters: this card's whole point is that there is one home for the constant. A fix that quietly forked it would satisfy every other row.
The exit-site map, old → new, read by me before landing
origin/main(pre-merge):324 process.exit(1)— the refusal:336 process.exit(EXIT_PREREQUISITE_NOT_MET):725 process.exit(1)— the self-test completion guard:916 process.exit(1):727 process.exit(code)·:729 process.exit(await main(…)):918·:920:722 const controlHardcodedExit = () => { process.exit(1); };⇒ Exactly one exit site moved, and it is the one the card is about.
⭐ The
:725judgement — the part the card never asked forI asked the dev to judge the second bare
process.exit(1)the card does not mention. It stays 1, and the third reason is the sharp one:⇒ the guard and the constant point in opposite directions, and telling them apart is the card. ⭐ Corroborated rather than inferred: the identical guard sits at eight sites, including inside the very files whose refusals were moved to 3.
⭐ The pin is two-sided
Ablation 2 — 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. And
:722'scontrolHardcodedExitis the same discipline one level up: the structural assertion is given a function that does the forbidden thing, so it can fail.The caller check, and the one that could have bitten
lint.yml:5902is a barerun:(any non-zero fails it, unchanged);check-ratchet-remedy-authority.mjsreads this gate's remedy text, not its code; a repo-wide grep found no consumer branching on the value. ⭐ And the risk was measured, not assumed:pnpmpropagates the new code (ELIFECYCLE Command failed with exit code 3), so what the CI step sees is 3.⭐ Not a class — and saying so was the right report
Every
scripts/**file printingPREREQUISITE NOT METwas enumerated: eleven already import the shared constant, three declare a localEXIT_PREREQ = 3, one documents its own exit 2. This file was the only site returning 1 for a refusal. ⇒ a singleton, and the right answer was to report that rather than open five more cards. ⛔ The three local-constant sites correctly not filed: the number is right, each pins it in its own self-test, andimport-prerequisite.mjs's header names that spelling as the prior art the constant was extracted from.Message preservation
The refusal's 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