Uh oh!
There was an error while loading. Please reload this page.
fix(devx): teach the cross-package input detector the findUp anchor seeds - #10852
Conversation
…eeds `check:cross-package-test-inputs` finds escaping tests by scanning source text, so a seed spelling it does not know yields no flag -- which means no declaration, silently. Every seed it knew answers "where am I?" off the module itself. A CJS-typed package cannot ask: plugin-auth publishes `dist/index.js` as CommonJS, so under `module: NodeNext` `import.meta` is a TS1470 there, and four of its tests walk up from `process.cwd()` to an anchor instead. That walk resolved to nothing, so every path built on it resolved to nothing too. Measured on `19f98fa1f^`: `rate-limit-storage-isolation.test.ts` read `packages/runtime/src` and `packages/services/service-sms/src` through such a seed, appeared in no roster, and turbo replayed a cached green over the scan it never re-ran -- #7802 exactly, by a fourth spelling. #10161 reseeded that one file; this closes the class it was an instance of. `findUpSeeds()` resolves the two predicates that are knowable without executing anything: a manifest `name` matching THIS package (-> the package root, named) and a WORKSPACE_ROOT_MARKERS file (-> the repo root, which escapes on its own exactly as `resolve(HERE, '../../..')` already does). A manifest naming some OTHER package keeps the escape verdict and loses the name, the trade `walkLiteral` already makes for an unreadable argument. Today's findUp population is clean, so no gate turns red and none turns newly green -- the proof is 14 `--self-test` cases, 8 of which fail without the detector change. Part of #10029 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
✅ ACCEPT — reviewer of record: |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32491506358 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes#10029 — direction B, the residual scope the 2026-08-20
pm:retriageruling retargeted this card to. Direction A landed separately as PR #10161 and is not revisited here.The class, not the instance
check:cross-package-test-inputsfinds escaping tests by scanning source text — deliberately, so a dependency-free detector cannot itself fail to resolve in CI. The price is that it sees only the spellings it knows, and its own header states the consequence: "A path written any other way yields no flag — which means no declaration, silently."Every seed it knew answers "where am I?" off the module itself:
import.meta.url,__dirname,import.meta.dirname,dirname(import.meta.filename), and the walkedimport.meta.url/.filenameforms — five spellings, all file-relative.A CJS-typed package cannot ask that question.
packages/plugins/plugin-authpublishesdist/index.jsas CommonJS, so undermodule: NodeNextimport.metais a TS1470 there however well it runs under vitest. Four of its tests therefore walk up fromprocess.cwd()to an anchor instead — its ownpackage.json, or the workspace root. That walk resolved to nothing, so every path built on it resolved to nothing too: no depth, no name, no flag.Measured on
19f98fa1f^, not reasoned:rate-limit-storage-isolation.test.tsreadpackages/runtime/srcandpackages/services/service-sms/srcthrough such a seed, appeared in no roster, and turbo replayed a cached green over the scan it never re-ran — #7802 exactly, by a fourth spelling. PR #10161 reseeded that one file from__dirname. This closes the class it was an instance of.What
findUpSeeds()resolvesTwo predicates are knowable without executing anything, and they are the two the idiom uses:
name=== this package's nameWORKSPACE_ROOT_MARKERSfile (pnpm-workspace.yaml)pkgSegs.lengthabove the package rootresolve(HERE, '../../..')already doesname=== some other packageThat last row is the trade
walkLiteralalready makes for an unreadablejoin()argument: keep the escape verdict, drop the name. Resolving it to this package's root instead would do both kinds of damage at once — a roster entry pointing at a file nobody reads, and a real escape hidden behind a depth of 0.Two things are new in kind, and both are stated in the header rather than left to be discovered:
fileSegs, and a caller with none (the depth-only--self-testshapes) gets no seed rather than a guessed one.DECLcannot read these. It stops an initialiser at the first;, and afindUppredicate is a block with statements in it. So the seeds are read by their own balanced pass and injected intoknownfirst, after which every later spelling composes with an anchor exactly as it composes with animport.meta.urlseed —join(REPO, 'packages', 'runtime', 'src')is the ordinary literal walk onceREPOhas a depth.balancedArgs()is extracted fromreadArgumentListsrather than written a second time beside it. A mirrored helper is the shape #10628 already had to undo in this file once.⭐ The evidence cannot be "a gate turned green"
Triage measured today's
findUppopulation clean, and re-measurement on this branch's base confirms it. So this lands with no gate turning red and none turning newly green — which makes the--self-testthe only proof there is.Both real-tree outputs are byte-identical before and after the change (
diffof the captured logs, exit 0):The ablation
findUpSeeds()was neutered with an earlyreturn seeds;— confirmed on disk, not by an editor's exit code: the injected marker went0 -> 1occurrences, the anchor text stayed at1, andgit hash-objectmoved529ca9d6…->500566b5….Predicted before running: the 8 presence-asserting cases fail; the 6 absence-asserting ones stay green;
--verifystays exit 0 on both legs.Observed: 8 of 104 failed, and they are exactly the 8 enumerated —
The second observation is the one worth keeping: under the same ablation
--verifyover the real tree still printedOKand exited 0. Today's population has nothing for it to catch, so the self-test is the only thing holding this rule — which is why the two cases that pin a NAME are load-bearing. A case asserting only "it does not flag" passes just as happily on a seed that resolved to nothing, which is precisely the bug.No rebuild was needed, and that is a property of the file rather than an assumption: this gate is a dependency-free
.mjsinvoked asnode scripts/check-cross-package-test-inputs.mjs, whose only import is the sibling./invoked-as.mjsby relative path. Nothing on its path resolves through any package'sexportsordist/, so no stale build can mask either leg.Restored and re-measured: marker
1 -> 0,git hash-objectback to529ca9d64fe0405558b9fc9795388598a423670f— byte-identical — worktree clean, self-test back toAll 104 self-test cases passed./ exit 0.Verification
Gate union derived with
node scripts/pm/dispatch-gates.mjs(no paths passed — it reads the merge-base changeset itself) after the final commit on a clean worktree, atgit rev-parse --short HEAD=5157cf58fe. It named 3 families for this surface; every one was run. Each exit code captured before any pipe, and each row quotes the gate's own verdict line.node scripts/check-cross-package-test-inputs.mjs --self-testAll 104 self-test cases passed.node scripts/check-cross-package-test-inputs.mjsOK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.node scripts/check-ci-filter-parity.mjs --self-test✓ check-ci-filter-parity --self-test: 36 assertions — …node scripts/check-ci-filter-parity.mjsOK: all 82 declared cross-package glob(s) (71 unique) are covered by core or crosspkg, every crosspkg entry still covers one, and the test job's if: still names both filters.Plus the two families
dispatch-gatesis structurally blind to — run by hand, because this diff is exactly their file surface:node scripts/check-entry-guard.mjs --self-test✓ check-entry-guard self-test: 47 cases pass — …node scripts/check-entry-guard.mjs✓ check:entry-guard: 128 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 86 export bindings, 76 of them inert on import (10 known-unsafe, ⛔ SHRINK-ONLY).node scripts/check-parse-guard.mjs --self-test✓ check:parse-guard self-test: 41 cases pass …node scripts/check-parse-guard.mjsnode scripts/check-nul-bytes.mjs --self-test✓ check-nul-bytes --self-test: 75 assertions over a temp git repo (real scan() path)node scripts/check-nul-bytes.mjscheck-nul-bytes: OK (scanned 6266 text file(s) -- 6266 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes)dispatch-gatesnames both blind spots itself and agrees on the reason:check:parse-guardis listed under "unreachable BY CONSTRUCTION —scripts: the tree HAS it; the covering rule refuses the literal as too generic (no path separator)", andcheck:entry-guardnever appears at all because it reads its population offKNOWN_IMPORT_UNSAFE, a roster of current violators.Control bytes swept beyond the gate, with its counter-check so a zero is not a silent no-op:
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over the changed file yields 0, while the same pattern over a deliberate BEL fixture yields 1.Notes
skip-changesetapplied additively (POST /issues/N/labels), following PR ci(devx): assert every cross-package declared glob can schedule the test job #10801's precedent on this same file.packages/plugins/plugin-auth/src/managed-extension-fields.test.tsis untouched — it is plugin-auth's only gate-visible escaping read and carries that package's declared radius by itself.CROSS_PACKAGE_TEST_INPUTSandturbo.jsonare untouched. No declaration changes; this is detector-only.globHolderVerdict()'s docblock told the next reader to ablate that limb by reseeding fromprocess.cwd(), calling it "a root walk this detector deliberately does not resolve". Half of that is now false: thefindUpanchor form — which is how plugin-auth actually spells a cwd walk — does resolve, so reseeding that way would leave the test escaping and ablate nothing, while the limb read as healthy. The sentence now says bareprocess.cwd(), and a--self-testcase pins that the bare form is still unresolved.Generated by Claude Code
Generated by Claude Code