Skip to content

chore(lint): tighten Biome to 96 rules beyond recommended - #9

Draft
sergeyzenchenko wants to merge 1 commit into
mainfrom
claude/linter-rules-strictness-40j5vj
Draft

chore(lint): tighten Biome to 96 rules beyond recommended#9
sergeyzenchenko wants to merge 1 commit into
mainfrom
claude/linter-rules-strictness-40j5vj

Conversation

@sergeyzenchenko

Copy link
Copy Markdown
Member

What this is

An empirical audit of Biome 2.5.10's rule surface against this repo, plus the tightening it justified.

Method: enumerated all 484 rules from Biome's config schema, enabled every one of the 302 non-recommended rules at error in a throwaway config, and linted the repo with --reporter=json. That produced 13,690 diagnostics across 111 rules — and, more usefully, 196 rules that fire zero times. A rule at zero violations is free: it changes no code today and stops the pattern from arriving tomorrow. That is most of what landed.

Full write-up with per-rule counts: docs/reviews/2026-08-lint-strictness.md.

Changes

pnpm lint now fails on warnings. This was the largest soundness hole and had nothing to do with which rules were on. biome check exits 0 when every diagnostic is warn/info, and 87 of the 182 recommended rules default to warn (57) or info (30)useConst, noApproximativeNumericConstant, useDefaultSwitchClauseLast, noConstEnum. Verified directly: a file with a useConst violation reports "Found 1 warning" and exits 0. The repo passes clean under --error-on-warnings, so this closed the hole without a code sweep.

Type-aware rules, all at zero violations: noFloatingPromises, noMisusedPromises, useExhaustiveSwitchCases, noUnsafePlusOperands, noUselessTypeConversion. noFloatingPromises returning zero on a codebase this async-heavy was surprising enough to verify against a synthetic floating promise — the rule fires, the repo is simply clean. Locking that in is the most valuable line in the diff, given the engine's correctness rests on awaited journal writes. These do not require domains.project (confirmed with project: "none"), which matters: that domain would also drag in noUnresolvedImports and noUndeclaredDependencies, both unusable here.

useImportExtensions, scoped. packages/*/src is already 100% extension-ful (293 .ts + 1 .tsx relative imports, zero extensionless) because the published packages are NodeNext ESM, where a missing extension is a runtime ERR_MODULE_NOT_FOUND. All 251 violations live in apps/ui, which is Vite-bundled. On globally, off for apps/ui via an override — zero code changed, and the "works in tests, breaks on install" class is now unrepresentable in the shipped packages.

96 rules enabled beyond recommended across suspicious (24), style (29), correctness (14), nursery (18), complexity (9), security (2).

Code changes

Nine small fixes, each unlocking a rule:

Fix Rule unlocked
core/src/jsonschema.tsconst seen = (ancestors ??= new WeakSet()) ×2; the mutation was dead, recursion already passes seen down noAssignInExpressions, noParameterAssign
cli/src/commands/answer.ts — parameter reassignment → locals noParameterAssign
gate/src/load.ts, store-fs/src/journal.ts, examples/03, isolation/test/tmp/nested.mts — string concat → template useTemplate, noUselessStringConcat
apps/ui/src/domain/views.ts!!valuevalue !== undefined noImplicitCoercions

And four real defects the test-integrity rules found:

  • core/test/review-regressions.test.ts had two describe blocks titled "codex review findings, round 33 (PR Initial Weft implementation: durable multi-agent workflows #1)"
  • host/test/task-removal-durability.test.ts declared afterAll before beforeEach
  • daemon/test/api.test.ts exported a type from a test file
  • gate/test/gate.test.ts used Array(4) rather than new Array(4)

Alongside them: noFocusedTests (a stray .only silently shrinks CI to one test), noSkippedTests, noDuplicateTestHooks, useTestHooksOnTop, noExcessiveNestedTestSuites.

What was deliberately rejected

Nine rules produce false positives from Biome's inference — every finding was read in context and is wrong. noUnresolvedImports (74) claims react has no StrictMode or Fragment; noUnnecessaryConditions (61) calls if (readyTimer.current) always-falsy because it cannot model a React ref; useAwaitThenable (5) flags await on T | Promise<T> unions; useNullishCoalescing (1) flags a deliberate || where 0 must fall through. These are worth re-testing on each Biome upgrade.

Deferred real debt, ranked: useErrorCause (21 rethrows discarding the original stack — highest value), noEvolvingTypes (10), noShadow (36), noExplicitAny (49, concentrated in 8 files and adoptable as a scoped override rather than a blanket off), noNonNullAssertion (241, of which 184 are tests).

Framework rules for stacks this repo doesn't use were excluded outright — note Biome runs them regardless of what's installed, so noReactSpecificProps (604) and noSolidDestructuredProps (213) are noise, not findings.

Verification

  • pnpm lint — clean under --error-on-warnings
  • pnpm typecheck — clean
  • pnpm test — 992 passed, 1 pre-existing failure (daemon/test/api.test.ts EACCES case, which cannot fail as root; confirmed identical on the untouched tree)
  • pnpm build, pnpm verify:examples — clean
  • pnpm sync:meta --check — fails on packages/testing/README.md, pre-existing on main, unrelated

Lint wall-clock goes from 0.65s → 7s — the type-inference scanner. That's the price of noFloatingPromises, in a 10-minute CI job.

Note for reviewers

18 of the 96 rules are in Biome's nursery group, which is explicitly unstable. @biomejs/biome is pinned to an exact 2.5.10, so they can't shift under CI — but a Biome upgrade should re-run this measurement.


Generated by Claude Code

Enumerated all 484 Biome 2.5.10 rules, enabled every non-recommended one at
`error` in a throwaway config, and linted the repo to measure real cost:
13,690 diagnostics across 111 rules — and 196 rules that fire zero times.
The zero-cost set is what landed.

`pnpm lint` now fails on warnings. `biome check` exits 0 when every
diagnostic is warn/info, and 87 of the 182 recommended rules default to that
severity — useConst, noApproximativeNumericConstant, useDefaultSwitchClauseLast
and friends were all advisory in CI. The repo passes clean under the stricter
gate, so this closes the hole without a code sweep.

Type-aware rules, all at zero violations: noFloatingPromises, noMisusedPromises,
useExhaustiveSwitchCases, noUnsafePlusOperands, noUselessTypeConversion. They do
not need `domains.project`, which matters — that domain would also drag in
noUnresolvedImports and noUndeclaredDependencies, both unusable here.

useImportExtensions is on for the packages and off for apps/ui: packages/*/src is
already 100% extension-ful because it ships NodeNext ESM, while all 251
violations are Vite-bundled UI code where extensionless is idiomatic.

Small fixes unlocked four more rules — noAssignInExpressions (a dead
`ancestors ??=` in jsonUnsafeAt), noParameterAssign, useTemplate,
noUselessStringConcat — and the test-integrity rules caught four real defects:
duplicate `round 33` describe block, afterAll declared before beforeEach, a type
exported from a test file, and bare `Array(4)`.

Rejected rules are recorded with counts and reasons in
docs/reviews/2026-08-lint-strictness.md, including nine whose findings are
false positives from Biome's inference and should be re-tested on upgrade.

Lint wall-clock: 0.65s -> 7s, the type-inference scanner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017nyvcrD1We7vV3Tjsjdq9j

Copy link
Copy Markdown
Member Author

CI: Build & packaging is red on main, not on this PR

5 of 6 checks pass — Lint (biome), Typecheck, Test (node 22), Test (node 24), Examples & CLI smoke. The one failure is Build & packaging, at its first step, pnpm sync:meta --check:

package metadata is out of sync:
  packages/provider-claude/README.md is out of date
  packages/provider-codex/README.md is out of date
  packages/provider-mock/README.md is out of date
  packages/testing/README.md is out of date

It is not this PR's. This branch's base is d9b7bdf, which is origin/main exactly (main is one commit behind this head, and that commit is mine). main's own CI run on d9b7bdf fails the same single job. I also reproduced it locally on the stashed, untouched tree — same four files. This PR touches no package.json metadata and no package README.

I did not spend the re-run: sync:meta --check is a byte comparison of generated files against the template, so it fails deterministically. Two independent reproductions already establish what a re-run would.

Why I did not port the obvious fix

The error says run pnpm sync:meta and commit the result, but that would be destructive here. scripts/sync-package-meta.mjs rewrites each package README wholesale from a fixed template — install line, source/issues/license links — and the four drifted files each carry a hand-written usage section that the template has no slot for. Running the generator deletes 62 lines of authored documentation:

 packages/provider-claude/README.md | 13 -------------
 packages/provider-codex/README.md  | 15 ---------------
 packages/provider-mock/README.md   | 11 -----------
 packages/testing/README.md         | 23 -----------------------
 4 files changed, 62 deletions(-)

For example packages/provider-mock/README.md loses the mock(...)/mockSequence(...) snippet and the paragraph explaining what strict mode rejects and what profile advertises. That is real content someone wrote on purpose, not drift.

So the generator and the hand edits are in genuine conflict, and picking a side is a docs/tooling call rather than a lint change. I ran pnpm sync:meta, read the diff, and reverted it rather than smuggle a documentation deletion into this PR.

Proposed fix, for a separate PR

Teach the generator to preserve an authored region instead of overwriting it — keep everything between a pair of markers and regenerate only the surrounding boilerplate:

# @techery/weft-provider-mock
…generated header, description, install…

<!-- sync:meta keep -->
```ts
import { mock, mockSequence } from "@techery/weft-provider-mock";
…
  • Source: … (generated footer)

`readme(pkg, dir)` would splice the existing file's keep-block into the template before comparing, so `--check` stays a real gate on the boilerplate while authored usage survives. The alternative — deleting the four sections to match the template — loses documentation that belongs on the npm package pages, so I would not recommend it.

Happy to open that PR if it is wanted; it is unrelated to the lint work here, and this PR's own six checks are otherwise green.

---
_Generated by [Claude Code](https://claude.ai/code)_

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant