Skip to content

Commit 7277c07

Browse files
objectstack-fleet[bot]hotlongclaude
authored
feat(scripts): check:pnpm-flag-spellings — no committed pnpm call spells -s, --prod=VALUE, --production=VALUE or --no-production, which a global pnpm 11+/12 frontend rejects (#23824)
## Problem #23777 removed every pnpm argv spelling that a contributor's global pnpm 11.x / 12.x `@pnpm/exe` frontend rejects before it hands off to the pinned pnpm 10.31: `-s`, and `--prod=false` on the objectui install. Nothing stops a script from bringing one back. CI cannot catch a regression. It gets pnpm through Corepack, which runs the real 10.31 binary with no frontend in front of it, and 10.31 accepts every one of these spellings. A regression stays green in CI and goes red only on a laptop, where it shows up as something other than a flag problem. Before #23777, `check:generated` read the frontend's exit 2 as "every artifact stale", the pre-commit regen gate refused every commit, and `pnpm objectui:build` installed nothing. Only a scan of the committed source can see it. ## Change - **`scripts/check-pnpm-flag-spellings.mjs`** (new). It reds on any pnpm run in tracked source that uses one of these spellings in a pnpm option position: | spelling | fix it prints | frontend says (measured 2026-10-16, #23777) | |---|---|---| | `-s` | `--silent` | unexpected argument '-s' found, exit 2 | | `--prod=VALUE`, `--production=VALUE` | `--config.production=VALUE` | unexpected value, exit 2 | | `--no-production` | `--config.production=false` | unexpected argument, exit 2 | `--silent`, `--reporter=silent`, `--no-prod` and `--config.production=…` are accepted, and counted on the success line so that a zero is a measurement. - **`check:pnpm-flag-spellings`** root script: `--self-test`, then the gate. - **`lint.yml`**: one step in the `Lint · CI, release & engine gates` lane, right after its sibling `check:agent-test-spelling`, with an `id:` and `continue-on-error: true` like every gate there. The lane's verdict step keeps any red, and the required `Lint & Repo Gates` aggregate carries it. ### What it reads - **Population.** Tracked files only (`git ls-files`, read from the working tree): - `scripts/**`, every `scripts/` directory under `packages/`, and `.githooks/**`. JS/TS files are recognised by extension; shell files by extension or by an `sh`/`bash` shebang, because the hooks have no extension. - The `scripts` values of every tracked `package.json`. - **Out of scope:** `.github/workflows/**`. CI's pnpm is Corepack's 10.31 binary, which no frontend fronts, so no workflow can hit this. - **Three source forms:** 1. shell text, with comments stripped; 2. the content of JS/TS string and template literals, with comments masked through `js-comment-mask.mjs` and regex literals not read; 3. JS/TS argv arrays. That covers both `spawnSync('pnpm', ['--filter', pkg, '-s', 'build'])` and the tuple form `['pnpm', '-s', 'build']`. - **Judged positions.** A flag is judged only where pnpm itself parses it: - before the command word; - after a built-in command such as `install` or `list`; - between `run`/`exec`/`dlx` and its target. Anything after a script name, a bin, a run target or a bare `--` is forwarded and never judged. So `pnpm build -s` is green, and so is `git ls-files -s`. - **Failure direction.** If the gate meets a value-taking flag it doesn't know, the run goes unjudged. That's a miss, never a false red. - **Refuses (exit 2) instead of passing** when any of these holds: - `git ls-files` fails; - a declared root holds no tracked file; - no manifest carries `scripts`; - a tracked manifest doesn't parse; - the whole population yields no pnpm run. - **Misses, named in the header:** - flags built from a variable (`pnpm${s}`, as in `ownerRunCommand`, whose output `git-merge-regen.mjs --self-test` already pins); - argv arrays assembled somewhere else; - combined short clusters such as `-rs`. - **Dispatch watch hints.** The gate declares its population in the spellings `scripts/pm/dispatch-gates.mjs` can match, so a card that adds a new script under one of these roots, or touches a manifest, is routed to it. Before this, the derivation read the `'scripts/'` label as a bare root no dispatch can name, and its battery said so. The header and a self-test pin cover the coverage measurements and both reconciliation directions. ## Evidence All on `adf2ca745e`, branched from `main` at `59f9763d3e`, which already contains #23777. **Green on main:** ``` $ pnpm check:pnpm-flag-spellings # exit 0 ✓ check-pnpm-flag-spellings self-test: 64 cases across 7 batteries pass. ✓ check-pnpm-flag-spellings: no rejected pnpm flag spelling in 583 tracked file(s). roots: scripts/ 434 · packages/**/scripts/ 174 · .githooks/ 2; read 478 JS/TS, 20 shell, 85 package.json (617 scripts); 111 data/prose file(s) not read, 0 tracked path(s) absent. 907 pnpm run(s), 18 of them argv arrays; 235 flag(s) in a pnpm option position, accepted spellings among them: --silent ×5, --reporter=silent ×0, --no-prod ×0, --config.production= ×1. ``` **Red on a planted occurrence.** I put the two spellings #23777 removed back into tracked files, ran the gate, restored both files from HEAD, and ran it again: ``` $ sed -i '' 's/pnpm install --frozen-lockfile --prefer-offline --config.production=false/pnpm install --frozen-lockfile --prefer-offline --prod=false/' scripts/build-console.sh $ sed -i '' 's/execSync(`pnpm --silent ${script}`/execSync(`pnpm -s ${script}`/' packages/spec/scripts/check-generated.ts $ node scripts/check-pnpm-flag-spellings.mjs # exit 1 ✗ check-pnpm-flag-spellings: 2 pnpm flag spelling(s) that a global pnpm 11+/12 `@pnpm/exe` frontend rejects. • packages/spec/scripts/check-generated.ts:690 pnpm -s ${} `-s`: the frontend has no short `-s` ("unexpected argument '-s' found", exit 2) fix: spell it `--silent` • scripts/build-console.sh:334 npm_config_registry="$NPM_CONFIG_REGISTRY_OVERRIDE" pnpm install --frozen-lockfile --prefer-offline --prod=false `--prod=false`: `--prod` / `--production` are booleans to the frontend, which refuses a value on them ("unexpected value", exit 2) fix: spell it `--config.production=false` ``` ``` $ git restore --source=HEAD --staged --worktree scripts/build-console.sh packages/spec/scripts/check-generated.ts $ git status --porcelain # (empty) $ node scripts/check-pnpm-flag-spellings.mjs # exit 0 ✓ check-pnpm-flag-spellings: no rejected pnpm flag spelling in 583 tracked file(s). ``` **`--self-test`:** 64 cases across 7 batteries pass, under a frozen roster with per-battery floors and a verdict handshake (shape copied from `check-agent-model-declared.mjs`, not imported). Besides the positive and negative spelling cases, the `TREE` battery runs the real sweep over throwaway git work trees on disk: - a clean tree is green; - each of these is red at the right `file:line`: a planted `pnpm -s` in a tracked `scripts/` shell file, in a `package.json` script, in a JS spawn under `packages/*/scripts/`, and in an extension-less git hook; - the same plant is green when untracked, when tracked outside every root, and when placed in the rule-owning file; - a missing root, a pnpm-free population, an unparseable manifest and a non-git directory are each REFUSED. Mutation probes on a throwaway copy, both red as intended: - deleting one RED case → `battery "RED — …" registered 18 case(s), below its pinned floor of 19`, exit 1; - a `return` above the floor → `selfTest() returned without reaching its verdict`, exit 1. **Neighbouring gates run locally, all exit 0:** - `check:pnpm-filter-targets` and `check:entry-guard`; - `check:parse-guard` and `check-comment-mask-adoption`; - `check-self-test-wired` and `check-aggregator-roster`; - `check-ci-filter-parity` and `check:doc-authoring`; - `check:watch-hint-literal` and `check:declared-population-live`; - `check:workflow-step-name-quoting`, `check:required-contexts` and `check:select-gate-families`; - `bare-root-worklist --self-test`, `check:issue-citations` and `check:pm-skill-id-lint`; - `check:scripts-symbol-anchors` and `check:ratchet-remedy-authority`; - `check-workflow-status-functions`; - `check:pm-dispatch-gates`, with all fast-tier cases passing; - eslint on the new file. `check:agent-test-spelling` is red locally only on `.claude/settings.local.json`, an untracked file that a global gitignore excludes. CI never has that file, and the red is unrelated to this diff. ## Notes - **Governed surface.** This diff touches `.github/workflows/lint.yml`, a Tier H surface under AGENTS.md Prime Directive 14. The PR is a draft and the landing is the maintainer's. - **No changeset.** Only root `scripts/`, the root `package.json` scripts block and a workflow change. None of them ships in a published package, so the PR carries `skip-changeset`. - **No full `pnpm test` run.** The diff touches no workspace package; the gate's own `--self-test` is its test. --- _Generated by [Claude Code](https://claude.ai/code/session_local_1904ee85-9672-427b-bc91-7e31318013c7)_ --------- Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent e375315 commit 7277c07

3 files changed

Lines changed: 1174 additions & 0 deletions

File tree

‎.github/workflows/lint.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4057,6 +4057,12 @@ jobs:
40574057
continue-on-error: true
40584058
run: pnpm check:agent-test-spelling
40594059

4060+
# Rationale: `scripts/check-pnpm-flag-spellings.mjs` header (#23777).
4061+
- name: No committed pnpm call spells a flag the pnpm 11 frontend rejects
4062+
id: no-committed-pnpm-call-spells-a-rejected-flag
4063+
continue-on-error: true
4064+
run: pnpm check:pnpm-flag-spellings
4065+
40604066
# Every `<pkg>#<task>` override in turbo.json must name a package the
40614067
# workspace HAS and a script that package RUNS (#12046).
40624068
#

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@
109109
"check:single-claim-paths": "node scripts/check-single-claim-paths.mjs --self-test",
110110
"check:closing-target-claim": "node scripts/check-closing-target-claim.mjs --self-test",
111111
"check:pnpm-filter-targets": "node scripts/pnpm-filter-targets.mjs --self-test && node scripts/check-pnpm-filter-targets.mjs --self-test && node scripts/check-pnpm-filter-targets.mjs",
112+
"check:pnpm-flag-spellings": "node scripts/check-pnpm-flag-spellings.mjs --self-test && node scripts/check-pnpm-flag-spellings.mjs",
112113
"check:turbo-task-graph": "node scripts/check-turbo-task-graph.mjs --self-test && node scripts/check-turbo-task-graph.mjs",
113114
"check:workspace-manifest-cycles": "node scripts/check-workspace-manifest-cycles.mjs --self-test && node scripts/check-workspace-manifest-cycles.mjs",
114115
"check:adr-anchors": "node scripts/check-adr-anchors.mjs --self-test && node scripts/check-adr-anchors.mjs",

0 commit comments

Comments
 (0)