tooling(devx): report source files ESLint reaches for NEITHER reason - #8467
Conversation
`check:lint-rule-coverage` gains a second predicate. The first reports files ESLint WALKS that resolve zero rules; this one reports source files ESLint never walks at all, because their extension is in neither a rule-bearing `files` glob nor ESLint's default lint set. The unreachable extension set is DERIVED from the live config on every run rather than listed, so it is not a `.mts` special case: on this base it is `.jsx` `.cts` `.mts`. Per file, the discrimination is a substitution on the candidate's own path -- a build output or a name-excluded file is excluded for a reason that is not this gate's business. This widens the gate's subject on purpose, and its header now says so. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
|
LANDED —
⭐ What landed that the card did not ask for: ⛔ Option 1 was measured and not taken: with
Generated by Claude Code |
Fixes #8337
Route taken: option 2 — gate the gap, as objectui#8337's triage routed it. Option 1 (widen the rule-bearing glob) is not taken here; its red set is measured below and handed on.
This PR WIDENS the subject of
check:lint-rule-coverage, on purposeBefore this PR that gate's subject was one predicate: every file ESLint walks must resolve at least one rule (objectui#7908). It now carries two, each with its own shrink-only ledger:
The gate's own header now opens on that widening and says which PR moved it, so the next reader sees a subject that moved deliberately rather than one that drifted. Predicate 1 cannot see predicate 2's population by construction: it reports files ESLint walks, and those files are not walked.
Premise re-derived on this branch's base, not carried from the card
868e825012, real install, ESLint v10.8.1, ESLint Node API. Three controls, one run, three distinct states — reproduced rather than collapsed:isPathIgnoredcalculateConfigForFilevitest.config.mtstrueundefinedplaywright.config.tsfalsescripts/github-slug.mjsfalse⭐
undefinedis not "zero rules" — it is ESLint declining to look. The premise stands:premise_still_valid: true.Population re-taken today, not carried: 4480 files walked (the card's
4440is stale — do not reuse it),.mtsfiles repo-wide: 1 (vitest.config.mts),.cts: 0.Corroborated from
lint:root's own output rather than only from the API: a realpnpm lint:rootwith--format jsonreports 275 files linted,0errors,32warnings (all pre-existing, none in files this PR touches) — andvitest.config.mtsappears in none of the 275, while both files this PR edits do.⭐ The measurement immediately corrected the card: it is THREE extensions, not one
The brief warned that a predicate knowing only about
.mtsreproduces the blind spot one size smaller, so the unreachable set is derived from the live config on every run — one synthetic probe per source extension — rather than listed. On868e825012:.jsxis unreachable here for exactly the same reason as.mts, and neither the card nor its triage names it. There are no.jsxfiles today, so nothing reported it — and nothing would have. Deriving the set is also what makes the ledger shrink in the right direction: widen a rule-bearing glob to cover an extension and that extension leaves the set, its row stops matching, and the row goes STALE.Per file, the discrimination is a substitution — not "isPathIgnored is true"
isPathIgnoredis alsotrueforpackages/core/dist/x.mts, and that exclusion is legitimate: it is about location. Measured in the same run — inside**/distand**/.sourceall eight source extensions come back ignored, while at the repository root, underscripts/, and underpackages/core/src/the answer is identical and extension-shaped. So the test is per file: take the candidate's own path, substitute a reachable extension, ask again. Walked under some substitution ⇒ the extension is the whole reason. Still ignored under every substitution ⇒ excluded by location or by name, and none of this gate's business.Why predicate 2 has a ledger, and why its rows are PATHS
One row today,
vitest.config.mts, so the first run is green with the file counted — the same shape predicate 1 uses foreslint.config.js. Reaching the file means widening a rule-bearing glob, which is a rule-strength decision the triage kept out of the gate that reports it.Its rows are exact paths where
VACUOUS_GROUPS's rows are population globs, and the asymmetry is deliberate and pinned by a test. There, a newscripts/check-foo.mjsadds no information — the row already declares that class. Here, the whole point is that the next.mtsor.ctsis reported: a**/*.mtsrow would waive precisely what this predicate exists to catch.A gate green on an empty population is not a gate
This predicate can reach an empty population legitimately — that is what fixing it looks like — so the non-vacuity control is on the instrument, and it survives the fix.
extensionProbeCollapserequires the probe to answer both ways in the same run: at least one source extension reachable (the config loaded, the rule-bearing globs are live), and a synthetic extension no config object can name coming back unreachable (the probe can say "no" at all). With both holding, an empty unreachable set is a reading.Verification
Every mutation below proved it landed on disk by hash and marker count before any result was read, ran under a
trap ... EXIT INT TERMwith absolute paths, and every restore is proven by state (git diff HEADempty), never by an exit code. Full transcript reproduced byscripts/check-lint-rule-coverage.mjsruns from the committed tree.The gate before and after.
b2b3e4bd, hash-matched on disk before reading):EXIT=0, verdictOK 4480 file(s) walked ... 124 resolve ZERO rules -- every one of those declared by 8 ledger row(s). Occurrences ofvitest.config.mtsin that output: 0. The defect is invisible to the gate as it stands.EXIT=0, same first verdict plus the new line —ESLint reaches .js .cjs .mjs .ts .tsx and NOT .jsx .cts .mts -- 1 source file(s) on disk carry an unreached extension, every one declared by 1 ledger row(s): 1 vitest.config.mts (objectui#8337).Non-vacuity, and both controls that make the red a reading.
scripts/ablation-injected-tool.ctsEXIT=1, names it:1 source file(s) ESLint does NOT walk, purely because of their extension.tsinsteadEXIT=0, walked count4480goes to4481— so B's red is about the extension, not the file.ctsinsidepackages/core/dist/EXIT=0, not reported — a location exclusion is not claimed.mtsEXIT=1in both shrink-only directions:unreached-over-broad(also matches 1 file(s) ESLint DOES walk: vitest.config.mts) andunreached-stale(Delete the row)Leg E is the one that matters for maintenance: the ledger row cannot become a permanent exemption, because the remedy itself reds the row and names the remedy.
Suite and gates.
pnpm exec vitest run scripts/__tests__/check-lint-rule-coverage.test.ts—Test Files 1 passed (1),Tests 27 passed (27)(20 before this PR, 7 added; wrapperVERDICT command-exit 0).pnpm lint:root—VERDICT command-exit 0,32 problems (0 errors, 32 warnings), the whole lane run verbatim frompackage.json, nothing narrowed.node scripts/check-changeset-presence.mjs—EXIT=0,No source or published contract of a released package changed in this range, so no changeset is owed(run, not guessed).node scripts/check-governed-queue-guard.mjs --teston both changed paths —EXIT=0,NOT GOVERNED — 2 path(s) checked against 5 governed surface(s); none matched.node scripts/check-control-bytes.mjs—EXIT=0; plus a directgrep -naPcontrol-character scan over both changed files, zero hits.node scripts/check-entry-guard.mjs—EXIT=0.⭐ Option 1's red set, measured and handed on — NOT taken here
Triage said option 1 requires measuring the red set first. It is now measured, under the same trap-and-restore discipline (marker counts 2 and 2, hash differed from the HEAD blob before anything was read,
git diff HEADempty afterwards). Addingmts,ctsto both rule-bearing globs:The red set is empty — zero errors and zero warnings. That does not make option 1 free: it is still a rule-strength change, and the triage routed this card to the gate, so it stays out of this PR. It is reported so whoever owns that decision has the number instead of the objection. It is filed separately rather than folded in.
四轴分析
① 实际业务需求 —— 实测,不是"读起来像有用"。 今天这条谓词的总体是一个真实文件,而且不是边角文件:
vitest.config.mts定义了整个测试运行的 project、include glob 与 setup。三个对照实测证明它落在每一次 lint 运行之外,lint:root的 275 个文件里没有它。这不是投机能力面 —— 它报告的是已经存在、且确实无人评判的文件。诚实的一半:.cts与.jsx今天零文件,所以这条谓词的价值有相当一部分是前瞻性的,而不是当下的。② 项目长远合理性 —— contract-first,不是补丁。 另一条路(拓宽 glob)是在红集未测时改规则强度;本 PR 让缺口可见而不替维护者做强度决定,并把强度决定的输入(空红集)量出来交出去。台账 shrink-only,且 leg E 证明"修好"这一步会机械地让豁免行变红并点名删除动作 —— 没有一条 waiver 能靠沉默活下去。⛔ 没有消费端宽容、没有
??回退。③ 防 AI 写代码犯错 —— 这条轴是本 PR 最强的理由。 今天一个 AI 新增
vitest.config.cts或一个.mts构建脚本,得到的反馈是两个方向都没有:eslint .从不打开该文件,退出 0,而下游每一个读数都把它读成 clean。这正是本轴禁止的那种"静默容错"形态 —— 声明了 lint 覆盖,运行时不兑现。改法把沉默换成一次响亮的、点名文件的拒绝,并在拒绝正文里写清两条合法出路(给它规则 / 立一行带卡号的台账),让下一个作者无法"什么都没发生地"落地。④ 创业阶段不扩散需求 —— 本轴默认反对新门禁,如实作答。 反对意见成立的部分:总体是 1,
.cts/.jsx今天零文件。支持的部分是四点实测:(a) 这不是新门禁 —— 没有新脚本、没有新 workflow、没有新 CI 分钟,它复用同一次目录遍历(isPathIgnored占该门禁约 2 秒里的 1.9 秒,再走一遍会把成本翻倍去重问一个已问过的问题);边际成本是 9 次扩展名探针加每个候选一次替换探针。(b) 它不新增任何可声明的能力面,不引入元数据键,对使用者零暴露。(c) 维护面就是那一行台账,而且只能缩不能扩。(d) 它不设过渡窗口:今天的行是 exact path,不是**/*.mts宽限期。⇒ 综合四轴推荐做,但把 ① 与 ④ 的真实代价(总体为 1、价值偏前瞻)如实写在这里而不是藏起来,由维护者过目。四轴之间没有真冲突;唯一的张力在 ①/④ 与 ③ 之间,而 ③ 的判据(AI 批量犯错被静默掩盖)在本仓是已实测发生过的形态,故权重取胜。
Notes for review
scripts/**/*.{js,mjs,cjs}resolves zero rules), so its own0 errorsinlint:rootis unjudged rather than clean. What enforces it is the pin test running insidepnpm test, exactly as objectui#7908's header records; no.github/workflows/**step is added here, and the inverse pin asserting that absence still holds.fedfa3e4aby the PR that landed it; every number this PR adds was re-taken on868e825012and is labelled with that base.findingsarray now passunreachedGroups: [], with a comment saying why — they are statements about vacuity alone, and predicate 2 has its own fixtures.Generated by Claude Code