argotdocs

What it catches

The six axes argot flags — a foreign dependency/API/paradigm the repo has never used, a pattern its own migrations left behind, a redundant function it already has, misplaced code, an import that breaks the repo's layering, and a test gamed to green a failing suite — plus an honest account of the in-vocabulary breaks it still won't gate on.

argot catches code that is technically fine but doesn’t fit this project — valid, typed, and lint-clean, but not how this codebase writes things. It works on six axes:

Every rule defaults to severity error — a finding fails argot check — except superseded and test-weakened, which ship warn (reported, never fail the check); every rule can be reconfigured per repo or per run. See Configure.

Everything below is a real result from the shipped binary (argot check on a planted hunk, fit on the repo’s own history). Where argot flags a line, the transcript is quoted verbatim. Where it doesn’t, that’s said plainly.

Foreign — a pattern the repo has never used

This is the base voice model — statistical, no neural net. Across the whole fixture set in 11 languages, when the foreign symbol is visible in the code — an explicit import, a fully-qualified call, a distinct API name — argot catches 604 of 618 (98%) on the honest, leak-free bench. Three shapes of foreign code:

1. A foreign dependency

# an agent adds an outbound call — with the HTTP client it reflexively reaches for
import requests                          # this codebase standardises on httpx
! foreign · requests
  ↳ 0 of 74 module specifiers in this repo
  common here: fastapi (357×), pydantic (129×), typing (129×) …

requests is a fine library — it’s just foreign to this repo. The import stage flags the new top-level module directly. No linter flags it without a hand-maintained banned-imports list; argot learned “this repo’s 74 imports are fastapi, pydantic, starlette… never requests” from git history and shows the receipts.

2. A foreign API

# a call into a data layer the repo standardises away from
from pymongo import MongoClient
_audit = MongoClient("mongodb://localhost:27017")["app"]["audit"]

def record_dependency_call(name: str, resolved: bool) -> None:
    _audit.insert_one({"dependency": name, "resolved": resolved})
? suspicious · unfamiliar-callee
  ↳ _audit.insert_one — 0 of 927 callees in this cluster

The tell here isn’t only the import — it’s the call. Even where the winning signal is the call-receiver stage flagging _audit.insert_one as a callee the corpus never attests, argot is reading how you use the library, not just what you imported. A dependency-allowlist watches package.json; it cannot watch method calls. argot does.

3. A foreign paradigm — the one that sells it

An agent writes a handler as a Django class-based view in a repo that is entirely FastAPI:

class ReceiptView(View):                 # FastAPI uses function endpoints + Depends()
    def get(self, request, user_id):
        receipt = self.repo.find(user_id)
        if receipt is None:
            return HttpResponseNotFound()
        return JsonResponse(receipt.to_dict())
! foreign · unfamiliar-callee   [score 11.05]
  ↳ JsonResponse, HttpResponseNotFound — 0 of 927 callees in this cluster

Every line is valid Python. mypy is happy. ruff is happy. There is no single “bad import” to ban — it’s the whole paradigm that’s foreign: View subclasses, JsonResponse, HttpResponseNotFound, the request-first signature. argot flags it because that entire vocabulary of callees is absent from a codebase built on typed function endpoints. No linter, type checker, or dependency policy can encode “we don’t write Django-style views here” — argot learns it. This is the gap between valid and ours, and it is the class the base voice model is built to close.

Superseded — new code written the old way

Still the base voice model (rule superseded), but pointed at time rather than vocabulary. At fit, argot mines the repo’s own accepted history for supersessions — “this repo replaced X with Y” — from commits that swap an import or a call for its replacement, repeatedly, across files, in one direction, with the old side declining and the new side rising ever since. Two effects follow, at check time: the replacement (Y) never reads as foreign — the model already knows it’s where the repo is heading — and new code that reaches for the old side (X) is flagged.

// an agent reaches for the date-handling library it's used elsewhere — not the
// one this repo has been migrating away from for two months
import moment from 'moment';
? suspicious · superseded
  ↳ this repo replaced 'moment' with 'date-fns' — 4 commits across 4 files (2026-01-08..2026-01-30, e.g. 631e692)

