Skip to content

ci: census pnpm acquisition by need, not by mechanism spelling - #11779

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11765-showcase-smoke-pnpm-path
Aug 24, 2026
Merged

ci: census pnpm acquisition by need, not by mechanism spelling#11779
os-steve merged 1 commit into
mainfrom
claude/issue-11765-showcase-smoke-pnpm-path

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#11765

Verdict: (b) — leave the acquisition, build the census. Measured, not assumed.

The card offered three shapes. The evidence picks (b), and it does so for a reason
stronger than "converting is awkward": converting would have made the reported defect
worse.

The defect is an invisibility, not a download. Today showcase-smoke.yml is the one
anomaly a reader can still trip over. Convert it and every mechanism-keyed count reads 0
corepack enable 0 (after the in-flight conversion), pnpm/action-setup 0 — and the
class reads "closed" while nothing guarantees it stays closed. The last visible oddity
that would prompt anyone to look would have been removed. So the census had to exist
regardless of the conversion, and once it exists the conversion stops being urgent.

The census (read by parsing all 26 workflows, not by grepping one key)

.github/workflows/, at merge base 5cb62d88b. This is the gate's own output:

 26 workflow(s) scanned - 24 job(s) need pnpm - 24 acquisition site(s), 17 of them uncached.
cached 7 composite uses: ./.github/actions/setup-pnpm
UNCACHED 1 action-setup uses: pnpm/action-setup@<ref> <- showcase-smoke.yml:22
UNCACHED 16 corepack-inline run: corepack enable
UNCACHED 0 npm-global run: npm i -g pnpm
UNCACHED 0 standalone run: get.pnpm.io/install.sh
1 job(s) this gate cannot read:
.github/workflows/release.yml:871 (job: docker) -- delegates to a reusable workflow

There is no fourth path. Checked, not assumed:

  • The seven workflows that mention pnpm with no acquisition step (check-links,
    docs-drift-check, half-state-patrol, partof-closing-keyword-guard,
    prerelease-pin-watch, required-set-patrol, single-claim-path-guard) are
    deliberately dependency-free and name pnpm only in comments explaining why they
    install none. A grep-shaped population sweeps all seven in; a parsed one does not.
  • release.yml's docker job delegates to docker-publish.yml, which contains 0
    pnpm mentions.
  • docker/Dockerfile installs the CLI with npm install -g, not pnpm.

Note the census counts corepack enable at 16, not 0 — the conversion PR that takes
it to 0 had not landed at my base. That is the point: this gate is correct on both sides
of that landing, because it is not keyed on that string.

Why the census is keyed on the NEED, not the mechanism

Any gate keyed on a mechanism spelling inherits the very defect being reported: it goes
blind the day someone reaches for a spelling it does not know, and it goes blind
silently, as a zero.

So the population is every job that runs pnpm in a run: step — defined by what
the job needs, not by how it gets it. A mechanism nobody has taught the gate cannot
become a zero; it surfaces as a job that runs pnpm with no recognised acquisition, a
hard failure naming both readings ("a new path, or a broken job"). The unknown becomes
the alarm instead of the blind spot.

For the same reason every mechanism keeps a row at count 0, and the census prints on
success. A zero beside its siblings is a measurement; a zero alone is what this gate
exists to stop being read as an all-clear.

The ordering constraint is real, and is now pinned

The filing dev's reading held up: line 26 is cache: pnpm, which makes setup-node
shell out to pnpm to locate the store — so pnpm must be on PATH before it. This repo
has paid for that twice; partof-closing-keyword-guard.yml and
single-claim-path-guard.yml both carry the receipt:

##[error]Unable to locate executable file: pnpm.

It is statically decidable, so the gate now checks it: a job whose setup-node asks for
the pnpm store cache must acquire pnpm strictly earlier. This is what makes leaving
showcase-smoke.yml alone safe
— the inverted order is now protected rather than
merely tolerated, and the naive conversion is caught rather than shipped.

For the record, conversion is not blocked, just not worth it here: the house
replacement for cache: pnpm already exists (pnpm store pathactions/cache, as
ci.yml does seven times). Converting means dropping cache: pnpm and adding that
pair — not moving the acquisition step. Against that: one nightly, non-blocking job, one
fetch rather than the per-queue-build multiplier the composite was built to remove, and
a green job's dependency cache to re-plumb with no way to test it outside a nightly run.
The card's own instruction was not to break the nightly to tidy the census.

