Skip to content

Commit fcdbc6d

Browse files
committed
fix(gates): answer PREREQUISITE NOT MET with the frame's exit code in the last three i18n gates
`#13983` moved `scripts/import-prerequisite.mjs` -- the frame 45 gates inherit -- onto `EXIT_PREREQUISITE_NOT_MET` (3), joining the five sibling sites that already meant 3 by those two words. Three gates still answered them with 1: - `scripts/check-i18n-coverage.mjs` 3 refusal paths - `scripts/check-i18n-bundles.mjs` 1 - `packages/cli/scripts/check-app-nav-i18n.mjs` 1 All five now import `EXIT_PREREQUISITE_NOT_MET` / `EXIT_FINDINGS` from that frame rather than re-picking them, so there is no new literal 3 in the diff, and each printed advisory announces the code it actually exits with. The real-verdict and self-test exits in these files are untouched; only the refusals that already say "nothing was measured" moved. `packages/cli/scripts/check-app-nav-i18n.mjs` also takes the converged pipe-shape paragraph (`#13429`'s, verbatim apart from this gate's own command). Its old sentence prescribed `echo "EXIT=$?"` without saying WHERE, so a reader who did the natural thing read `tail`'s status -- the exact false green the prescription exists to prevent. The census `git grep -l "no pipe shape repairs it"` goes 7 files to 8, and the census-negative string now has zero hits under `scripts/**` and `packages/**`. The lane doc `.claude/skills/pm-dispatch/references/lanes/services.md:22` keyed on the old number, so it moves in the same stroke rather than starting to lie. Governed surface: this lands as a draft for a human merge. Cards: `#14008` (the exit code) and `#13825` (the banner wording) -- declared once in the PR body, since this branch squashes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LAwHpn4uVuf4N1geBcD5i3
1 parent f3ae441 commit fcdbc6d

4 files changed

Lines changed: 68 additions & 19 deletions

File tree

.claude/skills/pm-dispatch/references/lanes/services.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
- **安全族卡的披露纪律**:复现配方不落任何公开面(卡、PR、评论);证据以抽象描述
2020
或私有通道承载。
2121
- 门禁读数不轻信聚合:`check:type-check-debt` 可以在包级 typecheck 绿时红;
22-
`check:i18n` 以「PREREQUISITE NOT MET — workspace CLI 未 build」退 1 不是漂移。
22+
`check:i18n` 以「PREREQUISITE NOT MET — workspace CLI 未 build」退 3 不是漂移。
2323

2424
## 席内判断
2525

packages/cli/scripts/check-app-nav-i18n.mjs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@
105105
import { existsSync } from 'node:fs';
106106
import { join, dirname } from 'node:path';
107107
import { fileURLToPath } from 'node:url';
108+
// The exit-code contract for a refusal, from the frame that owns it —
109+
// imported rather than re-picked, the shape `packages/lint/scripts/*` already
110+
// use to reach repo-root gate infrastructure from inside a package.
111+
import { EXIT_FINDINGS, EXIT_PREREQUISITE_NOT_MET } from '../../../scripts/import-prerequisite.mjs';
108112

