Skip to content

fix(gates): declare the workspace parents as globs, so dispatch-gates can read the whole declared population - #10540

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-9955-dispatch-gates-test-source-alias
Aug 21, 2026
Merged

fix(gates): declare the workspace parents as globs, so dispatch-gates can read the whole declared population#10540
os-zhuang merged 1 commit into
mainfrom
claude/issue-9955-dispatch-gates-test-source-alias

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes#9955

The card's premise did not survive verification, and the real mechanism is worse

The card says the trigger is a new cross-package import inside a test — content,
which "no path literal can describe". Measured, that is not what happens.
check-test-source-alias.mjsdoes declare its whole population in its own
source, as WORKSPACE_PARENT_DIRS, and 8 of those 11 entries were already read
by the derivation. Only 3 were not:

'packages' <- refused: no path separator
'packages/apps' <- read
'packages/adapters' <- read
… 6 more, all read
'apps' <- refused
'examples' <- refused

hintCovers refuses a literal with no separator as too generic, deliberately
and measured (#9626 priced admitting the class at +139084 fabricated gate/file
pairs). So the derivation's answer for this gate was decided by where a
package happens to sit in the tree
: 1832 of the 4844 tracked files under
packages/ derived it, and the misses were exactly the flat packages/<pkg>
layouts plus all of apps/ and examples/.

The specimen, run against the incident card's own path and its nested twin, on
origin/main:

$ node scripts/pm/dispatch-gates.mjs packages/cli/src/commands/serve.test.ts
(no check:test-source-alias)
$ node scripts/pm/dispatch-gates.mjs packages/services/service-cache/src/cache-service-plugin.test.ts
- pnpm check:test-source-alias [lint.yml] matched via … ⇢ gate source 'packages/services'

Same gate, same kind of change, two answers, decided by directory nesting and
stated nowhere in the output. That is worse than an honest blind spot: it works
for a third of the tree, so it reads as working — which is why four prior cards
recorded this gate as simply "underivable".

The census is 2, not 1 — with a positive control

Sweeping the 119 discovered families for the shape (a bare top-level literal
co-declared in an array with pathy siblings — a population declaration, not a
path component joined with something else) finds 3 of 119, of which:

familystatus before
check:test-source-aliashalf-blind — the card's instance
check:type-source-resolutionhalf-blind — byte-identical WORKSPACE_PARENT_DIRS, never reported
check:role-wordalready addressed in #10114; delta 0, so the sweep is not inventing work

Positive control, in both directions: the sweep found the known-missing
check:test-source-alias before the fix (3 hits), and reports 1 after it — so
it tracks the change rather than reporting a constant, which is the failure
mode #8999 recorded.

Two further classes were measured and are not closed here:

What changed

