Skip to content

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

@claude

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/main fa7d66c4, real pnpm install --frozen-lockfile, ESLint v10.8.1. eslint --print-config FILE counts the rules ESLint actually resolves for a path:

file rules resolved
scripts/github-slug.mjs 0
scripts/check-lint-coverage.mjs 0
eslint.config.js 0
eslint-rules/index.js 0
vitest.config.ts (control) 116
apps/console/src/__tests__/bootSplash.test.ts (control) 117

Why

Every rule-bearing config object in eslint.config.js is scoped to files: ['**/*.{ts,tsx}'] or narrower — including the no-console exemption block, which spells its scripts/** carve-out as scripts/**/*.{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.yml says "eslint.config.js is 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:

  • .ts 160, .tsx 3 — 117 rules each
  • .mjs 66, .js 22, .cjs 1 — 89 files, 0 rules each

⇒ 89 of 252 (35%) of what lint:root walks is checked against nothing, and that 89 is where this repo keeps its gate scripts: all of scripts/*.mjs, eslint.config.js, and the custom rules in eslint-rules/*.js.

Why it matters here specifically

⚠️ The failure direction is this repo's least visible one: pnpm lint:root exits 0 having enforced nothing on that population. It does not fail, it reports. That is the same shape scripts/check-upstream-port-parity.mjs and objectui#4690 exist to argue against — a check that cannot see its input must never read as clean.

⚠️ pnpm lint:coverage cannot see this. It prints 46/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:44 carried a three-rule eslint-disable block naming no-control-regex, no-misleading-character-class and no-useless-escape over 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 .mjs file. An author wrote a suppression for a rule that was never watching. The same three rules ARE live at error in .ts files (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

  • Whether the JS family SHOULD get js.configs.recommended (and what the existing red set would be) — unmeasured, and a gate-strength decision.
  • Whether eslint-rules/*.js linting 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

Activity

  1. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    on Sep 6, 2026
  2. claude commented on Sep 6, 2026

    @claude
    ContributorAuthor

    Measurement relevant to this card, from #4853's flip dispatch (PR #7934) — recorded by pointer, ⛔ no grade or label change. domain:devx @ objectui execution seat, PM session session_013uAaxiwgYDybsTNV9xwa1M, R44, 2026-09-06T03:41Z.

    Leg B of that PR's reverse verification planted // eslint-disable-next-line no-debugger in scripts/github-slug.mjs — one of the 89 files this card measures as linted against zero rules — with linterOptions.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-config on scripts/github-slug.mjs still resolves 0 rules before and after. Full transcript in #4853's os-dev-report 5556674191.


    Generated by Claude Code

  3. added theissue type on Sep 7, 2026
  4. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    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-config resolves 0 rules for scripts/github-slug.mjs, scripts/check-lint-coverage.mjs, eslint.config.js and eslint-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 from lint:root's own JSON: 89 of 252 files (35%) resolve zero rules — .mjs 66, .js 22, .cjs 1 — 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:coverage prints 46/46 packages linted, 0 with outstanding errors and 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, and check-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:44 carried a three-rule eslint-disable over 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 .mjs file. 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 at error in .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.recommended across 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 about eslint-rules/*.js and eslint.config.js themselves — 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 whether eslint-rules/*.js linting is wanted.

    ⚠️ One correction to inherit: .github/workflows/hook-selftests.yml states 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

  5. claude commented on Sep 7, 2026

    @claude
    ContributorAuthor

    Claim: PM loop round R46 — domain:devx @ objectui execution 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 under scripts/) and its test under scripts/__tests__/, plus one check:* line in the root package.json if a sibling is the chosen shape. ⛔ NOT eslint.config.js (turning rules on is a gate-strength decision with its own owner), ⛔ not .github/workflows/**.
    Dispatch: one os-dev subagent of this session (mode:subagent, model: opus); the dispatch note with the brief follows on this card. Labels pm:queue → pm:dispatched and assignee set in one write, read back at 2026-09-07T12:52Z.


    Generated by Claude Code

  6. claude commented on Sep 7, 2026

    @claude
    ContributorAuthor

    Dispatched — domain:devx @ objectui execution seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr, R46, 2026-09-07T12:54Z. Two of three slots in flight (with #7646; disjoint file surfaces).

    Dev. One os-dev subagent of this session on branch claude/issue-7908-lint-coverage-rules-predicate, worktree /home/user/objectui-issue-7908, base origin/main at fedfa3e4a or newer. Race re-read at 12:53Z: the newest Claim: on this card is 5570924559, naming that branch.

    Ruling applied: triage's graded deliverable (5570822400) verbatim — 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. ⛔ Not "turn rules on": enabling js.configs.recommended across the 89 files with an unmeasured red set is a gate-strength decision with its own owner and its own card, and eslint.config.js is outside the declared surface.

    Brief, in one paragraph. Re-derive the measurement rather than inherit it: eslint --print-config resolving 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 from lint:root's own JSON. The two-truths mechanism opens the PR body — lint:coverage prints 46/46 packages linted and 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 a files: glob so a covered file drops to zero. The unanswered question triage handed the dev — what the predicate does about eslint.config.js and eslint-rules/*.js themselves, 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.yml states 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-rule eslint-disable at scripts/github-slug.mjs:44 is stale only because those rules were never live for a .mjs file, 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

  7. claude commented on Sep 7, 2026

    @claude
    ContributorAuthor

    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

  8. claude commented on Sep 7, 2026

    @claude
    ContributorAuthor

    LANDED: PR #8339 merged at 2026-09-07T14:04:28Z as 9cefe8549; content probe on re-fetched origin/main passed (the script line, the gate, 8 glob rows), with eslint.config.js byte-unchanged as the control — the gate-strength question stayed out, as triage required.

    What is on main now. check:lint-rule-coverage walks 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:coverage still prints 46/46 packages linted and this gate prints 124 files resolve zero rules. Both were always true; only the second was missing.

    eslint.config.js and eslint-rules/**/*.js are 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 the object-ui/* ratchets the config sets to error being 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:

    1. 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 .ts is walked only when an object names it. The corrected leg used a rule-less **/*.mts block.
    2. The card's stale eslint-disable at scripts/github-slug.mjs:44 does not exist; a scan of all 124 zero-rule files finds zero real directives, consistent with reportUnusedDisableDirectives: 'error' being live there. ⛔ No card filed for a defect that is not there.
    3. A third state sits between covered and vacuous: --print-config returns undefined for vitest.config.mts because .mts/.cts are in neither the rule-bearing globs nor ESLint's default set. Filed as finding(devx): 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 #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:dispatched stripped in the same stroke. — domain:devx @ objectui seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr, 14:06Z.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopriority:p2tooling

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions