A strict linter

Biome with the opinionated rules switched on, run read-only in CI

Lint is the cheapest teacher

A linter tells an agent it is wrong within a second, with the line number, every time. That makes it the best place to encode style rules: an agent learns the house style from lint errors far faster than from prose.

Both repositories use Biome, pinned to an exact version, with many rules raised from "suggestion" to "error".

Rules worth switching on

From the tickets biome.json:

Rule Why it matters with agents
noExcessiveCognitiveComplexity (12 in src/) Stops the 80-line function with six nested branches. Agents grow functions instead of splitting them.
noForEach Forces for...of or the curried #fp helpers.
noAccumulatingSpread [...acc, item] inside a reduce is O(n²). Agents write it constantly.
noDoubleEquals with ignoreNull: false No loose equality, not even for null. This also kills a whole class of mutants before tests run.
noUnusedImports, noUnusedVariables, noUnusedFunctionParameters Debris from refactors does not survive.
noConsole Logging goes through the logger. Scripts are the exception.
noFocusedTests, noSkippedTests An agent cannot .only or .skip its way to a green run.
noExcessiveLinesPerFile (1,000) A hard ceiling under the softer file-length ratchet.
useThrowNewError, useConst, noVar, useTemplate One way to write each common thing.

The chobble-template goes further on complexity: its shared base config sets maxAllowedComplexity to 7 as an error, with 30 allowed in tests. Seven is low. It forces small functions composed with pipe, which is what the codebase wants anyway.

The rest of the strict config

The table shows the rules with the best stories. The tickets config turns on many more, all as errors. Most are small "one way to write it" rules. Each one alone is minor. Together they mean an agent's code looks like the rest of the codebase, so a reviewer reads meaning, not style.

Three settings go beyond single rules:

  • "domains": { "project": "all" } turns on every rule in Biome's project domain, the rules that need to see more than one file at a time.
  • The assist actions useSortedKeys, useSortedProperties, useSortedAttributes and useSortedInterfaceMembers are on. Object keys, JSX attributes and interface members are in alphabetical order, so a diff never argues about order, and two agents editing the same object do not both append to the end.
  • Cognitive complexity is set to warn, not error, but the gate runs with --error-on-warnings. A warning fails CI the same as an error.

Overrides by folder, not by line

Strictness changes by folder, in biome.json, where a reviewer can see it. Production code under src/ gets a complexity limit of 12, against 13 elsewhere. Tests may have unused variables and parameters and may log. Scripts may log. The logger itself may call console. There are no biome-ignore comments scattered through the code for an agent to copy.

A few rules are off. noUnresolvedImports and noUndeclaredDependencies are off because Deno resolves imports through its own import map, which those rules do not read. The unresolved-import job moves to the type checker (see Strict type checking).

The shared base config

The chobble-template keeps its rules in packages/js-toolkit/configs/biome.base.json, and every project's biome.json extends it. One change to the base reaches every site. On top of the rules above, the base adds noParameterAssign, noEmptyBlockStatements, useIsArray, noGlobalDirnameFilename, useNodejsImportProtocol, and the nursery rule noUnnecessaryConditions, which flags an if that can only ever go one way. Tests get Biome's test domain in full. The same configs/ folder holds shared base configs for jscpd, Knip and Bun's test runner.

Stylesheets have a linter too. The template's .stylelintrc.json runs Stylelint with the standard SCSS rules, a fixed property order, and a class-name pattern that allows only kebab-case with optional BEM __element and --modifier parts.

Two tasks: one to fix, one to judge

The tickets repository splits linting into two tasks:

  • deno task lint runs biome check --write --unsafe. Formatting and fixable findings, including unsafe fixes like removing unused imports, heal themselves.
  • deno task lint:ci runs biome check --error-on-warnings and changes nothing. This is the gate in precommit and CI, so local precommit is exactly as strict as CI and never edits the checkout while it is judging it.

A manual GitHub workflow, biome-fix.yml, runs the fixing task on a chosen branch and opens a pull request with the result, for the occasional branch that lands badly formatted.

Lint as a mutation filter

The mutation tester runs Biome on every mutant before running any tests. A mutant that turns === into == is rejected by noDoubleEquals and counts as killed for free. A strict lint config therefore makes mutation testing faster, and it means the list of known-equivalent mutants never needs entries for loose equality.