Linter as Law
Hard limits beat soft conventions.
A standard nobody enforces is a standard nobody respects. The line that lives in CONTRIBUTING.md is a wish; the line that lives in eslint.config.mjs is law.
Opinion
I've worked on enough codebases that arrived with a glossy CONTRIBUTING.md and a `legacy/` folder the size of a small city to know which direction the gravity runs. The standard nobody can break is the only standard the codebase keeps. Every limit the team agrees to in a retrospective (“functions should be short,” “no `any`s, please,” “use `===`”) survives precisely as long as the next deadline. Encode it as a lint error and the deadline argues with the linter, not with the reviewer; the reviewer wins every time.
That is why so many of the principles a reader might expect to see as their own tenets do not appear. Strict equality is not a Style tenet because eqeqeq is one line of config. Annotated returns is not a Types tenet because @typescript-eslint/explicit-function-return-type already does the job. Direct imports is not an Architecture tenet because no-restricted-imports covers it. Type-file naming is not a Types tenet because unicorn/filename-case handles it. Tenets argue principles; the linter executes them. The two surfaces have different jobs.1ESLint flat config reference. The catalogue of rules and their severity levels (off / warn / error). The execution surface for every claim in this tenet.
The corollary, the part Patterns and Architecture both keep colliding with, is that the linter has a ceiling.6A Philosophy of Software Design (2018). The strongest book-length statement that style tooling is a proxy for the thing teams actually care about; reaches the boundary the Patterns pillar exists to police. It cannot say that EmailNotificationServiceFactoryAdapter is the wrong name; it can say the class exceeds the file-size cap. It cannot say the Singleton is hiding global state; it can say the file has more than one default export. It cannot say that two functions with different signatures compute the same answer; it can say they share twenty tokens. The work that judgement does (naming, decomposition, pattern fit) is what PT1 Patterns Are Vocabulary, AI2 Persistent Brief, P7 Short-Lived Branches with Review all carry. Name the boundary; do not pretend the gate is the ceiling.
The third habit, the one I will die on the hill of: never disable a rule inline. The eslint-disable-next-line comment is the bug; the rule is doing its job. If the code earns the suppression, the rule is wrong: change it in eslint.config.mjs with a justification, with a scope, and with the name of the person who agreed. If the code does not earn it, fix the code. Either path is editable, audit-able, and reviewable; an inline comment is none of those things.
Copy a note and link
Grab this short comment and drop it into a PR comment or an LLM chat to prompt the right change.
Anything not enforced will erode. Every quality gate is an error, never a warning. The limit goes in `eslint.config.mjs` before the first violation lands; if you reach for `eslint-disable`, fix the code instead. /tenet/linter-as-law/TA1
AI eyes only
Rule: tooling enforces what prose suggests. If the rule matters, encode it.
Reject: prose-only rules in AGENTS.md / CLAUDE.md that have a lint, type, or test equivalent. Reject: silencing a rule inline with eslint-disable, // @ts-ignore, or --no-verify. Reject: shipping with warnings unaddressed.
Generate: every load-bearing rule has a build gate (lint, type, test, format, dup, coverage). The brief teaches only what the gates cannot reach.
Diagnostic: the build must fail for any violation of a load-bearing rule. If the build passes for a violation, the rule is not a rule yet.
Why?
- Settle every recurring style argument once. The rule lives in
eslint.config.mjsand the team stops re-litigating function length, equality, return annotations or import direction in every pull request. - Catches the violation at the keystroke, not the merge. The IDE underlines the line as you write it — per P4 Continuous Quality Feedback the cost of the fix is exponentially smaller before the PR opens.
- Forces the agent to fix the code rather than negotiate the rule. A polite ask in AGENTS.md decays under length pressure; a build error does not.
- Removes the silent-suppression class of bug. The codebase has no
eslint-disable-next-linecomments to grep for, because every legitimate exception lives ineslint.config.mjswith a scope and a justification. - Compounds with every other tenet. S2 Cyclomatic Caps, S3 Parameter Object, T2 No Escape Hatches all rely on the same gate; one configured rule does the work all three principles ask for.
- Reviewer attention shifts from style to substance. The boring class of objection (“this function is too long”) never reaches review; the interesting class (“this function is doing two things”) is what the team actually argues.
- Forces an honest perimeter. The rule list is the floor; the things the linter cannot reach — pattern fit, naming taste, decomposition judgement — are flagged as the work of culture and review, not silently ignored.
Origins
The principle predates the linter. Brian Kernighan and P. J. Plauger's The Elements of Programming Style (1974) pushed the position that style is not taste but contract: code is a thing other humans must read, and the rules that govern it are the rules that survive contact with the next maintainer. The book was written before continuous integration existed; the discipline still had to live in the head.2The Elements of Programming Style (McGraw-Hill, 1974; 2nd ed. 1978). The earliest book-length statement that style is contract, not taste, and the source of the rule numbers many later style guides cite directly.
Stephen C. Johnson's Lint, a C Program Checker (Bell Labs Computing Science Technical Report 65, 1977) is the artefact that gave the family its name. Lint stripped portability and correctness checks out of the C compiler so the compiler could stay fast while the policing got more aggressive.5Lint, a C Program Checker (Bell Labs Computing Science Technical Report 65, 1977). The progenitor of every modern linter; introduced the rule-engine-alongside-the-compiler shape that ESLint, Ruff, Checkstyle and PHP-CS-Fixer all inherit. Every modern lint — ESLint, Ruff, Checkstyle, PHP-CS-Fixer — is the same shape: a rule engine that runs alongside the compiler, gates the build, and keeps the style decisions out of the reviewer's hands.
The treat-warnings-as-errors discipline was already baked into Microsoft's compiler defaults by the early 2000s.7Compiler options: errors and warnings. The language-vendor's own statement of the treat-warnings-as-errors discipline at the compiler level, predating ESLint by a generation. The C# team made the case in their own documentation that the build is the cheapest place to catch a class of defects, and the warning is the awkward middle child of the diagnostic family: too soft to act on, too noisy to ignore. Either the rule is an error or it should not exist. Rust's lint subsystem formalised the same shape with #[deny] attributes; the Go team decided to ship gofmt as a hard formatter rather than a warning so that there would be nothing left to debate.
Martin Fowler's 2006 paper Continuous Integration placed lint as the first stage of the deployment pipeline: a ten-minute build that catches the easy class of defects so reviewers can spend their attention on the hard ones.8Continuous Integration (2006). Lint as the first stage of the deployment pipeline; the ten-minute build that catches the easy class of defects so reviewers can spend attention on the hard ones. The whole P4 Continuous Quality Feedback tradition runs through this observation: the cost of catching a defect is exponential in the time between writing it and finding it.
Quotes
The new program, lint, examines C source programs, detecting a number of bugs and obscurities … lint is intended to enforce the type rules more strictly than the C compiler does.
The fundamental benefit of continuous integration is that it removes sessions where people spend hours hunting bugs … the longer a defect stays in your code, the more expensive it is to remove.
The first step is acknowledging the data. I think anyone with sufficient programming experience accepts the inevitability of bugs in any large body of code, but they may not have ever quantified their experience.
A warning is a thing the compiler is willing to look the other way on. The team that tolerates warnings is the team that has chosen which subset of its standards to enforce by accident.
Evidence
Twenty external sources, ranked by author authority. The first five are the canon; expand to see the rest, including the qualifiers and the named opposers. Each links out to its primary source.
- 01Lint, a C Program CheckerSupportsThe progenitor: a rule engine alongside the compiler, gating on its own catalogue of policy. Establishes the architectural shape every modern lint inherits.
- 02Continuous IntegrationSupportsLint as the cheapest stage of the deployment pipeline; ten-minute build is the goal because the cost of a defect is exponential in the time between writing and finding.
- 03Continuous DeliverySupportsCodifies the no-warning policy at pipeline level. Warnings rot; the gate either blocks or is silent. The entire commit-stage chapter underwrites this tenet.
- 04Practitioner-blog defence: warnings are tolerated, errors are fixed. The behavioural argument that the discipline only works at the error severity level.
- 05Modern statement of the errors-not-warnings position. The argument: a warning is the compiler shrugging; either the rule is law or the rule has no business being on at all.
Eighteen sources across supports, qualifiers and opposers. The supporters cluster on the treat-warnings-as-errors discipline: Johnson's 1977 lint paper, Fowler on Continuous Integration, Humble & Farley on Continuous Delivery, plus Erik Dietrich and Phillip Carter making the modern field case. The qualifiers further down carry the steelman the reply has to address: lint dogma can hide more than it catches. The opposers keep the gate from drifting into the ceiling.
Enforcement
Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.
| Rule | Tool | Catches |
|---|---|---|
| max-lines-per-function | ESLint core | functions over 15 lines — forces extraction rather than vertical sprawl. |
| max-lines | ESLint core | files over 150 lines — caps the unit of comprehension; the file becomes the chapter, not the book. |
| max-params | ESLint core | functions with more than 3 parameters — pushes toward parameter-object types. |
| complexity | ESLint core | cyclomatic complexity over 3 — McCabe's metric encoded as a build-blocking gate. |
| eqeqeq | ESLint core | loose equality (== / !=) — absorbs the old S5 Strict Equality candidate as a single rule. |
| @typescript-eslint/explicit-function-return-type | typescript-eslint | missing return-type annotations — absorbs the old T2 Annotate Returns candidate. |
| import/no-cycle | eslint-plugin-import | import cycles between modules — absorbs half of the old A6 Direct Imports candidate. |
| no-restricted-imports | ESLint core | barrel-file imports (index.ts re-exports) — absorbs the second half of A6. |
| unicorn/filename-case | eslint-plugin-unicorn | kebab-case file naming and *.types.ts conventions — absorbs the old T3 Type Files candidate. |
| @eslint-community/eslint-comments/require-description | @eslint-community/eslint-plugin-eslint-comments | eslint-disable directives without a justification — keeps the override path honest. |
eslint.config.mjsconfiguration snippet
import tseslint from 'typescript-eslint';
import sonarjs from 'eslint-plugin-sonarjs';
import unicorn from 'eslint-plugin-unicorn';
import importPlugin from 'eslint-plugin-import';
import eslintComments from '@eslint-community/eslint-plugin-eslint-comments/configs';
export default tseslint.config(
eslintComments.recommended,
{
files: ['**/*.{ts,tsx}'],
plugins: { sonarjs, unicorn, import: importPlugin },
rules: {
'max-lines-per-function': ['error', { max: 15, skipBlankLines: true, skipComments: true }],
'max-lines': ['error', { max: 150, skipBlankLines: true, skipComments: true }],
'max-params': ['error', 3],
'complexity': ['error', { max: 3 }],
'max-depth': ['error', 3],
'eqeqeq': ['error', 'always'],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-function-return-type': 'error',
'import/no-cycle': 'error',
'no-restricted-imports': ['error', { patterns: ['**/index', '**/index.ts', '**/index.tsx'] }],
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
'@eslint-community/eslint-comments/no-use': 'error',
'@eslint-community/eslint-comments/require-description': ['error', { ignore: [] }],
'sonarjs/cognitive-complexity': ['error', 5],
},
},
);AI rules
.cursor/rules/ta1-linter-as-law.mdc---
description: Prickles TA1 — Linter as Law
globs: "**/*.{ts,tsx,js,jsx,py,java,php}"
alwaysApply: false
---
## Prickles TA1 — Linter as Law
Every quality gate is an error, never a warning. A warning gets ignored within a sprint; an error stops the build.
Encode the limit before the violation. Line count, complexity, parameter count, file size, equality, return annotations, type-file naming, barrel files, import direction — all in `eslint.config.mjs` or its language equivalent.
When the linter trips, fix the code. Never disable the rule inline. The disable comment is the bug; the rule is doing its job.
Where the linter cannot reach — pattern misuse, naming taste, decomposition judgement — culture, agent rules, code review, and test shapes carry the load. Name that boundary so nobody pretends the gate is the ceiling.Repo layout, CI, and ESLint wiring for these paths live on /implementation — not repeated on every tenet.
Counter-argument
The honest steelman is Embedded Artistry's.3"-Werror is Not Your Friend" (2017). The strongest practitioner-blog steelman against treat-warnings-as-errors: ratcheting a previously-warning rule onto an existing codebase has its own costs, and dogmatic application of -Werror discourages teams from adding new rules at all. Treating every warning as an error has a real cost: it converts a useful signal (“this might be a problem, please look”) into a binary stop, and the binary stop makes the team pessimistic about turning new rules on. The result, in long-running codebases, is a ratcheted floor: the rules that exist are enforced strictly, the rules that should exist never get added because nobody wants the breakage. Carmack's position on static analysis at id Software is adjacent: ratchet up slowly, suppress with discipline, do not pretend a clean lint pass means the code is correct.4"Static Code Analysis" (2011). On id Software's adoption of PVS-Studio and Coverity: ratchet up early, suppress with discipline, never trust a clean run as proof of correctness, but treat the clean run as the floor.
Counter-argument retort
Embedded Artistry's objection3"-Werror is Not Your Friend" (2017). The strongest practitioner-blog steelman against treat-warnings-as-errors: ratcheting a previously-warning rule onto an existing codebase has its own costs, and dogmatic application of -Werror discourages teams from adding new rules at all. is doing real work, but the conclusion does not hold once you separate two different kinds of lint.
New rule on existing code — ratcheting a previously-unenforced rule onto a hundred-thousand-line codebase — is the case Embedded Artistry warns about. Suppress the legacy violations through directory-scoped overrides in eslint.config.mjs; flip the rule to error for new code only; let the green-zone grow as the legacy zone is paid down. The principle stays intact: errors, not warnings, in the green zone. The directory override is itself an audit trail.
Existing rule on new code — the principal case in this canon — is the easy one. The rule was at error from day one; the codebase grew up green; the cost of a new violation is fifteen seconds, not fifteen hours. This is the case Carmack's static- analysis essay actually endorses: ratchet up early, suppress with discipline, never trust a clean run as proof of correctness, but use the clean run as the floor.4"Static Code Analysis" (2011). On id Software's adoption of PVS-Studio and Coverity: ratchet up early, suppress with discipline, never trust a clean run as proof of correctness, but treat the clean run as the floor.
The Ousterhout-style steelman — lint is a proxy for the thing you actually care about, and proxies leak — is also right, and is exactly why the Patterns pillar exists. The reply is the cross-reference, not the retreat. The line that hands judgement back is the one this tenet has been making from the top: PT1 Patterns Are Vocabulary, AI2 Persistent Brief, P7 Short-Lived Branches with Review, F2 Intention-Revealing Names — all four are where the lint-unreachable work goes.
The honest residue is the inline-disable case, and the answer there is the rule: eslint-disable in the source file is not editable, not auditable, and not reviewable.9"Warnings and Linter Errors: The Awkward Middle Children" (2022). The modern statement of the errors-not-warnings position; argues an inline-disable directive is unauditable in a way an eslint.config.mjs override is not. Move the exception to eslint.config.mjs with a path scope and a reason; if you cannot justify it there, the rule was right and the code is wrong. The tooling gives you everything you need; reach for the wrench, not for the silencer.
Notes
- [1]ESLint maintainers — ESLint flat config reference. The catalogue of rules and their severity levels (off / warn / error). The execution surface for every claim in this tenet.
- [2]Brian W. Kernighan & P. J. Plauger — The Elements of Programming Style (McGraw-Hill, 1974; 2nd ed. 1978). The earliest book-length statement that style is contract, not taste, and the source of the rule numbers many later style guides cite directly.
- [3]Embedded Artistry — "-Werror is Not Your Friend" (2017). The strongest practitioner-blog steelman against treat-warnings-as-errors: ratcheting a previously-warning rule onto an existing codebase has its own costs, and dogmatic application of -Werror discourages teams from adding new rules at all.