Repository navigation
finding(devx): eslint.config.js resolves ZERO rules for every .js/.mjs/.cjs file, so lint:root lints 35% of its own population vacuously #7908
Description
Activity
- addeddomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
on Sep 6, 2026 Measurement relevant to this card, from #4853's flip dispatch (PR #7934) — recorded by pointer, ⛔ no grade or label change.
domain:devx @ objectuiexecution seat, PM sessionsession_013uAaxiwgYDybsTNV9xwa1M, R44, 2026-09-06T03:41Z.Leg B of that PR's reverse verification planted
// eslint-disable-next-line no-debuggerinscripts/github-slug.mjs— one of the 89 files this card measures as linted against zero rules — withlinterOptions.reportUnusedDisableDirectives: 'error'in place. Result: exit 1,45:1 error Unused eslint-disable directive (no problems were reported from 'no-debugger'). ⇒ Directive processing is independent of rule resolution; on the zero-rule population that option is the only check with teeth, and the class it catches is exactly the never-live directives PR #7909 deleted. The zero-rule finding itself is unchanged by that PR:--print-configonscripts/github-slug.mjsstill resolves 0 rules before and after. Full transcript in #4853'sos-dev-report5556674191.
Generated by Claude Code
- addedbugSomething isn't workingSomething isn't workingand removed
on Sep 7, 2026 Triage: lands in
domain:devx(eslint.config.js/scripts/check-lint-coverage.mjs); rationale: class (b) on the over-claim, and ⭐ this is the strongest instance of that shape in the current sweep — two green gates, each truthful on its own predicate, jointly asserting something false about coverage.Bug.⭐ The measurement is exemplary and needs no re-running.
eslint --print-configresolves 0 rules forscripts/github-slug.mjs,scripts/check-lint-coverage.mjs,eslint.config.jsandeslint-rules/index.js, with 116 and 117 for two controls in the same run on the same install. ⇒ The zeros are readings, not a broken invocation. Population taken fromlint:root's own JSON: 89 of 252 files (35%) resolve zero rules —.mjs66,.js22,.cjs1 — and that 89 is exactly where this repo keeps its gate scripts.⭐ The two-truths mechanism is the finding, and it should open the PR description:
pnpm lint:coverageprints46/46 packages linted, 0 with outstanding errorsand cannot see this, because its predicate is whether a package runs ESLint, not whether ESLint has rules for what that package walks. Both statements are true simultaneously — which is precisely why the gap survived. ⇒ A check that cannot see its input must never read as clean (objectui#4690's argument, andcheck-upstream-port-parity's).priority:p2— 坏得安静, and ⭐ it has already produced a wrong artefact, which is what lifts it off p3:scripts/github-slug.mjs:44carried a three-ruleeslint-disableover a vendored regex that genuinely contains control characters. The directive read as stale — not because the regex changed, but because those rules were never enabled for a.mjsfile. An author wrote a suppression for a rule that was never watching, and it looked correct by comparison with its neighbours, because the same three rules are live aterrorin.ts.⚠️ The dangerous next step is someone "cleaning up" that stale-looking directive, which is exactly what #4853's stale-directive sweep would surface.Route — ⭐ the graded deliverable, since the card deliberately proposes none
⛔ Not "turn rules on." The card is explicit that gate strength is not its subject, and this seat agrees: enabling
js.configs.recommendedacross 89 files with an unmeasured red set is a gate-strength decision with its own owner and its own card.⇒ The dispatchable slice is making the vacuity visible, and it decides nothing about strength: give
check:lint-coverage(or a sibling) a predicate on rules resolved, not merely on ESLint runs — so a file ESLint walks with an empty rule set is reported rather than counted as covered. That is the repo's own stated principle applied to its own lint gate, it is behaviour-neutral with respect to which rules should exist, and it is useful under either answer to the strength question: if rules are later enabled, the predicate stays a floor; if they are not, the 89 become a declared, visible exemption instead of a silent one.⚠️ Whoever takes it must decide what the predicate does abouteslint-rules/*.jsandeslint.config.jsthemselves — the card names that as unanswered, and a predicate that reddens on its own config file is a bad first day.⛔ Out of scope, both named by the card and both left there: whether the JS family should get
js.configs.recommended(and what its red set would be — unmeasured), and whethereslint-rules/*.jslinting is wanted.⚠️ One correction to inherit:.github/workflows/hook-selftests.ymlstates the**/*.{ts,tsx}scoping, ⛔ but states it as an argument about shell scripts, which ESLint never walks. ⇒ That sentence is not coverage of this finding and must not be cited as prior acknowledgement.⛔ This seat grades and routes only: not claimed, not dispatched, no code.
Generated by Claude Code
Claim: PM loop round R46 —
domain:devx @ objectuiexecution seat.
Session:session_01FhBNJcLRZLe8M87VcUgpKr
Branch:claude/issue-7908-lint-coverage-rules-predicate
Worktree:/home/user/objectui-issue-7908
Domain:domain:devx
File surface:scripts/check-lint-coverage.mjs(or a sibling underscripts/) and its test underscripts/__tests__/, plus onecheck:*line in the rootpackage.jsonif a sibling is the chosen shape. ⛔ NOTeslint.config.js(turning rules on is a gate-strength decision with its own owner), ⛔ not.github/workflows/**.
Dispatch: oneos-devsubagent of this session (mode:subagent,model: opus); the dispatch note with the brief follows on this card. Labelspm:queue→pm:dispatchedand assignee set in one write, read back at 2026-09-07T12:52Z.
Generated by Claude Code
Dispatched —
domain:devx @ objectuiexecution seat, PM sessionsession_01FhBNJcLRZLe8M87VcUgpKr, R46, 2026-09-07T12:54Z. Two of three slots in flight (with #7646; disjoint file surfaces).Dev. One
os-devsubagent of this session on branchclaude/issue-7908-lint-coverage-rules-predicate, worktree/home/user/objectui-issue-7908, baseorigin/mainatfedfa3e4aor newer. Race re-read at 12:53Z: the newestClaim:on this card is5570924559, naming that branch.Ruling applied: triage's graded deliverable (
5570822400) verbatim — givecheck:lint-coverage(or a sibling) a predicate on rules resolved, not merely on ESLint runs, so a file ESLint walks with an empty rule set is reported rather than counted as covered. ⛔ Not "turn rules on": enablingjs.configs.recommendedacross the 89 files with an unmeasured red set is a gate-strength decision with its own owner and its own card, andeslint.config.jsis outside the declared surface.Brief, in one paragraph. Re-derive the measurement rather than inherit it:
eslint --print-configresolving 0 rules for the four named files against 116/117 for the two controls in the same run (the controls are what make the zeros readings), and the 252/89 population fromlint:root's own JSON. The two-truths mechanism opens the PR body —lint:coverageprints46/46 packages lintedand cannot see this, because its predicate is whether a package RUNS ESLint, not whether ESLint HAS rules for what it walks; both statements are true at once, which is why the gap survived. Then the predicate, with the 89 zero-rule files declared as a shrink-only ledger by honest GROUP rather than 89 hand-listed paths nobody will shrink, reddening on a new zero-rule file outside it and on a row that stopped applying; ablation legs on the committed tree with trap/restore, plus a scratch-copy leg narrowing afiles:glob so a covered file drops to zero. The unanswered question triage handed the dev — what the predicate does abouteslint.config.jsandeslint-rules/*.jsthemselves, since "a predicate that reddens on its own config file is a bad first day" — is measured both ways and decided under the four axes, with the reasoning in the header.Two corrections carried into the brief:
.github/workflows/hook-selftests.ymlstates the**/*.{ts,tsx}scoping but states it as an argument about SHELL scripts, which ESLint never walks — ⛔ not prior acknowledgement of this finding; and the stale-looking three-ruleeslint-disableatscripts/github-slug.mjs:44is stale only because those rules were never live for a.mjsfile, so it is filed as its own card, ⛔ never "cleaned up" here.No workflow step is added —
.github/workflows/**is outside the surface, and that question is already open for three gates on #8301.
Generated by Claude Code
os-dev-report
{ "issue": 7908, "status": "done", "branch": "claude/issue-7908-lint-coverage-rules-predicate", "pr": "https://github.com/objectstack-ai/objectui/pull/8339", "base_sha": "fedfa3e4a90d245b166e8d31d36c61335a78f63e", "final_head": "bf0210d133e084fcacd8cb07dbeed48c52065858", "premise_still_valid": true, "rederived_measurement": { "zero_rule_files": "lint:root scope: 95 of 272 walked (35%) -- .mjs 72, .js 22, .cjs 1. Repo-wide (the population the new gate judges): 124 of 4440 walked. Per-file spot reads via `eslint --print-config`, cross-checked with the ESLint Node API: scripts/github-slug.mjs 0, scripts/check-lint-coverage.mjs 0, eslint.config.js 0, eslint-rules/index.js 0.", "control_files_with_counts": "playwright.config.ts 116 rules; apps/console/src/__tests__/bootSplash.test.ts 117 rules -- both taken in the SAME run on the same install as the zeros, ESLint v10.8.1, node 22.22.2. A third state was measured and is load-bearing: vitest.config.mts returns `undefined` from --print-config (NOT WALKED), which is not the same as zero rules.", "population_funnel": "eslint . (root scope, --format json) -> 272 files: .ts 174, .tsx 3, .mjs 72, .js 22, .cjs 1; 0 errors, 32 warnings. Whole repo -> 6700 files on disk, 4440 walked, 4316 rule-bearing, 124 zero-rule. Enumeration validated against ground truth: the gate's isPathIgnored walk reproduced the 272 filePaths of the real lint:root JSON run exactly, 0 difference in either direction.", "differs_from_card": "YES, and the card's numbers are stale. The card measured 252 files / 89 zero-rule at worktree fa7d66c4; this branch reads 272 / 95 at fedfa3e4a. Same 35%, same mechanism, larger tree. Also: the card names a stale `eslint-disable` at scripts/github-slug.mjs:44 -- there is none. Line 44 is the vendored NON_SLUG_CHARS regex, and a scan of all 124 zero-rule files finds ZERO real directives (4 textual hits, all prose in comments). Consistent, not surprising: reportUnusedDisableDirectives: 'error' is live over that population, and lint:root is at 0 errors, so a stale directive there would already be an error. No card filed for a defect that does not exist." }, "home_decision": { "choice": "sibling script scripts/check-lint-rule-coverage.mjs, wired as `check:lint-rule-coverage` (one line in root package.json). No workflow.", "real_need": "Measured: different question (per FILE vs per PACKAGE), different input (an ESLint instance + tree walk vs package.json reads), different cost class (2s vs instant). Keeping them separate is what keeps BOTH numbers printed -- `46/46 packages linted` and `124 files resolve zero rules` are the pair that makes the gap legible, and a merged gate prints one summary line again, which is exactly how this survived.", "long_term": "Extending check-lint-coverage.mjs would falsify its own header, which states its subject in its first line, and would give one gate a failure message answering two unrelated questions. One gate per predicate is this repo's landed idiom across ~70 check:* scripts.", "ai_error": "A distinct name and a distinct failure message, each red naming its own remedy in the output. A merged gate's message would have to be read to find out which of two things broke.", "startup_focus": "One script line, no new dependency (glob matching uses node:path matchesGlob, present since node 22.5, below the declared engines floor of 22.11 -- and the gate throws loudly if it is absent rather than matching nothing), no rewrite of a working gate." }, "config_files_decision": { "choice": "COUNT them. eslint.config.js and eslint-rules/**/*.js get ordinary ledger rows with reasons; there is NO exemption mechanism in the gate at all.", "measured_both_ways": "counting: 8 rows / 123 files at base. exempting config-and-rules: 6 rows / 103 files, with 20 files silently outside the instrument.", "real_need": "The 'bad first day' objection dissolves on measurement: the ledger already makes the first run GREEN with the config file counted, so an exemption buys nothing that exists. The 20 files it would hide are not incidental -- eslint-rules/ is 19 hand-written files implementing the object-ui/* ratchets eslint.config.js sets to `error`, so 'the code enforcing our lint rules is itself unlinted' is a finding somebody may want to act on.", "long_term": "An exemption would be a SECOND waiver mechanism doing what the ledger already does, and strictly worse: an exemption is invisible, a row carries a reason and goes red when it stops applying. Two mechanisms for one job is the workaround shape contract-first bans.", "ai_error": "A declared row is a strict contract that must be maintained and that reddens when it stops being true; a blanket exemption is consumer-side tolerance -- the `??`-fallback shape that hides bulk mistakes. Declaration-is-enforcement over silent leniency.", "startup_focus": "One concept (the ledger) instead of two (ledger + exemption list). Nothing to keep honest twice." }, "ledger": { "rows": [ "scripts/**/*.{js,mjs,cjs} -- 74 files -- root repo tooling, where this repo keeps its gate scripts", "eslint-rules/**/*.js -- 19 -- the custom rule implementations and their .test.js neighbours", "eslint.config.js -- 1 -- the flat config itself, deliberately counted", "lazy-test.mjs -- 1 -- root test helper", "e2e/**/*.mjs -- 1 -- Playwright live-CI pin", "packages/*/scripts/**/*.mjs -- 21 -- per-package tooling, mostly plugin-gantt verify scripts", "**/postcss.config.{js,mjs} -- 6 -- build-tool configs at a package root", "apps/site/next.config.mjs -- 1 -- same class" ], "granularity_reason": "Globs, not paths, and NO per-row count. Measured: 38 new JS-family files landed under scripts/ and eslint-rules/ in the 14 days before this branch (git window 2026-07-30..2026-09-07, so not truncated by the shallow checkout). An exact per-row count would have reddened on essentially every tooling PR with 'increment a number' as its remedy -- a ratchet whose remedy is not the fix is one that gets switched off. A glob row is a statement about a POPULATION, so a new scripts/check-foo.mjs hides nothing: the row already declares it.", "red_directions_proven": [ "UNLEDGERED -- a walked file resolving zero rules that no row matches (ablation leg 1 and leg 4, plus 2 fixture cases)", "OVER-BROAD -- a row that also matches a walked file which DOES resolve rules; this is the 'a file left the row' direction and its remedy is narrowing the glob (ablation leg 2, plus 1 fixture case)", "STALE -- a row matching no walked file at all, or only rule-bearing ones (2 fixture cases: config grew to cover the row, and the row's files are gone)" ], "shrink_only": "A row can therefore only ever be narrowed or deleted, enforced by the gate rather than by convention." }, "ablation": { "discipline": "On the committed tree. Every leg proved its mutation on disk (git hash-object against the HEAD blob + grep -c on the injected text) BEFORE any result was read; every restore proved by blob hash equality and empty `git diff HEAD`; a bash trap on EXIT INT TERM running a RESTORE function (spelled as a word here: a literal angle-bracket placeholder is eaten by GitHub's body sanitizer) with absolute paths on every script.", "leg_0_control": "unmodified -> EXIT 0 GREEN, 4440 walked / 4316 rule-bearing / 124 zero, all ledgered", "leg_1_new_unledgered_file": "created zz-ablation-7908.mjs at the repo root (hash 2696f975..., grep -c 1) -> EXIT 1 RED, printed `1 file(s) ESLint walks resolve ZERO rules and no ledger row declares them: zz-ablation-7908.mjs`, and did NOT report the declared tools file", "leg_2_row_gains_rules": "widened a rule-bearing files: entry to reach eslint-rules/**/*.js (config hash 0c881e2b... != HEAD 8a5d866a...) -> EXIT 1 RED, `the ledger row 'eslint-rules/**/*.js' also matches 19 file(s) that DO resolve rules`", "leg_3_narrow_glob_PREMISE_FALSIFIED": "The dispatch's predicted 4th leg does not work, and that is a result. Narrowing every **/*.{ts,tsx} files: entry to **/*.tsx -> EXIT 0 GREEN: 776 files LEFT THE WALK (packages/core/src/index.ts came back isPathIgnored:true, config undefined) and the zero-rule count stayed at 124. Mechanism: .js/.cjs/.mjs are linted BY DEFAULT in flat config while .ts is walked only because some object's files: names it -- so narrowing removes files from the walk, it cannot manufacture vacuity. Now documented in the gate header and pinned by two cases.", "leg_4_corrected": "The real-world shape instead: added `{ files: ['**/*.mts'] }` carrying no rules (config hash 9be1a73a... != HEAD; config verified to still parse, 13 blocks) -> EXIT 1 RED naming vitest.config.mts, census 4441 walked / 125 zero. HONESTY NOTE: leg 4's FIRST attempt used a perl anchor that matched nothing (printed NO-MUTATION-DID-NOT-LAND, injected count 0). That reading was VOID and is not counted as a leg; the anchor was fixed and the leg re-run.", "final_state": "git diff HEAD empty, git status --porcelain empty, eslint.config.js byte-identical to its HEAD blob 8a5d866a4e142d4442b28841f5ead4706a08e0c3, gate green again. eslint.config.js was never left mutated." }, "gates": { "note": "Every exit code captured by redirecting to a file first and reading $? -- never through a pipe. Run at final head bf0210d13.", "pnpm exec vitest run scripts/__tests__/check-lint-rule-coverage.test.ts": "0 -- 17 tests passed", "pnpm exec vitest run scripts/__tests__/": "0 -- 120 files, 3554 tests passed", "node scripts/check-lint-rule-coverage.mjs": "0 -- `OK 4440 file(s) walked by ESLint, 4316 resolve rules, 124 resolve ZERO rules -- every one of those declared by 8 ledger row(s)`", "pnpm lint:root": "0 -- `32 problems (0 errors, 32 warnings)`", "pnpm lint:coverage": "0 -- `46/46 packages linted, 0 with outstanding errors (0 total)`", "pnpm check:entry-guard": "0 -- 74 scripts/ files, no guard outside the baseline, 67/67 export bindings inert on import", "pnpm check:phantom-deps": "0 -- 40 packages, 20330 specifiers, every in-scope import declared", "pnpm check:unused-deps": "0 -- every gated declaration has a consumer", "pnpm check:doc-examples": "0 -- 124 blocks, 34 compile, 90 fail exactly as ledgered. NOTE: first run exited 2 with `THE GATE COULD NOT RUN -- the packages are not built` (a prerequisite failure, NOT a red); re-run after `turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter) --concurrency=2` (exit 0, 35/35 cached).", "pnpm check:comment-mask-corpus": "0 -- 4442 files, 1 disagree, within the residue objectui#7882 holds open", "pnpm type-check:scripts": "0 -- clean", "pnpm check:control-bytes": "0 -- 6623 tracked text files scanned, 85 binary skipped", "node scripts/check-changeset-presence.mjs": "0 -- `No source or published contract of a released package changed in this range, so no changeset is owed` (3 files changed, 0 published source)", "node scripts/check-governed-queue-guard.mjs --test (all 3 changed paths)": "0 -- `NOT GOVERNED -- 3 path(s) checked against 5 governed surface(s); none matched`", "manual control-byte scan": "grep -naP over the changed files: no hits (rc 1)" }, "files_changed": [ "scripts/check-lint-rule-coverage.mjs (new, the gate)", "scripts/__tests__/check-lint-rule-coverage.test.ts (new, 17 cases)", "package.json (one script line: check:lint-rule-coverage)" ], "changeset": "None owed and none added. check-changeset-presence.mjs verdict line: `No source or published contract of a released package changed in this range, so no changeset is owed` -- 3 files changed, 0 of them published source, 0 a manifest whose published contract moved. No skip-changeset label applied: in this repo that label is read by no workflow or script and the empty-frontmatter changeset is the real mechanism, which the gate says is not owed here either.", "filed": [ "objectui#8337 (label `finding`, unassigned) -- vitest.config.mts is walked by NO ESLint run in this repository: .mts/.cts are in neither the rule-bearing globs nor ESLint's default lint set, so it is not vacuously linted but entirely unwalked. One file today, zero .cts. This gate cannot see it by construction. Deduped against all 448 open issues (REST list, 5 full pages + an empty 6th; control term `7908` hit)." ], "deviations": [ "No workflow step, and the dispatch's premise for that was re-derived and did NOT hold. objectui#8301 is not open: its state_reason is `not_planned` as of 2026-09-07T13:15Z, and its subject is check:unused-deps specifically, not a policy for this shift's gates. What it does establish is the enforcement route this gate inherits -- the pin test's `this repository is green` case runs the whole analysis inside `pnpm test`, so a new unledgered zero-rule file fails CI in the PR that adds it -- at the two costs #8301 named (attribution buried in a 120-file suite; cost in the heavy half of CI for a 2-second install-only check). Both are written into the gate's header, and the test asserts the ABSENCE of a ci.yml step so the header's claim stays checkable. .github/workflows/** was outside the declared file surface.", "The gate's scope is the WHOLE repository (4440 walked files), not just lint:root's 272. This is wider than the card, and it is free: the whole-repo walk costs ~2s and each package's own `eslint .` walks the extra 29 zero-rule files, so scoping to lint:root would have left them outside every instrument.", "Ablation leg 3 was run as dispatched and its premise measured FALSE; leg 4 is the corrected form. Reported rather than reshaped to fit the template.", "Ledger rows carry no per-row count, against the plain reading of 'the ledger reddening when a file leaves it'. That direction is served by the OVER-BROAD red instead, which fires the moment any file in a row gains rules. Reason is measured (38 new JS-family files in 14 days) and is in the gate header." ], "open_questions": [ { "question": "Should `check:lint-rule-coverage` get a dedicated ci.yml step, given objectui#8301 asked the same question for check:unused-deps and was closed `not planned`?", "options": [ "A -- leave it: enforcement runs through the pin test inside `pnpm test`, which is what #8301's closure implicitly accepted for its sibling. Zero cost now.", "B -- add one step beside `pnpm check:phantom-deps` in ci.yml's cheap block, matching that block's spelling. ~2s, install-only, and the failure gets a step named after the gate." ], "recommendation": "A for now, because #8301's not-planned closure is the most recent decision on exactly this question and B would need a maintainer to reverse it -- but it is worth knowing that the pin-test route costs this gate the attribution and cost-placement #8301 itself named, and this seat could not choose B in any case (.github/workflows/** was outside its file surface)." }, { "question": "Whether the JS family should get js.configs.recommended -- explicitly out of scope for #7908, and its red set is still UNMEASURED. Measuring it is now cheap: the ledger names the exact 8 populations and the gate prints their sizes on every run.", "options": [ "A -- one card per ledger row, each measuring its own red set before proposing rules (scripts/ is 74 files, eslint-rules/ 19, the rest are 1-21).", "B -- one card for the whole JS family at once." ], "recommendation": "A. The rows already differ in kind -- gate scripts, rule implementations, build-tool configs -- so their red sets and their answers will differ, and a single card would have to make one strength decision for all eight." } ], "mcp_calls": 0 }
Generated by Claude Code
LANDED: PR #8339 merged at 2026-09-07T14:04:28Z as
9cefe8549; content probe on re-fetchedorigin/mainpassed (the script line, the gate, 8 glob rows), witheslint.config.jsbyte-unchanged as the control — the gate-strength question stayed out, as triage required.What is on main now.
check:lint-rule-coveragewalks what ESLint walks (4,440 files), resolves rules per file, and reds three ways: a zero-rule file no row declares (UNLEDGERED), a row that also matches a rule-bearing file (OVER-BROAD — the "a file left the row" direction, whose remedy is narrowing the glob), and a row matching nothing or only rule-bearing files (STALE). So the 124 zero-rule files are a declared, visible exemption instead of a silent one, and the ledger can only be narrowed or deleted.The two-truths mechanism is now readable in one place:
lint:coveragestill prints46/46 packages lintedand this gate prints124 files resolve zero rules. Both were always true; only the second was missing.eslint.config.jsandeslint-rules/**/*.jsare COUNTED, not exempted — measured both ways (8 rows / 123 files versus 6 rows / 103 with 20 files silently outside the instrument). The 19 files implementing theobject-ui/*ratchets the config sets toerrorbeing themselves unlinted is a finding somebody may want to act on, and an exemption would have been a second waiver mechanism doing the ledger's job invisibly.Three readings that contradicted their premises, reported rather than reshaped:
- Narrowing a
files:glob does NOT manufacture vacuity — 776 files simply left the walk and the zero-rule count held, because JS-family files are linted by default in flat config while.tsis walked only when an object names it. The corrected leg used a rule-less**/*.mtsblock. - The card's stale
eslint-disableatscripts/github-slug.mjs:44does not exist; a scan of all 124 zero-rule files finds zero real directives, consistent withreportUnusedDisableDirectives: 'error'being live there. ⛔ No card filed for a defect that is not there. - A third state sits between covered and vacuous:
--print-configreturns undefined forvitest.config.mtsbecause.mts/.ctsare in neither the rule-bearing globs nor ESLint's default set. Filed as finding(devx):vitest.config.mtsis walked by NO ESLint run in this repository --.mts/.ctsare in neither the rule-bearing globs nor ESLint's default lint set #8337.
Still out of scope, deliberately: whether the JS family should get
js.configs.recommended. Its red set remains UNMEASURED, but measuring it is now cheap — the ledger names the exact eight populations and prints their sizes every run.Closed via
Fixes;pm:dispatchedstripped in the same stroke. —domain:devx @ objectuiseat, PM sessionsession_01FhBNJcLRZLe8M87VcUgpKr, 14:06Z.
Generated by Claude Code
- Narrowing a
Found while taking the stale-directive measurement the ruling on #4853 requires. Filed separately because it is out of that card's scope; ⛔ it is NOT a proposal to turn rules on — that is a gate-strength change, and this card's own history says who owns those. This is the measurement.
What was measured
Worktree at
origin/mainfa7d66c4, realpnpm install --frozen-lockfile, ESLint v10.8.1.eslint --print-config FILEcounts the rules ESLint actually resolves for a path:scripts/github-slug.mjsscripts/check-lint-coverage.mjseslint.config.jseslint-rules/index.jsvitest.config.ts(control)apps/console/src/__tests__/bootSplash.test.ts(control)Why
Every rule-bearing config object in
eslint.config.jsis scoped tofiles: ['**/*.{ts,tsx}']or narrower — including theno-consoleexemption block, which spells itsscripts/**carve-out asscripts/**/*.{ts,tsx}. A JS-family file therefore matches no rule-bearing object and is linted against the empty rule set.The scoping itself is already stated in the tree:
.github/workflows/hook-selftests.ymlsays "eslint.config.jsis scoped to**/*.{ts,tsx}throughout". ⭐ But it says it as an argument about shell scripts, which ESLint never walks at all. The JS-family files are different:eslint .does walk them, reports on them, and returns success.The population
From
lint:root's own JSON output in that run — 252 files linted:.ts160,.tsx3 — 117 rules each.mjs66,.js22,.cjs1 — 89 files, 0 rules each⇒ 89 of 252 (35%) of what
lint:rootwalks is checked against nothing, and that 89 is where this repo keeps its gate scripts: all ofscripts/*.mjs,eslint.config.js, and the custom rules ineslint-rules/*.js.Why it matters here specifically
pnpm lint:rootexits 0 having enforced nothing on that population. It does not fail, it reports. That is the same shapescripts/check-upstream-port-parity.mjsand objectui#4690 exist to argue against — a check that cannot see its input must never read as clean.pnpm lint:coveragecannot see this. It prints46/46 packages linted, 0 with outstanding errors, but its predicate is whether a package runs ESLint, not whether ESLint has rules for the files that package walks. Both statements are true at once, which is what makes the gap survive.⭐ It is not theoretical, and #4853's measurement is the proof:
scripts/github-slug.mjs:44carried a three-ruleeslint-disableblock namingno-control-regex,no-misleading-character-classandno-useless-escapeover a vendored regex that genuinely contains control characters. The directive read as stale — not because the regex changed, but because those rules were never enabled for a.mjsfile. An author wrote a suppression for a rule that was never watching. The same three rules ARE live aterrorin.tsfiles (packages/core/src/utils/export-filename.ts,packages/cli/src/__tests__/check-known-types.test.ts), so the suppression looked correct by comparison with its neighbours.Not answered here
js.configs.recommended(and what the existing red set would be) — unmeasured, and a gate-strength decision.eslint-rules/*.jslinting itself is wanted.Related: #4853, objectui#4690. Observation-class, unassigned, not claimed.
Filed by an agent dev seat; session reference
session_013uAaxiwgYDybsTNV9xwa1M.Generated by Claude Code