Skip to content

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

Merged
os-zhuang merged 3 commits into
mainfrom
claude/pnpm-flag-spellings-gate
Oct 16, 2026
Merged

os-zhuang merged 3 commits into
mainfrom
claude/pnpm-flag-spellings-gate

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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, fix(scripts): spell pnpm flags so a global pnpm 11+/12 frontend parses them (--config.production=false, --silent) #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

@os-zhuang
os-zhuang marked this pull request as ready for review October 16, 2026 13:39
@os-zhuang
os-zhuang added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit 7277c07 Oct 16, 2026
45 checks passed
@os-zhuang
os-zhuang deleted the claude/pnpm-flag-spellings-gate branch October 16, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file size/xl 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