Skip to content

Consolidate the private pnpm-workspace.yaml parsers behind one shared enumerator - #11797

Merged
os-steve merged 3 commits into
mainfrom
claude/issue-11510-workspace-enumerator
Aug 24, 2026
Merged

Consolidate the private pnpm-workspace.yaml parsers behind one shared enumerator#11797
os-steve merged 3 commits into
mainfrom
claude/issue-11510-workspace-enumerator

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#11510

Step 2 of #11190: nine scripts carried a private parse of pnpm-workspace.yaml's packages: block. They now share scripts/workspace-enumerator.mjs. Verified at 38a1d66c9.

The card's count, re-measured on the day

#10542#11190this cardmeasured today
discovered families119137140154
families reading the workspace file12151515
distinct scripts12151313

The farm grew 140 → 154; the 15/13 figure holds.

Assumption falsified: the thirteen are not thirteen parsers

The card's list is grep-shaped. Three of its thirteen never parse the packages: block, so there was nothing to consolidate in them and they are untouched:

scriptwhat it actually does
check-osv-exemptions.mjsnames the file in prose only ("same approach as check-override-consistency.mjs")
check-cross-package-test-inputs.mjsuses the filename as a workspace-root marker (WORKSPACE_ROOT_MARKERS)
check-prerelease-pin-watch.mjsparses the overrides: block — a different block, one reader

Two more carry no parser but a hardcoded 11-entry WORKSPACE_PARENT_GLOBS — these are the byte-identical pair the card names. See "the declaration" below. A fourth parser the family measurement could not see, ablation-dist-preflight.mjs (not a gate), was consolidated too.

The parsers were not equivalent — measured, not read

Each parser was sliced out of its real source bytes and run against 17 inputs. They agree on the repo's actual file (positive control) and split into four clusters on 9 of 17:

inputA: changeset-fixed, override-consistency, release-bodyB: published-files, readme-exports, type-check-coverageC: dev-prereqs, pnpm-filter-targetsD: ablation-preflightnow
- pkg/* # notestripstripkeep verbatimends liststrip
- vendor/c#sharptruncate → vendor/ctruncatekeepkeepkeep
full-line comment in listskipskipskipends listskip
packages : (space)acceptacceptignore keyignore keyaccept
second packages: blockappend bothfirst winsfirst winsfirst winsrefuse
no packages: key[]throwthrowthrowthrow
empty packages: block[]throw[] / throwthrowthrow
- packages/**expand to nothingthrowthrowexpand to nothingthrow
repo's real file today✅ identical✅ identical✅ identical✅ identical✅ identical

Every divergence is settled toward refusing over under-reporting. Three matter:

  • A # is a YAML comment only at line start or after whitespace. Four parsers stripped /#.*$/ unconditionally, silently truncating a legitimate member path to a directory that does not exist. Two others glued the comment onto the pattern. No old parser was right; the new one is.
  • [] for an absent/empty block is a clean run over an empty workspace — every "is every member covered" gate passes vacuously, loudly green.
  • A duplicate packages: key is invalid YAML; three parsers stopped at the first block and three appended the second. Neither is a reading anyone chose, so the input is refused.

No caller's answer changes on this tree. Proven, not assumed — each caller's member set computed from origin/main's bytes vs the shared module:

check-published-files 78 -> 78 IDENTICAL release-github-releases(names) 69 -> 69 IDENTICAL
check-published-readme-exports 78 -> 78 IDENTICAL check-dev-prereqs(dirs) 86 -> 86 IDENTICAL
check-type-check-coverage 78 -> 78 IDENTICAL pnpm-filter-targets(names) 78 -> 78 IDENTICAL
check-changeset-fixed(names) 69 -> 69 IDENTICAL ablation-dist-preflight(names) 78 -> 78 IDENTICAL
check-override-consistency(names) 69 -> 69 IDENTICAL
negative control: the harness DOES separate 86 member dirs from 78 with a manifest

The module consolidates the PARSE, never the DECLARATION

This is the load-bearing decision, and it is a measurement. Had the shared module spelled the workspace globs as literals, the import follow would have handed them to every importer:

check-changeset-fixed.mjs 1 -> 5396 check:type-check-coverage 98 -> 5398
check:release-body 0 -> 5395 ⛔ check:type-check-debt 98 -> 5398
check:pnpm-filter-targets 272 -> 5667 check-dev-prereqs.mjs 1245 -> 5395
check:published-readme-exports 2 -> 5397 check:published-files 5396 -> 5396
check:override-consistency 0 -> 5395 TOTAL +41725 pairs

13.6× the +3065 the follow already refuses — and ⛔ check:release-body carries a no-path-population marker, so a literal there turns that gate red via dispatch-gates' own "no family both DECLARES no path population and names paths anyway" assertion. Three callers had already measured and refused this declaration in writing.

So the module declares zero path population, verified with dispatch-gates' own extractor:

extractWatchHints(workspace-enumerator.mjs) -> [] LIVE PAIRS CONTRIBUTED: 0
positive control, same run: check-published-files.mjs -> 14 hints / 7254 pairs

'pnpm-workspace.yaml' is the one string it spells, and it is safe: hintCovers refuses a literal with no path separator.

Sweep: before → after

families 154 -> 154
tracked files 6577 -> 6578 (the new module)
(gate,file) pairs 49281 -> 49287 delta +6
families that LOST coverage: NONE

The +6 is exactly the one new file matched by the six scripts/** families. Every other family's count is unchanged; nothing was re-attributed.

The three named regression checks

Live derivations, before and after, on three card paths:

gatebeforeaftermatched via
check:published-files53965396its own ROOT_DIR_WATCH_HINTS
check:test-source-alias53955395its own WORKSPACE_PARENT_GLOBS
check:type-source-resolution53955395its own WORKSPACE_PARENT_GLOBS

All three still name real cards, via each gate's own declaration.

Counterfactual, measured: replacing WORKSPACE_PARENT_GLOBS with a runtime parse — i.e. consolidating the declaration too — drops check:test-source-alias to 0 matched-list occurrences for a packages/ card. That is "goes back to naming nothing", exactly as the card predicted.

Instead, the two byte-identical arrays stay and gain a both-directions reconciliation against the live parse (the check-published-files.mjs shape). That is what retires the drift: an entry that no longer matches the workspace now fails loudly rather than leaving both gates walking the old set.

Reverse verification

Every leg restores via trap and confirms the mutation on disk by anchor-text count (an editor's exit code is not evidence).

ablationexpectedobserved
drop examples/* from WORKSPACE_PARENT_GLOBSredexit=1 — "pnpm-workspace.yaml declares the workspace root examples, which WORKSPACE_PARENT_GLOBS does not"
add a root the YAML lacksredexit=1 — "WORKSPACE_PARENT_GLOBS declares nowhere, which pnpm-workspace.yaml does not"
plant ['packages/*','apps/*'] in the enumeratorredexit=1 in all five consolidated gates

The third ablation initially passed — and that is why it is in the diff. The zero-literal guard stripped block comments with a hand-rolled /\*...\*/ regex, and a workspace glob contains a comment opener: the / and * of packages/*are/*, so the stripper opened a comment at the literal it was hunting and ate forward to the next */. The planted literal really did contribute 5154 pairs per importer while the guard read green. Fixed to use maskComments from js-comment-mask.mjs (commit 2 of 3).

