Skip to content

fix(scripts): read a template-literal alias replacement, and shrink the two entries it mis-measured - #8107

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-8020-template-literal-alias-reader
Aug 12, 2026
Merged

fix(scripts): read a template-literal alias replacement, and shrink the two entries it mis-measured#8107
hotlong merged 1 commit into
mainfrom
claude/issue-8020-template-literal-alias-reader

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#8020

The defect

asPath in scripts/check-test-source-alias.mjs took the last string literal in a replacement expression. For path.resolve(__dirname, '../../spec/src/index.ts') that is the whole answer. For the template form — the one the subpath rule needs, because the $1 back-reference has to sit inside the path —

{find: /^@objectstack\/spec\/([a-z-]+)$/,replacement: `${path.resolve(__dirname,'../../spec/src')}/$1/index.ts`}

the only delimiter reached is the backtick, so the entire template body came back as the path. That text has no srcsegment in it — spec/src' is followed by a quote, not a separator — so pointsAtSource() said no, and a config aliasing every namespace correctly read as aliasing nothing.

Fail-closed, so never a false green. What it cost: @objectstack/plugin-audit and @objectstack/service-knowledge sat in the shrink-only registry as still resolving @objectstack/spec through dist/ on the strength of how their replacement was spelled, and a dev dispatched to remediate either would have found the alias already correct, the gate still red, and the failure text prescribing the alias they already had.

The fix

asPath now splits a template into its literal chunks and ${…} holes, resolves each hole by the same last-literal rule, and concatenates — so $1 survives into String.replace and joins the src chunk:

`${path.resolve(__dirname, '../x/src')}/$1/index.ts` -> ../x/src/$1/index.ts

Neither vitest.config.ts is touched. The configs are correct; the reader was what could not see them.

Registry delta — the complete before/after

Measured with --list on both sides. The blast radius is exactly the two dependencies predicted, with no package reclassified that nobody suspected:

beforeafter
@objectstack/plugin-audit['@objectstack/objectql', '@objectstack/spec']['@objectstack/objectql']
@objectstack/service-knowledge['@objectstack/objectql', '@objectstack/spec']['@objectstack/objectql']
registry entries6363 (unchanged — both keep objectql)
package-dependency pairs312310

Every other line of --list output is byte-identical; diff reports exactly those two rows.

The canary, pinned

plugin-audit and service-knowledge have now defeated three readers of vitest aliases, by two different parsing assumptions:

  • an objectstack/core grep census missed them because the anchored regex form writes the bytes @objectstack\/core — with an escaped slash, so the plain specifier never appears in the file;
  • this gate missed them because the subpath alias writes its replacement as a template literal.

Neither spelling is exotic and neither is going away — the escaped slash is forced by the regex literal, the template by the capture group. Both are now pinned together in one self-test fixture (packages/canary), shaped like the real configs, so the fourth reader inherits the two assumptions instead of rediscovering them. Three more fixtures keep the new branch honest rather than permissive: a template landing on dist/ is still unaliased; a template resolving through a file is still the ENOTDIR trap; a ${…} hole with no literal in it is unreadable, never "aliases nothing".

Why not evaluate the config

Measured, not assumed. This gate runs dependency-free on a bare checkout in ~3s — there is no node_modules in this repo as CI reaches it, and the self-test builds its fixture tree in tmpdir with none either — while every real config here opens with import { defineConfig } from 'vitest/config'. Evaluation would trade that for a gate that cannot run before pnpm install, and would make an alias list the gate merely reads today into one it executes. Rejected; the reasoning is recorded in the asPath header so it is not re-litigated.

The legal-but-still-unreadable spellings that remain — + concatenation, a non-literal path argument, a nested template — are enumerated there too, each noted as fail-closed. The + form is the same defect one spelling over and is filed separately rather than fixed here.

Verification

  • pnpm check:test-source-alias — self-test OK, gate OK (72 packages scanned, 63 registered).
  • check:nul-bytes — OK, 7365 files; plus a direct control-byte scan of the changed file.
  • scripts/pm/dispatch-gates.mjs re-derived against the actual changed path: no check family names it; check:nul-bytes applies by convention and was run.

Reverse verification, direction predicted first. Prediction: remove the reader's new template branch while the registry stays shrunk, and the failure flips to the added direction rather than STALE — the subpath aliases go invisible again, so @objectstack/spec re-enters the measurement while the registry no longer lists it. Observed, matching: two failures, both NEW unaliased artifact import(s) since this entry was measured: @objectstack/spec. The self-test went red on exactly the three predicted assertions (canary reported, the unreadable hole not failing as unreadable, the canary's both-directions audit), while the dist/ and ENOTDIR template fixtures stayed green — those two the old reader happens to answer correctly. Fix restored from the commit; tree clean.

Scope


Generated by Claude Code

…he two entries it mis-measured (#8020)
`asPath` took the last string literal in a replacement expression. For
`path.resolve(__dirname, '../x/src/index.ts')` that is the whole answer.
For the template form — the one the subpath rule needs, because the `$1`
back-reference has to sit INSIDE the path —
replacement: `${path.resolve(__dirname, '../../spec/src')}/$1/index.ts`
the only delimiter reached is the backtick, so the entire template body
came back as the path. That text has no `src` SEGMENT in it (`spec/src'`
is followed by a quote, not a separator), so a config aliasing every
namespace correctly read as aliasing nothing.
Fail-closed, so never a false green — but `plugin-audit` and
`service-knowledge` sat in the shrink-only registry as still resolving
`@objectstack/spec` through `dist/` on the strength of how their
replacement was SPELLED, and a dev dispatched to remediate either would
have found the alias already correct, the gate still red, and the failure
text prescribing the alias they already had.
`asPath` now splits a template into its literal chunks and `${…}` holes,
resolves each hole by the same last-literal rule, and concatenates — so
`$1` survives into `String.replace` and joins the `src` chunk. Both
packages' entries then shrink to exactly `['@objectstack/objectql']`;
measured delta is those two dependencies and nothing else (312 -> 310
package-dependency pairs, 63 entries unchanged). Neither
`vitest.config.ts` is touched: the configs are correct, the reader was
what could not see them.
Evaluating the config instead was measured and rejected: this gate runs
dependency-free on a bare checkout in ~3s, and its own fixture tree lives
in `tmpdir` with no `node_modules`, while every real config here opens
with `import { defineConfig } from 'vitest/config'`.
Self-test: `plugin-audit` and `service-knowledge` have now defeated three
readers of vitest aliases by two different parsing assumptions — an
escaped-slash regex `find` (`@fx\/core`, which hides the plain specifier
from a grep census) and a template-literal `replacement`. Both spellings
are pinned together in one canary fixture, plus a template landing on
`dist/` (still unaliased), a template resolving through a file (still
ENOTDIR), and a `${…}` hole with no literal in it (unreadable, never
"aliases nothing").
The remaining unreadable-but-legal spellings — `+` concatenation, a
non-literal path argument, a nested template — are enumerated in the
`asPath` header so the fourth reader looks them up instead of
rediscovering them.
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 3:39pm

Request Review

@hotlonghotlong added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 12, 2026 — with Claude
@hotlong
hotlong marked this pull request as ready for review August 12, 2026 16:23
@hotlong
hotlong added this pull request to the merge queueAug 12, 2026
Merged via the queue into main with commit 5cfbd2aAug 12, 2026
24 checks passed
@hotlong
hotlong deleted the claude/issue-8020-template-literal-alias-reader branch August 12, 2026 16:35
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

@hotlong@claude