diff --git a/.claude/skills/pm-dispatch/references/lanes/services.md b/.claude/skills/pm-dispatch/references/lanes/services.md index f3fd4177b2..d90c58f485 100644 --- a/.claude/skills/pm-dispatch/references/lanes/services.md +++ b/.claude/skills/pm-dispatch/references/lanes/services.md @@ -19,7 +19,7 @@ - **安全族卡的披露纪律**:复现配方不落任何公开面(卡、PR、评论);证据以抽象描述 或私有通道承载。 - 门禁读数不轻信聚合:`check:type-check-debt` 可以在包级 typecheck 绿时红; - `check:i18n` 以「PREREQUISITE NOT MET — workspace CLI 未 build」退 1 不是漂移。 + `check:i18n` 以「PREREQUISITE NOT MET — workspace CLI 未 build」退 3 不是漂移。 ## 席内判断 diff --git a/packages/cli/scripts/check-app-nav-i18n.mjs b/packages/cli/scripts/check-app-nav-i18n.mjs index 870321a580..00578ef694 100644 --- a/packages/cli/scripts/check-app-nav-i18n.mjs +++ b/packages/cli/scripts/check-app-nav-i18n.mjs @@ -105,6 +105,10 @@ import { existsSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; +// The exit-code contract for a refusal, from the frame that owns it — +// imported rather than re-picked, the shape `packages/lint/scripts/*` already +// use to reach repo-root gate infrastructure from inside a package. +import { EXIT_FINDINGS, EXIT_PREREQUISITE_NOT_MET } from '../../../scripts/import-prerequisite.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const CLI_ROOT = join(HERE, '..'); @@ -605,6 +609,25 @@ if (process.argv.includes('--self-test')) { * Answered once, before anything is imported — a missing build must cost one * stated verdict, never a node stack pointing at whichever package happened to * be imported first (the #5862 lesson on the neighbouring i18n gates). + * + * Exits `EXIT_PREREQUISITE_NOT_MET`, and the printed advisory says the same + * number: nothing was measured, so this is NOT a finding. + * + * ⛔ The closing paragraph is the frame's, verbatim apart from this gate's own + * command, and it is NOT a place to improvise. The wording it replaced was true + * but prescribed `echo "EXIT=$?"` without saying WHERE, so a reader who did the + * natural thing — `... | tail -4; echo "EXIT=$?"` — read `tail`'s status rather + * than this gate's, which is the exact false green the prescription exists to + * prevent. The one true half ("capture it BEFORE any pipe") had existed in + * `scripts/check-test-completeness.mjs` all along and simply never reached here. + * + * ⛔ That older phrasing is also the CENSUS-NEGATIVE string: the instrument for + * this advisory family is `git grep -n "no pipe shape repairs it"`, and the + * acceptance criterion is that no copy of the pre-convergence sentence survives + * anywhere under `scripts/**` or `packages/**`. So do not reintroduce it here — + * not even inside a comment, quoting it to explain what was wrong. (Measured: + * the first draft of THIS comment did exactly that, and put the criterion back + * into the red while the code beside it was already correct.) */ function checkBuildPrerequisite() { const probe = join(CLI_ROOT, 'node_modules', '@objectstack', 'setup', 'dist', 'index.mjs'); @@ -617,9 +640,15 @@ function checkBuildPrerequisite() { ` Fix: pnpm build (or: pnpm --filter '@objectstack/cli^...' build)\n\n` + ` Nothing was measured: no app was merged and no locale was compared, so this\n` + ` result says NOTHING about whether any nav label went untranslated.\n` + - ` (Exit code 1 — but piping this gate reports the PIPE's status. Use \`echo "EXIT=$?"\`.)`, + ` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` + + ` \`node packages/cli/scripts/check-app-nav-i18n.mjs > /tmp/check-app-nav-i18n.log 2>&1; echo "EXIT=$?"\`.\n` + + ` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` + + ` is the false green, and no pipe shape repairs it. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover\n` + + ` this gate's own code: \`| tail\` reads to EOF and forwards it, while \`| head -N\` closes the\n` + + ` read end early — the gate takes EPIPE, its verdict text is TRUNCATED, and a producer that\n` + + ` dies on SIGPIPE reports 141 rather than what it meant to say.)`, ); - process.exit(1); + process.exit(EXIT_PREREQUISITE_NOT_MET); } checkBuildPrerequisite(); diff --git a/scripts/check-i18n-bundles.mjs b/scripts/check-i18n-bundles.mjs index c7ee5eb6cb..f520562d21 100644 --- a/scripts/check-i18n-bundles.mjs +++ b/scripts/check-i18n-bundles.mjs @@ -100,6 +100,7 @@ import { resolveCliCommandFile, workspaceBuildFix, } from './cli-build-prerequisite.mjs'; +import { EXIT_FINDINGS, EXIT_PREREQUISITE_NOT_MET } from './import-prerequisite.mjs'; import { findExtractConfigs, flagsFromDocstring } from './i18n-bundle-surface.mjs'; /** @@ -971,9 +972,15 @@ if (process.argv.includes('--self-test')) { * ONE prerequisite and ONE command to satisfy it — never per package, and never * phrased so it can be mistaken for a verdict about the bundles. * - * Exits 1, the same code the two real verdicts use: any wrapper that treats - * non-zero as failure keeps behaving identically, and inventing a second failure - * code would be a new contract nobody asked for. + * Exits `EXIT_PREREQUISITE_NOT_MET` — the constant `import-prerequisite.mjs` + * exports, imported rather than re-picked, and printed by the advisory in the + * same stroke. ⛔ NOT a second failure code invented here: 3 is what every + * other gate in this repo already means by these two words (#13983 moved the + * 45-gate shared frame onto it), and this site was one of the last three + * contradicting them. Nothing mechanical changes — every consumer of these + * gates treats any non-zero as failure — so the whole benefit is that a reader + * who sees only the number learns what the text already says: nothing was + * measured, and this is NOT a finding. * * `scanned` is how many packages the loop had already attempted when the * prerequisite fired, and it is what keeps the closing paragraph TRUE. The @@ -1005,14 +1012,15 @@ function reportPrerequisiteNotMet(headline, detail, options = {}) { `\n\n Fix: ${fix}\n` + alsoFix.map((l) => ` ${l}\n`).join('') + `\n${nothingChecked}\n` + - ` (Exit code 1 — capture it BEFORE any pipe: \`pnpm check:i18n > /tmp/i18n.log 2>&1; echo "EXIT=$?"\`.\n` + + ` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` + + ` \`pnpm check:i18n > /tmp/i18n.log 2>&1; echo "EXIT=$?"\`.\n` + ` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` + ` is the false green, and no pipe shape repairs it. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover\n` + ` this gate's own code: \`| tail\` reads to EOF and forwards it, while \`| head -N\` closes the\n` + ` read end early — the gate takes EPIPE, its verdict text is TRUNCATED, and a producer that\n` + ` dies on SIGPIPE reports 141 rather than what it meant to say.)`, ); - process.exit(1); + process.exit(EXIT_PREREQUISITE_NOT_MET); } /** diff --git a/scripts/check-i18n-coverage.mjs b/scripts/check-i18n-coverage.mjs index 7957f769c8..024475b615 100644 --- a/scripts/check-i18n-coverage.mjs +++ b/scripts/check-i18n-coverage.mjs @@ -181,6 +181,7 @@ import { owningPackageOf, resolveCliCommandFile, } from './cli-build-prerequisite.mjs'; +import { EXIT_FINDINGS, EXIT_PREREQUISITE_NOT_MET } from './import-prerequisite.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); /** This script lives in `scripts/`, so the repo root is one level up (#10907). */ @@ -1141,9 +1142,15 @@ if (process.argv.includes('--self-test')) { * ONE prerequisite and ONE command to satisfy it — never per config, and never * phrased so it can be mistaken for a verdict about a config's translations. * - * Exits 1, the same code the real verdict uses: any wrapper that treats non-zero - * as failure keeps behaving identically, and inventing a second failure code - * would be a new contract nobody asked for. + * Exits `EXIT_PREREQUISITE_NOT_MET` — the constant `import-prerequisite.mjs` + * exports, imported rather than re-picked, and printed by the advisory in the + * same stroke. ⛔ NOT a second failure code invented here: 3 is what every + * other gate in this repo already means by these two words (#13983 moved the + * 45-gate shared frame onto it), and this site was one of the last three + * contradicting them. Nothing mechanical changes — every consumer of these + * gates treats any non-zero as failure — so the whole benefit is that a reader + * who sees only the number learns what the text already says: nothing was + * measured, and this is NOT a finding. * * The remedy is stated at TWO widths on purpose. `CLI_BUILD_FIX` is the command * that clears exactly what was checked, and nothing more — this probe measures the @@ -1172,7 +1179,7 @@ function reportPrerequisiteNotMet(headline, detail) { ` Nothing was measured: no config was linted and no count was compared, so this\n` + ` result says NOTHING about whether any declared label went untranslated — and\n` + ` the baseline was left exactly as committed (\`--update\` included).\n` + - ` (Exit code 1 — capture it BEFORE any pipe:\n` + + ` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` + ` \`pnpm check:i18n-coverage > /tmp/i18n-coverage.log 2>&1; echo "EXIT=$?"\`.\n` + ` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` + ` is the false green, and no pipe shape repairs it. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover\n` + @@ -1180,7 +1187,7 @@ function reportPrerequisiteNotMet(headline, detail) { ` read end early — the gate takes EPIPE, its verdict text is TRUNCATED, and a producer that\n` + ` dies on SIGPIPE reports 141 rather than what it meant to say.)`, ); - process.exit(1); + process.exit(EXIT_PREREQUISITE_NOT_MET); } /** @@ -1198,7 +1205,10 @@ function reportPrerequisiteNotMet(headline, detail) { * The same invariant `reportPrerequisiteNotMet` states: nothing measured, nothing * written. * - * Exits 1, the same code every other verdict here uses. + * Exits `EXIT_PREREQUISITE_NOT_MET`, NOT a finding's 1 — the closing paragraph + * already says nothing was compared, and the code now says the same thing. A + * partial round answered this gate's question about exactly nothing, which is + * what that code means everywhere else in this repo. */ function reportUnmeasuredConfigs(failures, measuredCount) { const groups = groupFailuresByCause(failures); @@ -1232,7 +1242,7 @@ function reportUnmeasuredConfigs(failures, measuredCount) { ` \`--update\` would freeze the survivors while silently dropping the rest. So this\n` + ` result says NOTHING about whether any declared label went untranslated, and the\n` + ` baseline was left exactly as committed (\`--update\` included).\n` + - ` (Exit code 1 — capture it BEFORE any pipe:\n` + + ` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` + ` \`pnpm check:i18n-coverage > /tmp/i18n-coverage.log 2>&1; echo "EXIT=$?"\`.\n` + ` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` + ` is the false green, and no pipe shape repairs it. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover\n` + @@ -1240,7 +1250,7 @@ function reportUnmeasuredConfigs(failures, measuredCount) { ` read end early — the gate takes EPIPE, its verdict text is TRUNCATED, and a producer that\n` + ` dies on SIGPIPE reports 141 rather than what it meant to say.)`, ); - process.exit(1); + process.exit(EXIT_PREREQUISITE_NOT_MET); } /** @@ -1253,7 +1263,9 @@ function reportUnmeasuredConfigs(failures, measuredCount) { * is to record it. Same invariant the two reports above state, and for the same * reason: nothing measured, nothing written. * - * Exits 1, the code every other verdict here uses. + * Exits `EXIT_PREREQUISITE_NOT_MET`, NOT a finding's 1 — an empty population is + * the sharpest case of "nothing was measured", and that is the code this repo + * reserves for it. * * @param {{ headline: string, detail: string[] }} verdict */ @@ -1266,7 +1278,7 @@ function reportEmptyPopulation(verdict) { ` Nothing was measured: no config was linted and no count was compared, so this\n` + ` result says NOTHING about whether any declared label went untranslated — and\n` + ` the baseline was left exactly as committed (\`--update\` included).\n` + - ` (Exit code 1 — capture it BEFORE any pipe:\n` + + ` (Exit code ${EXIT_PREREQUISITE_NOT_MET}, distinct from a finding's ${EXIT_FINDINGS} — capture it BEFORE any pipe:\n` + ` \`pnpm check:i18n-coverage > /tmp/i18n-coverage.log 2>&1; echo "EXIT=$?"\`.\n` + ` Piped, \`$?\` is the LAST command's status, and \`head\`/\`tail\` essentially never fail — that\n` + ` is the false green, and no pipe shape repairs it. \`\${PIPESTATUS[0]}\`/\`pipefail\` do recover\n` + @@ -1274,7 +1286,7 @@ function reportEmptyPopulation(verdict) { ` read end early — the gate takes EPIPE, its verdict text is TRUNCATED, and a producer that\n` + ` dies on SIGPIPE reports 141 rather than what it meant to say.)`, ); - process.exit(1); + process.exit(EXIT_PREREQUISITE_NOT_MET); } /**