Verification

Full pnpm lint (eslint . --no-inline-config, whole repo, no narrowing): green in 55s.

Gate union at 38a1d66c9, all exit 0: check:agent-test-spelling, check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, check:published-files, check:release-body, check:test-source-alias, check:type-check-coverage, check:type-source-resolution, check:override-consistency, check:nul-bytes, check:pm-dispatch-gates (579 cases), check-changeset-fixed.mjs, check-cross-package-test-inputs.mjs, check-osv-exemptions.mjs, check-dev-prereqs --self-test, ablation-dist-preflight --self-test.

Two gates could not be closed locally and are declared narrowings, both unbuilt-tree refusals in a worktree with no dist/:

  • check:published-readme-exports — output byte-identical to the pre-change capture at 006c181a8 (same 198 unbuilt packages, same lines). lint.yml builds at line 3592 before this step.
  • check:type-check-debt — refuses --re-measure without a built closure by design. lint.yml builds at line 3393 before it. Its coverage half printed byte-identical output before and after (65/78 type-checked, 13 DEBT, 19 hiding tests) — that output is the enumeration readout.

The shared module is a plain module: no check:* script names it, no workflow invokes it, so it is not a discovered gate file. It has no CI invocation of its own, so its selfTest() is folded into the --self-test of every consolidated gate that has one — five of them, each independently red in the third ablation.

skip-changeset: scripts-only, nothing published changes.

Generated by Claude Code


Generated by Claude Code

…s behind one shared enumerator
Nine scripts carried a private parse of the `packages:` block. Measured
against each other on their real source bytes, they formed four behaviour
clusters that agreed on this repo's workspace file and disagreed on nine of
seventeen adversarial inputs.
The shared module is deliberately NOT a gate file and declares NO path
population: priced on the live tree, spelling the workspace globs as literals
there would have handed importers +41725 (gate, file) pairs and turned
check:release-body red by contradicting its no-path-population marker.
Each gate keeps declaring its own population. check-test-source-alias and
check-type-source-resolution keep WORKSPACE_PARENT_GLOBS and gain a
both-directions reconciliation against the live parse, which is what retires
the drift between their byte-identical copies.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
…le to fail
The guard stripped block comments with a hand-rolled /\*...\*/ regex. A
workspace glob contains a comment opener -- the `/` and `*` of `packages/*`
ARE `/*` -- so the stripper opened a comment at the literal it was hunting
and ate forward to the next `*/`. An ablation planting
`['packages/*', 'apps/*']` in the module passed the guard while really
contributing 5154 pairs per importer.
Now uses maskComments from js-comment-mask.mjs (proper string/comment
scanning) and applies extractWatchHints' leading-`./` strip so a relative
import specifier is not misread as a declaration. Re-ablated: the planted
literal is now caught by all five consolidated gates.
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
@os-steveClaude

Copy link
Copy Markdown
Collaborator

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

⭐ Leg 3 first PASSED, and keeping that failure in the diff is the most valuable thing here

the zero-literal guard stripped block comments with a hand-rolled /\*...\*/ regex, and a workspace glob CONTAINS a comment opener — the / and * of 'packages/*' ARE /* — so the stripper opened a comment at the literal it was hunting and ate forward to the next */.

Verified, trivially and damningly: packages/* characters 9–10 are /*.

So the guard meant to prove the shared module declares zero path population was vacuous against the exact literal it existed to catch — and the planted literal really did contribute 5154 pairs per importer while the guard read green. A guard that is blind precisely to its own subject is the worst shape a guard can take, and nothing but an ablation would have found it: it passes every test you would think to write, because the thing it cannot see is the thing you are testing for.

You found it because leg 3 passed when you had predicted red, and you preserved the failure as its own commit (45c62e9f4) rather than amending it away. Fixing it to use maskComments from js-comment-mask.mjs (which exists and is the repo's real tool for this) and re-ablating red is the right repair. Publishing the miss is what makes the eventual red believable.

Both card assumptions falsified, and both verified

(1) The thirteen are not thirteen parsers. Checked each on main:

scriptwhat it actually does
check-osv-exemptions.mjs:36names the file in a prose comment — nothing to consolidate
check-cross-package-test-inputs.mjs:1396WORKSPACE_ROOT_MARKERS = ['pnpm-workspace.yaml'] — a root marker
check-prerelease-pin-watch.mjsparses the overrides: block, not packages:

Three of the card's thirteen were never in scope, and leaving them untouched is right. ⭐ And a fourth real parser the family measurement could not seeablation-dist-preflight.mjs: 0 occurrences in package.json (so it is invoked by no check: script, therefore not a discovered gate, therefore invisible to a family-derived census) and 4pnpm-workspace references. It was the strictest copy, and it silently truncated the workspace at any full-line comment inside the block. A census keyed on discovered families structurally cannot see it — the same shape as #11765's third pnpm acquisition path, one layer down.

(2) The parsers were not equivalent — which is the exact hazard I named in Zone 2 as "the way this PR could do real damage, and it would not show up as a red gate." Sliced from their real source bytes and run against 17 inputs: agreement on the repo's actual file, four clusters on 9 of 17. A shared answer that was only ever true by coincidence. Consolidating those blind would have changed several gates' populations with nothing going red.

Partial consolidation, and the pricing that defends it

The parse is shared; the declaration is not. Priced on the live tree: spelling the workspace globs as literals in the shared module hands importers +41725 (gate, file) pairs — 13.6× the +3065 the follow already refuses — and turns check:release-body RED by contradicting its no-path-population marker. Three callers had already measured and refused that exact declaration in writing.

And the counterfactual is measured rather than argued: consolidating the declaration too drops check:test-source-alias to 0 matched-list occurrences for a packages/ card — "naming nothing", which is precisely the regression the card named as its ready-made check. Instead the two WORKSPACE_PARENT_GLOBS holders keep their declarations and gain a both-directions reconciliation against the live parse — which is what actually retires the drift between their byte-identical copies, rather than deleting one copy and hoping.

This is the "partial consolidation you can defend beats a uniform one you cannot" outcome I asked for, arrived at by measurement rather than by caution.

The sweep closes exactly

families 154 → 154 tracked files 6577 → 6578
(gate,file) 49281 → 49287 delta +6 FAMILIES LOSING COVERAGE: NONE

and the +6 is accounted for one file at a time across the six scripts/** families. Zero re-attribution — the failure mode that matters most in this repo's gate arithmetic.

The three named regression checks all still name real cards (5396 / 5395 / 5395, before and after), and the via column shows each matched through its own declaration, not an inherited hint. Per-caller population equality is 9-for-9 identical, with a negative control: the same harness separates 86 member dirs from 78-with-a-manifest, so "identical" is a measurement and not a harness comparing nothing. Zero-population proof carries its positive control too (extractWatchHints[] / 0 pairs, against check-published-files.mjs → 14 hints / 7254 pairs in the same run).

Both declared narrowings are unbuilt-tree refusals, and both are disposed of properlycheck:published-readme-exports output diffed byte-identical to a capture taken at 006c181a8 before any edit, and check:type-check-debt's coverage half printed byte-identical output before and after, and that output is the enumeration readout, so the migration provably did not move it. Using the refusing gate's own unchanged output as the evidence that your change did not affect it is a neat inversion.

Full-repo pnpm lint green with no narrowing claimed.

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

size/xlskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-steve@claude