fix(turbo): stop the root-scoped test-input radius hashing node_modules, and gate the class - #17035
Merged
baozhoutao merged 1 commit intoSep 9, 2026
Conversation
A `$TURBO_ROOT$` glob resolves against the filesystem, not git's tracked set, so `packages/**/*.json` and friends descend into every installed dependency tree under `packages/`. vitest rewrites its own `node_modules/.vite/vitest/<hash>/results.json` on every run, so the task that hashes such a glob can never replay from cache on a runner that ran any vitest before it. Measured on turbo 2.10.10: a negation carrying the root token (`!$TURBO_ROOT$/packages/**/node_modules/**`, and three sibling spellings) is dropped rather than applied — 7307 input keys and 3 node_modules keys before and after, unchanged. A package-relative negation is honoured: `!**/node_modules/**` takes the same task to 7304 keys and 0 node_modules keys, removing exactly the three planted probe files and nothing else. Control for the negation mechanism itself: a package-relative `!LICENSE` on the same task removed exactly that one key. The second half is the rule, because fixing the globs alone leaves the next root-scoped declaration free to re-commit this silently: Layer B now asks what else a declared glob MATCHES, not only whether turbo hashes it. A glob whose reach includes a `node_modules` path — answered by the shared matcher against an assembled witness, never by sniffing the glob text — requires the exclusion on its owning task; a glob that NAMES the directory is refused outright. A declaration of literal file paths owes neither and stays green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
baozhoutao
marked this pull request as ready for review
September 9, 2026 03:55
baozhoutao
enabled auto-merge
September 9, 2026 03:55
baozhoutao
deleted the
claude/issue-16555-test-input-radius-node-modules
branch
September 9, 2026 04:33
This was referenced Sep 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #16555
A
$TURBO_ROOT$glob is resolved against the filesystem, not against git's tracked set, so thedeclared cross-package test-input radius (
packages/**/*.jsonand its siblings) descends into everyinstalled dependency tree under
packages/. vitest rewrites its ownnode_modules/.vite/vitest/HASH/results.jsonon every run, so a task hashing such a glob can neverreplay from cache on a runner that ran any vitest before it.
Two deliverables, and the second is the one that closes the class: fixing the globs alone leaves
the next root-scoped declaration free to re-commit the same defect with nothing going red.
1. The card's experiment, re-run — before leg first
Same worktree, nothing else touched between the two runs of each leg,
turbo run test:repo --filter=@objectstack/spec. Wall-clock figures are shared-box seconds (taken underscripts/pm/os-verify-lock.sh, which excludes other locked runs only).6da4b399a78da08a, 32.054s2bafab09a086a226, 30.784s6da4b399a78da08a, 197ms, FULL TURBO6da4b399a78da08a, 184ms, FULL TURBOThe before leg reproduces the card exactly: the only thing that changed between run 1 and run 2 was
vitest writing its own results cache, and the task hash moved with it.
The after-leg hash is a stronger reading than "the second run hits": it is the same hash the
unfixed declaration produced before any vitest had run (
6da4b399a78da08a, the before leg's run 1).So the exclusion subtracts vitest's own cache from the hash and nothing else.
2. The input map, with controls
turbo run test:repo --filter=@objectstack/spec --dry-run=json, on the tree after a real vitest run:node_modulessegmentnode_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json— the file the card names, hash and all. Keys added: 0..jsonfiles underpackages/**are still inputs —package.jsonand../lint/package.jsonare both present, and 506 non-node_modules.jsonkeys remain. The zero is a reading, not a glob that stopped matching.node_modulessegment (positive control: 5,471 tracked files live under asrcsegment), so the exclusion cannot subtract any tracked repo source from any task.3. Measured: turbo does NOT honour a negated
$TURBO_ROOT$inputThe card marked this "to be measured". It was measured, on turbo 2.10.10, against
@objectstack/spec#test:repowith three probe files planted under three packages' dependency trees:!$TURBO_ROOT$/packages/**/node_modules/**!$TURBO_ROOT$/packages/*/node_modules/**!$TURBO_ROOT$/**/node_modules/**!packages/**/node_modules/**(bare, package-relative)!**/node_modules/**(package-relative) — this PRControl for the negation mechanism itself, so the three inert rows are a reading about the root
token rather than about negation: a package-relative
"!LICENSE"on the same task removed exactlythat one key (7307 to 7306,
LICENSE1 to 0).So the answer is no — a negation carrying the root token is dropped rather than applied, silently.
This PR uses the package-relative form. It needs no per-package depth:
**spans zero or moresegments, so one string covers the task's own tree and the
../PKG/...keys a root-scoped globcontributes alike. The measurement is recorded where the constant is declared, with a "do not tidy
this into the
$TURBO_ROOT$form" warning, because that form reads as consistent and enforces nothing.4. The gate rule — the half that closes the class
The card's own words: the registry gate "checks that turbo hashes the declared globs, not what
else the glob happens to match". Layer B now asks the second question too.
node_modulespath requires the exclusion on its owningturbo task. Reach is answered by the shared matcher (
glob-match.mjs) against an assembled witnesspath, never by sniffing the glob's text for a wildcard — so a glob is judged by the same semantics
Layers A and B judge it by.
leaving a declaration that reads as a radius and hashes nothing.
The rule is shown to CATCH something
Ablation A — remove the fix, keep the rule. From the committed tree,
turbo.jsonreverted to themerge-base bytes (blob compared, not just "the file changed"), then restored:
Ablation B — the actual future case: a NEW root-scoped declaration. A
packages/**/*.jsonglobappended to a package that today declares only literal paths (
@objectstack/mcp), together with itsmatching turbo input — i.e. everything the OLD gate asked for:
Both ablations verified the mutation reached the loaded module, not merely the disk (the injected
literal was counted before and after, and the table was re-imported and printed), and both restored
by blob comparison with
git diff HEADempty. The first attempt at ablation B was a no-op — aninserted duplicate
globs:key that the later one overrode, gate exit 0 — and is reported hererather than silently retried.
Negative control (mandatory), on the live tree
A rule that rejected any root-scoped glob would satisfy the above and break the repo. It does not:
node_modulesand now carry the exclusion;11 declare only literal file paths, owe nothing, and are untouched and green.
live-tree case that fails on a repo whose
turbo.jsonhas drifted back (it is the case that firedin ablation A).
OK: 28 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split "test:repo" task).Verification
node scripts/check-cross-package-test-inputs.mjsexit 0;--self-testAll 152 self-test cases passed (was 134; the new battery adds 18 and is registered inSELF_TEST_BATTERIESwith the roster floor raised 8 to 9).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat the final commit: 33 commands, 33 run, 0 non-zero. Reconciled with--ran: "33 derived famil(ies) accounted for — 33 run, 0 NOT-MEASURED."@objectstack/spec#test:repoexecuted green in both before-leg runs (exit 0, 32.054s / 30.784s).eslint.config.mjswhich never enables type-aware linting (noparserOptions.project, no typed rules) for ANY file, so this diff cannot move a verdict on the 6,388 untouched files. Nonsense control: eslint refuses a path that does not exist.pnpm check:nul-bytesexit 0 (8,420 files); an independent control-character sweep over the three changed files matched nothing.Scope notes
no. Path limb —node scripts/pm/dispatch-gates.mjs --tierat this commit: "the surface hits none of the 3 declared glob(s)";SUSPECT_TIER_GLOBSispackages/spec/src/**, untouched. Content limb — no schema key, closed-set member, published export or registry entry moves; what moves is what turbo hashes plus one repo gate rule.skip-changeset, measured rather than assumed: 0 published packages namescriptsorturbo.jsoninfiles[](positive control: 70 published packages namedist). Nothing published moves.scripts/cross-package-test-inputs.mjs+turbo.json+ the registry gate's self-test. The rule itself cannot live in a self-test, soscripts/check-cross-package-test-inputs.mjscarries it (the gate whose self-test the brief names); no fourth file is touched. Declared here rather than done quietly.docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md), so no maintainer-briefing section is owed.packages/**/*.tsxwhile its tree-walking tests read.tsx— and the glob that would cover it is forbidden by a dispatch-gates self-test pin #16593 is not addressed here and remains open. It is the mirror of this card — the same declaration, narrower than the walk instead of wider. See the report to the PM for a reading on the derived-from shape; that ruling is the PM's.🤖 Generated with Claude Code
https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
Generated by Claude Code