109113
const HERE = dirname(fileURLToPath(import.meta.url));
110114
const CLI_ROOT = join(HERE, '..');
@@ -605,6 +609,25 @@ if (process.argv.includes('--self-test')) {
605609
* Answered once, before anything is imported — a missing build must cost one
606610
* stated verdict, never a node stack pointing at whichever package happened to
607611
* be imported first (the #5862 lesson on the neighbouring i18n gates).
612+
*
613+
* Exits `EXIT_PREREQUISITE_NOT_MET`, and the printed advisory says the same
614+
* number: nothing was measured, so this is NOT a finding.
615+
*
616+
* ⛔ The closing paragraph is the frame's, verbatim apart from this gate's own
617+
* command, and it is NOT a place to improvise. The wording it replaced was true
618+
* but prescribed `echo "EXIT=$?"` without saying WHERE, so a reader who did the
619+
* natural thing — `... | tail -4; echo "EXIT=$?"` — read `tail`'s status rather
620+
* than this gate's, which is the exact false green the prescription exists to
621+
* prevent. The one true half ("capture it BEFORE any pipe") had existed in
622+
* `scripts/check-test-completeness.mjs` all along and simply never reached here.
623+
*
624+
* ⛔ That older phrasing is also the CENSUS-NEGATIVE string: the instrument for
625+
* this advisory family is `git grep -n "no pipe shape repairs it"`, and the
626+
* acceptance criterion is that no copy of the pre-convergence sentence survives
627+
* anywhere under `scripts/**` or `packages/**`. So do not reintroduce it here —
628+
* not even inside a comment, quoting it to explain what was wrong. (Measured:
629+
* the first draft of THIS comment did exactly that, and put the criterion back
630+
* into the red while the code beside it was already correct.)
608631
*/
609632
function checkBuildPrerequisite() {
610633
const probe = join(CLI_ROOT, 'node_modules', '@objectstack', 'setup', 'dist', 'index.mjs');
@@ -617,9 +640,15 @@ function checkBuildPrerequisite() {
617640
` Fix: pnpm build (or: pnpm --filter '@objectstack/cli^...' build)\n\n` +
618641
` Nothing was measured: no app was merged and no locale was compared, so this\n` +
619642
` result says NOTHING about whether any nav label went untranslated.\n` +
620-
` (Exit code 1 — but piping this gate reports the PIPE's status. Use \`echo "EXIT=$?"\`.)`,
643+
` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` +
644+
` \`node packages/cli/scripts/check-app-nav-i18n.mjs > /tmp/check-app-nav-i18n.log 2>&1; echo "EXIT=$?"\`.\n` +
645+
` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` +
646+
` is the false green, and no pipe shape repairs it. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover\n` +
647+
` this gate's own code: \`| tail\` reads to EOF and forwards it, while \`| head -N\` closes the\n` +
648+
` read end early — the gate takes EPIPE, its verdict text is TRUNCATED, and a producer that\n` +
649+
` dies on SIGPIPE reports 141 rather than what it meant to say.)`,
621650
);
622-
process.exit(1);
651+
process.exit(EXIT_PREREQUISITE_NOT_MET);
623652
}
624653

625654
checkBuildPrerequisite();

scripts/check-i18n-bundles.mjs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ import {
100100
resolveCliCommandFile,
101101
workspaceBuildFix,
102102
} from './cli-build-prerequisite.mjs';
103+
import { EXIT_FINDINGS, EXIT_PREREQUISITE_NOT_MET } from './import-prerequisite.mjs';
103104
import { findExtractConfigs, flagsFromDocstring } from './i18n-bundle-surface.mjs';
104105

105106
/**
@@ -971,9 +972,15 @@ if (process.argv.includes('--self-test')) {
971972
* ONE prerequisite and ONE command to satisfy it — never per package, and never
972973
* phrased so it can be mistaken for a verdict about the bundles.
973974
*
974-
* Exits 1, the same code the two real verdicts use: any wrapper that treats
975-
* non-zero as failure keeps behaving identically, and inventing a second failure
976-
* code would be a new contract nobody asked for.
975+
* Exits `EXIT_PREREQUISITE_NOT_MET` — the constant `import-prerequisite.mjs`
976+
* exports, imported rather than re-picked, and printed by the advisory in the
977+
* same stroke. ⛔ NOT a second failure code invented here: 3 is what every
978+
* other gate in this repo already means by these two words (#13983 moved the
979+
* 45-gate shared frame onto it), and this site was one of the last three
980+
* contradicting them. Nothing mechanical changes — every consumer of these
981+
* gates treats any non-zero as failure — so the whole benefit is that a reader
982+
* who sees only the number learns what the text already says: nothing was
983+
* measured, and this is NOT a finding.
977984
*
978985
* `scanned` is how many packages the loop had already attempted when the
979986
* prerequisite fired, and it is what keeps the closing paragraph TRUE. The
@@ -1005,14 +1012,15 @@ function reportPrerequisiteNotMet(headline, detail, options = {}) {
10051012
`\n\n Fix: ${fix}\n` +
10061013
alsoFix.map((l) => ` ${l}\n`).join('') +
10071014
`\n${nothingChecked}\n` +
1008-
` (Exit code 1 — capture it BEFORE any pipe: \`pnpm check:i18n > /tmp/i18n.log 2>&1; echo "EXIT=$?"\`.\n` +
1015+
` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` +
1016+
` \`pnpm check:i18n > /tmp/i18n.log 2>&1; echo "EXIT=$?"\`.\n` +
10091017
` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` +
10101018
` is the false green, and no pipe shape repairs it. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover\n` +
10111019
` this gate's own code: \`| tail\` reads to EOF and forwards it, while \`| head -N\` closes the\n` +
10121020
` read end early — the gate takes EPIPE, its verdict text is TRUNCATED, and a producer that\n` +
10131021
` dies on SIGPIPE reports 141 rather than what it meant to say.)`,
10141022
);
1015-
process.exit(1);
1023+
process.exit(EXIT_PREREQUISITE_NOT_MET);
10161024
}
10171025

10181026
/**

scripts/check-i18n-coverage.mjs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ import {
181181
owningPackageOf,
182182
resolveCliCommandFile,
183183
} from './cli-build-prerequisite.mjs';
184+
import { EXIT_FINDINGS, EXIT_PREREQUISITE_NOT_MET } from './import-prerequisite.mjs';
184185

185186
const HERE = dirname(fileURLToPath(import.meta.url));
186187
/** This script lives in `scripts/`, so the repo root is one level up (#10907). */
@@ -1141,9 +1142,15 @@ if (process.argv.includes('--self-test')) {
11411142
* ONE prerequisite and ONE command to satisfy it — never per config, and never
11421143
* phrased so it can be mistaken for a verdict about a config's translations.
11431144
*
1144-
* Exits 1, the same code the real verdict uses: any wrapper that treats non-zero
1145-
* as failure keeps behaving identically, and inventing a second failure code
1146-
* would be a new contract nobody asked for.
1145+
* Exits `EXIT_PREREQUISITE_NOT_MET` — the constant `import-prerequisite.mjs`
1146+
* exports, imported rather than re-picked, and printed by the advisory in the
1147+
* same stroke. ⛔ NOT a second failure code invented here: 3 is what every
1148+
* other gate in this repo already means by these two words (#13983 moved the
1149+
* 45-gate shared frame onto it), and this site was one of the last three
1150+
* contradicting them. Nothing mechanical changes — every consumer of these
1151+
* gates treats any non-zero as failure — so the whole benefit is that a reader
1152+
* who sees only the number learns what the text already says: nothing was
1153+
* measured, and this is NOT a finding.
11471154
*
11481155
* The remedy is stated at TWO widths on purpose. `CLI_BUILD_FIX` is the command
11491156
* that clears exactly what was checked, and nothing more — this probe measures the
@@ -1172,15 +1179,15 @@ function reportPrerequisiteNotMet(headline, detail) {
11721179
` Nothing was measured: no config was linted and no count was compared, so this\n` +
11731180
` result says NOTHING about whether any declared label went untranslated — and\n` +
11741181
` the baseline was left exactly as committed (\`--update\` included).\n` +
1175-
` (Exit code 1 — capture it BEFORE any pipe:\n` +
1182+
` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` +
11761183
` \`pnpm check:i18n-coverage > /tmp/i18n-coverage.log 2>&1; echo "EXIT=$?"\`.\n` +
11771184
` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` +
11781185
` is the false green, and no pipe shape repairs it. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover\n` +
11791186
` this gate's own code: \`| tail\` reads to EOF and forwards it, while \`| head -N\` closes the\n` +
11801187
` read end early — the gate takes EPIPE, its verdict text is TRUNCATED, and a producer that\n` +
11811188
` dies on SIGPIPE reports 141 rather than what it meant to say.)`,
11821189
);
1183-
process.exit(1);
1190+
process.exit(EXIT_PREREQUISITE_NOT_MET);
11841191
}
11851192

11861193
/**
@@ -1198,7 +1205,10 @@ function reportPrerequisiteNotMet(headline, detail) {
11981205
* The same invariant `reportPrerequisiteNotMet` states: nothing measured, nothing
11991206
* written.
12001207
*
1201-
* Exits 1, the same code every other verdict here uses.
1208+
* Exits `EXIT_PREREQUISITE_NOT_MET`, NOT a finding's 1 — the closing paragraph
1209+
* already says nothing was compared, and the code now says the same thing. A
1210+
* partial round answered this gate's question about exactly nothing, which is
1211+
* what that code means everywhere else in this repo.
12021212
*/
12031213
function reportUnmeasuredConfigs(failures, measuredCount) {
12041214
const groups = groupFailuresByCause(failures);
@@ -1232,15 +1242,15 @@ function reportUnmeasuredConfigs(failures, measuredCount) {
12321242
` \`--update\` would freeze the survivors while silently dropping the rest. So this\n` +
12331243
` result says NOTHING about whether any declared label went untranslated, and the\n` +
12341244
` baseline was left exactly as committed (\`--update\` included).\n` +
1235-
` (Exit code 1 — capture it BEFORE any pipe:\n` +
1245+
` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` +
12361246
` \`pnpm check:i18n-coverage > /tmp/i18n-coverage.log 2>&1; echo "EXIT=$?"\`.\n` +
12371247
` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` +
12381248
` is the false green, and no pipe shape repairs it. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover\n` +
12391249
` this gate's own code: \`| tail\` reads to EOF and forwards it, while \`| head -N\` closes the\n` +
12401250
` read end early — the gate takes EPIPE, its verdict text is TRUNCATED, and a producer that\n` +
12411251
` dies on SIGPIPE reports 141 rather than what it meant to say.)`,
12421252
);
1243-
process.exit(1);
1253+
process.exit(EXIT_PREREQUISITE_NOT_MET);
12441254
}
12451255

12461256
/**
@@ -1253,7 +1263,9 @@ function reportUnmeasuredConfigs(failures, measuredCount) {
12531263
* is to record it. Same invariant the two reports above state, and for the same
12541264
* reason: nothing measured, nothing written.
12551265
*
1256-
* Exits 1, the code every other verdict here uses.
1266+
* Exits `EXIT_PREREQUISITE_NOT_MET`, NOT a finding's 1 — an empty population is
1267+
* the sharpest case of "nothing was measured", and that is the code this repo
1268+
* reserves for it.
12571269
*
12581270
* @param {{ headline: string, detail: string[] }} verdict
12591271
*/
@@ -1266,15 +1278,15 @@ function reportEmptyPopulation(verdict) {
12661278
` Nothing was measured: no config was linted and no count was compared, so this\n` +
12671279
` result says NOTHING about whether any declared label went untranslated — and\n` +
12681280
` the baseline was left exactly as committed (\`--update\` included).\n` +
1269-
` (Exit code 1 — capture it BEFORE any pipe:\n` +
1281+
` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` +
12701282
` \`pnpm check:i18n-coverage > /tmp/i18n-coverage.log 2>&1; echo "EXIT=$?"\`.\n` +
12711283
` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` +
12721284
` is the false green, and no pipe shape repairs it. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover\n` +
12731285
` this gate's own code: \`| tail\` reads to EOF and forwards it, while \`| head -N\` closes the\n` +
12741286
` read end early — the gate takes EPIPE, its verdict text is TRUNCATED, and a producer that\n` +
12751287
` dies on SIGPIPE reports 141 rather than what it meant to say.)`,
12761288
);
1277-
process.exit(1);
1289+
process.exit(EXIT_PREREQUISITE_NOT_MET);
12781290
}
12791291

12801292
/**

0 commit comments

Comments
 (0)