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

os-steve merged 3 commits into
mainfrom
claude/issue-11510-workspace-enumerator

Conversation

@claude

@claude claude Bot 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 #11190 this card measured today
discovered families 119 137 140 154
families reading the workspace file 12 15 15 15
distinct scripts 12 15 13 13

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:

script what it actually does
check-osv-exemptions.mjs names the file in prose only ("same approach as check-override-consistency.mjs")
check-cross-package-test-inputs.mjs uses the filename as a workspace-root marker (WORKSPACE_ROOT_MARKERS)
check-prerelease-pin-watch.mjs parses 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:

input A: changeset-fixed, override-consistency, release-body B: published-files, readme-exports, type-check-coverage C: dev-prereqs, pnpm-filter-targets D: ablation-preflight now
- pkg/* # note strip strip keep verbatim ends list strip
- vendor/c#sharp truncate → vendor/c truncate keep keep keep
full-line comment in list skip skip skip ends list skip
packages : (space) accept accept ignore key ignore key accept
second packages: block append both first wins first wins first wins refuse
no packages: key [] throw throw throw throw
empty packages: block [] throw [] / throw throw throw
- packages/** expand to nothing throw throw expand to nothing throw
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:

gate before after matched via
check:published-files 5396 5396 its own ROOT_DIR_WATCH_HINTS
check:test-source-alias 5395 5395 its own WORKSPACE_PARENT_GLOBS
check:type-source-resolution 5395 5395 its 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).

ablation expected observed
drop examples/* from WORKSPACE_PARENT_GLOBS red exit=1 — "pnpm-workspace.yaml declares the workspace root examples, which WORKSPACE_PARENT_GLOBS does not"
add a root the YAML lacks red exit=1 — "WORKSPACE_PARENT_GLOBS declares nowhere, which pnpm-workspace.yaml does not"
plant ['packages/*','apps/*'] in the enumerator red exit=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

claude added 3 commits August 24, 2026 16:40
…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
…migration

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026

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:

script what it actually does
check-osv-exemptions.mjs:36 names the file in a prose comment — nothing to consolidate
check-cross-package-test-inputs.mjs:1396 WORKSPACE_ROOT_MARKERS = ['pnpm-workspace.yaml'] — a root marker
check-prerelease-pin-watch.mjs parses 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 see — ablation-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 4 pnpm-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 properly — check: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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants