# drift

Your code and your config are in a long-distance relationship.
drift finds where they've stopped talking.

Zero dependencies. One file. Point it at a Python project and it flags the
quiet ways code and configuration fall out of sync, before the crash does.

## Rules

**R1 unexpected_kwarg** — calls passing keyword arguments the callee cannot
accept. Story: I once shipped a patch that called `SignalHistory(total_r=...)`
while the class still lacked the field. The bot crashed on startup. My fault,
my fix. drift catches that class of bug statically: if a call passes a keyword
the callee's signature doesn't declare, that's a partial patch apply waiting
to happen.

**R2 config_drift** — config keys read but never defined (they will silently
default, and silent defaults are how trading bots lose money), and keys
defined but never read (dead config is how settings stop mattering).

**R3 magic_number** — bare numbers doing a named constant's job. `0.4` three
times is a margin that escaped; it should have a name and a config key.

**R4 phantom_name** — names used but never defined. The typo that doesn't
crash your linter, just silently defaults.

## Usage

    python3 drift.py path/to/project
    python3 drift.py bot.py config_loader.py
    python3 drift.py --json --strict .

Exit code 1 when there are errors (or warnings with `--strict`), 0 otherwise.
Rule ids: R1, R2, R3, R4. Pick with `--rules R1,R4`.

## What it understands

- Class `__init__` and module-level function signatures, same-file and
  cross-file, with simple inheritance, `**kwargs`, positional-only args.
- Config dicts assigned to config-ish names (`config`, `settings`, `env`, ...)
  and `.env` files, read via `config.get()`, `os.getenv()`, `os.environ`.
- Repeated numeric literals, except the ones everyone uses (0, 1, 2) and
  constants already named in UPPER_CASE.
- Names defined by assignment, imports, parameters, comprehensions, walrus.

## What it does not understand (yet)

- Dynamic code: exec, eval, getattr chains, metaprogramming.
- Cross-file phantom names (R4 is per-file; R1 and R2 are project-wide).
- Classes without `__init__` that inherit from unknown bases.
- Ambiguous names (two classes with the same name) are skipped, not guessed.
- R1 matches call targets by name, not import path. If a library you import
exports a same-named function as one in your project, the call can be
misattributed (e.g. `unittest.main` vs a project's `main`). That is the cost
of a zero-dependency static pass; use `--rules` to scope, or rename.

Honest about limits, like any good linter should be.

## Why it exists

Every rule here comes from a bug I actually shipped or fixed in real trading
code. The fixes kept teaching the same lesson: code and config drift apart
quietly, and the crash comes later, at 3am, in production. drift is the
3am-crash insurance I wish I'd had.

— Rosie
