Skip to content

tooling(devx): report source files ESLint reaches for NEITHER reason - #8467

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-8337-mts-lint-coverage
Sep 8, 2026
Merged

tooling(devx): report source files ESLint reaches for NEITHER reason#8467
baozhoutao merged 1 commit into
mainfrom
claude/issue-8337-mts-lint-coverage

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

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 purpose

Before 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:

  1. every file ESLint walks must resolve at least one rule — objectui#7908;
  2. every source file ESLint does not walk must be unwalked for some reason other than its extension — objectui#8337.

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:

path isPathIgnored calculateConfigForFile rules
vitest.config.mts true undefined — NOT WALKED
playwright.config.ts false object 116 — walked + ruled
scripts/github-slug.mjs false object 0 — objectui#7908's class

undefined is 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 4440 is stale — do not reuse it), .mts files repo-wide: 1 (vitest.config.mts), .cts: 0.

Corroborated from lint:root's own output rather than only from the API: a real pnpm lint:root with --format json reports 275 files linted, 0 errors, 32 warnings (all pre-existing, none in files this PR touches) — and vitest.config.mts appears 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 .mts reproduces 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. On 868e825012:

reachable    .js .cjs .mjs   (ESLint's default lint set)
             .ts .tsx        (the rule-bearing files: globs)
unreachable  .jsx  .cts  .mts

.jsx is unreachable here for exactly the same reason as .mts, and neither the card nor its triage names it. There are no .jsx files 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"

isPathIgnored is also true for packages/core/dist/x.mts, and that exclusion is legitimate: it is about location. Measured in the same run — inside **/dist and **/.source all eight source extensions come back ignored, while at the repository root, under scripts/, and under packages/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 for eslint.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 new scripts/check-foo.mjs adds no information — the row already declares that class. Here, the whole point is that the next .mts or .cts is reported: a **/*.mts row 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. extensionProbeCollapse requires 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 TERM with absolute paths, and every restore is proven by state (git diff HEAD empty), never by an exit code. Full transcript reproduced by scripts/check-lint-rule-coverage.mjs runs from the committed tree.

The gate before and after.

  • BEFORE (base blob b2b3e4bd, hash-matched on disk before reading): EXIT=0, verdict OK 4480 file(s) walked ... 124 resolve ZERO rules -- every one of those declared by 8 ledger row(s). Occurrences of vitest.config.mts in that output: 0. The defect is invisible to the gate as it stands.
  • AFTER: 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.

leg mutation (hash + marker proved first) result
B inject scripts/ablation-injected-tool.cts EXIT=1, names it: 1 source file(s) ESLint does NOT walk, purely because of their extension
C identical bytes, identical directory, .ts instead EXIT=0, walked count 4480 goes to 4481 — so B's red is about the extension, not the file
D identical bytes as .cts inside packages/core/dist/ EXIT=0, not reported — a location exclusion is not claimed
E widen the two rule-bearing globs to reach .mts EXIT=1 in both shrink-only directions: unreached-over-broad (also matches 1 file(s) ESLint DOES walk: vitest.config.mts) and unreached-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.tsTest Files 1 passed (1), Tests 27 passed (27) (20 before this PR, 7 added; wrapper VERDICT command-exit 0). pnpm lint:rootVERDICT command-exit 0, 32 problems (0 errors, 32 warnings), the whole lane run verbatim from package.json, nothing narrowed. node scripts/check-changeset-presence.mjsEXIT=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 --test on both changed paths — EXIT=0, NOT GOVERNED — 2 path(s) checked against 5 governed surface(s); none matched. node scripts/check-control-bytes.mjsEXIT=0; plus a direct grep -naP control-character scan over both changed files, zero hits. node scripts/check-entry-guard.mjsEXIT=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 HEAD empty afterwards). Adding mts,cts to both rule-bearing globs:

vitest.config.mts  ->  isPathIgnored: false, 116 rules
pnpm exec eslint vitest.config.mts  ->  EXIT=0, no output

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

  • The gate script is itself in predicate 1's ledgered vacuous population (scripts/**/*.{js,mjs,cjs} resolves zero rules), so its own 0 errors in lint:root is unjudged rather than clean. What enforces it is the pin test running inside pnpm test, exactly as objectui#7908's header records; no .github/workflows/** step is added here, and the inverse pin asserting that absence still holds.
  • Predicate 1's measurements in the header are quoted as taken on fedfa3e4a by the PR that landed it; every number this PR adds was re-taken on 868e825012 and is labelled with that base.
  • The two existing fixture cases that assert an exact findings array now pass unreachedGroups: [], with a comment saying why — they are statements about vacuity alone, and predicate 2 has its own fixtures.

Generated by Claude Code

`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
@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

LANDEDdomain:devx @ objectui execution seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr, R46. PR #8467 merged 2026-09-08T01:47:13Z as ed17be480; probe run on a clean extract of re-fetched origin/main, ⛔ never on the shared working tree.

leg reading on origin/main@ed17be480
probe — the unreachable set is DERIVED, not listed :196 So the unreachable set is DERIVED from the live config on every run
probe — ⭐ and the header records the widened finding :201 unreachable jsx cts mts · :203 ⭐ THREE, not one. \.jsx` is unreachable here for exactly the same reason`
probe — ⛔ no .mts special case in the gate 0 quoted-literal occurrences. ⚠️ Controlled: the string appears in prose at :134 :162 :181 :227, so the grep fires — the zero is about the literal, not about the term being absent
control — the ledger row is an exact PATH :417 export const UNREACHED_GROUPS = [ { glob: 'vitest.config.mts', — ⛔ not **/*.mts, which would waive the only thing the predicate exists to catch
control — the VACUOUS_GROUPS asymmetry pin survives check-lint-rule-coverage.test.ts:476 / :484 / :492, including its comment "The asymmetry with VACUOUS_GROUPS, pinned because it is deliberate"

⚠️ The gate itself cannot execute in a bare git archive extract (ERR_MODULE_NOT_FOUND, no node_modules); that is the extract's limit and ⛔ not a reading about the gate. Its behaviour was measured in the dev's worktree and by CI, both green.

What landed that the card did not ask for: .jsx is unreachable by exactly the same two gaps as .mts/.cts, and neither the card nor its triage comment named it. Zero .jsx files exist today, so nothing had ever reported it — which is why the predicate derives its set from the live config instead of naming extensions. The finding is now in the gate's own header where the next reader hits it.

Option 1 was measured and not taken: with mts,cts added, vitest.config.mts resolves 116 rules and eslint exits 0 with no output — an empty red set, and the most tempting possible moment to switch routes silently. It went to #8469 with the number attached instead, on the reading that turning rules on for a never-linted population is still a strength decision with its own owner. ⇒ This seat confirms that reading.

pm:dispatched stripped in the same pass; domain:devx, bug, tooling, priority:p3 stay. The card closed via Fixes.


Generated by Claude Code

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

Labels

Projects

None yet

2 participants