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.
- Complexity:
noStaticOnlyClass,noThisInStatic,noUselessStringConcat,noUselessTernary,noUselessThisAlias,noUselessUndefined,useArrowFunction,useDateNow,useFlatMap,useLiteralKeys,useOptionalChain,useRegexLiterals,useWhile. - Style:
noUnusedTemplateLiteral,noUselessElse,noYodaExpression,useAsConstAssertion,useCollapsedElseIf,useConsistentArrayType,useConsistentArrowReturn,useExponentiationOperator,useNumberNamespace,useShorthandAssign,useShorthandFunctionType,useSingleVarDeclarator,useTrimStartEnd. - Suspicious:
noAssignInExpressions,noImplicitAnyLet. - Nursery (new rules Biome has not finished yet):
useSortedClasses, which keeps the class names in aclassattribute in a fixed order, beside the file-length ceiling in the table.
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,useSortedAttributesanduseSortedInterfaceMembersare 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, noterror, 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 lintrunsbiome check --write --unsafe. Formatting and fixable findings, including unsafe fixes like removing unused imports, heal themselves.deno task lint:cirunsbiome check --error-on-warningsand 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.