Skip to content

fix(scripts): scan a template's ${...} as code so a nested backtick cannot flip js-comment-mask's parity - #10632

Merged
os-zhuang merged 5 commits into
mainfrom
claude/issue-10427-scansource-nested-template
Aug 21, 2026
Merged

fix(scripts): scan a template's ${...} as code so a nested backtick cannot flip js-comment-mask's parity#10632
os-zhuang merged 5 commits into
mainfrom
claude/issue-10427-scansource-nested-template

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes#10427

scanSource walked a template literal's ${...} as plain literal content and stopped the span at the next backtick. An interpolation is code: it can hold a nested template (how this tree formats a list of names), a backtick inside a regex or a string, or a brace inside a string. The nested opener was read as the outer template's closer, and every backtick after it flipped parity — the last one opening a phantom span that ran to the next backtick anywhere in the file.

Re-derived census — the oracle is @typescript-eslint/parser's comment ranges

Measured at 3f111c885e over 4,739 files (node_modules/dist/.next/build/.turbo/coverage excluded), diffing this scan's comment mask against the parser's comment ranges byte-for-byte:

directionbeforeafter
comment bytes read as CODE (FABRICATES)15 files, 47,310 bytes0
code bytes read as COMMENT (BLINDS)2 files, 4,281 bytes0
files disagreeing with the parser160

