Repository navigation
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) |
… cannot resolve a findUp walk PR #10852 taught scripts/check-cross-package-test-inputs.mjs the findUp ANCHOR seeds: a walk keyed on the scanned package's own manifest name resolves to that package root, and one keyed on a WORKSPACE_ROOT_MARKERS file (pnpm-workspace.yaml) resolves to the repo root. Three test-file docblocks still told authors the opposite, and in two of them that sentence was the stated reason for a code decision. - serve-multi-node-cap-advisory.pin.test.ts: the unresolvable-spelling list now says BARE process.cwd() instead of "a findUp walk from process.cwd()". The other two examples are unchanged. - rate-limit-storage-isolation.test.ts: the "not a spelling it resolves" claim is restated in the past tense (true when this file was reseeded in #10161), and the "process.cwd() appears nowhere in that detector" clause — wrong when written — is replaced by what actually holds: the detector reads the anchor predicate as text and calls nothing. - managed-extension-fields.test.ts: the seed prohibition STAYS; only its reason is restated. What holds packages/**/*.object.ts is the heldBy witness, satisfied only while this file remains an escaping test; a findUp-anchored reseed keeps it escaping (measured), while an unresolvable seed drops it and reds the gate by name since #10566's unheld-glob limb — not the silent green the old sentence described. Each corrected sentence carries the measurement that makes it true, so the next reader can re-derive instead of trusting prose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
…tant `scripts/check-cross-package-test-inputs.mjs` is a source scan: a path spelling it does not know produces no flag, so a test whose reads escape its package goes undeclared silently. That is why its recognised set is published in AGENTS.md rather than left in the implementation -- and nothing compared the two copies. The mirror drifted three times (#10163, #10854, and this one). Twice the stale line was the stated REASON FOR A PROHIBITION, so a rotting mirror does not merely misinform: it launders an obsolete rule into a live one. And one claim was already false BEFORE the PR that supposedly staled it, so a lag-only check would not have caught it either. Adds `scripts/check-published-list-mirrors.mjs`, which asserts line-for-line EQUALITY between a declared constant and the block a document publishes: - equality, not containment -- a comment-only drift is invisible to containment, and the comments are where the prohibitions live; - the block is located by heading + fence, never by line number (this card was filed against `AGENTS.md:96-106`; the block sat at `:92-104` three days later); - every unreadable state REFUSES: renamed heading, duplicate heading, re-tagged fence, unterminated fence, empty block, two candidate fences, and a constant that is missing, renamed, empty or not a string list; - it can only ever go RED. AGENTS.md is governed, human-merge-only, so the gate never repairs -- it prints the exact block to paste. Stated in its header. Repairs the published block in the same change: it was short by 13 of the constant's 24 lines, including the two `findUp` ANCHOR seeds PR #10852 added, and the lead-in prose named only two of the three seed kinds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
… cannot resolve a findUp walk (objectstack-ai#11891) PR objectstack-ai#10852 taught scripts/check-cross-package-test-inputs.mjs the findUp ANCHOR seeds: a walk keyed on the scanned package's own manifest name resolves to that package root, and one keyed on a WORKSPACE_ROOT_MARKERS file (pnpm-workspace.yaml) resolves to the repo root. Three test-file docblocks still told authors the opposite, and in two of them that sentence was the stated reason for a code decision. - serve-multi-node-cap-advisory.pin.test.ts: the unresolvable-spelling list now says BARE process.cwd() instead of "a findUp walk from process.cwd()". The other two examples are unchanged. - rate-limit-storage-isolation.test.ts: the "not a spelling it resolves" claim is restated in the past tense (true when this file was reseeded in objectstack-ai#10161), and the "process.cwd() appears nowhere in that detector" clause — wrong when written — is replaced by what actually holds: the detector reads the anchor predicate as text and calls nothing. - managed-extension-fields.test.ts: the seed prohibition STAYS; only its reason is restated. What holds packages/**/*.object.ts is the heldBy witness, satisfied only while this file remains an escaping test; a findUp-anchored reseed keeps it escaping (measured), while an unresolvable seed drops it and reds the gate by name since objectstack-ai#10566's unheld-glob limb — not the silent green the old sentence described. Each corrected sentence carries the measurement that makes it true, so the next reader can re-derive instead of trusting prose. Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx Co-authored-by: Claude <noreply@anthropic.com>
…nt (objectstack-ai#11908) * feat(gates): ratchet AGENTS.md's published spellings list to the constant `scripts/check-cross-package-test-inputs.mjs` is a source scan: a path spelling it does not know produces no flag, so a test whose reads escape its package goes undeclared silently. That is why its recognised set is published in AGENTS.md rather than left in the implementation -- and nothing compared the two copies. The mirror drifted three times (objectstack-ai#10163, objectstack-ai#10854, and this one). Twice the stale line was the stated REASON FOR A PROHIBITION, so a rotting mirror does not merely misinform: it launders an obsolete rule into a live one. And one claim was already false BEFORE the PR that supposedly staled it, so a lag-only check would not have caught it either. Adds `scripts/check-published-list-mirrors.mjs`, which asserts line-for-line EQUALITY between a declared constant and the block a document publishes: - equality, not containment -- a comment-only drift is invisible to containment, and the comments are where the prohibitions live; - the block is located by heading + fence, never by line number (this card was filed against `AGENTS.md:96-106`; the block sat at `:92-104` three days later); - every unreadable state REFUSES: renamed heading, duplicate heading, re-tagged fence, unterminated fence, empty block, two candidate fences, and a constant that is missing, renamed, empty or not a string list; - it can only ever go RED. AGENTS.md is governed, human-merge-only, so the gate never repairs -- it prints the exact block to paste. Stated in its header. Repairs the published block in the same change: it was short by 13 of the constant's 24 lines, including the two `findUp` ANCHOR seeds PR objectstack-ai#10852 added, and the lead-in prose named only two of the three seed kinds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx * fix(gates): publish the spellings, not the failure box's notes Two standing rulings on AGENTS.md govern what a mirror may put there, and a byte-identical copy of the whole constant breaks one of them outright: - the shrink-only line ceiling (`check-skill-line-ratchet`), and - "operative text carries lessons self-contained ... numbers go" (maintainer ruling 2026-08-12, `check-skill-id-lint`). The constant is the text of a CLI failure box: spellings (code, each with the comment that annotates it) plus free-standing NOTE PARAGRAPHS set off by blank entries, two of which carry issue-ID citations. Publishing all 24 lines put those citations into the instruction surface and check:pm-skill-id-lint went red -- the gate meant to keep the document honest would have broken the document's own prose standard. So the mirror judges a projection with no judgement in it: an entry is PUBLISHED unless it is blank, or it is a comment line that no published entry directly precedes. A comment continuing a spelling travels with that spelling; a note paragraph standing alone stays in the failure box. Every spelling is code, so no spelling can hide from it. 24 entries project to 17 published lines, zero issue-ID citations, and the published block grows 11 -> 17: the two `findUp` anchor seeds, the ⛔ manifest prohibition that qualifies them, and the `-> repo root` annotation. A self-test case now pins the citation property against the live constant, so an issue number added to a spelling line goes red HERE, naming the ruling, instead of landing in AGENTS.md. check:pm-skill-id-lint: 22 file(s) clean. check:pm-skill-ratchet is red by 8 lines (969 vs 961) and needs a maintainer ruling -- see the PR body. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx * chore(ratchet): raise the AGENTS.md ceiling 961 -> 969 per the maintainer ruling The one-line follow-up PR objectstack-ai#11908's body pre-wrote: the published-spellings mirror re-sync costs +8 lines, lossless rewrap headroom measured 0, and the mirror is now mechanically enforced. Maintainer ruling 2026-08-25, verbatim: 「同意,帮我合并,然后继续」 (option A as presented in the PM-chat batch review; provenance on PR objectstack-ai#11908). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H9StxQgG2DPA26XzZZqnJB * chore(ratchet): re-measure the AGENTS.md ceiling on the reflowed layout, 1150 -> 1158 The branch's original raise (961 -> 969) was measured against the pre-objectstack-ai#11948 layout and was dissolved by the merge. Re-measured on current main: AGENTS.md is 1150 there and 1158 with the mirror re-sync applied, so the honest cost is again +8 -- published block 11 -> 17 lines, prose paragraph 4 -> 6. The maintainer's option-C1 ruling is quoted verbatim and untranslated beside the value, per the map's convention. Gate reads at the new value: check-skill-line-ratchet: AGENTS.md is 1158 lines (ceiling 1158; headroom 0). check-skill-line-ratchet self-test: 71 cases pass. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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