Skip to content

fix(scripts): print the measured specifiers in check-test-source-alias remediation - #8486

Merged
qq9340100 merged 1 commit into
mainfrom
claude/issue-8256-measured-specifier-hint
Aug 13, 2026
Merged

fix(scripts): print the measured specifiers in check-test-source-alias remediation#8486
qq9340100 merged 1 commit into
mainfrom
claude/issue-8256-measured-specifier-hint

Conversation

@qq9340100

Copy link
Copy Markdown
Collaborator

Fixes#8256

check-test-source-alias names an unaliased dependency and then tells you how to repair it. What it
printed was the dependency's bare name and one anchored-bare alias entry for deps[0].

That is right for a package imported bare, and a dead end for one whose test-reachable specifiers are
all subpaths. Measured on origin/main, packages/formula reaches three specifiers —
@objectstack/spec, @objectstack/spec/api, @objectstack/spec/data — and the printed rule
/^@objectstack\/spec$/ covers exactly one of them. Apply it verbatim and the gate is still red,
reprinting the same message with no further guidance. The worse half: the obvious next guess is the
object form, which makes this gate pass while matching by PREFIX and then dying with ENOTDIR at
run time (the trap recorded in #7778).

What it prints now

The gate already knew the specifiers — that is how it decided the package was unaliased — so they are
carried through the scan instead of being reduced to the dependency's bare name. Real output for
packages/formula, from this branch:

 Measured — the specifiers these tests really import, and where each one lands today:
@objectstack/spec no alias entry matches it
@objectstack/spec/api no alias entry matches it
@objectstack/spec/data no alias entry matches it
Add ONE ANCHORED entry per specifier above to this package's vitest.config.* (array form).
Anchoring is what makes the entries order-independent and stops a bare key from swallowing
the subpaths:
alias: [
{ find: /^@objectstack\/spec$/, replacement: path.resolve(__dirname, '../spec/src/index.ts') },
{ find: /^@objectstack\/spec\/api$/, replacement: path.resolve(__dirname, '../spec/src/api/index.ts') },
{ find: /^@objectstack\/spec\/data$/, replacement: path.resolve(__dirname, '../spec/src/data/index.ts') },
]
Each replacement above names a file that EXISTS in this checkout; confirm it is what that
package's `exports` entry for the subpath is built from — this gate measures the tree, it
does not read the export map.
⛔ Do NOT collapse the subpath entries into the object form `{ '@objectstack/spec': … }`.
It matches by PREFIX, so `@objectstack/spec/api` resolves to `…/src/index.ts/api` —
ENOTDIR at run time, in a config that reads as correct. This gate fails that as the
alias-through-a-file rule, and it is the trap this hint exists to keep you out of.

The object-form warning is emitted only when a subpath specifier was measured — a package imported
bare cannot hit prefix-matching, and warning it anyway is how a diagnostic becomes noise. A bare
importer keeps exactly the anchored-bare entry that was always right for it.

The replacement side cannot be a template either

The card rules out answering with a different one-size rule, and the repo proves why. Both of these
now come out of the same block, measured per specifier against the tree:

{ find: /^@objectstack\/platform-objects\/plugin$/, replacement: path.resolve(__dirname, '../platform-objects/src/plugin.ts') },
{ find: /^@objectstack\/platform-objects\/audit$/, replacement: path.resolve(__dirname, '../platform-objects/src/audit/index.ts') },

That matches the package's real exports map, where ./plugin is a file (dist/plugin.js) and
./audit a directory (dist/audit/index.js). A capture rule deriving the path from the specifier
gets one of those two wrong — and fails on whoever next writes that import, not on the author of the
rule. Targets are therefore resolved with the same file-then-index.* candidate list the walk already
uses, and a specifier with no counterpart under the dependency's src/ prints an explicit unmeasured
placeholder rather than an invented path.

Output only — the verdict is untouched

Nothing here changes which inputs the gate accepts or rejects.

  • --list is byte-for-byte identical before and after (63 lines, 61 registry entries, diff clean).
  • Full-repo run before and after: OK — 72 packages with tests scanned; 61 registered as still resolving a workspace dep through dist/.
  • KNOWN_UNALIASED_TEST_IMPORTS is untouched, and no vitest.config.* was read for anything but input.

Mutation testing

Five deliberate breaks, each direction predicted in writing before running, each caught by a named
assertion:

breakpredictedobserved
emit only the first row's entry (the old deps[0] template)3 namedexactly those 3
replace measurement with a one-size capture rule2 namedexactly those 2
emit the object-form warning unconditionally1 named (negative control)exactly that 1
collapse the two ledger reasons into one1 namedexactly that 1
flip the verdict itself (offending.length >= 0)pre-existing assertions fire6 fired, incl. compliant + canary

The capture-rule break is the card's counterexample reproduced mechanically: it left the
directory-served subpath assertion green while breaking the file-served one — right for one
package, wrong for the other, exactly as predicted. The last break confirms the verdict path is still
guarded after the internal anyUnaliased flag became a list.

New fixture packages/subpath-only carries all three remediation shapes in one importer that writes
no bare specifier at all: a subpath served by a file, one served by a directory, and one with no
counterpart under src/.

Known tradeoff

The block is now one ledger line and one alias entry per specifier, uncapped. For the worst case in
the repo (packages/cli, 27 unaliased dependencies) that is 96 lines instead of 8. No cap was added
deliberately: every line is a specifier the reader has to alias, a cap would hide exactly the data the
card asked to surface, and this branch of the diagnostic only fires for an unregistered package —
never in normal operation.

Verification

  • node scripts/check-test-source-alias.mjs --self-test — OK
  • node scripts/check-test-source-alias.mjs (full repo) — OK, verdict unchanged
  • pnpm check:nul-bytes — OK (7642 files, no raw control bytes); plus a targeted control-character
    self-scan of the changed file
  • npx eslint scripts/check-test-source-alias.mjs — clean
  • node scripts/pm/dispatch-gates.mjs scripts/check-test-source-alias.mjs re-derived against the
    actual changed path: no path-scoped family, no delta from the dispatch list

Scripts-only, no user-visible surface ⇒ skip-changeset.

Generated by Claude Code


Generated by Claude Code

…s remediation (#8256)
The unaliased-dependency diagnostic named the bare dependency and printed one
anchored-BARE alias entry for `deps[0]`. That is right for a package imported
bare and a dead end for one whose reachable specifiers are all subpaths:
`/^@objectstack\/spec$/` matches none of the specifiers the same message had
just named, so applying the printed fix leaves the gate red with the message
unchanged and no further guidance.
The gate already knows the specifiers — that is how it decided the package was
unaliased — so they are now carried through the scan and printed, each with
where it lands today (no entry matched, or an entry that lands on `dist/`),
plus one anchored entry per specifier.
The replacement side cannot be a template either: `@objectstack/spec` serves
every namespace from a directory while `@objectstack/platform-objects` maps
`./plugin` to a file, so a single capture rule is right for one and wrong for
the other — and wrong on whoever next writes that import. Each target is
therefore measured against the tree, and printed as unmeasured when nothing
under the dependency's `src/` answers to it. A subpath importer is also warned
off the object form, which passes this gate by prefix-matching and then dies
with ENOTDIR at run time.
Output only: no input changes which packages the gate accepts or rejects.
`--list` is byte-for-byte identical before and after (61 entries), and the
full-repo run is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jqe56GnYFddggeAyfkZFVz
@vercel

vercelBot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 13, 2026 3:35pm

Request Review

@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 13, 2026
@qq9340100
qq9340100 marked this pull request as ready for review August 13, 2026 16:01
@qq9340100
qq9340100 added this pull request to the merge queueAug 13, 2026
Merged via the queue into main with commit c60698aAug 13, 2026
24 checks passed
@qq9340100
qq9340100 deleted the claude/issue-8256-measured-specifier-hint branch August 13, 2026 16:19
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@qq9340100@claude