The coding standards and linting decision record: why the auto-format model you chose determines your PR review friction surface and your codebase consistency degradation gap

The auto-format model, the linting enforcement strictness, and the standards evolution process are team coordination decisions that are almost never made explicitly — they emerge from a quality initiative that produces a style guide document without automated enforcement, a senior engineer's linting configuration that becomes the team default without a rationale document, and a migration policy adopted at a retrospective that specifies intent without a completion criterion. Three failure patterns: the B2B SaaS that produced a 60-page formatting guide without a formatter and discovered six months later that 34% of PR review time was spent on style comments a Prettier configuration would have eliminated; the developer tooling company that enforced 47 CI-blocking linting rules without documenting any of them and delayed new engineer independence by six to eleven weeks per hire; and the enterprise SaaS that adopted an opportunistic migration policy for a JavaScript-to-TypeScript conversion with no completion criterion and produced a three-pattern codebase after 18 months.

A 31-person B2B SaaS company built operational workflow tools for mid-market logistics companies — route optimization dashboards, carrier compliance checklists, and a shipment exception management console. The engineering team of eighteen had grown from four to eighteen in fourteen months. During a team quality initiative in year two, the three most senior engineers co-authored a coding standards document. The document was 60 pages and covered Python and TypeScript conventions: naming styles for variables, functions, classes, and modules; import ordering rules (stdlib before third-party before local, alphabetical within groups); function length limits (40 lines maximum, with a 60-line exception requiring a comment explaining the exception); error handling patterns (never swallow exceptions silently, always log with context, avoid bare `except:`); comment style (docstrings for public functions, inline comments only for non-obvious logic); and formatting preferences (4-space indentation, single quotes for strings, trailing commas in multi-line structures). The document was linked from the team's Notion onboarding checklist and reviewed with each new hire. No automated formatter or linter enforced any section of the document.

In the first month after the document was published, a review of PR comments by the engineering manager found that 23% of review comments were style-related. The most common style comments were: "imports should be alphabetically ordered within groups," "trailing whitespace on lines 14 and 22," "this function is 54 lines — please split or add the exception comment," and "single quotes here, not double quotes." The engineers leaving these comments were applying the document faithfully. The engineers receiving the comments were responding with follow-up commits. The PR round-trip cycle time for style-comment iterations added between 30 minutes and 4 hours per PR depending on how many style violations were caught in the first review pass.

In month six, the engineering manager conducted a second review. Style-related PR comments had grown to 34% of all review comments. The median PR review cycle time had grown from 18 hours to 31 hours. Two patterns had emerged that the standards document had not anticipated: three engineers had recurring stylistic disagreements in PR comments — one engineer consistently pushed back on the 40-line function limit for functions that were long due to comprehensive error handling rather than complexity; another had a different reading of the import ordering rule when a local package had the same name prefix as a third-party package — and these disagreements had escalated to the engineering manager twice. Additionally, the two founding engineers who had authored the standards document had interpreted several sections differently from each other in their own PR reviews, creating a situation where the same code pattern was approved by one reviewer and flagged by another.

The senior engineer who had drafted the majority of the document left the company in month nine. The sections she had authored — particularly the error handling section and the comment policy — were interpreted inconsistently by the engineers hired after her departure because the document described outcomes ("always log with context") without the examples and reasoning that had been in her head but not on the page. By month twelve, the engineering manager estimated that the team was spending approximately 6 engineer-hours per week on style-related PR feedback: writing comments, responding to comments, making follow-up commits to address style comments, and re-reviewing after style fixes. Six hours across an eighteen-person team was 3% of total weekly engineering capacity. The coding standards document had been adopted to raise code quality; it had produced a recurring PR review tax without an associated measurable improvement in defect rate, incident frequency, or time-to-first-commit for new engineers. The retrospective that followed identified the root decision: the team had decided to have coding standards but had not decided how to enforce them. The style document without automated enforcement had distributed the enforcement cost across every PR review, every reviewer, and every reviewed engineer, indefinitely.

A 44-person developer tooling company built a platform for API performance monitoring and SLA analytics — latency tracking, error rate dashboards, and an anomaly detection service that alerted on degradations. The engineering team of twenty-two wrote Go. The three founding engineers had deep Go experience and strong opinions about Go code quality; they had configured `golangci-lint` with 47 enabled rules early in the company's life, and the configuration had become the canonical linting standard as the team grew. The CI pipeline failed any PR that violated a rule configured in error mode, which was all 47 rules. The enabled rules included `gocognit` with a maximum cognitive complexity of 8, `funlen` with a maximum function length of 40 lines, `cyclop` with a maximum cyclomatic complexity of 5, `wsl` enforcing strict blank-line placement between statements, `gocritic` with 23 enabled sub-checkers, `godot` requiring all comments to end with a period, `exhaustruct` requiring all struct fields to be explicitly initialized in struct literals, and `ireturn` requiring that interfaces, not concrete types, be returned from constructors. These rules reflected real Go engineering knowledge: `gocognit` and `cyclop` caught genuinely hard-to-read functions; `exhaustruct` caught bugs from missed struct field initialization; `ireturn` enforced testable interfaces. The founding engineers could pass the linter without consulting documentation because the rules matched their intuitions about Go code quality that they had developed over years.

The configuration had never been documented. There was no file explaining what each rule prevented, why the team had decided to enable it, or what the idiomatic Go pattern was for satisfying it. The onboarding documentation said "run `golangci-lint run ./...` before pushing"; it did not explain the 47 rules. For the engineers who had joined the company before the team grew beyond ten people — all of whom had been hired partly on the basis of Go experience and had been onboarded through pairing with a founding engineer — the configuration was navigable by experience and informal knowledge transfer. For engineers joining a larger team where pairing with a founding engineer was not the default onboarding structure, the configuration was a wall of unexplained CI failures.

A staff engineer was hired in month eighteen of the company's growth phase from a primarily Python background; she had Go experience but not at the level of the company's founding team. In her first three weeks she had CI failures on five consecutive PRs covering bugs she had been assigned to fix. The failures were on `wsl` (blank line between variable declaration and the first use of the variable), `godot` (comment "// returns the latency percentile" should be "// Returns the latency percentile."), `exhaustruct` (a struct literal initializing four of seven fields in a helper function that was called exclusively in tests), and `funlen` (a function that was 43 lines, three over the limit, that she split into two functions, whereupon `cyclop` flagged the caller as exceeding the complexity threshold because the split required an additional conditional in the call site). She asked in Slack for help on the `exhaustruct` failure; the founding engineer who responded said "yeah that rule is annoying for test helpers, just add the zero values." The founding engineer knew the pattern from memory. The staff engineer had never seen `exhaustruct` before. By day eleven she had shipped her first production change. She mentioned in a 30-day check-in that the linting configuration had been the largest friction point in her first three weeks; she had gone back to re-read Go documentation on interfaces and struct initialization in ways she had not needed to since learning Go.

A junior engineer hired two months later had a more difficult experience. Her first independent task was a bug fix in the anomaly detection service that required adding an error handling branch to an existing function. The function was 38 lines before her addition; her branch added three lines of error handling, bringing it to 41 lines and triggering the `funlen` violation. She split the function, but the split required extracting a helper that had a parameter list of six arguments (the function had been accumulating state over multiple calls in the original), which triggered `gocritic`'s `hugeParam` checker and a `funlen` violation on the extracted helper. She gave up on the split approach, added a `//nolint:funlen` comment, which the CI pipeline failed because the company's linting configuration prohibited inline disables. She asked a senior engineer to pair on the PR. The senior engineer refactored the function using a context struct that eliminated the six-parameter list; the refactoring took 40 minutes. The junior engineer shipped the bug fix on her seventh day of working on it. She had a second similarly blocked PR six weeks later, and a third two months after that. By month three she could independently navigate the linter, but her first three months had required pairing support on every PR that involved refactoring existing code. A post-hire survey administered at the 60-day mark across all engineers hired in the prior 18 months found that 7 of 11 respondents identified the linting configuration as a significant onboarding friction point. The founding engineers were surprised; they had never experienced the linting configuration as friction because the configuration matched their existing intuitions.

A 53-person B2B SaaS company built an enterprise workflow automation platform — approval routing, policy enforcement, and a process compliance dashboard for HR and finance teams. The frontend was a React application that had been built in JavaScript class components beginning in year one. In year three, the engineering team adopted TypeScript for all new frontend code, citing improved refactoring safety and better IDE support in a codebase that had grown to 140,000 lines. The adoption decision was made at a team retrospective, and the policy was recorded in the team's engineering handbook: "New frontend components should be written in TypeScript functional components. Existing JavaScript class components should be migrated to TypeScript functional components opportunistically when they are touched for feature work or bug fixes." The ESLint configuration was updated to include the TypeScript rules. The existing JavaScript files were added to the `parserOptions.project` configuration so TypeScript could analyze them as JavaScript with type annotations optional.

The migration progressed unevenly, as opportunistic migrations always do. The approval routing component — touched frequently for feature additions — was migrated to TypeScript functional components in month two. The process compliance dashboard — touched infrequently, mostly for bug fixes — had been partially converted by month six (the main dashboard file was now TypeScript, but the four utility components it depended on were still JavaScript class components). The HR policy enforcement module — a stable section of the application that had not required feature work in eight months — had not been touched at all. At month six, the frontend codebase was approximately 60% TypeScript functional components, 25% JavaScript class components, and 15% files in a partially migrated state where the file extension had been changed to `.tsx` but the component structure was still class-based.

New engineers onboarding to the frontend began encountering the question that the migration policy had not answered: when adding a new component adjacent to an existing JavaScript class component, should they match the existing pattern (to avoid introducing a pattern inconsistency in a subsection of the codebase where all other components were class-based) or introduce the new pattern (to advance the migration)? The policy said "new components should be TypeScript functional components" — which answered the question in favor of the new pattern — but the PR review guidance said nothing about the inconsistency created by a TypeScript functional component added to a subsection of the application that was otherwise class-based JavaScript. Three different senior engineers answered the question differently in code review comments on different PRs: one approved the new pattern in a legacy section, one asked the engineer to convert the surrounding class components first (a larger scope change than the engineer had planned), and one left the comment "see the migration policy" without further guidance. Two engineers submitted duplicate questions to the engineering Slack channel in the same month asking which pattern to use in the settings module, which had not been touched since month three and was still entirely JavaScript class components.

At month eighteen, the frontend engineering lead reviewed the migration state. The codebase had 61% TypeScript functional components, 24% JavaScript class components, and 15% mixed or transitional files. The 24% of class components remaining were concentrated in the sections of the application that received the fewest feature requests: the admin console, the onboarding wizard, and the settings module. At the current opportunistic migration rate — governed by the frequency with which these sections were touched — the admin console would complete migration in approximately eight months, the settings module in approximately fourteen months, and the onboarding wizard in approximately twenty months. The estimated effort for a dedicated migration sprint covering all remaining class components was three to four engineer-weeks. The opportunistic policy had been adopted because it required no upfront time investment and appeared to impose no opportunity cost. After eighteen months, the cost was visible: three to four weeks of dedicated engineering time deferred into eighteen months of a multi-pattern codebase where every engineer who touched a legacy section asked the same question about which pattern to follow, and where the absence of a migration horizon had made the inconsistency permanent rather than bounded.

Structural properties set by the coding standards decision

Three structural properties are determined when a team decides — or fails to explicitly decide — how to approach coding standards: what the auto-format model determines about the PR review friction surface as the team grows and the standards document accumulates interpretive disagreements, what the enforcement strictness determines about the developer onboarding gap as new engineers encounter CI-blocking rules whose rationale and satisfaction patterns are undocumented, and what the standards evolution process determines about the codebase consistency degradation surface as opportunistic migration policies produce indefinitely mixed codebases with no completion criterion. None of these are labeled as decisions in the conversations that produce them. The auto-format decision emerges from the authoring of a style guide without a question about what will enforce it. The enforcement strictness emerges from a senior engineer's linting configuration being committed to the repository and becoming the team default without a documentation step. The migration policy emerges from a retrospective motion that specifies the intent to migrate without specifying when the migration will be complete.

Property 1: The auto-format model and the PR review friction surface. The auto-format model is the decision about whether formatting and style enforcement is implemented by automated tools — formatters and linters with autofix capability — or by human reviewers interpreting a written style guide. The PR review friction surface is the proportion of review comments, reviewer time, and round-trip cycle time attributable to style enforcement rather than correctness, design, and logic review. When formatting is enforced by humans against a document, the friction surface is proportional to the number of engineers times the ambiguity of the document times the disagreement surface among reviewers — all three of which grow as the team scales. When formatting is enforced by a deterministic tool, the friction surface for the decisions the tool covers is zero: the code is what the formatter produces, and reviewers cannot comment on formatting because the formatting question was resolved before the PR was opened. The decision is not whether to have coding standards — both the style-document approach and the auto-format approach enforce standards — it is whether the enforcement is done by humans or by tools. For JavaScript and TypeScript, Prettier at its default configuration closes the formatting debate entirely: indentation, semicolons, quote style, trailing commas, and line-wrapping are all determined by the formatter output. For Python, Black is the equivalent. For Go, gofmt is part of the toolchain. The supplementary standards document is then scoped to the decisions these tools do not make: naming conventions for domain concepts, error handling patterns, test organization, and comment policy. The auto-format decision is correctly made before the standards document is written, because it determines the scope of what the document needs to cover. Connect this property to the developer experience decision record: the PR review cycle time and the comment composition (style vs logic vs design) are DX metrics that can be measured before and after formatter adoption to quantify the friction reduction; a team that measures its PR cycle time before adopting a formatter and one sprint after adoption will see the friction reduction directly in the data rather than relying on anecdotal reports of improved review quality.

Property 2: The enforcement strictness and the developer onboarding gap. The enforcement strictness is the decision about which linting rules are enabled, how strictly their parameters are set, and whether violations block the CI pipeline as errors or produce non-blocking warnings. The developer onboarding gap is the time between a new engineer's first day and the date they can independently ship a production change without linting CI failures or requiring pairing to navigate the configuration. The gap is invisible in the hiring process — candidates see the role description, the team, and the codebase, but not the linting configuration — and invisible in standard onboarding metrics — time-to-first-commit measures wall-clock days, not whether days were spent on CI failures that a documentation page would have prevented. A linting configuration that is strict without being documented produces an onboarding gap that is proportional to the distance between the new engineer's prior Go (or Python, or TypeScript) experience and the founding team's experience at the time the configuration was authored. Senior engineers who share the founding team's background navigate the configuration by intuition; engineers from different language backgrounds or experience levels encounter rules they have never seen and must discover their satisfaction patterns through trial and error or pairing. The documentation requirement per rule is three components: what the rule prevents (the concrete failure mode — "prevents functions from accumulating enough logic paths that a reader cannot hold the full control flow in working memory"), why the team decided to prevent it (the decision rationale — "we had two production incidents in year one traced to edge cases in functions exceeding complexity 8 that reviewers had missed"), and the idiomatic pattern for satisfying the rule in common cases (the practical resolution — "for functions approaching the limit due to comprehensive error handling, extract the error handling into a named function that returns a typed result"). These three components convert the linting configuration from a gatekeeping mechanism that senior engineers navigate implicitly into an onboarding tool that junior engineers can use explicitly. The documentation should be maintained alongside the linting configuration file — either as inline comments in the configuration or as a linked document indexed by rule name — so that an engineer reading a CI failure message can navigate from the rule name to the documentation without a Slack question. Connect this property to the build system decision record: the CI pipeline configuration that runs linting as a PR-blocking step is where the enforcement gate lives; the build system decision determines whether the linting step runs against all files, only changed files, or only staged files; running against only changed files in CI is faster but can miss violations introduced by changes in a different file that affect the linting output of an unchanged file — the enforcement gate specification should document the scope of the linting check and the rationale for the chosen scope.

Property 3: The standards evolution process and the codebase consistency degradation surface. The standards evolution process is the decision about how coding standards change over time — whether new standards are adopted with a migration plan, a migration horizon, and a completion criterion, or adopted as opportunistic policies that specify intent but not completion. The codebase consistency degradation surface is the proportion of the codebase that does not conform to the current standard, growing over time as the legacy-pattern code is not migrated under an opportunistic policy. The degradation is not uniform: active feature areas migrate quickly under opportunistic policies because they are frequently touched; stable utility code, settings modules, admin consoles, and infrequently-touched infrastructure never migrate because they are touched rarely or never. The result is a codebase partitioned by age: code modified recently conforms to the current standard; code last modified before the standard was adopted does not; the partition boundary is the git blame date, not any documented architectural boundary. New engineers who encounter a legacy section must determine which pattern is canonical — the one in the file they are reading or the one described in the current standards documentation — without a clear answer from the documentation because the documentation describes the current standard and the file is still in the old standard. The standards evolution process should specify three components: a migration horizon (the date by which all files will conform to the new standard, set at adoption date plus estimated migration effort), a migration approach (automated codemod for rules with tooling support, sprint-allocated migration work for rules requiring manual refactoring), and a transition period gating policy (new files must use the new standard; files adjacent to new files being touched should be migrated in the same PR within a defined scope — typically files in the same directory or module, not the entire codebase). The migration horizon creates a completion criterion; "opportunistically when touched" does not. Connect this property to the branching strategy decision record: large migration PRs — covering multiple files in a module or subsystem — should follow a defined branching pattern (typically a migration branch off the main branch, reviewed and merged in sections to avoid large merge conflicts with ongoing feature work); the standards evolution process should specify the PR scope limit for migration work and the branch convention to prevent migration commits from blocking feature development in the same area of the codebase during the migration window. The WhyChose extractor finds the coding standards decisions buried in your AI chat history — the quality initiative conversation where the team discussed whether to adopt a formatter or write a style guide and chose the guide because "we already know what we want, we just need to document it," the linting configuration design session where each rule was selected with clear reasoning that was never written down outside that conversation, and the retrospective where the migration policy was adopted without the follow-on question of when the migration would be complete.

The coding standards ADR: five sections

Section 1: Formatter selection and auto-format configuration. Specify which automated formatter tools are adopted for each language in the codebase, whether the formatter is configured at its default settings or customized, and the enforcement points — pre-commit hook (autofix on staged files), CI check (fail if files are not formatted), or both. For each language with a standard formatter: state whether the default configuration is adopted (preferred, because it eliminates configuration debates and divergence between versions) or a customized configuration, with each customization documented alongside the specific reason it was adopted rather than the default. Document what the formatter's adoption means for the supplementary standards document: any section of the standards document that duplicates a formatter rule is deprecated at the point of formatter adoption and should be removed to prevent the document from diverging from the formatter output over time. The formatter selection should precede the standards document authoring, not follow it, because the formatter's scope determines the document's scope. The pre-commit autofix hook is the correct enforcement point for formatters: it reformats staged files before the commit lands, so the commit lands formatted without requiring the engineer to run the formatter manually; the CI check is the authoritative gate for PRs where the pre-commit hook was bypassed or not installed. Connect to the developer experience decision record: adopt the DX measurement model before the formatter to establish a baseline PR cycle time and style-comment proportion; measure the same metrics one sprint after formatter adoption; the difference is the formatter's friction reduction, which should be documented in the ADR as evidence for the decision's effectiveness and as justification for the investment if the formatter introduces an initial configuration debate.

Section 2: Linting rule selection and per-rule documentation. Specify which linting rules are enabled for each language, the configuration values for parameterized rules, the enforcement mode (error vs warning), and the per-rule documentation covering three components: what the rule prevents, why the team decided to enable it, and the idiomatic pattern for satisfying the rule in common cases. The rule documentation should be maintained in the linting configuration file as structured comments or in a linked document indexed by rule name, so an engineer reading a CI failure message can navigate from the rule name to the documentation directly. For rules imported from a shared configuration (e.g., `eslint:recommended`, a community preset), the ADR should document which rules from the preset were explicitly accepted, which were overridden, and why each override was made — the preset provides a starting point, not a rationale; the rationale comes from the team's decision about which rules match its goals. The rule documentation requirement applies retroactively to existing configurations: every currently-enabled rule that lacks documentation should be treated as a documentation debt item to be resolved before new rules are added. Connect to the build system decision record: document whether the CI linting step runs against all files or only changed files, the rationale for the scope choice, and the handling of linting violations introduced by changes in files not directly modified by a PR (transitive violations that arise when a refactored interface changes the linting output for unchanged callers).

Section 3: Standards evolution protocol and migration horizon specification. Specify the process for adopting new coding standards after the initial configuration is established, the migration approach for existing code that does not conform to the new standard, and the migration horizon — the date by which all files will conform. The protocol for adding a new CI-blocking rule should follow a four-step pattern: (1) enable in warning mode initially, documenting the rule with the three-component documentation from Section 2; (2) run any available automated codemod to migrate the majority of existing violations; (3) set a migration horizon based on the estimated remaining manual migration effort; (4) promote to error mode at the horizon, with any remaining exceptions tracked in the exception register (Section 5). The migration approach for large pattern migrations (language migrations, framework upgrades, class-to-functional component conversions) should specify a migration horizon, the PR scope limit for migration work (files in a given module or directory per PR, not the entire codebase in one pass), and a completion criterion that is a date and a conformance percentage (100% conformance by [date], not "migrate opportunistically"). An opportunistic migration policy without a horizon should be rejected at the standards evolution protocol step — the retrospective adoption motion that says "migrate when touched" must include the answer to "what is the latest date by which the migration will be complete regardless of touch frequency?" If the answer is "we don't know," the migration plan is incomplete and the horizon should be set before the policy is published. Connect to the branching strategy decision record: large migration PRs follow the branching convention specified in the branching decision; the migration horizon should account for the branch review and merge cycle time in the total migration estimate.

Section 4: Onboarding integration and standards documentation structure. Specify where coding standards are documented, the documentation structure (formatter configuration, linting configuration, supplementary standards document for decisions neither covers), and how the standards documentation is integrated into the new hire onboarding process. The supplementary standards document should cover only decisions that formatters and linters cannot enforce: naming conventions for domain concepts (with concrete examples from the production codebase — "a 'resource' is an API-addressable entity owned by a customer; an 'entity' is a database row that may not have a direct API surface; see UserResource vs UserEntity in src/users/"), error handling patterns (with before/after examples showing the violation and the correct pattern), test organization conventions (test file naming, fixture pattern, mock library selection), and comment policy (with examples of comments that add value vs comments that duplicate the code). The onboarding checklist should include a step for reading the supplementary standards document — with a time estimate (30 minutes for reading, not "read before your first PR") and a checkpoint question that the onboarding manager asks at the end of the first week ("name one linting rule that surprised you and what it prevents"). The checkpoint converts the documentation from passive reading to active learning and surfaces documentation gaps when new engineers cannot answer the question using the documentation alone. Connect to the developer portal decision record: the coding standards documentation and the linting configuration rule index should be surfaced in the team's internal developer portal as a first-class onboarding resource, linked from both the new hire onboarding section and the contributing guidelines in the codebase root; the portal discoverability of the standards documentation determines whether new engineers find it before their first CI failure or after.

Section 5: Exception protocol and standards debt register. Specify the process for marking code that intentionally violates a coding standard, the required content of an inline exception comment, the lifetime of an exception, and the register for tracking exceptions. Every linting tool has an inline exception mechanism — `// nolint:funlen`, `# noqa: E501`, `/* eslint-disable-next-line @typescript-eslint/no-explicit-any */` — and every codebase that uses one of these mechanisms without a governance protocol accumulates exceptions over time until the exceptions are effectively a second standard: "the code with dense nolint comments was written before the rule was added and has never been migrated." The exception protocol should specify four requirements: (1) every inline exception comment must be followed by a comment explaining why the exception is necessary — not which rule is being skipped, but why the violation cannot be resolved without the exception; (2) every exception must be either permanent (accompanied by a ticket reference or a documented architectural constraint that explains why the violation is structural and cannot be removed) or temporary (accompanied by a ticket reference for the migration work, reviewed at the next sprint planning); (3) the CI pipeline should reject inline exceptions that do not include the explanation comment, enforced by a custom lint rule or a pre-commit hook that validates the exception comment format; (4) the exception register — a quarterly review of all inline exceptions in the codebase — produces a migration decision or permanent-exception classification for each entry. The exception register converts exception accumulation from invisible technical debt to a tracked category with a review cadence and a closure criterion. Connect to the API schema design decision record: generated code — protobuf-generated files, OpenAPI-generated clients, GraphQL-generated types — should be excluded from linting at the configuration level via path exclusion patterns rather than via per-file inline exceptions; per-file inline exceptions in generated code are wiped on regeneration; configuration-level path exclusions are regeneration-stable and should be documented in the ADR with the rationale (generated code cannot conform to coding standards without modification that is lost on next generation) and the path patterns for each code generator in use.

FAQ

When should formatting be enforced by an automated tool rather than a code review guideline?

Formatting should be enforced by an automated tool whenever a deterministic tool exists for the language. For JavaScript and TypeScript, Prettier at its default configuration closes the formatting debate entirely: indentation, semicolons, quote style, trailing commas, and line-wrapping are all determined by the formatter output before the PR is opened. For Python, Black is the equivalent. For Go, gofmt is part of the toolchain. For Rust, rustfmt. The cases where a supplementary style guide appropriately covers decisions the formatter does not make are: naming conventions for domain concepts, error handling patterns, test organization, and comment policy. A style guide section that duplicates what the formatter already enforces is documentation debt — it will diverge from the formatter output the first time the formatter version is updated, and then both the guide and the formatter are authoritative, which means neither is. The correct division is: the formatter owns all whitespace, punctuation, and structural formatting; the linter owns correctness and high-signal style decisions; the supplementary document owns only the decisions neither tool can enforce. The formatter selection should happen before the standards document is written, because it determines the document's scope and prevents the document from covering decisions that are already closed by the tool.

How should a team introduce a new linting rule without breaking the existing codebase?

The standard pattern for introducing a new CI-blocking rule to a codebase with existing violations is: (1) Enable the rule in warning mode initially, not error mode. This gives the team visibility into the violation count and distribution without blocking existing PRs. (2) Run the automated codemod if one exists — ESLint's --fix flag, golangci-lint's --fix flag, Rubocop --auto-correct — to migrate the majority of existing violations in a single pass. (3) Set a migration horizon: a date by which all existing violations must be resolved and the rule will be promoted from warning to error mode. Four to six weeks is appropriate for rules with autofix support; six to twelve weeks for rules requiring manual refactoring. (4) At the promotion date, promote the rule to error mode. Remaining violations should have a documented exception with a ticket reference, not an indefinite inline disable. This pattern avoids both the big-bang approach (enabling in error mode on day one, requiring an emergency remediation sprint) and the warning-forever approach (enabling in warning mode and never promoting, producing a growing backlog of ignored warnings). The migration horizon creates a completion criterion; the warning period gives the team a measured view of the migration scope before committing to the horizon date.

What is the right enforcement gate for linting — local pre-commit hook or CI pipeline failure?

Both, with different scopes and purposes. The local pre-commit hook catches violations at the lowest cost — before the engineer pushes, before CI runs, before a reviewer sees the PR. For formatters, the pre-commit hook should autofix staged files (reformat before the commit lands, so the engineer never sees a formatting CI failure). For linters with autofix support, the pre-commit hook can autofix violations in the same pass. For linting rules without autofix, the pre-commit hook blocks the commit with an actionable error message. The CI pipeline is the authoritative enforcement gate — the pre-commit hook is best-effort (engineers can bypass it with --no-verify, or may not have configured it) and the CI check is the reliable gate that every PR must pass regardless of local configuration. The practical configuration is: formatter runs as autofix pre-commit hook; linter with autofix rules runs as autofix pre-commit hook; full linting suite runs as a PR-blocking CI check. Warning-mode rules should produce a separate non-blocking CI report — not fail the same step as error-mode rules — so that engineers learn to treat a failing lint step as a merge blocker rather than a list of items to investigate. The documentation for new engineers should explain both gates: the pre-commit hook catches most violations locally with zero PR-review round-trips; the CI check is the source of truth that the PR must pass before merge.

How should generated code (protobuf-generated, OpenAPI-generated) be handled in a linting configuration?

Generated code should be excluded from the linting configuration at the path level, not suppressed with per-file inline exception comments. Most linting tools support path exclusion patterns in the configuration file: ESLint's 'ignorePatterns' or '.eslintignore', golangci-lint's 'exclude-rules' with path patterns, Rubocop's 'Exclude' directive. The exclusion should match the generated file paths specifically — 'src/generated/**', '**/*.pb.go', 'lib/openapi-client/**' — rather than broad exclusions that could inadvertently cover non-generated files. Per-file inline exceptions in generated code (// eslint-disable, // nolint:all) are fragile: they survive regeneration only if the generator preserves them, and when a regeneration wipes the inline exceptions the CI will fail until the exceptions are re-added. Configuration-level path exclusions are regeneration-stable because they live in the linting configuration, not in the generated code. The coding standards decision record should document the exclusion pattern for each code generator in use, the rationale (generated code cannot be modified to conform to standards without losing the modification at next regeneration), and the process for updating the exclusion patterns when generator output paths change. If the generator output path changes without updating the exclusion pattern, the CI will fail on all generated files until the configuration is updated — making the exclusion pattern update a required step in any generator output path migration.

Further reading

  • Developer experience decision record — the DX measurement model and the friction measurement infrastructure for quantifying coding standards friction before and after formatter adoption; PR cycle time and review comment composition (style vs logic) are the primary DX metrics that coding standards decisions move, and the DX decision record specifies how those metrics are collected and reviewed.
  • Build system decision record — the CI pipeline configuration that determines where linting runs, what scope it covers (all files vs changed files), and how formatter and linter failures are reported; the linting enforcement gate lives in the build system and the build system decision specifies its configuration alongside the test and deployment pipeline steps.
  • Branching strategy decision record — the branch convention and PR scope model that determines how large migration PRs (language migrations, formatter adoption, rule addition with existing violations) are structured to avoid blocking ongoing feature development during the migration window; the standards evolution protocol references the branching convention for migration work.
  • Developer portal decision record — the self-service portal scope and discoverability model that determines how coding standards documentation is surfaced to new engineers; a standards document that lives only in a Notion page linked from the onboarding checklist will be found by engineers who complete the checklist and missed by engineers who do not; the portal decision specifies how first-class resources like the linting rule index and standards document are discoverable without checklist completion.
  • API schema design decision record — the schema design conventions and generated code management decisions that interact with the linting exception protocol; protobuf-generated and OpenAPI-generated code requires path-level linting exclusions rather than per-file inline exceptions, and the schema design decision record covers the generator toolchain and output path conventions that the linting configuration must account for.
  • Open-source extractor — find the coding standards decisions buried in your AI chat history: the quality initiative conversation where the team chose a style guide over a formatter and the reasoning behind that choice, the linting configuration design session where each rule was selected with rationale that was never written down, and the retrospective where the migration policy was adopted without the question of when it would be complete.