Skip to content

Step 2: consolidate the thirteen private pnpm-workspace.yaml parsers behind one shared enumerator — now unblocked, and the enumerator must NOT be a gate itself #11510

Description

@os-steve

Step 2 of #11190, filed rather than built: that card's step 1 (teach scripts/pm/dispatch-gates.mjs to follow first-party imports) landed on its own branch, and #11190's own words are why the order matters — "doing 2 without 1 is a regression with no red gate".

What is left to do

Re-measured on today's tree (140 discovered families), not recalled: 15 discovered families across 13 distinct scripts read pnpm-workspace.yaml, each with its own block parser:

scripts/check-changeset-fixed.mjs            scripts/check-published-readme-exports.mjs
scripts/check-cross-package-test-inputs.mjs  scripts/check-test-source-alias.mjs
scripts/check-dev-prereqs.mjs                scripts/check-type-check-coverage.mjs
scripts/check-osv-exemptions.mjs             scripts/check-type-source-resolution.mjs
scripts/check-override-consistency.mjs       scripts/pnpm-filter-targets.mjs
scripts/check-prerelease-pin-watch.mjs       scripts/release-github-releases.mjs
scripts/check-published-files.mjs

(#10542 measured twelve when the farm was 119; #11190 measured fifteen at 137. The figure to use is the one you measure on the day.)

scripts/i18n-bundle-surface.mjs is the shape the shared module should take. Two of those scripts once carried byte-identical 11-entry WORKSPACE_PARENT_DIRS arrays with the same blind spot in both, which is the drift a single declaration site retires.

The constraint step 1 imposes on this work — read before designing the module

The follow that unblocks this is deliberately narrow, and two of its rules decide where the enumerator may live:

  1. The shared module must not be a discovered gate file of its own. The follow refuses an import into a module that is itself resolved from some workflow's check: invocation, because such a module's population already reaches the tree through its own family and attributing it to every importer was measured at +3065 fabricated (gate, file) pairs for a single caller. So: a plain module (the i18n-bundle-surface.mjs / regen-artifacts.mjs shape), never a module that some workflow also invokes directly.
  2. The import must be static, relative, and land inside scripts/. Bare specifiers, dynamic import(), and first-party modules outside scripts/ are all refused (each with a measurement in firstPartyImportTargets' docblock).
  3. One level only. A declaration in a module the enumerator itself imports does not reach the gates. Keep the declaration in the module the gates import directly.

How to know it worked

Sweep the same way step 1 did — the (gate, file) pair count and the per-family matched counts, before and after — and confirm no family loses coverage. The three declarations #11190 names as at risk are the ready-made regression check: check:test-source-alias and check:type-source-resolution (their WORKSPACE_PARENT_GLOBS arrays, #9955 / PR #10540) and check:published-files (PR #11189). All three name real cards today; if any of them goes back to naming nothing, the consolidation has deleted the declaration it was meant to centralise.

Related

#11190 (the parent finding and step 1), #10542 (the original observation), #10540 / #9955 (the declarations at risk), #10114 (the merged precedent for declaring a population).

Activity

  1. self-assigned this
    on Aug 24, 2026
  2. os-steve commented on Aug 24, 2026

    @os-steve
    CollaboratorAuthor

    Claim: devx lane PM seat, session e2eac1a7-8000-5c95-9749-38aec2ace6fc, branch claude/issue-11510-workspace-enumerator.

    Graded pm:queue → pm:dispatched in one step (the card was carrying tooling + domain:devx with no pm: label). The card is self-contained and its step-1 blocker is discharged, so it needs no further triage from me beyond the dispatch.

    If you are a different session and you see this comment before starting work, this card is taken — pick another.


    Generated by Claude Code

  3. claude commented on Aug 24, 2026

    @claude
    Contributor
    {
      "issue": 11510,
      "status": "done",
      "branch": "claude/issue-11510-workspace-enumerator",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/11797",
      "premise_still_valid": true,
      "summary": "Nine private pnpm-workspace.yaml `packages:` parsers now share scripts/workspace-enumerator.mjs, a plain module that is NOT a discovered gate file and declares ZERO path population. Re-measured on the day: 154 discovered families (card said 140), 15 families across 13 scripts read the file — the 15/13 figure holds. TWO CARD ASSUMPTIONS FALSIFIED. (1) The thirteen are not thirteen parsers: check-osv-exemptions.mjs names the file in prose only, check-cross-package-test-inputs.mjs uses it as a workspace-ROOT MARKER, and check-prerelease-pin-watch.mjs parses the `overrides:` block, not `packages:` — all three untouched, nothing to consolidate. A fourth real parser the family measurement could not see (ablation-dist-preflight.mjs, not a gate) was consolidated too: it was the strictest copy and silently truncated the workspace at any full-line comment inside the block. (2) The parsers were not equivalent: sliced from their real source bytes and run against 17 inputs, they agree on the repo's actual file and split into four clusters on 9 of 17 — a shared answer that was only ever true by coincidence. PARTIAL CONSOLIDATION, DEFENDED: the PARSE is shared, the DECLARATION is not. Priced on the live tree, spelling the workspace globs as literals in the shared module would have handed importers +41725 (gate, file) pairs — 13.6x the +3065 the follow already refuses — and turned check:release-body RED by contradicting its no-path-population marker. Three callers had already measured and refused that exact declaration in writing. So each gate keeps its own declaration; 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 actually retires the drift between their byte-identical copies. Counterfactual measured: consolidating the declaration too drops check:test-source-alias to 0 matched-list occurrences for a packages/ card — 'naming nothing', exactly as the card predicted.",
      "tests": "All evidence measured at final commit 38a1d66c9 (gate union re-run after the last commit).\n\nSWEEP (before -> after, dispatch-gates' own discoverFamilies/trackedFiles/hintCovers): families 154 -> 154; tracked files 6577 -> 6578; (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 (check:agent-test-spelling 353->354, check:cross-package-test-inputs 3248->3249 x2, check:entry-guard 242->243, check:parse-guard 242->243, check:pnpm-filter-targets 272->273). No other family's count moved; zero re-attribution.\n\nTHE THREE NAMED REGRESSION CHECKS, live derivations before (006c181a8, separate worktree) and after, over three card paths (packages/spec/src/data/filter.zod.ts, packages/qa/dogfood/src/x.ts, examples/app-showcase/src/a.ts): check:published-files 5396 -> 5396, check:test-source-alias 5395 -> 5395, check:type-source-resolution 5395 -> 5395 — all three still name real cards, and the `via` column shows each matched through its OWN declaration ('gate source packages/*'), not an inherited hint. COUNTERFACTUAL (trapped ablation in the BASE worktree, disk-confirmed): replacing WORKSPACE_PARENT_GLOBS with a runtime parse gives 0 matched-list occurrences of check:test-source-alias for a packages/ card, and it falls into the residue naming only package NAMES.\n\nPER-CALLER POPULATION EQUALITY (before side computed from origin/main's source bytes via git show, after side from the shared module): check-published-files 78->78, check-published-readme-exports 78->78, check-type-check-coverage 78->78, check-changeset-fixed(names) 69->69, check-override-consistency(names) 69->69, release-github-releases(names) 69->69, check-dev-prereqs(dirs) 86->86, pnpm-filter-targets(names) 78->78, ablation-dist-preflight(names) 78->78 — ALL IDENTICAL. NEGATIVE CONTROL: the same harness does separate 86 member dirs from 78 with a manifest, so 'identical' is a measurement, not a harness that compares nothing.\n\nZERO-POPULATION PROOF, with positive control: extractWatchHints(scripts/workspace-enumerator.mjs) -> [] and 0 live pairs contributed; same run, check-published-files.mjs -> 14 hints / 7254 pairs. 'pnpm-workspace.yaml' measured as a non-hint (hintCovers false — no path separator).\n\nREVERSE VERIFICATION — 3 legs, each with `trap '<restore>' EXIT INT TERM` and each mutation confirmed on disk by anchor-text grep -c before AND after (an editor's exit code is not evidence), plus a restore-leg count check. No build/dist is involved: these are plain .mjs run directly, so there is no dist staleness leg to report. Leg 1 — drop 'examples/*' from WORKSPACE_PARENT_GLOBS (x1 -> x0 on disk): exit=1, 'pnpm-workspace.yaml declares the workspace root examples, which WORKSPACE_PARENT_GLOBS does not'; restored x1, green. Leg 2 — add a root the YAML lacks (x0 -> x1): exit=1, 'WORKSPACE_PARENT_GLOBS declares nowhere, which pnpm-workspace.yaml does not'; restored x0. Leg 3 — plant ['packages/*','apps/*'] in the enumerator (x0 -> x1): exit=1 in ALL FIVE consolidated gates (published-files, published-readme-exports, type-check-coverage, dev-prereqs, pnpm-filter-targets); restored x0, green. LEG 3 FIRST PASSED, and that failure is in the diff as its own commit (45c62e9f4): 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 plus extractWatchHints' leading-./ strip, then re-ablated red. Direction observed matches the prediction in all three legs (red).\n\nGATE UNION at 38a1d66c9, all exit 0, verdict lines quoted from the gates themselves: check:agent-test-spelling, check:cross-package-test-inputs ('OK: 16 package(s) read outside themselves, all declared'), check:entry-guard ('144 scripts/ file(s)'), check:parse-guard, check:pnpm-filter-targets ('134/167 --filter occurrence(s) across 26 file(s) resolve against 78 workspace package(s)'), check:published-files ('69 publishable package(s) of 78 workspace member(s)'), check:release-body ('49 assertions'), check:test-source-alias ('72 packages with tests scanned'), check:type-check-coverage ('65/78 workspace packages type-checked'), check:type-source-resolution ('77 packages with a tsconfig.json scanned'), check:override-consistency ('8 published-manifest declaration(s)'), check:nul-bytes ('scanned 6573 text file(s) ... no raw ASCII control bytes'), check:pm-dispatch-gates ('dispatch-gates self-test: 579 cases pass' — this is the gate that owns the no-path-population contradiction assertion my change could have broken), check-changeset-fixed.mjs ('in sync with 69 public workspace packages'), check-cross-package-test-inputs.mjs, check-osv-exemptions.mjs, check-dev-prereqs --self-test, ablation-dist-preflight --self-test. Exit codes captured by redirect-then-capture, never after a pipe.\n\nFULL REPO LINT: `pnpm lint` (eslint . --no-inline-config, whole repo, NO narrowing) green in 55s under the verify lock. Gate list derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` and no paths (12 changed paths off the merge base), not from the dispatch prompt.\n\nTWO DECLARED NARROWINGS, both unbuilt-tree refusals in a worktree with no dist/, both of which CI builds past: (a) check:published-readme-exports exits 1 with '198 package(s) are not built' — output diffed BYTE-IDENTICAL to the capture taken at 006c181a8 BEFORE any edit, and lint.yml builds at line 3592 before this step; (b) check:type-check-debt refuses --re-measure without a built closure BY DESIGN, and 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, 436 frozen raw errors), and that output IS the enumeration readout, so the migration provably did not move it. Not run locally: a full workspace build, which is CI's run.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions