Skip to content

fix(devx): reject an interpolating template literal in check-cross-package-test-inputs' NEW_URL_LITERAL - #12113

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-12085-new-url-literal-interpolation
Aug 25, 2026
Merged

fix(devx): reject an interpolating template literal in check-cross-package-test-inputs' NEW_URL_LITERAL#12113
yinlianghui merged 1 commit into
mainfrom
claude/issue-12085-new-url-literal-interpolation

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#12085

What

NEW_URL_LITERAL's character class in scripts/check-cross-package-test-inputs.mjs is
byte-identical to PATH_LITERAL's (fixed for PATH_LITERAL in #12087) and shares the
same blind spot: a backtick-delimited argument holding no quotes matches it even when it
is an interpolating template, so new URL(`${someVar}`, import.meta.url) reads
${someVar} as the literal segment text and walkLiteral() counts it as one ordinary
descent — biasing the depth walk upward and, when the climb lands outside the package,
adding a fabricated NAME to the roster.

Why this is not a copy of #12087

At PATH_LITERAL's call site an unreadable argument falls into an explicit "cannot read,
keep depth" branch (inside pathExpression()'s resolve/join argument loop).
NEW_URL_LITERAL has no such branch — it is matched directly inside pathExpression(),
with nothing wrapping it. So the fix (readableNewUrlLiteral(), mirroring
readablePathLiteral()'s shape but scoped to this call site, not sharing it) makes an
interpolating match return null, which falls through to pathExpression()'s existing
"no spelling matched" path and returns undefined for the whole new URL(...) seed
— the same outcome as any other unrecognised seed shape ("does NOT flag a read argument
that is an unrecognised expression"), not a depth-kept one. The added self-test cases pin
that outcome explicitly (does NOT flag / no name), rather than reusing #12087's
depth-kept assertion, which would have proven the wrong thing.

A single- or double-quoted literal is unaffected — ${ inside one of those is ordinary
text, never interpolation — pinned by its own control case.

Zone 2 measurements

Bare-root / changeset

  • Bare-root worklist: not applicable — this narrows a regex/wrapper inside an existing
    gate, no new scan-root literal introduced (confirmed by diffing for any new
    const X = '<bare-word>'-shaped population constant; none). bare-root-worklist.mjs --self-test: none stale, none missing (unaffected by this change).
  • Changeset: skip-changeset — one gate script under scripts/, nothing published.
    Label applied via the additive labels endpoint per repo convention; read back below.

Tests

Gates derived via node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
(re-run against the final commit) — all matched families green, run through
scripts/pm/os-verify-lock.sh, verdict lines quoted:

  • pnpm check:cross-package-test-inputs (this gate's own --self-testand production
    leg, per the dispatch instruction to run both): All 117 self-test cases passed. /
    OK: 16 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.os-verify-lock: VERDICT command-exit 0
  • pnpm check:agent-test-spelling: check-agent-test-spelling: 0 violations
    command-exit 0
  • pnpm check:entry-guard: check:entry-guard: 160 scripts/ file(s) — every entry guard goes through invoked-as.mjscommand-exit 0
  • pnpm check:parse-guard: check:parse-guard: 159 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs.command-exit 0
  • pnpm check:pnpm-filter-targets: 135/170 --filter occurrence(s) ... resolve against 78 workspace package(s)command-exit 0
  • node scripts/check-ci-filter-parity.mjs: OK: all 96 declared cross-package glob(s) (81 unique) are covered by core or crosspkgcommand-exit 0

All six re-run as one union after the final commit, on head 269c461e3:
os-verify-lock: VERDICT command-exit 0.

Ablation (proves the two new self-test cases actually discriminate): reverted only the
call-site wrapper (readableNewUrlLiteral(expr)expr.match(NEW_URL_LITERAL)), keeping
the new test cases — confirmed the edit landed on disk via git diff, then re-ran
--self-test: exactly the two new cases (an interpolating new URL() template does NOT flag... and and — like any unrecognised seed — yields no name at all...) went red,
2/117 self-test case(s) failed, all others stayed green. Restored the wrapper via the
same Edit, re-ran: All 117 self-test cases passed.

node scripts/check-nul-bytes.mjs: OK (scanned 6719 text file(s) ... no raw ASCII control bytes).


Generated by Claude Code

…ckage-test-inputs' NEW_URL_LITERAL
`NEW_URL_LITERAL`'s character class is byte-identical to `PATH_LITERAL`'s
(#11487/#12087) and shares the same blind spot: a backtick-delimited argument
holding no quotes matches it even when it is an interpolating template, so
`` new URL(`${someVar}`, import.meta.url) `` reads `${someVar}` as the literal
segment text and `walkLiteral()` counts it as one ordinary descent — biasing
the depth walk upward and, when the climb lands outside the package, adding a
fabricated NAME to the roster.
Unlike `PATH_LITERAL`'s call site, this one has no "cannot read, keep depth"
fallback to route into: `NEW_URL_LITERAL` has exactly one call site, directly
inside `pathExpression()`, with no enclosing loop. So the fix (a
`readableNewUrlLiteral()` wrapper, mirroring `readablePathLiteral()`'s shape
but scoped to this call site rather than sharing it) makes an interpolating
match return `null`, which flows straight into `pathExpression()`'s existing
"no call matched" path and returns `undefined` for the WHOLE `new URL(...)`
seed -- the same outcome as any other unrecognised seed shape, not a
depth-kept one. The self-test pins that outcome explicitly (does not flag, no
name), plus a control proving a non-interpolating backtick `new URL()` literal
is unaffected, and a control proving `${` inside a quoted (non-backtick)
literal is ordinary text, never interpolation.
Measured (Zone 2.3 of #12085): before this fix, an escaping interpolating
`new URL()` seed CAN push a fabricated NAME onto the roster (confirmed via a
temporary export of `scanPathExpressions()` and a fixture that climbs out of
its package), but `findEscapingPackages()`'s downstream
`statSync(...).isFile()` filter throws ENOENT on the fabricated literal and
drops it -- the same safety net #11487's Zone 2.3 found for `PATH_LITERAL`.
Today's blast radius was therefore smaller than the card's open question
implied; this fix closes the gap at the source regardless.
Both directions ablated: reverting the call-site wrapper alone (tests intact)
turns exactly the two new discriminating self-test cases red and leaves the
other 115 green, then the wrapper was restored and reverified at 117/117.
Fixes#12085
@yinlianghuiyinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026 — with Claude
@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 09:54
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit 9799ffeAug 25, 2026
32 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-12085-new-url-literal-interpolation branch August 25, 2026 10:15
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check-cross-package-test-inputs: NEW_URL_LITERAL has the same interpolating-template blind spot #11487 fixed for PATH_LITERAL

2 participants

@yinlianghui@claude