Filed unassigned by the domain:devx PM seat (#6023), session session_01DdCnBGcHeufjrq7drTD3wt, from PR #10275 (#10086). The dev raised it as a process observation and deliberately did not file it — "the remedy is not obvious and inventing one unprompted would widen this PR" — which was the right call. Recording it so the next occurrence is a second data point rather than a first. ⛔ No domain:* grading beyond the lane it was found in; no shape is proposed as decided.
What happened, and why it is a class rather than an incident
PR #10275 gave 33 scripts/*.mjs files a shared entry-guard predicate, adding import { isEntrypoint } from './invoked-as.mjs' to each. CI went red on three tests in packages/create-objectstack/src/template-version-stamps.test.ts:
Error: Cannot find module './invoked-as.mjs' imported from
/tmp/sync-template-versions-9554-K5i1BN/scripts/sync-template-versions.mjs
That test builds a synthetic checkout and copied exactly one named file into it. Nothing in the test mentioned the sibling, so nothing in the test had to be edited for it to break — and nothing in the repo could have said so in advance.
The dev's round-one census was over copy-into-a-fixture consumers inside scripts/ (the --self-test paths). It was a reasonable boundary and it was wrong: the population also contains vitest suites under packages/. scripts/pm/dispatch-gates.mjs cannot close that gap either — it derives repo gate families and runs no package test suite, so nothing anyone ran locally executed the failing file.
Why the one gate that looks like it should catch this, does not
scripts/check-cross-package-test-inputs.mjs is exactly the right idea: it makes a package that reads outside itself declare the read, and checks turbo.json hashes every declared glob. But its own header says it is a source scan that recognises a fixed list of spellings, and the fix PR #10275 landed makes the fixture derive its file set — copy the script, then every relative import it makes, transitively — rather than quote it.
⇒ After that fix, scripts/invoked-as.mjs appears in no quoted string the flat literal collector can see, while a change to it genuinely does break the test. The declaration had to be added by hand, and nothing would have complained if it had not been.
That is the same silence-reads-as-pass family the originating card was about, one level up: the gate is green because it matched nothing, not because there was nothing to match.
The second-order defect this already produced (fixed in #10275, recorded as evidence)
Declaring scripts/invoked-as.mjs as a cross-package input immediately turned check:cross-package-test-inputsred — because turbo.json's create-objectstack#test inputs did not hash it. Left alone, the cache would not invalidate on a change to the sibling, so the test could go red on main while every PR reported green (#7802's shape). Both halves are fixed on that PR; the point here is that the declaration and the hash are two places one fact has to be written, and only one of them had a guard.
What is NOT claimed
- ⛔ No remedy is proposed as correct. Candidates the next owner may weigh, none measured here: teaching the collector to follow a derived closure (hard — it is a source scan by design, and its header argues for that); requiring fixtures to copy a whole directory rather than a file set (the two
packages/spec/scripts/dist-freshness*.test.ts fixtures already do exactly this, via symlinkSync(REPO_ROOT/scripts, …, 'dir'), and are safe by construction as a result); or a runtime check that a fixture's module graph resolved entirely inside the fixture. - The population figure below is a floor, not a census of the repo — see the bound.
Population as measured, so a next owner starts from data
Independently re-derived by this seat over 2,472 test files outside scripts/ at d3d528e9, looking for files that both name a root scripts/ path and carry a materialising op:
| file | verdict |
|---|
packages/create-objectstack/src/template-version-stamps.test.ts | the break — copied one hand-named file; now derives the closure |
packages/spec/scripts/dist-freshness.test.ts | safe — symlinkSync of the whole root scripts/ dir |
packages/spec/scripts/dist-freshness-adoption.test.ts | safe — same whole-dir symlink |
packages/spec/scripts/schema-tree-freshness.test.ts | not in population — imports ../../../scripts/check-regen-pending.mjsin place; its mkdtempSync sandbox holds test data, not a copy of the script |
⚠️ The two safe ones are safe precisely because they take the whole directory instead of picking files — which is the observation a remedy should probably start from. Note also that a whole-dir symlink is not universally available: template-version-stamps needs the copied script to self-locate into the fixture, and node resolves symlinks for the module graph, so a symlink there would send it back to the real checkout — the very thing the copy exists to prevent. Any rule of the form "just symlink the directory" has that counter-example in it already.
⚠️Bound on the sweep: it is a heuristic over two co-occurring signals (a scripts/ path literal and a materialising op in the same file), run at one commit. It cannot see a fixture that builds its path by concatenation, and it is why schema-tree-freshness surfaced and had to be hand-classified out. Treat 4 as a floor.
Refs: PR #10275 / #10086 (origin, and the derived-closure fix) · scripts/check-cross-package-test-inputs.mjs (the gate whose blind spot this is) · #7802 (the cache-invalidation shape of the second-order defect).
Filed unassigned by the
domain:devxPM seat (#6023), sessionsession_01DdCnBGcHeufjrq7drTD3wt, from PR #10275 (#10086). The dev raised it as a process observation and deliberately did not file it — "the remedy is not obvious and inventing one unprompted would widen this PR" — which was the right call. Recording it so the next occurrence is a second data point rather than a first. ⛔ Nodomain:*grading beyond the lane it was found in; no shape is proposed as decided.What happened, and why it is a class rather than an incident
PR #10275 gave 33
scripts/*.mjsfiles a shared entry-guard predicate, addingimport { isEntrypoint } from './invoked-as.mjs'to each. CI went red on three tests inpackages/create-objectstack/src/template-version-stamps.test.ts:That test builds a synthetic checkout and copied exactly one named file into it. Nothing in the test mentioned the sibling, so nothing in the test had to be edited for it to break — and nothing in the repo could have said so in advance.
The dev's round-one census was over copy-into-a-fixture consumers inside
scripts/(the--self-testpaths). It was a reasonable boundary and it was wrong: the population also contains vitest suites underpackages/.scripts/pm/dispatch-gates.mjscannot close that gap either — it derives repo gate families and runs no package test suite, so nothing anyone ran locally executed the failing file.Why the one gate that looks like it should catch this, does not
scripts/check-cross-package-test-inputs.mjsis exactly the right idea: it makes a package that reads outside itself declare the read, and checksturbo.jsonhashes every declared glob. But its own header says it is a source scan that recognises a fixed list of spellings, and the fix PR #10275 landed makes the fixture derive its file set — copy the script, then every relative import it makes, transitively — rather than quote it.⇒ After that fix,
scripts/invoked-as.mjsappears in no quoted string the flat literal collector can see, while a change to it genuinely does break the test. The declaration had to be added by hand, and nothing would have complained if it had not been.That is the same silence-reads-as-pass family the originating card was about, one level up: the gate is green because it matched nothing, not because there was nothing to match.
The second-order defect this already produced (fixed in #10275, recorded as evidence)
Declaring
scripts/invoked-as.mjsas a cross-package input immediately turnedcheck:cross-package-test-inputsred — becauseturbo.json'screate-objectstack#testinputs did not hash it. Left alone, the cache would not invalidate on a change to the sibling, so the test could go red onmainwhile every PR reported green (#7802's shape). Both halves are fixed on that PR; the point here is that the declaration and the hash are two places one fact has to be written, and only one of them had a guard.What is NOT claimed
packages/spec/scripts/dist-freshness*.test.tsfixtures already do exactly this, viasymlinkSync(REPO_ROOT/scripts, …, 'dir'), and are safe by construction as a result); or a runtime check that a fixture's module graph resolved entirely inside the fixture.Population as measured, so a next owner starts from data
Independently re-derived by this seat over 2,472 test files outside
scripts/atd3d528e9, looking for files that both name a rootscripts/path and carry a materialising op:packages/create-objectstack/src/template-version-stamps.test.tspackages/spec/scripts/dist-freshness.test.tssymlinkSyncof the whole rootscripts/dirpackages/spec/scripts/dist-freshness-adoption.test.tspackages/spec/scripts/schema-tree-freshness.test.ts../../../scripts/check-regen-pending.mjsin place; itsmkdtempSyncsandbox holds test data, not a copy of the scripttemplate-version-stampsneeds the copied script to self-locate into the fixture, and node resolves symlinks for the module graph, so a symlink there would send it back to the real checkout — the very thing the copy exists to prevent. Any rule of the form "just symlink the directory" has that counter-example in it already.scripts/path literal and a materialising op in the same file), run at one commit. It cannot see a fixture that builds its path by concatenation, and it is whyschema-tree-freshnesssurfaced and had to be hand-classified out. Treat 4 as a floor.Refs: PR #10275 / #10086 (origin, and the derived-closure fix) ·
scripts/check-cross-package-test-inputs.mjs(the gate whose blind spot this is) · #7802 (the cache-invalidation shape of the second-order defect).