Skip to content

fix(devx): let a gate declare a scan surface, not just its baseline artifact - #10873

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-10784-gate-scan-surface
Aug 21, 2026
Merged

fix(devx): let a gate declare a scan surface, not just its baseline artifact#10873
os-zhuang merged 2 commits into
mainfrom
claude/issue-10784-gate-scan-surface

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#10784

node scripts/pm/dispatch-gates.mjs never named check:entry-guard for a newscripts/ file — the gate whose population is exactly that directory. Its only module-body literals were the ten KNOWN_IMPORT_UNSAFE entries, the allowlist of files that already violate the import-safety half, so the declared population was an enumeration of the members that already exist and a newly added scripts/*.mjs could never appear in it, by construction. At runtime the gate walks all 129 files there and judges new ones, so the derivation answered "silent" for exactly the input most likely to fire. The cost was already paid on #10703's CI round-trip, after the dev had reported.

The sibling check:parse-guard was invisible the opposite way: its only literal was the bare root scripts, which hintCovers refuses as too generic. Two scripts/** gates, two different routes, and anyone adding a script got neither named.

What changed

Triage option 1: a gate now declares a scan surface distinct from its baseline artifact, using the existing, copyable ROOT_DIR_WATCH_HINTS idiom (check-role-word.mjs, check-examples-live-imports.mjs). No extractor was widened — hintCovers' refusal of a bare root word stays exactly as measured (+139084 fabricated pairs priced in its docblock).

  • scripts/check-entry-guard.mjs — declares scripts/**; KNOWN_IMPORT_UNSAFE stays data. Self-test derives both directions from the walked root and pins that the surface covers a file the roster can never contain.
  • scripts/check-parse-guard.mjs — declares scripts/**. Its walkOutside(REPO_ROOT) census side is deliberately not declared (it cannot fail the gate; naming the repo root would put this gate in every card's brief), and that refusal is pinned rather than left in prose.
  • scripts/pm/dispatch-gates.mjs — the discharged ledger row deleted, plus triage option 3.

⭐ The ledger interlock, walked end to end

PR #10839's ESCAPABLE_LITERAL_LEDGER is shrink-only and bidirectionally enforced, and one of its two seeded rows was check:parse-guard scripts — on this surface. Declaring the subtree made it stale, exactly as that PR's leg-C ablation recorded. Observed here, before touching the ledger:

✗ and no ledger row is stale — STALE: check:parse-guard scripts. Good news, and the list must say so:
delete each one from ESCAPABLE_LITERAL_LEDGER.

The row is deleted — the sanctioned path the failure text names. No row was added, and the ledger was not weakened: the ablation below shows its FRESH-row half still fires if the declaration is removed. One row remains (check:published-files scripts), whose remedy is the other one the idiom allows and belongs on its own card.

Option 3 — telling a weak silence from an inverted one

silent is the derivation's weakest claim, and the residue block already named two ways to earn it that have nothing to do with the caller's paths. This adds the third, as output rather than prose: a family whose declared literals are all tracked FILES has named artifacts — a baseline, or an allowlist of current members — not a population. Counted in the summary on every run, named with the discriminator under --residue.

It deliberately stops short of claiming the gate reads your file. Both shapes are live and indistinguishable from the tree: check:where-matcher names one baseline JSON under scripts/ and walks packages/** test files, while check-entry-guard named ten files under scripts/ and walked all of it. What it says instead is exactly true of both — a list of the files that already exist can never contain one added tomorrow, so the verdict is not evidence in either direction — and it hands over the discriminator. A pinned self-test case holds it to that.

Proof — the before/after of whether the derivation NAMES these gates

Both measured against origin/main in a dedicated worktree, never the shared checkout. Card path: one new scripts/ file.

beforeafter
check:entry-guardabsent from matched, from Unreachable, from the tier lines; grep count 0 in the default output. --residue files it under Silentmatched via scripts/check-new-thing.mjs ⇢ gate source 'scripts/**'
check:parse-guardSilent (names: scripts) andUnreachable — dead: 'scripts' — the tree HAS it; the covering rule refuses the literal as too genericmatched, same covering reason
matched families35
unreachable by construction32

Before, the whole of what the derivation said about check:entry-guard:

- pnpm check:entry-guard [lint.yml] names: scripts/check-changeset-no-major.mjs,
scripts/check-empty-changeset.mjs, scripts/check-error-status-conformance.mjs, …

For this PR's own diff the tool now names itself correctly, including both gates for the two sibling files.

The gates run BY HAND, before and after

The derivation is structurally blind to these two, which is the card's whole subject, so neither run relies on it.

gatebeforeafter
check:entry-guard✓ check:entry-guard: 129 scripts/ file(s) … exit 0same, exit 0
check:parse-guard✓ check:parse-guard: 128 scripts/ file(s) — every TypeScript parse goes through ts-parse.mjs. exit 0same, exit 0
check-entry-guard --self-test47 cases52 cases
check:parse-guard --self-test41 cases46 cases
dispatch-gates --self-test474 cases501 cases

The self-test cases that fail without this change

Ablation, run from the committed state: both ROOT_DIR_WATCH_HINTS declarations replaced with [], confirmed on disk by marker count (injected 2, removed literal 0 — never by an editor's exit code), then restored with git checkout and confirmed byte-clean by an empty git status.

✗ no gate has NEWLY joined the escapable-literal species — FRESH: check:parse-guard scripts …
✗ check:entry-guard is MATCHED for a brand-new scripts/ file, not silent and not unreachable
✗ …and it is the subtree declaration doing it: strip it and check:entry-guard goes back to silent
✗ check:parse-guard is MATCHED for a brand-new scripts/ file, not silent and not unreachable
✗ …and it is the subtree declaration doing it: strip it and check:parse-guard goes back to silent
✗ dispatch-gates self-test: 5 of 501 case(s) failed.

The derivation under ablation reproduced the base state exactly: 3 matched, entry-guard grep count 0, parse-guard only in Unreachable.

The FRESH-row line is the ledger's other direction still armed after the deletion — remove the fix and the ledger demands it back rather than accepting a re-added row.

One case first passed vacuously under ablation (with no subtree hint to strip, silent === silent reads as a pass). It carries a length check now, and the second ablation run confirms it goes red — 3 failures became 5.

Gates

Derived with node scripts/pm/dispatch-gates.mjs (no paths — it reads its own merge-base change set), all run at 2f61b2e878, each quoting the gate's own verdict line:

gateverdict
pnpm check:entry-guard✓ check:entry-guard: 129 scripts/ file(s) … exit 0
pnpm check:parse-guard✓ check:parse-guard: 128 scripts/ file(s) … exit 0
pnpm check:pm-dispatch-gates✓ dispatch-gates self-test: 501 cases pass. exit 0
pnpm check:cross-package-test-inputsOK: 13 package(s) read outside themselves, all declared … exit 0
node scripts/check-ci-filter-parity.mjsOK: all 82 declared cross-package glob(s) (71 unique) … exit 0
node scripts/check-cross-package-test-inputs.mjsOK: 13 package(s) read outside themselves … exit 0
pnpm check:nul-bytescheck-nul-bytes: OK (scanned 6284 text file(s) … no raw ASCII control bytes) exit 0

Run under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0 · held the lock 20s · waited 76s), each exit code captured before any pipe.

No changeset: scripts/ repo tooling, no published package changes — declared with the skip-changeset label.

Serial

⛔ Not widened into #10840 (the ~33 gates whose population literal carries no separator at all). This PR adds no third idiom: it uses ROOT_DIR_WATCH_HINTS, already carried by two gates and pinned in each one's self-test, which is what #10705 asks for. #10840 is left slightly cheaper — two more worked instances of the idiom, and the residue now counts and names the artifact-roster species out loud, which is a different population from #10840's but overlaps the reader's triage path.


Generated by Claude Code

…rtifact
`dispatch-gates` never named `check:entry-guard` for a NEW `scripts/` file —
the gate whose population is exactly that directory. Its only module-body
literals were the ten `KNOWN_IMPORT_UNSAFE` entries, an enumeration of the
files that already violate the import-safety half, so the declared population
was a roster of current members and a newly added script could never be in it,
by construction. The sibling `check:parse-guard` was invisible the opposite
way: it declared the bare root `scripts`, which the covering rule refuses as
too generic.
Both gates now declare the scan surface with the existing `ROOT_DIR_WATCH_HINTS`
idiom, keeping their artifacts as data. Declaring it discharged the
`check:parse-guard scripts` row in `ESCAPABLE_LITERAL_LEDGER`, which failed as
STALE by name; the row is deleted, the sanctioned shrink-only path.
Also splits the `silent` verdict in the residue output: a family whose declared
literals are all tracked FILES has named artifacts, not a population, and where
that roster sits in a directory the card edits the verdict is not evidence in
either direction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
Measured on the ablation run: with both declarations removed there is no
subtree hint to strip, so `silent === silent` read as a pass while the two
cases above it went red. The length check makes the strip real.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@os-zhuangos-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 21, 2026 — with Claude
@claude

claudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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

The interlock landed exactly as PR #10839's leg-C predicted

Declaring the subtree reddened check:parse-guard scripts as STALE; you deleted the row. Verified in the diff — the ledger Set shows one removed line and zero added:

- 'check:parse-guard scripts',

⛔ shrink-only held, ledger not weakened, and a tombstone comment (// DISCHARGED and deleted (#10784)) explains why it discharged so the next reader does not re-add it. ROOT_DIR_WATCH_HINTS = ['scripts/**'] in both gates — the existing idiom lifted, not a third one invented (#10705's standing prohibition).

⭐ And the self-tests derive the declaration from the walked root (relative(REPO_ROOT, SCRIPTS)), with t('the declared form is NOT the walk root itself', …), so a renamed root cannot leave a stale declaration behind. That closes the failure mode this whole card is about — a declaration that stops describing what the gate actually walks — rather than just fixing today's instance of it.


Two places where you audited yourself. Both are the reason this is an ACCEPT rather than a "looks green"

1. You caught a fabricated lead in your own output

my first draft printed "the gate very likely reads your file", which is a FABRICATED lead for check:where-matcher (one baseline JSON under scripts/, walks packages/** test files) — retightened to the claim that is exactly true of both live shapes, with that restraint pinned as a self-test case

That is hintCovers' +139084-fabricated-pairs concern applied to your own new output — the exact currency this file's central refusal is denominated in. Widening a derivation is easy; noticing that your widening manufactured a false lead, and then pinning the restraint so nobody re-widens it, is the part that usually does not happen.

2. ⭐ Your ablation exposed a vacuous case in your own test suite

one case passed VACUOUSLY (with no subtree hint to strip, silent === silent reads as a pass) — 3 red, not 5. Added a hints-length check and re-ablated to confirm it now reds.

This is the single best thing in today's round. The ablation did not merely confirm the change — it audited the instrument, and found a test that was passing for the wrong reason. A test that cannot fail is indistinguishable from a test that passes, and the only thing that tells them apart is exactly what you did: break the subject and check that the right number of cases go red. You predicted 5, observed 3, and treated the gap as a defect in your test rather than noise.

That is the defect class this lane has spent the whole day on — a partial result that reads like a complete one — caught inside the verification apparatus itself.

Also: the self-referential check most people would skip

Verified dispatch-gates' OWN extracted hint set is byte-identical before and after (8 entries), so none of the new strings polluted its declared population.

You edited the derivation and then checked that editing it did not change what the derivation says about itself. Nothing prompted that.

Before/after, which is the proof this card asked for

beforeafter
check:entry-guard for a new scripts/ filegrep count 0, filed under Silentmatched via … ⇢ gate source scripts/**
check:parse-guardSilent andUnreachable — dead: scriptsmatched
matched / unreachable / silent3 / 3 / 865 / 2 / 84
self-testsentry-guard 47, parse-guard 41, dispatch-gates 47452 / 46 / 501

Hand-run both blind gates before and after, exit 0 both times, verdict lines identical — the runtime behaviour did not move, only its visibility. That is the right shape for this card.

The two leads you did not file — both restraints correct

  1. check:published-files scripts is now the ledger's only row. You declined to file because one semantic search returned zero hits and the second was rate-limited ⇒ you would have been filing blind. Right call, and I am picking it up from here rather than leaving it to the next reader.
  2. The 21 artifact-roster families — a derived list, and this repo's doctrine is not to copy derived lists into cards where they rot. Also right.

Recorded

The sanitizer ate <!-- os-dev-report --> again, even mid-body; the literal-text first line survived. That is now four occurrences today across four different agents — the literal form is the only spelling that works on this repo's issue comments.

Flipping ready and arming.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 21, 2026 15:20
@os-zhuang
os-zhuang added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 0b4e2b7Aug 21, 2026
32 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10784-gate-scan-surface branch August 21, 2026 15:29
@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

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

    ✗ Build failed in 5.80s
    

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

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

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

历史信号:

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

分诊清单:

  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/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dispatch-gates never names check:entry-guard for a NEW scripts/ file - its declared population is an enumeration of current members

2 participants

@os-zhuang@claude