A migration can also be declared before history carries enough signal — two lines in argot.toml, effective immediately, no refit needed:

[[migration]]
from = "moment"
to = "date-fns"
reason = "Q2 date-handling refactor"
? suspicious · superseded
  ↳ argot.toml migration to 'date-fns' — Q2 date-handling refactor

Every migration — mined or declared — is never a black box: the finding cites the evidence (real commits, files, dates, an example commit sha) or the declared reason, and argot conventions lists the whole migration alongside every corpus file still on the old side — the repo-wide “what did the refactor forget” report. Unlike the rest of the base voice model, superseded ships warn by default: a nudge mid-migration, not a gate — configurable like any rule (see Configure for the declared side).

Validated on a probe over 16 real repositories: every corpus with a real migration in its history is mined exactly it, and zero false candidates surface on the 11 corpora with no migration to find. Full methodology: docs/research/evidence/supersession-mining-probe.md.

Redundant — a function you already have

The semantic layer embeds every function at fit and indexes it. At check, a new function that duplicates one already in the repo is flagged — the nearest cross-file neighbour, with a similarity margin:

.  already implemented here (redundant)
   ↳ duplicates slugify (src/utils/text.py:1) — similarity 0.86

Real repos hold real duplication, and sometimes a second slugify is a deliberate call — argot shows you the original and lets you judge. redundant findings are pinned to the unusual confidence tier (the evidence is a similarity lookup, not a hard fact), and like every rule the severity is yours: it fails the check by default, or one config line makes it report-only (redundant = "warn" in argot.toml’s [rules]).

Misplaced — the right code, wrong place

Also the semantic layer — a function whose nearest semantic neighbours concentrate in a different package or area than the one it was filed under:

.  unusual location (misplaced)
   ↳ looks like core/downloader code filed under commands/

Same posture — a nudge to check whether a helper landed in the wrong module, pinned to the unusual confidence tier and configurable per rule (misplaced = "warn", or semantic = "off" to turn the whole group off). Both redundant and misplaced come from a per-repo code-embedding index, built with a small model compiled into the binary — local, no download, no cloud, no LLM. See How it works for the mechanics.

Layering — an import that breaks your architecture

The architecture detector. At fit, argot builds a module-dependency graph of the repo (.argot/layering.json); at check, an added internal import that reverses an established layer direction — a low-level module suddenly importing from the layer above it, or an edge out of a module the graph knows as a (near-)sink — is flagged:

.  crosses a module boundary (layering)
   ↳ core/parser now imports cli/commands — the repo's edges run the other way

This is the structural cousin of the foreign axis: every token can be repo-familiar, but the edge is one the codebase has never drawn. The canonical benchmark result covers 25 corpora across 12 languages and caught 264 of 272 (97.1%) authored layering violations. This is a detector-specific recall measure, not a product-wide accuracy claim. Findings are pinned to the unusual confidence tier and fail the check by default (layering = "warn" or "off" to downgrade). The fit-time import resolver covers Python in v1; the graph and benchmark methodology are language-agnostic.

Integrity — a test gamed to green a failing suite

The integrity detector — the complement of the other four. Where foreign-import, redundant, misplaced, and layering catch foul play with code, this one catches foul play with tests: an AI agent asked to make a failing suite pass that quietly deletes, skips, or weakens the test instead of fixing the bug.

At fit, a mini-replay of the repo’s own accepted history learns which test-editing patterns are normal here — moved/renamed tests, assertions extracted to helpers, tests retired with their feature, bulk migrations — and stores the per-repo gates in .argot/integrity.json. At check, any changeset that touches production source and weakens, disables, or removes a test is flagged; tests-only commits (suite curation, refactors, renames with no production change) are excused by design:

!  test disabled (test-disabled)
   ↳ test `test_parse` disabled — skip/ignore marker added; this change also modifies parser.py

Three rules: test-deleted — a test removed while the production code it exercised still exists; test-disabled — a skip/ignore marker added, or a test gutted to a vacuous pass; test-weakened — assertions removed, tautologized, or loosened, or an expected literal retargeted. All three are pinned to the suspicious confidence tier. test-deleted and test-disabled default to error; test-weakened ships warn — reported on every run, never fails the check on its own, downgradeable or upgradeable per repo.

The canonical benchmark result covers 23 corpora / 12 languages and caught 155 of 164 (93.9%) authored gaming tactics, with 0 of 106 legitimate-refactor controls (moves, renames, deletions alongside a removed feature, genuine strengthening) fired. In accepted-history replay, 45 of 3,602 (1.25%) test-touching commits were flagged at gating (error) severity. These are separate detector-specific measures, not a general false-positive rate. The hardest tactic is expected-value retargeting (16/21, 76%): a bare literal retarget is statically indistinguishable from a healthy TDD update, so it only fires in repos whose accepted history shows zero isolated literal flips — elsewhere the per-repo gate keeps it silent by design, a documented limit, not a bug. Full numbers, per-tactic breakdown, and the FP-hardening journey: docs/research/evidence/test-integrity-capstone.md.

A clean integrity check means no known gaming pattern fired — not that the tests are good, and not that every test is trustworthy. A fraudulent test that asserts the wrong behaviour from the start needs an oracle, not a diff, and is out of scope by design.

What argot does not reliably catch

Honesty is a feature. When a break reuses only vocabulary the repo already has — every token, callee, and import is corpus-present, and the mistake is a choice among familiar things — argot usually does not flag it, and the published numbers deliberately do not gate on it.

Wrong exception type — usually not caught

@router.get("/{user_id}", response_model=UserResponse)
async def get_user(user_id: int, db=Depends(get_db)) -> UserResponse:
    user = db.get(user_id)
    if user is None:
        raise ValueError(f"User {user_id} not found")   # repo always raises HTTPException — argot stays silent
    return user

argot does not flag this. ValueError and HTTPException are both in the repo’s vocabulary; only the choice is wrong. The semantic layer added real code understanding — it’s what powers the reinvention and placement checks above — but separating “wrong choice” from a legitimate ValueError elsewhere is a finer-grained call than a nearest-neighbour lookup makes, and forcing the base model to fire on it drives false alarms on the repo’s own code (the recovery investigation measured +1 recall for +45 false positives, blowing the ≤1.17% budget). So argot leaves it — a line it won’t cross. Same story for a manual if status_code >= 400 instead of raise_for_status(), or a sync def endpoint in an async repo: structurally non-idiomatic, but built from entirely attested tokens — and verified clean on the bench.

This is a deliberate scope line, recorded in docs/research/evidence/issue92-investigation-capstone.md: argot gates on the danger an LLM actually poses — dragging in a whole foreign pattern — not on subtle misuse of your own vocabulary.

The one-line summary

argot flagsargot won’t gate on
A dependency the repo has never imported (foreign-import)A wrong value on an attested construct
A call into a library the repo standardises away from (unfamiliar-callee)A wrong exception type where both are attested
A whole foreign paradigm — Django view, Flask route, manual validation (rare-tokens / unfamiliar-callee)A structural shape built from only-familiar tokens
A new function that reinvents one you already have (redundant)
The right code filed in the wrong package (misplaced)
An internal import that reverses your layering (layering)
A test deleted, disabled, or weakened alongside a prod change (test-deleted / test-disabled / test-weakened)A fraudulent test that asserts the wrong behaviour from the start

Treat a hit as a prompt to look, never a verdict. Foreign catches are reliable — 98% when the symbol is visible in the change. Redundant and misplaced surface the nearest existing code and let you judge; layering shows the edge that runs against the graph. Integrity catches 94.1% of authored gaming tactics at a 1.12% cost on real accepted history, and reports test-weakened findings without failing the check by default. Every rule is configurable (error / warn / off — see Configure). And there’s a line it won’t cross — a wrong choice built entirely from vocabulary you already have; argot won’t gate on that, and says so.

See these catches on real code: Caught in the wild collects verified findings from running argot over 33 open-source repositories’ git history — each one adversarially attacked by a reviewer briefed to disprove it, and every one survived.