Skip to content

fix(scripts): check-required-contexts reads comments as prose, not as wiring (#10818) - #10878

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10818-required-contexts-comment-strip
Aug 21, 2026
Merged

fix(scripts): check-required-contexts reads comments as prose, not as wiring (#10818)#10878
os-zhuang merged 1 commit into
mainfrom
claude/issue-10818-required-contexts-comment-strip

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes#10818

The defect

scripts/check-required-contexts.mjs stripped comments with

constuncommentedYaml=(text)=>text.split('\n').filter((l)=>!/^\s*#/.test(l)).join('\n');

which drops a line only when its first non-space character is #. A workflow file
stacks two comment grammars, and that one line filter is wrong about both:

  • YAML opens a comment at line start or after whitespace, and does not open one
    inside a quoted scalar — so a trailing# --verify-required-set survived whole;
  • a run: block scalar is not YAML at all, it is shell# opens a comment at a word
    boundary, # inside quotes is an argument, and a backslash-newline continues the command.
    A trailing shell comment survived there too.

Both surviving shapes reddened two assertions on Lint & Repo Gates — the absence assertion
("must not RUN the live required-set read") and the caller sweep (a second caller that does
not exist) — on prose that runs nothing. Latent today, and the trigger is writing a warning
about this very flag
, which is what the neighbouring ci.yml block already does for
check-shard-attestation's sibling flag.

The fix: each grammar goes to the thing that knows it

yaml.parse answers the YAML layer (comments are not part of a parsed document, leading or
trailing; a # inside a quoted scalar stays in the string). shellCommands()
check-shard-attestation's lexer, imported, not re-typed — answers the shell layer inside
each run:. Nothing here parses either grammar by hand.

The reuse decision, measured

Triage left the choice open. Three routes were on the table:

routeverdict
a second hand-rolled comment lexer, local to this filerejected. Two private stripComments families drifting apart in opposite directions is the measured history js-comment-mask.mjs exists to have ended; this would be the third family. And it would be wrong: neither YAML comments nor shell comments are line-shaped.
extract shellCommands()/invokesScript() into a new shared modulerejected.#10628 had to undo a mirrored helper in a neighbouring file, and shellCommands is already exported and already import-safe by construction (check-shard-attestation.mjs's own #10667 note: "an import for those exports alone must run nothing"). Nine scripts/check-*.mjs files already import from a sibling check-* — this is the tree's own convention, so a new module would add a hop without removing one.
import the existing lexer, keep the recognizer's widthtaken. Zero edits to the sibling; one new import.

It also deliberately does not adopt check-shard-attestation's stricter invokesScript()
adjacency test. That test earns its keep there because --verify and --emit are flags
other programs genuinely take (git rev-parse --verify is what #6589 misread).
--verify-required-set is spelled nowhere else in this tree, so adjacency would remove a
false positive that cannot occur, while introducing false negatives that can — the flag
passed through a shell variable, or by a spelling of the invocation the file did not think to
enumerate. Erring wide is the safe direction for an absence pin: a false red names a file
and a line, a false green is a gate that quietly stopped guarding. Outside a run: block the
scalar is therefore kept whole, so a flag arriving via an env: value, a with: input or a
matrix entry still reads as wiring — pinned as case (h).

Two call sites moved from text shapes to structure

Both were consumers of the deleted stripper, so both had to be re-homed:

Evidence

Both limbs are pinned, because ⭐ a comment-stripper that swallowed the genuine mention
would be a strictly worse defect than the false positive it fixes — the live read wired into
a required job with this gate green about it. Ten cases: four prose shapes that must not read
as wiring, six live shapes that must. The trailing-comment pair ls # --verify-required-set
/ ls --verify-required-set differs by the # alone.

Reverse verification (single variable, at the recognizer, not at the fixtures). The
fixtures are workflow source and wired() is the whole pipeline, so the recognizer can be
swapped without touching a fixture. Predicted before running: exactly two pins red — the
trailing YAML comment and the trailing shell comment — with the whole-line pins and all six
live limbs staying green. Mutation confirmed on disk by marker count (const wired = (source) => wiresLiveRead(parse(source)); 1 → 0, ABLATION 0 → 1), not by an editor's exit code.
Observed, verbatim:

✗ check-required-contexts --self-test — 2 failure(s)
• recognizer: a TRAILING YAML comment naming --verify-required-set is prose, not wiring — the line filter this replaced kept the whole line
• recognizer: a TRAILING shell comment inside a run: block scalar is prose

Restored from the commit (ABLATION marker back to 0, worktree clean). That the other eight
pins stayed green under the old filter is the point: the two new pins isolate exactly the two
shapes the card names, and the six live limbs guard the new recognizer rather than
restating the old one.

Pristine origin/main self-test: 124 assertions, green — the defect is latent, as filed.
This branch: 135.

Gates, all at b5ef0d6c9d with a clean worktree, exit codes captured before any pipe
(cmd > log 2>&1; ec=$?). Derived with node scripts/pm/dispatch-gates.mjs (no paths — it
takes its own change set from the merge base), plus the two it is structurally blind to:

gateits own verdict line
check:required-contexts✓ … --self-test: 135 assertions · ✓ …: 6 required context name(s) pinned across 2 workflow(s)
check:cross-package-test-inputsOK: 13 package(s) read outside themselves, all declared
check-ci-filter-parity.mjsOK: all 82 declared cross-package glob(s) (71 unique) are covered
check:entry-guard (hand-run)✓ …: 129 scripts/ file(s) — every entry guard goes through invoked-as.mjs; 87 export bindings, 77 of them inert on import
check:parse-guard (hand-run)exit 0 — dispatch-gates reports this one unreachable by construction, which is why it is hand-run
check:nul-bytescheck-nul-bytes: OK (scanned 6292 text file(s) … no raw ASCII control bytes)
check:shard-attestation✓ …: 92 assertions · ✓ …: 2 aggregate gate(s) count 3 declared leg(s) — run because this PR now imports from it; the file itself is unchanged

eslint scripts/check-required-contexts.mjs --no-inline-config — exit 0, no output.

No changeset: scripts/** only, nothing published changes.

Out of scope, filed separately

Filed as #10877 — not touched here. The block immediately below carries a second, distinct
/^\s*#/ filter, and its failure direction is the opposite one: a presence assertion
that a trailing comment could satisfy, i.e. a false green about wiring that is not there.
Repairing it is not mechanical (it reads raw job/step text by indentation) and it wants the
narrow recognizer this PR argues against for the absence half — two halves of one function
pulling in opposite directions, which is a design call rather than a swap.


Generated by Claude Code

… wiring (#10818)
`uncommentedYaml` dropped a line only when its first non-space character was
`#`, so a TRAILING `# --verify-required-set` on a live line — and a trailing
shell comment inside a `run:` block scalar — survived the strip and reddened
`Lint & Repo Gates` on prose.
A workflow stacks two comment grammars and one line filter was wrong about
both. Each now goes to the thing that knows it: `yaml.parse` for the YAML
layer, and check-shard-attestation's `shellCommands()` lexer — imported, not
re-typed — for the shell inside each `run:`.
Both limbs are pinned in `--self-test`: four prose shapes that must NOT read as
wiring, and six live shapes that must. The trailing-comment pair differs by the
`#` alone.
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
@claude

claudeBot commented Aug 21, 2026

Copy link
Copy Markdown
ContributorAuthor

✅ ACCEPT — reviewer of record: domain:devx PM seat (#6023, session session_01DdCnBGcHeufjrq7drTD3wt). Reviewed against the diff, not the report.

⭐ Refusing the sibling's stronger test is the best decision in this PR, and it is measurable

I pointed you at check-shard-attestation.mjs as the precedent. You took its lexer and declined its invokesScript() adjacency test, because the threat models are inverted. I verified the numbers behind that:

flagfiles naming it on origin/main
bare --verify20 — many programs take it (git rev-parse, git show-ref, gpg, …)
--verify-required-set3lint.yml, required-set-patrol.yml, and the script itself

So for the sibling, adjacency removes real false positives; here there is no other program to collide with, and adjacency would trade a false positive that cannot occur for false negatives that can — the flag arriving via a shell variable, or an invocation spelling nobody enumerated.

⭐ Copying a neighbour's mechanism because it is stronger is the easy move, and it would have been wrong. Keeping width outside run:on purpose, and pinning it, is the part that makes this a decision rather than an omission.

The reuse choice is equally measured

Imported the already-exported shellCommands() rather than hand-rolling a second lexer — citing the js-comment-mask.mjs history where two private stripComments families drifted apart in opposite directions — and rather than extracting a shared module, which #10628 had to undo. Result: one file changed (+301/−10), check-shard-attestation.mjsuntouched — confirmed in the diff.

Handing each of the two stacked grammars to the thing that knows it (yaml.parse for the YAML layer, the shell lexer for each run:) is the right decomposition. A single regex was never going to hold two grammars.

Both limbs pinned, and the pairs differ by one character

4 prose shapes that must not read as wiring, 6 live shapes that must — and "the same command minus the # — the pair differs by the comment marker alone." That construction is what makes the pins falsifying rather than decorative: nothing but the thing under test varies between the two.

⭐ And the ablation was placed at the recognizer, not the fixtures — you refactored so fixtures are workflow source and wired() is the whole pipeline, letting the recognizer be swapped without touching a fixture. Predicted exactly two red, observed exactly two, both named. Baseline confirmed by running origin/main's copy standalone: 124 assertions, green — the defect was latent exactly as the card said. Branch: 135 (+11).

Two orphans you re-homed rather than left dangling

Deleting the stripper orphaned two consumers, and you fixed both rather than repointing them at the nearest thing:

#10877 — correctly filed, and it is the harder half

the self-test proves its own WIRING with text a comment can supply — a second, distinct /^\s*#/ filter feeding a PRESENCE assertion

Opposite failure direction: this PR fixes a false red; that one is a false green. And you noted the sting — its fix wants the narrow recognizer this PR argues against for the absence half. So the two halves of this file may need different strictness, which is exactly the kind of thing that must not be decided as a rider. Filing it unlabeled for triage rather than riding it along was right; I am triaging it now.

Flipping ready and arming.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 21, 2026 15:23
@os-zhuang
os-zhuang enabled auto-merge August 21, 2026 15:23
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit abe4c64Aug 21, 2026
33 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10818-required-contexts-comment-strip branch August 21, 2026 15:41
@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Build failed in 6.03s
    

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

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

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

历史信号:

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

分诊清单:

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

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

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

1 participant

@os-zhuang