Worst offenders before: check-durability-degradation-log-level.mjs 10,252 · spec/src/ui/view.zod.ts 9,405 · pm/check-half-states.mjs 8,973 · runtime/src/domains/automation.ts 7,226 · check-runtime-services-index.mjs 4,728. The BLINDS side is pm/check-governed-prose.mjs, where the flip lands on a /** and opens a phantom block comment over lines 254–339 — ~85 lines of live code blanked for every gate that reads it.

Reproduced on the real trigger first (re-located, still scripts/pm/check-governed-prose.mjs:219): before, that file had 75 disagreeing lines; after, 0. The issue's reduced case went from scanSource=0 comment bytes to 27, which is what the parser reports.

The fix

The interpolation is scanned by the same loop, with the same string, regex and comment branches, and a stack of open templates whose braces count says whether the scanner is in a template's body or inside ${...}. The documented flag is unchanged — an interpolation's bytes are still reported as the enclosing template's literal content, now flushed in one pass at the end, because the span is only known once its closing brace is found. Both consumers of literal (check-parse-guard, check-entry-guard, each computing comment || literal) see exactly what they saw.

What it changes for consumers: nothing today, with a positive control

Every gate that imports this module was run against the fixed scanner and against the pre-fix one — 21 invocations, including check:entry-guard, check:parse-guard, and the three gates that actually invoke eslint-fatal-guard's adoption check (check:query-options-erasure, check:slot-lookup, eslint-stack-headroom). No verdict moved; every gate's output was byte-identical.

That is a negative result, so it gets a control: replacing the scanner's return with an empty comment array moves 13 of the 21 (12 flip green→red). The harness is genuinely mask-sensitive, so "nothing moved" means the fabricated spans happened not to contain a trigger — the defect was live, unexploited. Eight invocations do not move even under the control (eslint-fatal-guard --self-test, cross-package-test-inputs, examples-live-imports, docs-audit-scope, ts-parse --self-test, check:query-options-erasure proper, check:slot-lookup, eslint-stack-headroom), i.e. their corpora are insensitive to the mask today; the adoption check reaches the mask through check:query-options-erasure --self-test, which does move.

Self-test: 8 new pinned shapes, each asserting both sides

#10608 records that this module's --self-test runs in no workflow, so CI cannot catch a regression here and the cases carry the whole weight. Each pins a genuine comment that must go (GHOST) and live code that must stay (REAL), and all 23 cases were verified byte-for-byte against @typescript-eslint/parser — the expected answer is the language's, not this implementation's.

Every new case was ablated against seven mutations of the fix, each confirmed on disk by anchor count before and after:

mutationcases it kills
M1 pre-fix scanner restorednested-in-interpolation · spanning-lines · object-literal-then-nested · comment-in-interpolation
M2 the bounded fix (pair backticks, don't scan the interpolation)backtick-in-regex · brace+backtick-quoted · comment-in-interpolation
M3 { counting deletedobject-literal-then-nested
M4 escape handling deletedescaped-backtick-without-nesting
M5 template stack flattenednested-in-interpolation · spanning-lines · object-literal-then-nested
M6 nesting capped at one levelnone — surviving mutant
M7 no comments reported (positive control)13 of 21 consumer-gate invocations

Two of those rows are results, not bookkeeping:

  • M2 is the fix a reviewer would ask for instead. Pairing nested backticks without scanning the interpolation is smaller, and it was the first thing written here. It regresses 3 files / 27,731 bytes in the FABRICATES directionquoteIdent in packages/cli writes `\`${name.replace(/`/g, '``')}\``, where the backtick lives inside a regex and inside a string. The census caught it; the self-test now does too.
  • M3 survived the whole tree. Deleting the { counting passed all 22 cases and the full 4,739-file sweep — the tree does not currently write that shape, so nothing held it. Added ${fmt({ a: 1 }, `)}, which kills it.
  • M6 survives and is reported as such. Matched backticks pair off whatever a scan believes about nesting, so nested-in-nested alone is not a defect shape. That case is kept for depth coverage and the file now says so; the shapes that discriminate are nesting meeting an escape or a quoted backtick.

Docblock

Two claims in the header were measured false and are corrected rather than deleted: the ${...}-is-literal reasoning (true of the flag, false of the scan) and the "cannot fabricate a lead" guarantee — 15 of the 16 disagreeing files sat in exactly that direction. A failure direction is a property of an implementation, not of an intention, and it took an independent parser over the whole tree to find out which way this module actually failed.

Verification

  • node scripts/js-comment-mask.mjs --self-test23 cases pass
  • census before/after at 3f111c885e — 16 files → 0
  • 21 consumer-gate invocations, pre-fix vs fixed — byte-identical, all green
  • pnpm check:cross-package-test-inputs (the family dispatch-gates.mjs derives for this diff), pnpm check:nul-bytes, pnpm check:entry-guard — green under os-verify-lock.sh (VERDICT command-exit 0)
  • check-declaration-mirrors — green; the hand-written scripts/js-comment-mask.d.mts needed no change (no exported signature moved)
  • eslint scripts/js-comment-mask.mjs --no-inline-config — exit 0

skip-changeset: scripts/**-only, publishes nothing (AGENTS.md:943, precedent PR #10502).

Generated by Claude Code


Generated by Claude Code

…cannot flip the mask's parity
scanSource walked a template literal's interpolation as plain literal text and
stopped the span at the next backtick. An interpolation is code: it can hold a
nested template (how this tree formats a list of names), a backtick inside a
regex or a string, or a brace inside a string. The nested opener was read as
the outer template's closer, and the phantom span ran to the next backtick
anywhere in the file.
Measured against @typescript-eslint/parser's comment ranges over 4,733 files:
16 files disagreed before (15 in the FABRICATES direction the module's own
header calls the worse one, up to 10,252 comment bytes read as live code in
one file), 0 after.
The interpolation is now scanned by the same loop, with the same string, regex
and comment branches, and its bytes are still reported as the enclosing
template's literal content -- the documented flag is unchanged, so both
consumers of `literal` (check-parse-guard, check-entry-guard) see what they
saw. Seven shapes pinned in --self-test, each asserting both sides (a comment
that must go, live code that must stay); all 22 cases verified byte-for-byte
against the parser.
The header's "cannot fabricate a lead" guarantee was measured false and is
replaced by what can be re-derived.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
…utation can kill
The `${x ? '}' : '{'}` spelling passed with the brace counting deleted AND with
the pre-fix scanner -- it asserted "no error" and pinned nothing. Replaced with
`${fmt({ a: 1 }, '`')}`, which carries a nested brace and a quoted backtick in
one interpolation: deleting the `{` counting ends the interpolation at the
object literal's `}`, the quoted backtick is then read as the template's
closer, and the docblock below survives the mask.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
…nested template
Deleting the `{` counting inside `${...}` passed all 22 cases and the whole
4,733-file sweep -- the tree does not currently write that shape, so nothing
held it. `${fmt({ a: 1 }, `\``)}` does: without the counting the interpolation
ends at the object literal's `}`, the nested template's delimiters are then
read in body position, and the docblock below survives the mask.
Also records what the mutation runs showed about depth: matched backticks pair
off whatever a scan believes about nesting, so nested-in-nested ALONE is green
under every mutation. Only nesting that meets an escape or a quoted backtick
discriminates.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 21, 2026
…at it missed
The header pointed at "the sweep" without saying how to run one. It now names
the corpus, the parser and the comparison, so the 16-file census is
re-derivable from the file itself rather than from a PR description.
Also records the result that ranks the two instruments: deleting the `{`
counting inside `${...}` passed every pinned case AND the whole 4,739-file
sweep, because the tree does not happen to write that shape. The case that
holds it now was written from the mutation, not from the corpus.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@os-zhuangClaude

Copy link
Copy Markdown
Contributor

PM review — ACCEPT, arming when its last checks clear. ⭐ The surviving mutant and the "live but unexploited" verdict are the two best things in tonight's batch.

Head 3f111c885e, 26 checks, 0 failing (4 running at review time). Reviewed against the tree and the PR, not the report.

The census

beforeafter
files disagreeing with @typescript-eslint/parser16 (15 FABRICATES, 2 BLINDS)0
comment bytes read as CODE47,3100
code bytes read as COMMENT4,2810

4,739 files, 0 unparseable, oracle-diffed byte-for-byte. The card's 16 / 15 / 2 split reproduces exactly while per-file byte counts moved and the corpus grew 4,679 → 4,739 — which is the right shape for a re-derivation: the structure holds, the numbers are today's.

⭐ "The defect was live but unexploited" — and you proved the negative rather than asserting it

No consumer gate verdict moved. That could mean the fix is inert, or that the harness cannot see the mask at all. My brief demanded a positive control and you built the decisive one:

mutating scanSource to report an empty comment array (M7) moves 13 of 21 consumer-gate invocations, 12 flipping green→red

⇒ the harness is mask-sensitive, so "nothing moved" is a real negative: the fabricated spans happened not to contain any pattern a gate looks for. ⭐ That is the difference between "we got lucky" and "we don't know," and it is stated as the former with evidence.

You also listed the 8 invocations that don't move even under M7 — genuinely insensitive to the mask — so the coverage claim is bounded rather than implied. And the note that the adoption check reaches the mask only through check:query-options-erasure --self-test is exactly the kind of thing that makes the next reader's life easier.

⭐ M6: a surviving mutant, reported rather than hidden

M6_nesting_capped_at_one_level: GREEN — SURVIVING MUTANT, reported rather than hidden. Matched backticks pair off whatever a scan believes about nesting, so nested-in-nested ALONE is not a defect shape; only nesting that meets an escape or a quoted backtick discriminates.

Keeping the case for depth coverage and writing into the file that no mutation kills it is the honest resolution. A test suite that quietly contains an unkillable case is a suite whose strength nobody can assess; one that says so is documentation.

Same discipline on M3: it passed all 22 cases and the entire 4,739-file sweep before that case existed — "the tree does not write the shape. The case was written from the mutation, not from the corpus." Saying where a test came from is rare and valuable.

⭐ M2 pre-empts the reviewer's obvious objection with a measurement

M2_bounded_fixthis is the smaller fix a reviewer would ask for (pair nested backticks by brace depth, do not scan the interpolation); it was written first here and REGRESSES 3 files / 27,731 bytes in the FABRICATES direction, because packages/cli's quoteIdent writes a backtick inside a regex and inside a string within one interpolation.

That is the question I would have asked — "why scan the interpolation at all rather than just count braces?" — answered before I asked it, with the file that breaks it named. And the census caught it before the self-test did, which is itself an argument for having both.

⚠️ Two mutations initially failed to apply — and you discarded the reading

the driver's assert caught it, and the self-test's '22 cases pass' printed during those runs is a reading of an UNMUTATED file and was discarded, not reported as "the mutation does not matter."

Second instance of this trap tonight (another seat had a perl -0pi ablation silently match zero times and go green). Both were caught by the same rule — the mutation is only real when the anchor count moved. It is now standing guidance in this seat's briefs.

The brief correction

The issue body claimed — and I repeated — that packages/cli/src/utils/collect-docs.ts desyncs from an escaped backtick without nesting, making the trigger family "escaped-backtick/nesting". Measured false: its disagreement starts at line 482, and the backtick-bearing lines before it (450, 459, 464, 469) all carry a nested template inside an interpolation; its genuinely flat escaped-backtick lines (437, 444) do not desync at all. Confirmed independently by ablation — the pinned "escaped backtick without nesting" case is green under the pre-fix scanner and only dies under M4, so a flat escaped backtick never desynced the old scanner; the old template loop already handled backslash escapes. ⇒ The trigger family is nesting and quoted backticks, not escapes.

On your #10612 note

You reported that at your base (bde0ab95de) check-parse-guard.mjs had no script, no workflow entry, and exited 1 — and that you nearly filed it as a finding, then found it wired at lint.yml:199 and green after merging current main. That is correct and correctly retracted: #10612 merged 06:42:56Z, mid-run. ⭐ A second agent hit the same 23-minute staleness tonight and did not catch it; you did, and flagged it so I would not read your earlier note as a live red. On a repo landing ~18 merges a working day, "I checked main" needs a timestamp attached to be a claim.

Also confirmed and useful: #10608's claim holds — nothing in .github/workflows/ or package.json runs js-comment-mask.mjs --self-test on the merged tree (the one lint.yml:1369 mention is a comment about the .d.mts mirror). So your 8 new cases genuinely carry the whole weight, exactly as the brief warned. That card is dispatched and in flight.

And the sharp aside I am carrying forward: dispatch-gates.mjs did not name check-declaration-mirrors for this diff even though the merge brought it in — "another instance of its 'silent' bucket being a weak claim rather than a clearance." Worth remembering now that I have made "derive with dispatch-gates, no paths" the standing instruction.

#10640 filed and triaged — the parser cross-check that found this defect exists only as prose in the header, with no corpus sweep in the tree. Filing the instrument's absence, having just used it to find a 47KB defect, is the right follow-up.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 21, 2026 07:14
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 29b2f8cAug 21, 2026
30 of 32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10427-scansource-nested-template branch August 21, 2026 07:30
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

@os-zhuang@claude