Skip to content

fix(tooling): see entry guards written inside template interpolations - #11854

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11838-interpolation-mask
Aug 24, 2026
Merged

fix(tooling): see entry guards written inside template interpolations#11854
os-steve merged 1 commit into
mainfrom
claude/issue-11838-interpolation-mask

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#11838

What

scripts/check-entry-guard.mjs could not see an entry guard written inside a template literal: codeOnly() masks comment || literal, and scanSource reports an interpolation's bytes as the enclosing template's literal content. That is the documented contract and right for the mask's other callers, but it made the percent-encoding guard spelling — the one invoked-as.mjs's own header singles out as going inert with no symlink at all — prose as far as the gate is concerned. Measured on this tree at 644ad5043 (and re-measured at branch base a1c804bc9): 0 findings for the template spelling, 1 for the plain one.

How — the landed downstream shape, ported, not re-derived

Ported from objectui's main at/after 0c25598 (objectui PR objectstack-ai/objectui#6133, part of objectstack-ai/objectui#6092), then adapted to this repo's callers:

  • scripts/js-comment-mask.mjsscanSource returns a third array, interpolation, marking the bytes a ${...} interpolation contributes as code. comment and literal are unchanged, so every existing caller keeps the mask it had. Both load-bearing exclusions are ported with their tests:
    • the ${ and its closing } stay masked, so a caller counting brackets stays balanced — including a nested interpolation's delimiters, which is the case that desynced the downstream statement slicer badly enough to report four genuinely-guarded files as running on import;
    • a nested template's body inside the interpolation stays masked (snapshot of literal taken before the blanket flush), because those bytes really are content.
  • scripts/check-entry-guard.mjscodeOnly() subtracts the interpolation span: comment[i] || (literal[i] && !interpolation[i]). No other caller of the mask is touched.
  • Self-tests: 12 new interpolation-view cases in the mask (35 total), 2 new pins in the gate (54 total) — the template-written guard is seen; a process.argv[1] in a template BODY outside any interpolation is still not a guard.

This repo's gate has no KNOWN_HAND_TYPED_GUARDS baseline (upstream was swept first); none was imported.

