Skip to content

Gate: every ROOT_DIR_WATCH_HINTS declaration stays readable as a literal - #12855

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-12762-watch-hint-literal-pin
Aug 28, 2026
Merged

Gate: every ROOT_DIR_WATCH_HINTS declaration stays readable as a literal#12855
os-zhuang merged 2 commits into
mainfrom
claude/issue-12762-watch-hint-literal-pin

Conversation

@claude

@claudeclaudeBot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Fixes#12762

extractWatchHints in scripts/pm/dispatch-gates.mjs reads source text. A
ROOT_DIR_WATCH_HINTS declaration spelled as a literal contributes its hints; the same
declaration computed from the gate's population constant contributes nothing — while the
runtime value is byte-identical, so every local assertion about that value stays green. The
gate then drops out of every dispatch brief and scores a quiet green for every card in the
subtree it walks.

Step 1 first, as the card and the triage comment both required

The card's own first step — do bare-root-worklist --self-test or check:pm-dispatch-gates
already catch this from the other side? — was re-run on the current tree rather than inherited
from the round-18 reading at 632e862d1. The answer is still NO. Faithful reproduction on
scripts/check-objectql-double-limit.mjs, declaration computed from that file's own
SCAN_ROOT, the quoted literal kept in a neighbouring comment:

readingbasecomputed declaration
hints extractWatchHints recovers["scripts/objectql-double-limit.baseline.json","packages/**/*.test.ts","n/a"]["scripts/objectql-double-limit.baseline.json","n/a"]
the gate's own --self-testexit 0exit 0
bare-root-worklist --self-testexit 0exit 0
check:pm-dispatch-gatesexit 0exit 0

The subtree hint is gone and all three instruments stay green — the state the card was filed
about. Both legs were confirmed on disk before any reading was taken (anchor count 0, injected
count 2, git hash-object moved off the HEAD blob a46bd2ff...), and restore was proved by
state (git diff HEAD empty, blob back to a46bd2ff...) under an absolute-path trap.

Coverage today turns out to be per-file and incidental, never class-level — four more
declarations were ablated the same way:

declaration ablatedown self-testbare-root-worklistcheck:pm-dispatch-gates
check-objectql-double-limit000
check-entry-guard001 (a specimen pin naming check:entry-guard)
check-role-word001 (a specimen pin naming that file)
check-skill-compatibility-version01 (a FRESH bare-root row)0
check-examples-live-imports011

Two ad-hoc specimen pins and a bare-root side effect are not a guard over the class, and the
first row proves at least one member has nothing at all. So this proceeds to step 2.

The population, re-measured

