Uh oh!
There was an error while loading. Please reload this page.
fix(gates): declare the workspace parents as globs, so dispatch-gates can read the whole declared population - #10540
Conversation
…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-zhuang
commented
Aug 21, 2026
PM review — ⭐ accepted, and both open questions ruled below.⭐ The best line in this report is the one that reframes four earlier cards:
That is the finding. The card said "no path literal can describe it" — falsified: ⇒ On What I checked hardest
RulingsQ1 — 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 Q2 — A. B would put a hand-maintained 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
⛔ Correct not to re-file #10314 / #10516 — same Nothing for you to change. CI still finishing; I will flip ready and arm once it is green. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
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 ownsource, as
WORKSPACE_PARENT_DIRS, and 8 of those 11 entries were already readby the derivation. Only 3 were not:
hintCoversrefuses a literal with no separator as too generic, deliberatelyand 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 flatpackages/<pkg>layouts plus all of
apps/andexamples/.The specimen, run against the incident card's own path and its nested twin, on
origin/main: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:
check:test-source-aliascheck:type-source-resolutionWORKSPACE_PARENT_DIRS, never reportedcheck:role-wordPositive control, in both directions: the sweep found the known-missing
check:test-source-aliasbefore the fix (3 hits), and reports 1 after it — soit 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:
check:examples-live-imports(finding: dispatch-gates cannot name check:examples-live-imports — its root literal is the bare wordexamples, the same class #10114 fixed for check-role-word #10314) andcheck:entry-guard([finding] dispatch-gates leaves check:entry-guard in the undetermined bucket though its population is exactly scripts/** — a dev adding a scripts/ file shipped a red it should have run first #10516) are the same refusal in the non-co-declared shape(a bare literal used as a
join()argument).examples-live-importsis alsothe one case the tool already prints loudly, in the
Unreachablesection withthe exact reason, so it is visible rather than silent. Its population spans
two trees (
packages/**tests reachingexamples/**), so the correctdeclaration is a judgement call, not the mechanical edit this PR makes.
pnpm-workspace.yamlat runtime and therefore carry no population literalat all; they name none of four layout specimens (flat package, nested package,
apps/,examples/). Two of the seven are rescued byCHANGE_KIND_GATES,leaving 5 unrescued. That is a strictly larger blind spot than this one and
needs per-gate triage, so it is its own card rather than a rider 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:
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 thenneeded four self-test cases to stop it describing a population
ROOTShad movedaway 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
existsSyncand skipping a root in silence — is avoided byconstruction here, because the glob is stripped before the walk ever sees it,
and it is measured below rather than argued.
The extractor and
hintCoversare untouched, for #9626's reason.The bounded second file
scripts/check-type-source-resolution.mjsis fixed in the same commit under thein-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 thiscontainer — 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:
Whole-corpus derivation, 119 families x 6174 tracked files:
check:test-source-aliascheck:type-source-resolutionThe 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:
packages/cli/src/commands/serve.test.ts(flat)apps/docs/package.jsonexamples/showcase/package.jsonpackages/spec/src/index.tspackages/services/service-cache/src/index.ts(nested)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.tsfrom 7 matched families to 34.The incident card now derives the gate that reddened it:
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
readFileSyncthe gate's source, there is noexports/distin the path, so no stale build can mask either leg.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 listcheck-test-source-alias OK — 72 packages with tests scanned; …check:test-source-alias, keepscheck:type-source-resolution(only one leg ablated)check:cross-package-test-inputs check:slot-lookup check:type-source-resolutionRestored with
git checkout -- scripts/check-test-source-alias.mjs; anchorcounts return (1 / 0) and
md5sumon both files matches the pre-ablationcapture exactly,
git status --porcelainempty.Gates, at 57ef907
Union derived by
node scripts/pm/dispatch-gates.mjsfrom the real diff with nohand-written path list, re-derived after the final commit. Exit codes captured
by redirecting to a file before any pipe, never read through
tail; eachline quotes the gate's own verdict.
node scripts/check-test-source-alias.mjs --self-testexit 0 —check-test-source-alias --self-test OKnode scripts/check-test-source-alias.mjsexit 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-testexit 0 —check-type-source-resolution --self-test OKnode scripts/check-type-source-resolution.mjsexit 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.mjsexit 0 —OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.node scripts/pm/check-dispatch-gates.mjsexit 0 —✓ dispatch-gates self-test: 443 cases pass.node scripts/check-nul-bytes.mjsexit 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_modulesandscripts/**.mjsis covered by one rule ineslint.config.mjs(
no-restricted-importsfor@objectstack/specsubpaths), which this diff doesnot touch. CI's ESLint job runs it regardless.
No changeset:
scripts/**only, nothing published changes. Rootpackage.jsonis
private: trueand is not touched (it is fenced by #9465).Not done here
dispatch-gates.mjsis unmodified, so no governed instruction file needs anedit 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, notundetermined, and the residuealready 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