The array already documented itself as "directory globs from
pnpm-workspace.yaml, which are all <dir>/*" — so it is now spelled that way,
and the directory names are re-derived from it:

constWORKSPACE_PARENT_GLOBS=['packages/*','packages/apps/*',,'apps/*','examples/*'];constWORKSPACE_PARENT_DIRS=WORKSPACE_PARENT_GLOBS.map((glob)=>glob.replace(/\/\*$/,''));

Every entry now carries a separator, so the derivation reads all 11.

Why this shape rather than #10114's

#10114 is the standing precedent for this class and it added a provenance-only
constant (ROOT_DIR_WATCH_HINTS = ['skills/**']) that nothing reads, which then
needed four self-test cases to stop it describing a population ROOTS had moved
away from. Deriving the walk from the declaration instead means there is no
second list, so that drift cannot exist. #10114's stated hazard — the glob
spelling reaching existsSync and skipping a root in silence — is avoided by
construction here, because the glob is stripped before the walk ever sees it,
and it is measured below rather than argued.

The extractor and hintCovers are untouched, for #9626's reason.

The bounded second file

scripts/check-type-source-resolution.mjs is fixed in the same commit under the
in-place exemption: same defect class, byte-identical declaration block, no open
PR holds it, same gate family, no new verification surface. That overlap check
was made through the GitHub API rather than gh (not installed in this
container — filed separately), against the dispatch brief's named set — #10506,
#10502, #10501, #10450, #10494 — plus file-level reads of #10439 and #10513, the
two other open PRs that add or edit gate tooling. It is named here because an unnamed
drive-by is unreviewable — it was never reported by any card, and it is found by
the sweep above, not by inspection.

Measured, at 57ef907

The walk is unchanged — both gates' verdict lines are byte-identical before and
after:

before (origin/main 4544be2600) and after (57ef9075c6), both:
check-test-source-alias OK — 72 packages with tests scanned; 61 registered …
check-type-source-resolution OK — 76 packages with a tsconfig.json scanned; 51 registered …

Whole-corpus derivation, 119 families x 6174 tracked files:

beforeafter
matched (family, tracked-file) pairs3790343356 (+5453)
check:test-source-alias18335116 (+3283)
check:type-source-resolution29465116 (+2170)

The two deltas sum exactly to the total, so no other family moved; and
exactly two families resolve to the two changed scripts, measured in both trees
rather than assumed. Per-card, matched-family counts:

specimenbeforeafter
packages/cli/src/commands/serve.test.ts (flat)46
apps/docs/package.json35
examples/showcase/package.json35
packages/spec/src/index.ts1314
packages/services/service-cache/src/index.ts (nested)44 — unchanged

That last row is the control: this is not a blanket widening, it only fills the
half that was blind. For scale, the alternative #9626 measured and refused took
packages/spec/src/index.ts from 7 matched families to 34.

The incident card now derives the gate that reddened it:

$ node scripts/pm/dispatch-gates.mjs packages/cli/src/commands/serve.ts packages/cli/src/commands/serve.test.ts
- pnpm check:test-source-alias [lint.yml] matched via … ⇢ gate source 'packages/*'
- pnpm check:type-source-resolution [lint.yml] matched via … ⇢ gate source 'packages/*'

Ablation

Direction predicted before running: the self-test reddens while the live
gate stays green
— the whole point of the assertion is that this regression is
silent. Mutation confirmed on disk by anchor count in both directions
('packages/*', 1 -> 0, 'packages', 0 -> 1), never by an editor's exit code.
No rebuild is involved: both tools readFileSync the gate's source, there is no
exports/dist in the path, so no stale build can mask either leg.

legpredictedmeasured
gate self-testred, naming the parent and the reasonexit 1 — check-test-source-alias --self-test FAILED: / workspace parent packages carries no path separator, so scripts/pm/dispatch-gates.mjs refuses it as too generic and every package under it drops out of the derived gate list
the live gategreen, unchangedexit 0 — check-test-source-alias OK — 72 packages with tests scanned; …
derivation over the flat test pathloses check:test-source-alias, keeps check:type-source-resolution (only one leg ablated)check:cross-package-test-inputs check:slot-lookup check:type-source-resolution

Restored with git checkout -- scripts/check-test-source-alias.mjs; anchor
counts return (1 / 0) and md5sum on both files matches the pre-ablation
capture exactly, git status --porcelain empty.

Gates, at 57ef907

Union derived by node scripts/pm/dispatch-gates.mjs from the real diff with no
hand-written path list, re-derived after the final commit. Exit codes captured
by redirecting to a file before any pipe, never read through tail; each
line quotes the gate's own verdict.

  • node scripts/check-test-source-alias.mjs --self-test exit 0 — check-test-source-alias --self-test OK
  • node scripts/check-test-source-alias.mjs exit 0 — check-test-source-alias OK — 72 packages with tests scanned; 61 registered as still resolving a workspace dep through 'dist/'; 44 published subpath(s) resolved through every alias table.
  • node scripts/check-type-source-resolution.mjs --self-test exit 0 — check-type-source-resolution --self-test OK
  • node scripts/check-type-source-resolution.mjs exit 0 — check-type-source-resolution OK — 76 packages with a tsconfig.json scanned; 51 registered as still resolving a workspace dep's types through 'dist/'.
  • node scripts/check-cross-package-test-inputs.mjs exit 0 — OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
  • node scripts/pm/check-dispatch-gates.mjs exit 0 — ✓ dispatch-gates self-test: 443 cases pass.
  • node scripts/check-nul-bytes.mjs exit 0 — check-nul-bytes: OK (scanned 6169 text file(s) … no raw ASCII control bytes).

Declared narrowing: ESLint was not run locally — this worktree has no
node_modules and scripts/**.mjs is covered by one rule in eslint.config.mjs
(no-restricted-imports for @objectstack/spec subpaths), which this diff does
not touch. CI's ESLint job runs it regardless.

No changeset: scripts/** only, nothing published changes. Root package.json
is private: true and is not touched (it is fenced by #9465).

Not done here

dispatch-gates.mjs is unmodified, so no governed instruction file needs an
edit and none is proposed. The card's routing note suggested a residue-text
honesty fix in that file; the measurement above says the wording is not the
defect for this gate — it scored silent, not undetermined, and the residue
already names the too-generic refusal as an escapable cause. What that text
cannot say today is that a population may be partly refused, which is the
shape this PR found; making the tool report that is the follow-up card, not a
rider on the gate fix.


Generated by Claude Code

…eriver can read the whole population
`scripts/pm/dispatch-gates.mjs` decides which cards are told to run a gate by
scanning that gate's module body for the path literals it operates on, and its
covering rule refuses a literal with no path separator as too generic (measured
and deliberate: admitting bare top-level words takes it from 19k watch-hint
pairs to 158k).
`check-test-source-alias.mjs` and `check-type-source-resolution.mjs` each
declared their whole population as `WORKSPACE_PARENT_DIRS`, of whose 11 entries
8 carried a separator and 3 did not (`packages`, `apps`, `examples`). So the
derivation's answer was decided by WHERE a package happens to sit: measured,
1832 of the 4844 tracked files under packages/ derived check:test-source-alias
and the rest did not, the misses being exactly the flat `packages/<pkg>`
layouts plus all of apps/ and examples/. Same test file, two layouts, two
different answers, with nothing in the output saying so.
Spelling the array as the pnpm-workspace globs it already documents itself to
be (`packages/*`, `apps/*`, …) and re-deriving the directory names from it puts
a separator in every entry. The walk is unchanged — both gates still report the
same package census — and there is no second list to keep in sync.
Measured on this tree, whole-corpus, before -> after:
matched (family, tracked-file) pairs 37903 -> 43356 (+5453)
check:test-source-alias 1833 -> 5116 (+3283)
check:type-source-resolution 2946 -> 5116 (+2170)
The two deltas sum to the total, so no other family moved; exactly two families
resolve to the two changed scripts, measured in both trees. For scale, the
alternative #9626 measured and refused (admitting bare top-level literals
globally) was +139084, and took `packages/spec/src/index.ts` from 7 matched
families to 34. This takes it from 13 to 14.
Each gate's self-test now pins the readability property, because the regression
is a tidy-up nobody would flag: the live gate stays green when an entry loses
its separator, so only an assertion can be loud about it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@os-zhuangClaude

Copy link
Copy Markdown
Contributor

PM review — ⭐ accepted, and both open questions ruled below.

The best line in this report is the one that reframes four earlier cards:

the derivation was not blind, it was HALF blind, and which half you got was decided by where the package sits … it works for a third of the tree, so it reads as working, which is why four prior cards recorded this gate as simply underivable.

That is the finding. The card said "no path literal can describe it" — falsified: WORKSPACE_PARENT_DIRS declares all 11 entries as path literals and 8 of 11 were already read. Only packages, apps, examples were refused, because hintCovers rejects a literal with no separator — a refusal #9626 priced at +139,084 fabricated pairs, so it is correct and must stay.

⇒ On origin/main, packages/cli/src/commands/serve.test.ts derived nothing while packages/services/service-cache/src/cache-service-plugin.test.ts derived the gate via packages/services. 1832 of 4844 files under packages/ named it; all of apps/ and examples/ never did. ⭐ A partially-working instrument is worse than a broken one, because nobody goes looking. This is the third time tonight that shape has been the real defect — after #10466 (OK (24 of 29 …) over the one package it could not grade) and #8999 (a constant 50% blind spot three sweeps "confirmed").

What I checked hardest

Rulings

Q1 — adopt your answer as stated: A where the walk array is the population, B where it is not. ⛔ Not a blanket "A wins". #10114's provenance-only constant needed four self-test cases to stop describing a population ROOTS had moved away from; deriving the walk from the declaration makes that drift unrepresentable, which is strictly better when the two are the same thing. Where roots are computed, or the declared subtree is deliberately wider than the walk, B remains right. ⭐ Deviating from a merged precedent knowingly and saying so is the correct way to deviate — that is why this is a ruling and not a correction.

Q2 — A. B would put a hand-maintained CHANGE_KIND_GATES entry into the file whose whole contract is derived, never listed — and the table's own deletion criterion says such an entry is redundant once the gate grows a discoverable literal, which this fix gives it. The over-claim is real but pre-existing: these gates already matched every file under the eight nested parents, LICENSE and CHANGELOG included, and check:slot-lookup is the live precedent for a gate declaring packages/** across 4846 paths. ⭐ A makes an existing imprecision uniform; the asymmetry the card is actually about — same change, different answer by directory nesting — is what A removes.

Follow-ups

#10542 — 7 of 119 families enumerate the workspace at runtime, so they carry no population literal at all and can never be named for any package (5 unrescued by CHANGE_KIND_GATES). ⭐ Correctly filed rather than ridden in: it is a strictly larger blind spot needing per-gate triage, and folding it here would have turned a measured fix into an open-ended sweep.

⚠️#10543 is the one I want carried into every seat's practice: gh is not installed in this container, so the gh … || echo "none" idiom returns a reassuring false negative — and "it silently produced two clean overlap checks in this very run before being caught." That is the night's recurring lesson in its purest form: an instrument that answers instead of failing. I hit the same class twice today (a queue-membership ref that only exists during builds; counting every CI check run instead of the latest per name).

⛔ Correct not to re-file #10314 / #10516 — same hintCovers refusal in the non-co-declared shape, already open, and #10314's population spans two trees so its declaration is a judgement rather than a mechanical edit.

Nothing for you to change. CI still finishing; I will flip ready and arm once it is green.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 21, 2026 02:42
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 24d8bc8Aug 21, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9955-dispatch-gates-test-source-alias branch August 21, 2026 03:17
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants

@os-zhuang@claude