Uh oh!
There was an error while loading. Please reload this page.
fix(devx): read escaping IMPORT specifiers in the cross-package input gate, and gate the hand-written .d.mts mirrors - #10607
Conversation
… gate, and gate the hand-written .d.mts mirrors check-cross-package-test-inputs' collector recognised only path-shaped file reads seeded from `import.meta.url`/`__dirname`. An ES module specifier is none of those -- it is a bare string in `import` position that the module resolver, not `node:path`, turns into a file -- so a test importing across the package boundary went undeclared silently, which is #7802 by another spelling. Measured on 2d3860d: with the hand-added glob removed, the gate printed `OK: 12 package(s) read outside themselves, all declared` and exited 0 over two live `packages/cli` tests importing `scripts/js-comment-mask.mjs`. Specifiers are now walked by the same `walkLiteral`, in the same two coordinates, judged on the same shallowest point. Only RELATIVE specifiers are collected: a bare one is an installed dependency no turbo glob can name. The collector found six couplings nothing had ever declared -- `@objectstack/client` imports five sibling packages' route ledgers with no graph edge to any of them. Also adds check-declaration-mirrors, which keeps a hand-written `scripts/*.d.mts` in step with the module it declares. A `.d.mts` has no runtime existence, so nothing executed it and nothing noticed drift; consumers see only the declaration, so a drift type-checks GREEN against a signature the module does not implement. It asserts name, kind and required arity per declared export and DISCOVERS its corpus, so a third mirror is covered by existing. Refs #10452, #10549.
os-zhuang
commented
Aug 21, 2026
PM review — ACCEPT, arming. You recovered work my brief told you did not exist, then reviewed it instead of trusting it.Head ⛔ My brief's premise was false, and you handled it the right wayI wrote "branch empty,
⭐ And what you did with it is the part worth pinning: you "reviewed it as a draft rather than a result — which is how the three defects below were found." Recovered work is a submission, not an answer. ⭐ The defect you found in the recovered code, which is the best thing here
That is the same self-referential trap another seat hit tonight on Two further corrections you made to the recovered code's own measured claims, both by re-measuring rather than reasoning: the docblock claiming "none is commented out" (actually 6 of 4174 relative specifiers exist only inside comments — none escaping, so none reaches the roster), and the one claiming three And the third: ⭐ What the fix actually foundSix couplings nothing had ever declared, including Your correction to my "redundant" framing — acceptedI asked you to verify the hand declarations became redundant. You answered that they cannot, by construction: the roster is what the gate checks a declaration against, so the gate never writes one for you. What changed is that they stopped being voluntary — before, cli's Also accepted: your qualification that the card's headline measurement reproduces only when the What earns the ACCEPT
The |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10452
Fixes#10549
Two halves of the same undeclared coupling: the gate that decides what a test's
real inputs are could not see an escaping import, and the file at the centre
of that import had a hand-written type mirror with nothing keeping it honest.
All evidence below was measured on this branch at
505e9dd905.#10452 — the collector now reads escaping import specifiers
check-cross-package-test-inputsrecognised only path-shaped file reads seededfrom
import.meta.url/__dirname. An ES module specifier is none of those:it is a bare string in
importposition that the module resolver, notnode:path, turns into a file. The file said so as its stated boundary —"Reads that reach another package through Node's RESOLVER rather than through
fsare outside this gate entirely." That sentence is what this PR removes.Specifiers are now walked by the same
walkLiteral, in the same twocoordinates, judged on the same shallowest point reached. The recognised
spellings are published as
RECOGNISED_IMPORT_SPELLINGSand printed in thefailure text, beside the path list, under the same rule: a spelling that is not
there yields no flag.
The boundary that makes it safe. Only specifiers starting
./or../arecollected. A bare specifier (
@objectstack/verify,node:fs,vitest) is aninstalled dependency resolved through
node_modules— the same exclusionwalkLiteral'svendoredflag already makes, for the same reason: no turboglob can name it, and collecting them would put every package's suite on every
workspace sibling. Four self-test cases pin that direction rather than one.
Reproduction — the pre-change gate is blind, positively controlled
Zero-hit results need a positive control, so there are two, both run with the
origin/maingate placed inside this tree so itsREPO_ROOTresolves here.Control A — a constructed escaping import in a package with no declaration at
all (a temporary test in
packages/services/service-i18nimporting../../../../scripts/js-comment-mask.mjs):origin/mainOK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.@objectstack/service-i18n has test(s) that read outside the package but declares no input radius.Control B — the real specimen from #10452. With only the hand-added
scripts/js-comment-mask.mjsglob removed from@objectstack/cli's roster:origin/mainOK: 12 package(s) read outside themselves, all declared, …scripts/js-comment-mask.mjs (named in packages/cli/src/commands/serve-audit-registration.contract.test.ts)The hand declarations became ENFORCED, not redundant
The card asked whether PR #10450's hand-added declarations become redundant.
They do not, and could not: the roster is what the gate checks a declaration
against, so the gate never writes a declaration for you. What changed is that
they stopped being voluntary. Before, cli's
scripts/js-comment-mask.mjsglob was a courtesy nothing demanded; now removing it turns the gate red (table
above). They are kept, and both are now derived rather than trusted.
One nuance worth recording, since it partly qualifies the card's own
measurement: on today's tree, removing both hand declarations reddens the
origin/maingate too — but onscripts/js-comment-mask.d.mts, which a secondcli test names in prose and the flat literal collector has always seen. The
.mjsimport itself — the actual subject of #10452 — is invisible to thepre-change gate either way, which is what Control B isolates.
What the new half found
Six couplings nothing had ever declared, all real inputs:
@objectstack/client— five tests import six sibling packages' routeledgers by relative specifier. Of the six, only
@objectstack/runtimeappearsin the manifest at all (a devDependency);
@objectstack/rest, three servicesand
plugin-authare not dependencies in any form, soturbo ls --affectedcould not reach client from a ledger-only diff and
client#testhashed thesame before and after one.
mainis red for every PR that touchespackages/spec: #7769 gavesys_api_keyupdatewithoutbulk, and the conformance scan that catches it lives in a package #7769 never touched #7802's shape exactly, by the other spelling.@objectstack/dogfood— three service ledgers imported by the live-mountparity test.
@objectstack/cli—packages/spec/src/system/translation.zod.ts, namedin a comment and reached only because the pre-filter now admits import-only
escapes.
Each is declared per-file rather than by subtree, and mirrored into
turbo.jsonso Layer B hashes them.
A defect found while verifying, and fixed here
resolveImportTargetjudged "extensionless" with!/\.[A-Za-z0-9]+$/, i.e.does the last segment contain a dot. This repo's authored metadata is
contact.view.ts/semantic-zoo.object.ts/task-triage.page.ts, importedas
…/views/contact.view— read as already having a.viewextension, so nocandidate was tried and the specifier resolved to nothing. Measured: that is
exactly the three
packages/clii18n-coverage imports, whose globs were on theroster by hand. It now tests against the known module extensions.
Ablation, both resolvers on a tree with that one glob dropped:
@objectstack/lintonly@objectstack/cliand@objectstack/lintSelf-test cases
The gate's own instruction is a case per spelling, and the issue made it part of
acceptance. 60 → 79 cases: one per entry in
RECOGNISED_IMPORT_SPELLINGS(static,
import type,export … from,export * from, side-effect, dynamicawait import,require), four boundary cases pinning bare specifiers notflagged, and naming cases pinning each extension rule against a real file on
disk.
Reverse-verified: with the import half disabled (
if (true) continuein place ofthe relative-specifier test, injected and confirmed on disk by anchor count),
12 of 79 cases fail and the live gate goes red on the now-stale
@objectstack/clientdeclaration. The 7 new cases that stay green under thatablation are exactly the "does NOT flag" boundary ones, which are correctly still
true — that asymmetry is the expected direction, not a gap. Restore confirmed on
disk (
git diff --statempty, anchor counts inverted).#10549 —
scripts/check-declaration-mirrors.mjsjs-comment-mask.d.mtswas landeddeliberately by #10398 and the hand-maintenance is its stated design. Nothing
here contradicts that trade — the modules stay
.mjsbecausepre-commitandthe gates invoke them with bare
node. What this adds is the check that thetrade always assumed and never had.
The sweep the card deliberately left undone (
git ls-files 'scripts/**/*.d.mts')returns two files:
check-regen-pending.d.mtsandjs-comment-mask.d.mts.The gate does not list them — it discovers every
scripts/**/*.d.mts, so athird mirror added tomorrow is covered by existing.
Per declared export it asserts NAME (a declared export the module does not
export — the fail-green direction), KIND (
export functionmust be afunction at runtime) and required ARITY against
Function.length.Deliberately not asserted: parameter and return types.
maskCommentsreturning
string[]where the declaration saysstringis invisible to anyruntime check, and a gate overstating its coverage is worse than one stating its
limit. Likewise a module export the declaration omits is not fatal:
check-regen-pending.mjsexports seven and declares three on purpose, and thatpartial mirror cannot fail green — a consumer importing an undeclared name gets
TS2305, loud and immediate.Reject side asserted positively — three ablations, restores proven on disk
Every mutation was applied by anchor-counted replacement (
n !== 1aborts), andevery restore re-counted both the injected and the original anchor plus an empty
git diff --stat. An editor's exit code was never the evidence.maskComments→maskCommentzin the.d.mtsmaskCommentz, but scripts/js-comment-mask.mjs does not export it"maskCommentswith 2 required parameter(s), but … implements 1"check-regen-pending.mjsexports a value where a function is declareddistIsStaleas a function, but … exportsobject"One self-reference, measured and recorded rather than discovered later
This gate imports
maskCommentsfrom./js-comment-mask.mjs, one of the twomodules in its own corpus. A KIND ablation on that module therefore kills the
gate with
TypeError: maskComments is not a functioninstead of printing averdict. Accepted, and written into the file's header: the crash is loud and
exits non-zero, so CI is red either way, and this family's whole danger is the
failure that goes green. The alternative — a second copy of the comment masker —
would add an untested duplicate of the thing five gates were consolidated onto,
to improve an error string in a case that already fails. The KIND rule stays
demonstrable on the other mirror, which this gate does not import (row 3 above).
A fail-green found in the new gate itself, and fixed before merge
While probing the parser:
balanced()counted>as a closing bracket, but>is also the tail of
=>. A callback parameter truncated the parameter list atthe arrow, so
f(cb: (x: number) => void, y: string)reported arity 0instead of 2 — silently, with nothing in
unrecognised. That is this gate's ownfailure mode turned on itself: a wrong number believed, rather than a spelling
refused. Fixed in all three places the arrow bites (
balanced,splitParams,and the default-value test in
requiredArity), with three cases pinning it.Reverse-verified: reverting the
=>exception fails exactly those 2 cases,restore confirmed on disk. Neither mirror uses a callback parameter today; the
day one does, this holds.
Wiring
Added to
lint.ymlas anodestep rather than apnpm check:*alias, matchingthe three neighbouring steps that state the same reason: that alias belongs in
root
package.json, declared territory of the @changesets/cli v3 lane (#9465)while it runs, and
dispatch-gates.mjsderives gate families from eitherspelling. Confirmed —
node scripts/pm/dispatch-gates.mjsnamesnode scripts/check-declaration-mirrors.mjs [lint.yml]from the diff itself.Also corrected the
examples/** live-import inventorycomment next door, whichclaimed the cross-package gate "detects tests whose FILESYSTEM READS escape their
package" as the distinction between the two. That stopped being true in this PR;
the old wording is quoted in place so the correction is legible as one.
Validation, at
505e9dd905check-cross-package-test-inputs --self-testAll 79 self-test cases passed.check-cross-package-test-inputsOK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.check-declaration-mirrors --self-testAll 23 self-test cases passed.check-declaration-mirrorsOK: 2 hand-written declaration(s) agree with their modules on name, kind and required arity.pnpm check:nul-bytespnpm check:node-versionpnpm check:pm-half-statespnpm check:required-contextspnpm check:shard-attestationpnpm check:workflow-status-functionspnpm check:type-check-coverageeslinton both changed scriptsGate families derived with
node scripts/pm/dispatch-gates.mjs(no paths — ittakes the change set off the merge base itself), re-derived after the final
commit.
pnpm check:type-check-debtwas not run tocompletion. It exits 1 here with a refusal, not a verdict: "--re-measure cannot
run: 55 workspace dependenc(ies) of the ledgered packages have no built type
entry point on disk" — it needs a full
turbo run buildof the workspace inthis fresh worktree. This diff touches zero TypeScript (
.mjs, a workflowcomment,
turbo.json), so no ledger number can move; the half of that gatesensitive to the
lint.ymledit ischeck:type-check-coverage, which is greenabove. CI runs the re-measure regardless.
No changeset:
scripts/**+ workflow +turbo.jsonpublishes nothing, andAGENTS.md:943— "Pure bug fixes do not require a changeset". Theskip-changesetlabel is applied (additivePOST /labels); theempty-frontmatter route is rejected by
scripts/check-empty-changeset.mjs.Precedent: PR #10502.
Generated by Claude Code