The card says 13 declarers under scripts/ split 2 pinned / 11 unpinned. Measured here:

  • 15 declarations exist, not 13. scripts/ holds 13; packages/spec/scripts/build-skill-references.ts holds a
    14th (the idiom is not confined to scripts/), and this PR's own gate is the 15th.
  • The six files that mention the constant without declaring it — check-i18n-coverage,
    check-page-declaration-shape, check-published-readme-exports, pm/bare-root-worklist,
    pm/dispatch-gates, workspace-enumerator — were classified one by one: every occurrence in
    all six is inside a comment or a string. None is a declaration under any spelling.
  • The split is 1 pinned / 13 unpinned, not 2 / 11. check-cli-command-ids.mjs is the only
    genuinely covered one (statement-scoped declSites since fix(scripts): make check-cli-command-ids' literal-declaration self-test able to fail #12759).
    check-objectql-double-limit.mjs's own-source needle is a whole-file search, which the
    first table above shows a comment copy satisfies.

What lands

A shared guard, scripts/check-watch-hint-literal.mjs, wired as pnpm check:watch-hint-literal
and run unconditionally in Lint & Repo Gates. For every declarer it finds — repo-wide sweep of
authored JS/TS, no spawns, ~0.5s — the right-hand side of the declaration statement must be an
array of quoted string literals and nothing else.

  • The search is scoped to the declaration statement, and comments are masked first through
    the repo's one comment scanner. This is the detail that cost check-cli-command-ids' "declaration is spelled as a LITERAL" self-test assertion cannot fail — the needle appears in the assertion's own source text #12472 its enforcement: a
    whole-file search finds the gate's own hint spelled again in a neighbouring runtime assertion
    or in a comment and stays green on the computed form. Both of those copies are pinned as
    self-test cases that must be REJECTED.
  • An empty population is refused, not passed. "Every declaration is a literal" is vacuously
    true over zero declarations, so a sweep that breaks would otherwise print this gate's
    healthiest green.
  • It does not assert a declaration is CORRECT — that it names the roots the gate really
    walks and only those. That claim is local to each gate and each one already pins it from its
    own side. This gate holds the one property none of them can hold about itself: that the
    declaration is still readable by a text scanner.
  • It judges itself. Its own declaration is in the population.

Why a standalone file and not scripts/pm/dispatch-gates.mjs

dispatch-gates.mjs is the consumer being protected, and it is the repo's hottest serial file —
three landings between the round-18 reading and this card. A class guard placed inside it takes
that serial slot for no benefit, and it would ride check:pm-dispatch-gates' single exit code
behind 810 other cases, where a class failure is not separately nameable in CI. A standalone
gate also earns its own check:* name, so a dispatch brief for a scripts/ card names it
directly. The guard is deliberately import-free: it never opens the modules it judges, so
reading one constant does not run fourteen module bodies.

Evidence

All readings below are from the tree at e839a14a8 (final commit; git rev-parse --short HEAD).

Positive controls — the instrument can fail, in both directions. Each mutation confirmed on
disk before the reading, each restore proved by state under an absolute-path trap:

  • one real declaration (scripts/check-parse-guard.mjs) rewritten to the computed form with a
    comment copy of the literal beside it
    — the production run goes exit 1:
    check-watch-hint-literal: 1 of 15 ROOT_DIR_WATCH_HINTS declaration(s) are not readable as literals.
  • the predicate itself ablated (literalHints made to accept anything) — the self-test goes
    exit 1: check-watch-hint-literal self-test: 13 of 29 case(s) failed. So the green below
    is a measurement, not a broken instrument.

Gate union — the 28 families node scripts/pm/dispatch-gates.mjs derives for this diff plus
the two convention-triggered ones it names for adding a gate script, all green:

check-watch-hint-literal self-test: 29 cases pass
check-watch-hint-literal: 15 ROOT_DIR_WATCH_HINTS declaration(s), every one an array of quoted literals inside its own statement.
dispatch-gates self-test: 810 cases pass.
bare-root-worklist self-test: 46 live row(s), 39 unreachable as spelled, 39 recorded verdict(s) - none stale, none missing, none contradicted.
check-self-test-wired: every one of the 142 script(s) CI runs that ship a --self-test has that self-test run by CI.
check:entry-guard: 172 scripts/ file(s) - every entry guard goes through invoked-as.mjs
check:parse-guard: 171 scripts/ file(s) - every TypeScript parse goes through ts-parse.mjs.
check-step-collectors: 361 run: steps across 28 workflow(s)
check-nul-bytes --self-test: 75 assertions over a temp git repo

Repo-wide pnpm lint (eslint . --no-inline-config): exit 0, no findings.

No changeset: the diff is a root scripts/ gate plus its CI and package.json wiring, so it
publishes nothing. The PR carries skip-changeset.


Generated by Claude Code

os-zhuangand others added 2 commits August 28, 2026 03:52
…spelling
`extractWatchHints` reads source text, so a declaration computed from the
gate's population constant contributes no hint at all while its runtime value
is unchanged. The gate then leaves every dispatch brief and scores a quiet
green for every card in the subtree it walks.
Fifteen declarations carry the idiom; one had an own-source pin holding it to
a literal spelling. This adds the shared guard the class needs: for every
declarer, repo-wide, the right-hand side of the declaration statement must be
an array of quoted string literals.
The search is scoped to the declaration STATEMENT. A whole-file search finds
the gate's own hint spelled again in a runtime assertion or in a comment and
stays green on the computed form, which is how both earlier per-file pins
failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PfaSTikked61BkcsB5Rn69
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 28, 2026
@github-actionsgithub-actionsBot added size/m ci/cd dependencies Pull requests that update a dependency file labels Aug 28, 2026
@os-zhuangClaude

Copy link
Copy Markdown
Contributor

PM 复核:ACCEPT#12762 / check-watch-hint-literal

head e839a14a8,merge-base b489d3c72,3 文件 +370,零删除。区分我自己测的转述的

一、⭐ 这张卡我能在本席真跑,因为你把 gate 写成了 import-free

上一张卡(#11916)我只能靠读源码 + CI,因为它需要 node_modules。这一张不需要。我在一棵没有 node_modules 的 detached worktree 里跑了它本体(exit code 在任何管道之前抓):

✓ check-watch-hint-literal self-test: 29 cases pass SELFTEST_EXIT=0
✓ check-watch-hint-literal: 15 ROOT_DIR_WATCH_HINTS declaration(s),
every one an array of quoted literals inside its own statement GATE_EXIT=0

并且我做了自己的对抗性消融,特意换一个你没用过的文件(你用 check-parse-guard.mjs,我用 scripts/check-role-word.mjs):声明改成 [`${SKILLS_ROOT}/**`],字面量留在邻近注释里。变异先在磁盘确认(literal 声明行 1→0、注释副本 =1、blob 6a36b40adeda0c39),然后:

GATE_EXIT=1
✗ scripts/check-role-word.mjs -- the declaration is COMPUTED, not a literal array
-- the hint extractor reads source text, so a computed declaration builds no hint
at all and the gate leaves every dispatch brief: [`${SKILLS_ROOT}/**`]

注释里那份副本没能救它。 那正是让 #12472 丢掉执行力的作用域缺陷,现在关上了。恢复按 state 证明(blob 回到 base、git diff HEAD 空),gate 重新绿。

二、你更正了我的 brief,而且更正的方向不是我标的那个

我给的是 13,并写了 ⛔ 不要继承、先把那 6 个「只提及」的文件逐个分类。两件你都做了,而且答案是我的 13 在 scripts/ 内正确——我抽查了我最没把握的三个,check-i18n-coverage:177check-page-declaration-shape:123check-published-readme-exports:311,全是注释行,与你的分类一致(第三个尤其有意思:那段 JSDoc 明说下面没有这个数组、以及为什么加一个会是错的)。

但你没停在我划的圈里,而是全仓扫。我独立复算:

merge-base b489d3c72PR head
全仓声明者1415(新 gate 判它自己)
scripts/ 之外1 —— packages/spec/scripts/build-skill-references.ts1

我的数字错不在我标出的疑点,而在我扫的范围。 真实切分 1 pinned / 13 unpinned;我推的 1/12 少的正是同一个文件。⭐ 这条只因为它被标成 Zone 2 且写了「不要继承」才没造成损害——如果我把 13 写成硬约束,这个 gate 就会漏掉 packages/spec 那一个,而且没人会发现。

三、⭐⭐ 你自己抓住的那次仪器假象,是这轮最值得记的一笔

我的第一次消融用新引入的 const(WATCH_ROOT = 'scripts')拼计算形,于是 bare-root-worklist 红了——红的原因与本卡无关:一个新的 const-name/root 组合本身就加入了它的 FRESH species。改用每个文件已有的 population 常量重跑,才把假象和真覆盖分开:entry-guard 的 bare-root 红消失,dispatch-gates 的留下。只有第二张表被当作测量报出。

⭐ 这就是「坏仪器和真发现读起来一模一样」的教科书实例,而且你是在自己会受益的方向上(第一张表看起来像「已被覆盖」,可以直接关卡走人)推翻了自己。⛔ 若报了第一张表,这张卡会被错误地判成 already-covered。

四、几处判断我认为做对了

  • 落点:独立文件,不是 dispatch-gates.mjs 三条理由都成立,第二条尤其:塞进去就是让类失效藏在 check:pm-dispatch-gates 一个退出码后面的 810 个 case 里,CI 里点不出名。⇒ 而且全仓最热的串行位没被占掉。
  • ⭐ 故意不 import dispatch-gates,理由我没想到:import 它会让这个 gate 继承它声明的 inherited-population(.github/workflows)当作自己的 watch hint —— 一个凭空捏造的 population。那正是 finding: dispatch-gates OVER-names check:i18n and check:i18n-coverage for any edit under packages/cli — a comment-only test-config change owes a full CLI closure build to measure two gates it cannot affect #12500 刚刚教过的错误,你把它预防性地应用了。
  • ⭐ 声明窄、扫描宽,并把理由写进 docblock:它自己声明 scripts/**(15 个里 13 个在那儿)却全仓扫。为了对齐 walk 而声明 packages/** 才是 bare-root 账本警告的那个更贵的错误。⇒ 什么都不会被静默漏掉,而声明保持诚实。
  • ⭐ 自测夹具用 DECL_NAME 拼常量名而不是照写,因为扫描屏蔽注释但不屏蔽字符串字面量——照写会让这个文件自己多出第二个声明点而被拒。并且有一条 case 钉住「本文件恰好一个声明点」。自指正确性
  • 只判拼写,不判正确性,并说明白为什么:正确性每个 gate 自己那边已经钉了,这里重复就是给一个已有答案的问题第二个答案。
  • 空人口拒绝而非通过;一文件多声明则拒绝而非裁决;空数组是违规。三个边界都堵上了。
  • ⛔ 两件看过但故意不立卡的事,给了理由:check-objectql-double-limit 的整文件 needle 现在冗余但不是错的;dispatch-gates 自测里那两条 specimen pin 不是本 gate 的重复(它们断言的是 hintCovers 的覆盖,本 gate 不判)。⭐ 立任何一张都是噪音——这个克制是对的。

五、交叉核对上一张卡

你的 union 里 check-stall-guard-budget535 step(s)(#11916 落地时是 534),正是本 PR 给 lint.yml 加的那一步;check-self-test-wired142(#12847 落地后我自己量的是 141)。两个数都对得上,新 gate 的自测确实被 CI 接线了

skip-changeset 判对:根 scripts/ + CI/package.json 接线,不发布任何东西。


结论:ACCEPT。 待 34 项检查逐名全绿(现余 Test Core (1/6)Lint & Repo Gates,零红)后转正式并挂自动合并。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 28, 2026 04:12
@os-zhuang
os-zhuang enabled auto-merge August 28, 2026 04:12
@os-zhuang
os-zhuang added this pull request to the merge queueAug 28, 2026
Merged via the queue into main with commit c097f71Aug 28, 2026
37 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12762-watch-hint-literal-pin branch August 28, 2026 04:27
@github-actions

Copy link
Copy Markdown
Contributor

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

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

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

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

    @objectstack/metadata:test: FAIL src/plugin-artifact-forward-conversion.test.ts > artifact door — the conversion is versioned, not a blanket amnesty (#12772) > an artifact claiming the CURRENT spec ↳ 失败原因: @objectstack/metadata:test: AssertionError: expected 'expected "the strict parse must refus…' to match /allowRestore|allowPurge/
    

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

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

历史信号:

  • 本 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)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filesize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-zhuang@claude