Case file TA1

Linter as Law

Every gate is an error, never a warning.

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.

ByAdam LewisPublished3 May 2026Revised13 Oct 2026Reading12 minVersionv1.2ConfidenceHigh
§0b

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 that cannot be broken 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.1

The corollary, the part Patterns and Architecture both keep colliding with, is that the linter has a ceiling.6 It cannot say that EmailNotificationServiceFactoryAdapter is the wrong name, but it can say the class exceeds the file-size cap. It cannot say the Singleton is hiding global state, but it can say the file has more than one default export. It cannot say that two functions with different signatures compute the same answer, but 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
Copy for paste
§0c

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: releasing 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.

§0d

Why?

  • Settle every recurring style argument once. The rule lives in eslint.config.mjs and the team stops re-litigating function length, equality, return annotations or import direction in every pull request.
  • Catches the violation at the keystroke rather than 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.
  • Makes the agent 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-line comments to grep for, because every legitimate exception lives in eslint.config.mjs with 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 job 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 argues.
  • Draws 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.
The receipts
Origins, quoted passages, evidence, the strongest counter-argument and the reply.
§1

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.2

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.5 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 an opt-in switch, /warnaserror, in Microsoft's C# compiler by 2008, off unless a project turned it on.7 Turned on, it reports every warning as an error and the build halts. Jimmy Hartzell later called warnings and linter errors the awkward middle children of a language: a model in which a program is simply valid or invalid rounds them away, yet many of Rust's important safety features are warnings, and he argues that obligatory warnings belong in any language spec.9 Rust's lint subsystem formalised the same shape with #[deny] attributes. The Go team decided to build in gofmt as a hard formatter rather than a warning so that there would be nothing left to debate.

Martin Fowler's Continuous Integration (2000, rewritten 2006 and 2024) made the self-testing commit build the check on every push to the mainline, held to a ten-minute guideline; the 2024 rewrite names linters among the additional verification tools a team should evaluate for it.8 The whole P4 Continuous Quality Feedback tradition runs through the article's observation that bugs are cumulative: the more there are, the harder each one is to remove.

§2

Quotes

Lint is a command which examines C source programs, detecting a number of bugs and obscurities. It enforces the type rules of C more strictly than the C compilers.

Stephen C. Johnson, Lint, a C Program Checker (1977)

Continuous Integration doesn’t get rid of bugs, but it does make them dramatically easier to find and remove.

Martin Fowler, Continuous Integration (2006)

The first step is fully admitting that the code you write is riddled with errors. That is a bitter pill to swallow for a lot of people, but without it, most suggestions for change will be viewed with irritation or outright hostility.

John Carmack, Static Code Analysis (2011)

A function that has dead code after a return statement: this is a warning, but also a serious correctness issue.

Jimmy Hartzell, Warnings and Linter Errors: The Awkward Middle Children (thecodedmessage, 2022)
§3

Evidence

18 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.

  1. 01
    Stephen C. Johnson · 1977
    The progenitor: a rule engine alongside the compiler, gating on its own catalogue of policy. Establishes the architectural shape every modern lint inherits.
  2. 02
    Martin Fowler · 2006
    The self-testing commit build, kept under ten minutes; the 2024 rewrite lists linters among the extra verification tools teams should evaluate. The exponential effect it names is that bugs are cumulative.
  3. 03
    Jez Humble & David Farley · 2010
    Codifies the no-warning policy at pipeline level. Warnings rot. The gate either blocks or is silent. The entire commit-stage chapter underwrites this tenet.
  4. 04
    Carlos Schults (SubMain) · 2018
    Practitioner-blog defence: warnings are tolerated, errors are fixed. The behavioural argument that the discipline only works at the error severity level.
  5. 05
    Jimmy Hartzell (thecodedmessage) · 2022
    Argues that a language is not well described by a binary valid-or-invalid model, because many of its real safety rules ship as warnings rather than as errors. The warning is part of the standard, not an aside to it.
  1. 06
    Microsoft · 2024
    Language-vendor's documentation for /warnaserror. Demonstrates the pattern at compiler level, predating ESLint by a generation.
  2. 07
    Scott Obert · 2021
    Recent quality-gates summary that situates lint as one stage of many. Useful for the modern restatement. Aligns with P3 + P4 in the cross-reference graph.
  3. 08
    Linus Torvalds and the kernel community · 1995 onwards
    The most-read style guide on the planet. Strongly opinionated but explicit that the rules are guidelines, not gates: kernel uses checkpatch.pl as advisory, not blocking. Useful as the principled middle position.
  4. 09
    John Carmack · 2011
    id Software made Microsoft /analyze the default for 360 builds, on top of warning level 4 with warnings-as-errors; PVS-Studio and PC-Lint then found more in code already clean to /analyze, and Coverity was only demoed. Each new tool found errors the last had passed, so a clean run is not proof of correctness.
  5. 10
    Embedded Artistry · 2017
    Not against the discipline: the author runs a zero-warning policy that fails builds and blocks pull requests on CI. The objection is to hard-coding -Werror in the build files, which ties the project to specific toolchain vendors and versions.
  6. 11
    John Ousterhout · 2018
    Chapter 17 (Consistency): the best way to enforce conventions is a tool that checks for violations, with code that cannot be committed unless it passes the checker. Chapter 9 cuts the other way on length caps: length by itself is rarely a good reason to split a method.
  7. 12
    DORA (Google Cloud) · 2023
    DORA's trunk-based development capability: short-lived branches merged to trunk at least daily, with fast automated tests that must pass before a merge. It does not mention lint.
  8. 13
    ESLint maintainers · 2024
    The execution surface for every claim in this tenet. Defines flat-config syntax, severity levels, per-glob overrides: the mechanism the discipline depends on.
  9. 14
    Astral · 2024
    Ports pylint, pycodestyle, eradicate, flake8-fixme, pydocstyle and many more under one binary and one TOML block. The Python-side referent for cross-language lint-as-law.
  10. 15
    Checkstyle maintainers · 2024
    Java-side referent. Demonstrates the same discipline in a different ecosystem. The file-by-file XML override model parallels eslint.config.mjs's per-glob overrides.
  11. 16
    ESLint Community · 2024
    The lever that polices the inline-disable case: any eslint-disable directive without a justification fails the build. Concrete answer to the 'inline disable' objection.
  12. 17
    Rust Project · 2024
    The #[deny] / #[forbid] severity attributes. Cross-language proof of the pattern: the strongest forbid level cannot be unset by inner scope, the closest analogue to a no-inline-disable policy at the language level.
  13. 18
    PHP-CS-Fixer maintainers · 2024
    PHP-side referent. Auto-fixing fixer with hard error gating. Supports the cross-language claim that lint-as-law is not a JavaScript-only pattern.

Eighteen sources across supports, qualifiers and opposers. The supporters run from Johnson's 1977 lint paper, which gave the checks a program of their own beside the compiler, through Fowler on Continuous Integration and Humble & Farley on Continuous Delivery, to Carlos Schults and Jimmy Hartzell making the modern case for taking warnings seriously. The qualifiers are the Linux kernel style guide and Carmack. The opposers hold the steelman the reply has to address, Embedded Artistry's case against hard-coding -Werror into the build files, and keep the gate from drifting into the ceiling.

§4b

Enforcement

Viewing: TypeScript.

Apply these rules in eslint.config.mjs. The full enforcement across every tenet lives on the implementation page.

RuleToolCatches
max-lines-per-functionESLint corefunctions over 15 lines: forces extraction rather than vertical sprawl.
max-linesESLint corefiles over 150 lines: caps the unit of comprehension; the file becomes the chapter, not the book.
max-paramsESLint corefunctions with more than 3 parameters: pushes toward parameter-object types.
complexityESLint corecyclomatic complexity over 3: McCabe's metric encoded as a build-blocking gate.
eqeqeqESLint coreloose equality (== / !=): absorbs the old S5 Strict Equality candidate as a single rule.
@typescript-eslint/explicit-function-return-typetypescript-eslintmissing return-type annotations: absorbs the old T2 Annotate Returns candidate.
import/no-cycleeslint-plugin-importimport cycles between modules: absorbs half of the old A6 Direct Imports candidate.
no-restricted-importsESLint corebarrel-file imports (index.ts re-exports): absorbs the second half of A6.
unicorn/filename-caseeslint-plugin-unicornkebab-case file and directory naming. It enforces case only, so it cannot require types to live in *.types.ts files.
@eslint-community/eslint-comments/require-description@eslint-community/eslint-plugin-eslint-commentseslint-disable directives without a justification: keeps the override path honest.
eslint.config.mjsconfiguration snippet
import { defineConfig } from 'eslint/config';
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 defineConfig(
  eslintComments.recommended,
  {
    files: ['**/*.{ts,tsx}'],
    extends: [tseslint.configs.base],
    languageOptions: { parser: tseslint.parser },
    settings: {
      'import/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'] },
      'import/resolver': { node: { extensions: ['.ts', '.tsx', '.js'] } },
    },
    plugins: { '@typescript-eslint': tseslint.plugin, 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', 15],
    },
  },
);
§4c

AI rules

File.cursor/rules/ta1-linter-as-law.mdc
---
alwaysApply: true
---

## Prickles TA1: Linter as Law

Make every quality gate 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 take 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.

§5

Counter-argument

Counter

The honest steelman is Embedded Artistry's.3 Phillip Johnston keeps a zero-warning policy, and his build server rejects pull requests and fails builds that carry a warning. What he objects to is hard-coding -Werror into the build files: “-Werror creates a project dependency on specific toolchain vendors and versions.” Every new compiler version adds warnings, so a user who changed nothing builds the project with a newer or different toolchain and watches it fail. Put the error in the configuration everyone builds with and the next tool upgrade breaks code nobody touched; enforce it in CI and the policy holds without that cost. Carmack's position on static analysis at id Software is adjacent: clear the backlog slowly before making the analyser the build default, do not pretend a clean lint pass means the code is correct.4

§6

Counter-argument retort

Reply

Embedded Artistry's objection3 is doing something, but the conclusion does not hold once you separate two different kinds of lint.

New rule on existing code is the hard case, whether the team ratchets a previously-unenforced rule onto a hundred-thousand-line codebase or a tool upgrade brings new warnings, the break Embedded Artistry warns about. The post sets its own condition for using -Werror: note the versions the project is expected to work with, or capture the build environment. Pin the linter's version and a new rule arrives only in the pull request that upgrades it. 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 simple case. 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 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

The Ousterhout-style steelman (lint is a proxy for the thing you care about, and proxies leak) is also right, and is exactly why the Patterns pillar exists. The reply is the cross-reference rather than the retreat. This tenet has been drawing the line that hands judgement back from the top: PT1 Patterns Are Vocabulary, AI2 Persistent Brief, P7 Short-Lived Branches with Review, F2 Intention-Revealing Names: the lint-unreachable work goes to those four.

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.10 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.

§7

Notes

  1. [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. [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. [3]Embedded Artistry: "-Werror is Not Your Friend" (2017). Phillip Johnston keeps a zero-warning policy and has CI fail any build or pull request with warnings. His objection is to hard-coding -Werror in the build files, because a newer or different compiler adds warnings and breaks the build for anyone who uses it.
Disagree? Found a hole in the argument? Take issue with this tenet →
Last revised: 2026-10-13