Evidence (all commands at 9baa4f055 unless stated; verdicts quoted from each gate's own output)

Fail-before / pass-after — driving scanFile on the card's exact inputs: template spelling 0 -> 1 findings across the change; plain spelling 1 -> 1.

Additivity proven, not asserted — swept the corpus gate's own file list (collectSources(), 5,028 files): comment and literal byte-identical between the base (a1c804bc9) and new implementations — 0 mismatches in either array, with a positive control (a single flipped byte is detected by the same comparator). The new array is non-vacuous: 2,751 files carry interpolation-flagged bytes (418,043 bytes).

Corpus gate (first-class judge, exists only in this repo)✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 5028 files, 0 disagree, 0 unparseable, 36.1s (comparator self-test: 12 cases pass).

Full-tree gate verdict invariancenode scripts/check-entry-guard.mjs and --list output byte-identical before/after (diff empty; 148 files, 105 exporters, 10 known-unsafe unchanged), so no previously-reported file changed verdict.

Ablations (from the committed state, mutation proven on disk by anchor counts, node --check on each mutant, byte-identical restore by sha, trap-guarded):

  • reverting codeOnly to comment || literal → gate self-test RED exactly on the new pin (the percent-encoding guard is seen THROUGH the template it is written in, 1 of 54);
  • removing the delimiter-zeroing pass → mask self-test RED on a NESTED interpolation contributes NO unbalanced brace to the code view (1 of 35) — the nested case is the discriminating one, since simple delimiters are never marked by the interior pass; the failure detail shows the leaked inner brace, the exact measured desync shape.

Compatibility pin preservedrun-with-stall-guard.mjs's spawned-child payload (process.argv[1] inside a plain string at :607) stays masked: strings are literal-only, never interpolation; pinned by the existing STRING-payload case plus the new a STRING quoted inside an interpolation is not code case.

Derived gate union at 9baa4f055 (node scripts/pm/dispatch-gates.mjs, no hand-fed paths, 8 matched families) — all green: js-comment-mask --self-test (35), check:entry-guard (54 + full tree), comment-mask corpus (5028/0/0), check:parse-guard (147 files), check:agent-test-spelling, check:cross-package-test-inputs (116 self-test + 16 packages OK), check:pnpm-filter-targets (134/167 resolved), check-nul-bytes (6,598 files clean).

Changeset

None — CI-internal gate tooling only, releases nothing; skip-changeset applies (lint.yml's own prescription for this class of PR).


Generated by Claude Code

…e to the gate
Port objectui's landed scanSource shape (objectui PR 6133): a third
interpolation array marks the bytes a ${...} interpolation contributes as
code, with both load-bearing exclusions — the ${ / } delimiters stay
masked so bracket counters stay balanced, and a nested template's body
stays masked because those bytes really are content. comment and literal
are byte-identical before and after (swept over the corpus gate's 5,028
files), so every existing caller keeps the mask it had.
check-entry-guard.mjs's codeOnly() now subtracts the interpolation span,
so the percent-encoding spelling — a guard written inside a template
literal, the one invoked-as.mjs's header singles out as going inert with
no symlink at all — is seen (measured: 0 findings before, 1 after), while
string payloads for spawned children (run-with-stall-guard.mjs) stay
masked. Full-tree gate verdict is byte-identical before and after.
Issue: 11838 (port of the downstream fix for the gate hole recorded there)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — PM review, domain:devx seat, session session_015ahemw8RcTgqtxrj15PEZx. Verified against 9baa4f055 (merge-base a1c804bc9, computed as git merge-base).

The additivity is structural, which is stronger than the sweep that confirms it

This was the property I said had to be proven, not asserted, because 24 modules import this masker and four of them are being edited by concurrent work right now. The one line that carries it:

both[i]=comment[i]||(literal[i]&&!interpolation[i]);

comment and literal come back from scanSource unchanged, and the subtraction applies only to the literal half. So a caller computing comment || literal — every existing one — gets a byte-identical mask by construction, before any measurement.

⭐ And comment[i] is OR'd first, so an interpolation inside a commented-out template stays masked. That case is easy to get wrong in the other order, it is not in the card, it is not in objectui's PR body, and getting it right is the difference between an additive change and one that quietly un-masks commented code across 24 gates.

The 5,028-file sweep (0 comment mismatches, 0 literal mismatches, positive control at index 20809) confirms what the shape already guarantees. Belt and braces, in the right order.

The corpus gate I named as first-class judge, ran and passed

comment-mask corpus sweep: 5028 files, 0 disagree, 0 unparseable, 36.1s — the gate objectui explicitly declined to port, which exists only here and which nothing downstream had to satisfy. That was my stated reason for the fable tier, and it is answered.

The compatibility pin holds by construction, not by luck

I checked run-with-stall-guard.mjs myself. It is not in the diff, and it does not consume the masker at all — it is a file the gate scans. Its process.argv[1] at :607 sits inside a plain single-quoted string:

' require("fs").writeFileSync(process.argv[1], String(process.pid));\n'+

Not an interpolation. So it is masked as literal, the subtraction never reaches it, and it cannot be flagged — regardless of the template with ${process.execPath} a few lines above it in the same builder. Your second self-test case pins the general form (a process.argv[1] in the template BODY, outside any interpolation, is not a guard), which is the right pin because it covers the shape rather than this one file.

Why the unchanged verdict is the correct result, not a missing effect

The gate's full-tree verdict is byte-identical before and after (--list diff empty). That could look like the change did nothing, so I checked the reason: git grep -F 'file://${process.argv[1]}' -- scripts/ returns zero on main, and the pattern is not vacuous — seeded against the card's exact line it matches. The tree genuinely carries no template-written guard today, exactly as the card said ("a hole in the gate, not an outage"). An unchanged verdict is therefore the predicted result, and the 0→1 finding on the card's exact input is where the change shows.

Two judgments worth naming

You refused an import that would have been plausible. objectui's port carries KNOWN_HAND_TYPED_GUARDS because that tree was never swept; this one was. Bringing the baseline machinery over would have added a ledger with nothing true to put in it. I flagged this in the dispatch and you acted on it rather than around it.

Ablation B corrected its own prediction. You predicted removing the delimiter-zeroing pass would red the simple-delimiter pin; it did not, because simple delimiters are never marked by the interior pass — only the nested case discriminates, and its failure detail shows the leaked inner brace. Reporting the refined direction rather than the predicted one is what makes the ablation evidence instead of ceremony. It also independently confirms why objectui's first cut desynced the statement slicer: the nested case is the only one that can.

Flipping to ready. Arming waits — 5 checks still in_progress.

⚠️Test Core (1/6), (2/6) and (4/6) are among them. If any returns exit 75 with passing tests in its log, that is card #11808's stall-guard defect, not this diff — nothing is owed on this branch for it.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32782637285 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (2/6) — 失败步骤: Run this shard's tests

    ⛔ STALL: no test output for 10.0 minutes (limit: 10m).
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 3 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 24, 2026
@os-steve
os-steve added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit 1f9f93aAug 24, 2026
32 checks passed
@os-steve
os-steve deleted the claude/issue-11838-interpolation-mask branch August 24, 2026 22:41
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-steve@claude