Non-vacuity: both ablations, controls in both directions

Each ablation carried a trap ... EXIT INT TERM restore; the tree was verified
byte-identical to the commit afterwards (git status empty).

Ablation 1 — delete the acquisition step. On-disk confirmed on the exact anchor
^ - uses: pnpm/action-setup@v6$ (1 → 0):

beforeafter
gate exit01
action-setup census row10
composite (control)77
corepack-inline (control)1616

Reported: showcase-smoke.yml:51 (job: smoke) -- runs pnpm but no step in the job acquires it by any recognised mechanism. This is the probe's positive control: the count
can be non-zero, and the gate does read that file.

Ablation 2 — the naive conversion this card invites (move the acquisition below
setup-node, leave cache: pnpm). On-disk confirmed: acquisition step still present
(1), cache: pnpm still present (1), order inverted (lines 50 / 51).

Gate exit 0 → 1, reporting the setup-node ordering failure — while every mechanism
count stayed put
(7 / 1 / 16 / 0 / 0). That is the sharpest control here: the failure
is about order, not population, and the census proves it did not move.

Verification

Run on the tree at 44fecec83 (clean working tree).

  • pnpm lint — the full repo scan, no narrowing: eslint . --no-inline-config,
    5019 files linted, 0 findings, exit 0. Population read from eslint's own config and
    file count from --format json; scripts/check-pnpm-acquisition.mjs is confirmed
    present in that population with 0 errors / 0 warnings — so the zero is a measurement,
    not an exclusion.
  • pnpm check:pnpm-acquisition — exit 0. Self-test: 12 cases pass over real fixture
    roots through the real scan(), including both population traps (pnpm-lock.yaml and
    a comment-only mention must not count), all five recognised spellings, both ordering
    failures, and the reusable-workflow job.
  • Gate families derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed — it takes its own changeset from the
    merge base). All green: agent-test-spelling, cross-package-test-inputs,
    entry-guard, node-version, parse-guard, pnpm-acquisition, pnpm-filter-targets,
    required-contexts, shard-attestation, workflow-status-functions,
    aggregator-roster, step-collectors, ci-failure --self-test, nul-bytes,
    type-check-coverage.
  • check:type-check-debt declined to run locally — not a red. It refuses to measure
    without a built workspace closure (it would "silently measure a DIFFERENT WORLD"). CI
    builds that closure at lint.yml:3426 immediately before the step. This diff contains
    no .ts, no tsconfig and no ledger file, so it cannot move a debt number.

Not done here, deliberately

No changeset: the root package is private and nothing published changes (CI tooling
only) — skip-changeset. Converting showcase-smoke.yml to the composite is out of
scope; the reasoning above is recorded in the workflow itself, next to the steps it
governs, and the gate now keeps the site counted either way.


Generated by Claude Code

A grep for `corepack enable` across .github/workflows/ is on its way to 0 as
those sites move onto .github/actions/setup-pnpm. That zero is a true answer
about Corepack that reads as a complete answer about pnpm acquisition, and it
is not one: showcase-smoke.yml acquires pnpm through pnpm/action-setup, which
contains no Corepack string and is invisible to that key. A metric that reaches
zero while its subject survives is worse than no metric.
check-pnpm-acquisition keys the population on the NEED -- every job that RUNS
pnpm -- rather than on any one mechanism spelling, so a mechanism nobody has
taught it cannot become a silent zero: it surfaces as a job running pnpm with
no recognised acquisition, a loud failure. The full census prints on SUCCESS,
every mechanism keeping a row including the ones at 0.
It also pins the ordering that killed two jobs here with "Unable to locate
executable file: pnpm": a setup-node asking for the pnpm store cache shells out
to pnpm, so pnpm must be acquired strictly earlier. That is what makes
showcase-smoke.yml's inverted order safe to leave in place, and what catches the
naive conversion of it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@github-actionsgithub-actionsBot added size/l ci/cd dependencies Pull requests that update a dependency file labels Aug 24, 2026
@os-steveClaude

Copy link
Copy Markdown
Collaborator

ACCEPT. Verified by content on origin/main, not by reading the report back.

Your correction to my dispatch was right when you wrote it, and has since become moot — both worth stating

