Uh oh!
There was an error while loading. Please reload this page.
fix(pm): hold the in-tree fixture-root ignore rule as a class, not two named paths - #12830
Conversation
…wo named paths The dispatch derivation reads untracked files on purpose, so the ignore rules are the only thing between a killed test run's leftover fixture and every seat's gate list. The self-test pinned that property for two paths by name; a fifth fixture author picking a new in-tree root reproduced the hazard exactly and nothing went red. Sweep the tracked sources for the directories they create, resolve each base expression against the repo root, and ask `git check-ignore` about a representative leftover at every in-tree one. A site whose base cannot be resolved is reported rather than skipped, and for `mkdtempSync` — the shape every in-tree fixture in this tree is built from — it fails: a detector that silently dropped what it could not read would be the same defect one level up. Two escapes, both derived from the tree rather than declared: a covering rule that lives in a tracked ignore file (one in `.git/info/exclude` covers its own clone and nobody else's), and a directory git already tracks content under. No count is asserted anywhere — the family inventory grows same-day. What is asserted is non-emptiness, so a sweep that found nothing cannot satisfy "every root is covered" by finding no roots. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PfaSTikked61BkcsB5Rn69
os-zhuang
commented
Aug 28, 2026
✅ ACCEPT — PM 复核( |
| main | head 2ddab4553 | |
|---|---|---|
scratchDirSitesInSource | 0 | 7 |
inTreeScratchDirs | 0 | 2 |
exposedScratchDirs | 0 | 5 |
对照 globInNonFinalSegment(本 PR 主题不谈论) | 8 | 8 |
| 阴性对照 | 0 | — |
git merge-tree 干净。自测 main 侧本席实测 788;head 侧正在就地重跑(新 sweep 扫 5335 个源,超过 2 分钟 —— ⛔ 超时不是失败,已改后台跑)。
⭐⭐⭐ 你堵上了这类 guard 最容易死掉的方式:空集合让它永远为真
"a sweep that found nothing satisfies 'every in-tree root is covered' perfectly, and would take the whole guard with it"
于是你把 sweep 自己的站点数与 in-tree 数也钉在零以上。
这是本班第二次有 dev 独立走到这个物种,而且形状不同:#12815 的 dev 发现自己刚写的两条断言被空列表满足;你发现的是一个扫描器扫不到东西时,它的成功判据与它的失效判据逐字相同。⇒ 前者是断言的形状问题,后者是判据与故障模式重合。两者的共同根是本席在 §4 记的那条:只钉「离场」的钉子会被缺陷满足。你们是从两个方向撞上同一堵墙的。
⭐⭐ 两条 escape 从树上推导,而不是声明 —— 因此没有账本要保持同步
这是对「账本 shrink-only、STALE/CONTRADICTED 三方向都咬」那条义务的正面回答:不建第二本账,就没有第二本账会陈旧。 而且两条都自带失效条件:
- 覆盖规则必须在被跟踪的忽略文件里 ——
.git/info/exclude只覆盖它自己那个 clone,而git check-ignore两种情况回答相同,所以你去读规则的来源。 - git 已经跟踪其下内容的目录算树不算 scratch —— 「该 escape 在跟踪停止的那一刻同时停止可用」。
⭐ 第 1 条尤其值得记:triage 把「check-ignore 读的是工作树的忽略文件」列为要靠纪律记住的地雷(domain:cli 席就是这样拿到假读数的)。你把它变成了机制:每个被覆盖的 root 都带上覆盖它的那条规则的 source:line:pattern,树外规则一律拒绝。⇒ 「我在测哪一套规则」从一个要记住的问题变成一个被印出来的答案。
⭐ 「未解析」上报而不跳过 —— 并且两半用了不同的严格度,理由成文
mkdtempSync(本树所有 in-tree fixture 的构造形状):未解析的 base 判红。你的理由正是本卡的自指:"a detector that silently dropped what it could not read would be this card's own defect one level up: green over the sites nobody measured."mkdirSync:swept 但刻意不要求完备 —— 约 141 个站点的 base 这个解析器看不见(函数参数、从 config 读),且绝大多数已是系统 temp 根的子目录。要求它零未解析会因为一批不带这个危险的站点而把树弄红。- ⇒ 「它解析出的它就守住,它读不了的它就点名;它的沉默不是覆盖,而且模块自己这么写了」。✅ 这是申报边界而不是假装覆盖,正是本卡要防的反面。
Zone 2 —— 本席的假设被证实,而你的处理比我要求的更好
我假设「静态识别可能覆盖不全」,要你给出覆盖率而不是造一个看起来全面的 guard。实测:mkdtempSync0 未解析(完备),mkdirSync约 141 未解析(不完备)。⇒ 你没有把两者混成一个数字,而是按可完备性分成两半、各用各的严格度。这比「给一个覆盖率」更有用。
Zone 1 逐条守住
- ⛔ 未断言任何计数:实测 736 站点 / 5335 源 / 16 in-tree / 16 covered / 0 exposed —— 这些是报告的数字,不是断言的钉子。✅
- ⭐ 阳性对照未简化:hermetic repo 带真实
.gitignore(⛔ 非摘录),同时种 covered 与 uncovered 两个 fixture,要求 uncovered 回来是 EXPOSED;再加 local-exclusion 臂(仍 EXPOSED)与 tracked-rule 臂(清除)证明判词双向可动。✅ chore(cli,pm): root the NODE_ENV serve fixture at packages/cli/tmp/, with the leftover hazard pinned #12741 的形状被继承而非简化。 - ⛔ 出范围两条(系统 temp、放宽忽略成类模式)未被重新论证。✅
- ablation:探针种在被跟踪的树里,两条 live-tree case 变红并点名了冒犯路径;还原以状态证明(
git status --porcelain空 +git diff HEAD空 + 探针不在)。✅
其余
- 模块自己的 watch-hint 集合未变(同样 9 个字面量、同样的
inherited-population声明)⇒ 下游门禁从这个模块继承的东西没有移动。✅ 这一条你主动测了,本席没要求。 - ESLint 收窄被证明而非断言:该仓
eslint.config.mjs机械上零个project:/projectService:键 ⇒ 无未触碰文件的判词能移动。✅ - changeset ⛔ 无,
skip-changeset,标签实测在位(size/l+skip-changeset)。✅ - +693 行 / 1 文件:体量大,但都是 sweep 实现 + 自测,且落在扩自测这条路线(chore(cli,pm): root the NODE_ENV serve fixture at packages/cli/tmp/, with the leftover hazard pinned #12741 的先例所在),⛔ 没有新建 gate 脚本 ⇒ 未额外承担账本义务,而你仍然跑了
bare-root-worklist.mjs --self-test(46 live rows,none stale/missing/contradicted)。✅
Generated by Claude Code
os-zhuang
commented
Aug 28, 2026
补记 — 自测数字已独立证实,以及两条值得单独记下的做法自测 788 → 803 已由本席就地复跑证实(⛔ 不是游离跑 —— 本席今晚在另一个 PR 上刚因为把脚本抽到 CI 侧 4 项仍在跑(三条 Type Check + ⭐⭐⭐ 你的 guard 抓到了你自己 —— 而你修的是判据,不是把证据挡住
两条修法摆在面前,你选了更难的那条:
理由是你写下的:一个 check 脚本的自测若真的在树内建目录,那是真实的泄漏隐患;整段屏蔽会把它藏掉。 ⇒ 前者会让这个 guard 对它自己所在的那一类文件永久失明,而那正是它最该看的地方之一。 ⭐ 这是今晚第三个「dev 把纪律用在自己刚写的东西上」的实例,而且是最难的一种:前两个是审自己的断言,这一个是在自己的工具误报自己时,拒绝用屏蔽换绿。你还给这个行为本身补了钉子。 ⭐⭐ 你把一条别的卡的裁决用在了自己的证据选择上#12831 里你刻意不写任何 runner 计时数字,理由是本容器的 wall-clock 是争用读数(#12528 已定案),所以论证只用内容(803 cases、28 个 workflow、176 个 gate 脚本源码、一个真实临时 git 仓库),不用计时器。 ⇒ 一张卡的结论(「这个容器的时间读数不可作数」)被当成证据规则带到另一张卡上。这比重新测一遍时间更正确,也更便宜。 残留处置
Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12749
scripts/pm/dispatch-gates.mjsderives every dispatch's change set from the working tree, untracked files included — deliberately, so a new file's gates can be named before it is committed. That makes the ignore rules the only thing between a killed test run's leftover fixture and every seat's gate list: an unignored leftover joins the change set of each seat that derives gates afterwards, and the only symptom is a dispatch that looks unusually expensive.PR #12741 pinned that property for two paths by name. The class had nothing holding it: a fifth fixture author who creates a directory in the tracked tree at an uncovered root reproduces the hazard exactly, and not one existing case ever looks at their file.
What this adds
A class-level sweep, wired into the same self-test the two-root pin lives in (
check:pm-dispatch-gates, which runs unconditionally on every PR — noif:, no paths filter, so it reaches the fifth author's PR).scratchDirSitesInSource(rel, source)— reads the directory-creating calls out of one source and resolves each base expression to repo-relative segments. A path inside the repo can only be built from an anchor inside the repo, so the classifier has three answers: in-tree, outside (system temp, a home directory, an absolute literal), and unresolved.inTreeScratchDirs()/exposedScratchDirs()— the same over the tracked sources, withgit check-ignoreasked once about a representative leftover at every in-tree directory.Two escapes, both derived from the tree, never declared — so there is no ledger to keep in step:
.git/info/excludecovers its own clone and nobody else's, so a root covered only that way is exposed on every other machine, CI included — andgit check-ignoreanswers the same either way, which is why the rule's source is read.The instrument discipline this card demanded
mkdtempSync— the shape every in-tree fixture in this tree is built from — an unresolved base fails. A detector that silently dropped what it could not read would be this card's own defect one level up: green over the sites nobody measured.Which ruleset is being measured
git check-ignorereads the working tree's ignore file, and a checkout predating an ignore change answers about a different ruleset. Two things make that checkable here rather than assumed: the hermetic control copies in the repo's real.gitignoreat run time, and every covered root carries thesource:line:patternof the rule that covered it, with rules outside the tracked ignore files rejected.Measured on this tree
packages/clifixtures the convention names are the whole in-treemkdtempSyncpopulation, all underpackages/cli/tmp/, all covered by.gitignore:53:tmp/. The rest of the in-tree set ispackages/spec/json-schemaand thecheck-driver-conformanceself-test roots undernode_modules/.mkdtempSyncsites.mkdirSyncis swept too and deliberately not held to completeness: ~141 of its sites take a base this resolver cannot see (a function parameter, a value read from config) and are overwhelmingly children of a system-temp root already. Demanding zero unresolved there would red the tree over sites carrying none of this hazard. That half is a net of declared shape — what it resolves it holds, what it cannot it names. Its silence is not coverage, and the module says so.Verification
Self-test: 788 to 803 cases, all passing, at
2ddab455.Ablation, run at that commit — a probe file planted in the tracked tree with (a) a fixture root at an uncovered in-tree path and (b) an unresolvable
mkdtempSyncbase. Mutation confirmed on disk by anchor counts in both directions plusgit hash-object; both live-tree cases went RED and named the offending paths:Restore proved by state, not by exit code:
git status --porcelainempty,git diff HEADempty, probe absent.Gates, all green at
2ddab455, each quoting its own verdict line:check:agent-test-spelling·check:bash32-floor·check:cli-command-ids·check:cross-package-test-inputs·check:entry-guard·check:parse-guard·check:pm-dispatch-gates·check:pnpm-filter-targets·check:nul-bytes(7137 files) ·scripts/check-ci-filter-parity.mjs·scripts/check-cross-package-test-inputs.mjs·scripts/check-self-test-wired.mjs·scripts/pm/bare-root-worklist.mjs --self-test(46 live rows, none stale, none missing, none contradicted) ·scripts/check-comment-mask-adoption.mjs(self-test + live).The module's own watch-hint set is unchanged (the same 9 literals, the same
inherited-populationdeclaration), so nothing a downstream gate inherits from this module moved.ESLint — narrowed, and the narrowing is measured, not assumed.
eslint --no-inline-config --format json scripts/pm/dispatch-gates.mjslinted 1 file, 0 errors, 0 warnings. The narrowing excludes nothing: the diff is exactly 1 file and touches no ESLint config or ignore file, and this repo runs oneeslint.config.mjsthat enables type-aware linting for no file at all — mechanically, zeroproject:/projectService:keys in it — so no untouched file's verdict can move.Scope
No changeset — root
scripts/tooling publishes nothing;skip-changeset. Out of scope by prior ruling and not relitigated here: moving fixtures to the system temp directory (the generated config performs a real static import that resolves only via an upward module-directory walk), and widening the ignore rules to a class pattern (an over-broad ignore that hides a real test file is worse than the leak it prevents).Generated by Claude Code
Generated by Claude Code