@dcwt/drift
v0.1.0
Published
Semantic lint with calibrated floors: prose conventions judged by a model, trusted in CI because every rule is measured against fixtures.
Downloads
266
Readme
drift
Semantic lint for the conventions AST linters can't see. You write a rule as prose — the same sentence that already lives in your CONTRIBUTING.md or AGENTS.md — and a model judges every applicable file against it, returning the probability the file violates it. What makes that trustworthy enough to gate CI is not the model; it's the harness around it:
- Calibrated floors. A finding only becomes an error or warning past a
per-rule probability floor, and floors are never guessed:
drift evalmeasures each rule against its own fixtures (real compliant files, real or corrupted violating ones) and reports precision/recall across a sweep of candidate floors. A rule whose error floor can't hold 0.8 precision fails the eval. - De-flicker. Models sample; a borderline score moves run to run. A score within ±0.1 of a floor is re-asked once and the lower of the two samples kept, so a shaky error demotes and CI doesn't flap red/green day to day.
- Honest failure. A file the judge couldn't score is a reported skip,
never a crash and never an exit-code hit. A judge that answers fewer
questions than asked is a loud
BAD_RESPONSE, never a silently unscored rule.
Linting is cheap: the default judge is a System One model (calibrated probabilities, no prose) at roughly two hundredths of a cent per file.
A rule
One directory per rule:
drift/rules/repository-no-logic/
├── rule.md
└── fixtures/
├── compliant/
│ └── plain-select.ts
└── violating/
├── dto-mapping.ts
└── domain-branching.tsrule.md is frontmatter, context prose, and the question:
---
title: Repository contains business logic
docRef: conventions.md §8
include:
- "**/repository.ts"
- "**/repository/**"
exclude:
- "**/*.test.ts"
floors:
error: 0.9
warn: 0.7
canonicalPath: "src/labels/{stem}/repository.ts"
---
A repository contains queries only. No business logic: no DTO mapping, no
unit conversion, no domain branching.
## Question
Does the repository in file "{path}" contain business logic — DTO mapping,
unit or date conversion, domain branching — beyond plain query construction?
Answer true only if logic that belongs a layer up is present.The question is the whole prompt: floors calibrate exactly this text against
exactly these fixtures, so editing the question means re-running drift eval
and re-reading the sweep. canonicalPath is the path a fixture pretends to
live at during eval — the path is part of the state the model judges, so a
fixture should be seen at the layer it claims. Keep the fixtures directory
outside your include globs.
A rule that needs a sibling file for context declares gathers (templates
over the linted path, or { find, replace } regex rewrites); at most three
related files ride along, 8 000 characters each. In fixtures, name a gathered
sibling <stem>-related.<ext>.
Running
drift # lint files changed vs origin/main
drift src/ lib/thing.ts # lint explicit paths
drift eval # measure every rule's floors against its fixtures
drift rules # list loaded rulesRules load from drift/rules (or .drift/rules, DRIFT_RULES, --rules).
The key comes from DRIFT_API_KEY / OPENROUTER_API_KEY / AI_DEFAULT_API_KEY.
Exit codes: 1 when any error-severity finding (or, for eval, any rule below
the precision bar), 2 on setup problems, 0 otherwise.
Judges
system-one(default):POST /v1/systemonethrough OpenRouter — a model that answers typed questions with calibrated probabilities and no prose.chat(--judge chat --model <id>): any OpenAI-compatible endpoint, asked for a JSON object of probabilities. Noisier scores — which is why floors are per-judge: switching judges means re-runningdrift eval, not rewriting rules.
Both are behind one seam (Judge), so the library embeds anywhere:
import { createSystemOneJudge, loadRules, runLint } from "drift";
const summary = await runLint({
judge: createSystemOneJudge({ apiKey }),
rules: await loadRules("drift/rules"),
files,
});