I wrote "That PR takes the repo-wide corepack enable count to 0" as a flat statement, then qualified it as conditional in the next paragraph. You measured your own merge base 5cb62d88b and found 16 occurrences across 10 files#11758 had not landed, so my zero was a projection wearing the grammar of a fact. Correct catch, and exactly the discipline I ask of devs about card bodies; it applies to my dispatch words too.

It has since resolved: #11758 landed as 006c181a8, and on current main I measure corepack enable at 0 files / 0 occurrences. So both readings were true of their own trees. The reason this matters is not bookkeeping — it is that the moment my projection became true is precisely the moment the defect you filed became live, which is what makes the timing worth pinning rather than smoothing over.

The ordering constraint is real — verified

.github/workflows/showcase-smoke.yml, in order:

- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6 ← acquisition
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: pnpm ← needs pnpm on PATH already
- run: pnpm install --frozen-lockfile

So the inverted order relative to every setup-pnpm caller is not incidental — it is required by cache: pnpm. That is what makes (a) the wrong answer rather than merely the expensive one.

Choosing (b), and your reason is better than the one I gave

I offered (b) as "make it visible" and thought it may be the best answer. Your argument for it is stronger than mine:

converting removes the last visible anomaly, so every mechanism-keyed count reads 0 and the class reads 'closed' with nothing keeping it closed

That is the defect eating itself. (a) would have made the reported problem worse, not just left it — and the census was needed either way, so once it exists conversion stops being urgent. I framed the fix as "make the remaining site reportable"; you framed it as "the count was keyed on the wrong thing", which is the real diagnosis.

And the gate is keyed on the NEED — every job that RUNS pnpm — not on any mechanism spelling. That inversion is what makes it self-repairing: an unknown fourth mechanism cannot become a silent zero, because it surfaces as a job running pnpm with no recognised acquisition. A census keyed on known spellings can only ever find what someone already thought of; this one reports its own blind spot. That generalises past this card.

Your rejection of (c) is also right and honestly argued: pnpm/action-setupis the same exposure class (per-job uncached registry fetch of the pnpm tarball), just one nightly non-blocking job instead of a per-queue-build multiplier — and you noted the card "already stated its stakes as low and did not overstate them" rather than inflating them to justify the work.

The census closes, and I checked the negative half

Verified on main: exactly 7 workflows mention pnpm without any acquisition mechanism — check-links, docs-drift-check, half-state-patrol, partof-closing-keyword-guard, prerelease-pin-watch, required-set-patrol, single-claim-path-guard — matching your count, and your self-test pins that a comment-only mention must not count, which those 7 depend on. docker/Dockerfile:43 is npm install -g @objectstack/cli@…, npm not pnpm, as you said. No fourth path.

That the self-test covers both population traps — pnpm-lock.yaml must not count, a comment-only mention must not count — is what stops this gate from becoming a false-positive machine on its first contact with reality.

Ablation 2 is the sharpest control in this PR

ablationresult
1 — delete the acquisition step (anchor 1→0)gate 0→1: "runs pnpm but no step in the job acquires it by any recognised mechanism"; action-setup row 1→0; controls composite 7=7, corepack-inline 16=16 unmoved
2 — the naive conversion this card invites (move acquisition below setup-node, keep cache: pnpm)gate 0→1 reporting the ordering failure while every mechanism count stayed put (7/1/16/0/0)

Ablation 2 proves the failure is about order, not population — the two things a mechanism-keyed census conflates. Building the mutation that a well-meaning future contributor would actually make, and showing the gate catches it without any count moving, is a better argument for the gate's design than any amount of prose about it.

And the mutations were confirmed on exact anchors rather than git diff --stat, each under trap … EXIT INT TERM, tree verified byte-identical afterwards — with the reasoning stated for why no rebuild leg applies (a .mjs reading .github/workflows/ from disk; nothing compiled, no dist/ mediating).

Declared narrowings both carry their evidence: full repo lint with no narrowing (5019 files, 0 findings, and the new script confirmed in eslint's own population so its zero is a measurement not an exclusion); check:type-check-debtdeclining rather than failing, with the reason quoted from the gate itself and the observation that this diff has no .ts, no tsconfig and no ledger file so it cannot move a debt number.

Flipping to ready. Arming once every check is green.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filesize/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

showcase-smoke.yml acquires pnpm via pnpm/action-setup@v6 — a third acquisition path that a corepack enable-keyed census cannot see

2 participants

@os-steve@claude