Skip to content

[finding] Nothing can tell you a packages/** test copies a root scripts/*.mjs into a fixture — check-cross-package-test-inputs is a source scan, and a DERIVED path is invisible to it #10315

Description

@os-zhuang

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-inputs red — 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.mjs in 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).

Activity

  1. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    ContributorAuthor

    Hold made legal — trigger on the gate this is a blind spot of

    devx lane PM, session session_01PfaSTikked61BkcsB5Rn69, round 13. Authorization: maintainer instruction this session to clear the eight illegal pm:on-hold cards flagged by half-state patrol H9.

    Held with no Restart-when: in either channel — illegal. Repaired rather than closed: this one records a demonstrated CI break with a named mechanism, which is worth more than most held cards.

    Restart-when: a SECOND instance appears — a packages/** test that copies a root
      scripts/*.mjs into a fixture and breaks on an undeclared sibling — making this a class
      with two data points rather than one. Machine-adjacent form: check:cross-package-test-inputs
      goes red on a create-objectstack or packages/spec fixture declaration.
    Restart-touch: scripts/check-cross-package-test-inputs.mjs,
      packages/create-objectstack/src/template-version-stamps.test.ts
    

    Verified on origin/main: scripts/check-cross-package-test-inputs.mjs still exists and still carries the create-objectstack entry (6 references), so the trigger file is real and the hold is live.

    ⚠️ Restart-touch deliberately collides with #10694's, and that is a feature, not a mistake: both cards name scripts/check-cross-package-test-inputs.mjs, so the next person who opens that file wakes both — one cheap prose repair (#10694) and one design question (this card). Two fixes for one context load.

    ⭐ Why hold rather than queue: the card is explicit that ⛔ no remedy is proposed as correct, and the three candidates it lists all have real counter-examples already in them. In particular, the tempting rule "just symlink the whole directory" — which is exactly what makes the two packages/spec/scripts/dist-freshness*.test.ts fixtures safe — has its counter-example on this very card: 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 sends it back to the real checkout, defeating the copy's whole purpose. Queueing this without a decided shape would dispatch a dev into that trap.

    ⚠️ Carried forward: the population figure of 4 is a floor, not a census — the sweep is a heuristic over two co-occurring signals at one commit, blind to a fixture that builds its path by concatenation. ⛔ Do not cite it as "there are four".


    Generated by Claude Code

  2. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    Contributor

    关 not_planned —— 维护者逐张复核 on-hold 卡时同意关闭;纯开发工具卡

    分诊席(session_01Tw7jnJinGHvoGSi8aFkhPJ),2026-09-23T09:07Z。维护者 2026-09-23 在分诊会话里逐张复核 pm:on-hold 卡,对第五组的回复原文:「10164 不考虑旧的;11182 当前版本就优先处理,统一为cel;其他同意」。 本卡在那一组里的建议是关闭。

    呈给维护者的事实

    • scripts/check-cross-package-test-inputs.mjs 是一个源码扫描,测试「推导出来」要复制的根目录脚本时,它看不见。PR fix(scripts): route every entry guard through one symlink-correct predicate, and close the class #10275 那一次 CI 变红,当时已经用手动声明修好。

    • 卡面自己说 ⛔ 没有哪种修法被认定为正确,列出的三种候选都已经有反例。开卡 5 周,没有出现第二例。

    • 本卡是 tooling,对产品没有影响。SKILL.md:374:产品仓的 P0/P1 开着时,没有解锁对象的 p2/p3 tooling 卡一律关闭。

    卡面的测量和设计笔记原样保留;重开是免费的。

    关闭理由:not_planned,同时摘掉 pm:on-hold。


    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

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions