---
title: "Convention Drift"
type: "ai-smell"
slug: "convention-drift"
url: "http://localhost:3000/en/ai-smells/convention-drift.md"
category: "Maintenance"
description: "AI-generated code that quietly ignores a repo's established conventions — reinventing helpers, picking the wrong library, and using off-house naming and error-handling — so the codebase drifts toward generic, internet-average style."
---
# Convention Drift

> AI-generated code that quietly ignores a repo's established conventions — reinventing helpers, picking the wrong library, and using off-house naming and error-handling — so the codebase drifts toward generic, internet-average style.

## Signs and Symptoms

A reviewer recognizes Convention Drift when an AI change _works_ but doesn't look like it belongs in the codebase. It reaches for the global average pattern instead of the local one:

* A new helper is written inline even though a battle-tested utility already exists (`utils/date.ts`, `lib/apiClient`, a shared `Result` type).
* A different library or API than the repo standard appears (`axios` in an app that standardized on a `fetch` wrapper; `moment` where the repo uses `date-fns`).
* Naming, file layout, and error-handling style diverge — `camelCase` where the module is `snake_case`, raw `throw new Error("...")` where everything else returns a typed error, bespoke `try/catch` logging instead of the shared logger.
* Each new requirement gets its own freshly-minted, narrowly-tailored block rather than reusing an abstraction (Ox Security's "Over-Specification," seen in 80–90% of AI code).
* Symptom signature: duplicated blocks multiply. GitClear measured an 8× jump in 5+ line clone blocks in 2024, with copy/pasted lines overtaking moved (refactored) lines for the first time.

```ts
// House style (already in the repo)
import { apiClient } from "@/lib/apiClient";   // wraps auth, retries, base URL
import { formatDate } from "@/utils/date";      // app-wide, locale-aware

// AI-generated change — drifts away from both
import axios from "axios";                       // not a project dependency pattern
async function getUser(id: string) {
  try {
    const res = await axios.get(`https://api.example.com/users/${id}`); // hardcoded base URL
    return { ...res.data, joined: new Date(res.data.joined).toLocaleDateString() }; // reinvents formatDate
  } catch (e) {
    console.log("error", e);                     // not the shared logger; swallows the error
  }
}

```

The tell is consistency, not correctness: three reviewers each find a _different_ "wrong-but-working" choice, and none of them match the file next door.

## Reasons for the Problem

**Why models produce it**

* _Regression to the training mean._ LLMs are trained on a vast corpus of internet-average code, not your repository. When unprompted, they emit the statistically most common idiom (`axios`, `moment`, `console.log`), not your house idiom — your conventions are a tiny, out-of-distribution signal next to the global average.
* _Next-token self-consistency over repo-reach._ It is locally easier to complete a self-contained block (inline a date formatter) than to "know" that `@/utils/date` exists and import an identifier the model never saw. Reuse requires repo context the model doesn't hold; reinvention only requires the current buffer.
* _Limited / lossy repo context._ The canonical helper, the lint config, and the ADR that says "use the fetch wrapper" are usually outside the prompt. The model can't follow a convention it was never shown. As Factory's Eno Reyes put it, much of a convention is _tacit_ — patterns humans absorb by reading the codebase that agents simply never see.
* _Sycophancy / literal task focus._ Asked to "add `getUser`," the model does exactly that and won't volunteer "actually we already have a client for this." It optimizes for completing the stated task, not for fitting the system. Ox Security's "By-The-Book Fixation" (80–90% of samples) is the same force: it follows a generic textbook convention rather than evaluating the project's own.
* _Training-cutoff staleness._ If the repo migrated to a newer library or pattern after the model's cutoff, the model confidently reintroduces the version it was trained on.

**Why it hurts**

* _Maintainability & cognitive load._ Every drifted choice is one more way to do the same thing. Readers must hold N variants of "how we format dates" in their head; the codebase loses its single source of truth.
* _Correctness on change._ Duplicated/reinvented logic diverges silently — a bug fixed in the shared helper is _not_ fixed in the AI's copy. GitClear ties the clone explosion directly to AI assistants making tab-to-insert cheaper than reuse, while refactoring's share of changes fell from 25% (2021) to under 10% (2024).
* _Security._ Reinvented validation, auth, or query-building bypasses the hardened shared path (hardcoded base URLs, swallowed errors, ad-hoc string SQL). Ox calls the aggregate effect an "Army of Juniors": fast, functional, no architectural judgment.
* _Review load & tech-debt accrual._ Drift isn't caught by tests (the code works), so it lands in review or not at all, compounding into the "Scattered Functionality / Modular Mirage" architectural smell where related behavior is fragmented across files with no real cohesion.

## Treatment

**Prompting / workflow tactics**

* _Show the conventions._ Put the rules where the agent reads them — a `CLAUDE.md`/`AGENTS.md`/rules file listing the sanctioned client, logger, error type, naming, and "use X not Y." Conventions the model can't see, it can't follow.
* _Point at the canonical code._ "Use `@/lib/apiClient` and `@/utils/date`; do not add new HTTP libraries. Match the error-handling in `services/orders.ts`." Few-shot the repo's idiom by pasting one exemplar file.
* _Ask before it writes._ "What existing helpers/abstractions cover this? Reuse them; only add new code if none fit." This converts reinvention into reuse up front.
* _Make the linter the gate._ Require the agent to run the formatter, linter, and type-checker and fix all findings before returning. Factory's advice is to systematize quality signals (lint/format/type-check/test) so the agent optimizes toward them instead of drifting.
* _Review for fit, not just function._ Diff the imports for non-sanctioned libraries; grep for logic that should have called the shared helper; run a copy-paste detector on the PR.

**The refactor** — name the classic moves: _Remove Duplication_ / _Replace Inline Code with Function Call_, _Substitute Algorithm_, and _Extract Function_ if the right abstraction doesn't yet exist.

```ts
// BEFORE — drifted, reinvents client + date logic, hardcoded URL, swallowed error
import axios from "axios";
async function getUser(id: string) {
  try {
    const res = await axios.get(`https://api.example.com/users/${id}`);
    return { ...res.data, joined: new Date(res.data.joined).toLocaleDateString() };
  } catch (e) {
    console.log("error", e);
  }
}

// AFTER — reuses the established conventions
import { apiClient } from "@/lib/apiClient";
import { formatDate } from "@/utils/date";

async function getUser(id: string) {
  const user = await apiClient.get<User>(`/users/${id}`); // base URL, auth, retries, typed errors handled here
  return { ...user, joined: formatDate(user.joined) };
}

```

If the same drifted block appears in several PRs, that's a signal the convention is undiscoverable — fix the root cause by documenting it in the rules file and/or exposing it through a single obvious entry point, not by re-reviewing each instance.

## Detected by

- **jscpd** `duplication threshold (min-lines / min-tokens)` — Copy/paste detection (https://github.com/kucherenko/jscpd)
- **PMD CPD** `CPD duplicated code blocks` — Copy-Paste Detector (https://pmd.github.io/latest/pmd_userdocs_cpd.html)
- **ESLint** `no-restricted-imports` — no-restricted-imports (https://eslint.org/docs/latest/rules/no-restricted-imports)
- **typescript-eslint** `@typescript-eslint/naming-convention` — naming-convention (https://typescript-eslint.io/rules/naming-convention/)
- **SonarQube** `Source files should not have any duplicated blocks` — Duplicated blocks (https://docs.sonarsource.com/sonarqube-server/latest/user-guide/code-metrics/metrics-definition/)
