drift verifies that requirements stated in an AI coding session's transcript actually landed in the code that shipped, by extracting them once with an LLM and checking each one deterministically against a code graph.
A diff shows what the code is, never what is missing. In a multi-turn AI coding session, a requirement stated in turn one can be silently dropped during a refactor in turn nine, and git records nothing about it.
Intended user: someone auditing an AI agent's coding session — a reviewer, or the agent's own operator — who needs to know whether stated requirements actually landed, not just what changed.
Why it matters: the obvious way to check "did the agent do what was asked" is to have a model read the diff and confirm it. That reproduces the exact failure mode — confident agreement — that let requirements go missing in the first place. drift's core bet is separating the one reading-comprehension task (what was asked) from verification (did it land), and never letting a model do both.
Track 1: Privacy Boundary.
Entire is essential, not incidental: entire checkpoint explain <id> --transcript is the only source of the deterministic, replayable multi-turn
transcript (including tool_use payloads) that lets drift see what was asked
and what was written, turn by turn. entire graph snapshot / impact /
search / diff are the only source of a deterministic, language-aware code
graph to check those requirements against. The project's own rule — shell out
to entire via subprocess, never reimplement it — isn't a style preference;
reimplementing either piece would turn drift into exactly the kind of
unverifiable tool it exists to catch.
Single file drift.py (~398 lines), Python 3 stdlib + requests.
run_entire()shells out toentire checkpoint explain <id> --transcriptandentire graph snapshot --repo . --format ndjson.parse_transcript()groups raw JSONL content-blocks into turns (a streamed assistant message spanning multiple content-block lines collapses into one turn, keyed by API message id).extract_requirements()— the one LLM call in the whole program — turns user-prompt turns into a requirement + assertion list, schema-constrained at decode time (Ollamaformat/ Anthropicoutput_config). Backend is swappable:local(Ollama) orapi(Anthropicclaude-haiku-4-5), same return shape either way.build_graph_index()turns the ndjson snapshot into symbol-by-name and edge-by-source lookups.ASSERTION_HANDLERSdispatches each of the five closed assertion types (symbol_exists,has_inbound_edge,calls,test_references,literal_in_body) to a deterministic evaluator, producing a verdict (landed/absent/unverified), an evidence string, and anevidence_class.find_dropped_turn()re-scansabsentrows backwards throughWrite/Edittool_use payloads to catch requirements that existed mid-session and vanished before the final graph (dropped).main()prints the table and exits non-zero on anyabsent/droppedrow.
entire graph capabilities --jsonestablished that Python here is a fully semantic language (CALLS,ASYNC_CALLS,DATA_FLOWS, etc.) but has noWRITES_FIELD/READS_FIELD/ACCESSESrelation support, and the actual snapshot never emits function parameters or instance attributes as their own symbol records. That single fact is the root cause of today's timeout/test bug (see Curveball, below) — confirmed by direct inspection of the demo-repo's ownentire graph snapshotoutput, not by re-running the LLM.entire graph impacton every function in the extraction backend, transcript parser, and verdict/evidence path (saved in.artifacts/graph_impact.txt) surfaced a real tooling limitation worth recording: dict-based dispatch is invisible to static impact analysis.entire graph impact --symbol eval_symbol_exists(and every othereval_*handler) reports zero callers, even though they're live code reached through theASSERTION_HANDLERSdict — the dict literal itself isn't a symbol the graph resolves call edges through.entire graph diff --base main --head stage-1-4-progress(saved in.artifacts/graph_diff.json) confirms the change at symbol granularity:parse_transcript,build_graph_index, andmainbodies changed; one new CLAUDE.md section;test_drift.pyis new. It does not show theASSERTION_HANDLERS/EXTRACTION_SYSTEMedits as discrete entries, for the same module-level-literal limitation noted above — an honest gap in what the graph diff can see, not something drift hides.
Assumption invalidated: the design assumed a transcript could be sent wholesale to a hosted model, and that all verdicts carry equal epistemic weight. Both are false under the Privacy Boundary constraint.
Design changed: local (Ollama) is now the enforced default with no
external call; --backend api requires explicit opt-in via
--allow-external and prints the destination. Every verdict carries an
evidence_class — confirmed_structural, heuristic, or
requires_verification — assigned by which evaluator produced it, not by
whether it passed. Redacted or missing fields degrade to a partial report
instead of crashing or silently dropping rows.
Why the result is safe: confirmed_structural rows are graph relation
lookups a judge can independently verify; heuristic rows are visibly marked
as textual/regex-based and were never claimed as stronger;
requires_verification rows were previously silent and now surface instead
of vanishing. Nothing that was landed, absent, or dropped under the old
evidence path changed meaning — only its trust label became explicit.
The commit message's claim that graph impact analysis preceded every edit
(rather than ran alongside it) was independently checked against the
recorded session checkpoint transcript, tool-call by tool-call, before this
document was written — not assumed. All entire graph commands (capabilities,
search, impact, and the diagnostic snapshot) ran and were reported at
tool-call indices 9–23; the first Edit to drift.py was index 34. The claim
held.
Repo: https://github.com/ComputerTech99/drift
Entire does not expose a standalone web URL per checkpoint in this workflow —
checkpoints are git refs on this same repo. The correct and complete way to
reference or inspect one is: the repo URL above, the checkpoint's ID, and the
command entire checkpoint explain <id> (add --transcript for the raw
JSONL) run against a clone of that repo. That's not a limitation being
apologized for — it's how this tool's checkpoints actually work, and it's
what was used throughout this build and this document to verify every claim
made about them.
Eight checkpoints exist today (2026-09-06), each tied to one committed stage
on this branch (verified via entire checkpoint explain <commit-sha>, not
assumed from commit messages). The four required milestones are the first
four — Stage 1, Stage 2, Stages 3–4, and the Privacy Boundary curveball. The
remaining four are documentation/evidence/cleanup follow-ups made while
preparing this submission itself, included for a complete history rather than
trimmed to look tidier than what actually happened:
The four required milestones:
| Checkpoint | Commit | Created | What it proves |
|---|---|---|---|
01M1TF95A2HA835AYCY52A9RZN |
ee233a4 |
04:23:03 | Stage 1 landed: transcript JSONL parsing (parse_transcript) and graph-snapshot parsing (build_graph_index) exist, built before any extraction or evaluation logic. |
01M1TNGEWC61CPW6SRDVFBJXMM |
98d7410 |
06:11:52 | Stage 2 landed: extract_requirements with a swappable local/api backend and schema-constrained JSON output, as the single LLM call in the program. |
01M1TQ54CDJMHNHTPCR34RVHDY |
7091380 |
06:40:38 | Stages 3–4 landed, plus a self-caught regression: the transcript shows the extraction prompt over-forcing background prose into requirements and avoiding unverifiable, both fixed and re-verified against the tool's own self-referential checkpoint; find_dropped_turn added; the timeout/test false-dropped bug was identified but explicitly left unfixed when this checkpoint closed. |
01M1TRF08X18M5CXSVY310AW0Z |
8eb8e77 |
07:03:30 | Today's curveball: diagnosed the timeout/test bug as an extraction-assertion mismatch (not an evaluator bug) via direct graph inspection before any edit; then enforced the Privacy Boundary (local-default backend, --allow-external gate, evidence_class tiering, redaction hardening), backed by 14 passing tests. |
Submission-prep follow-ups (documentation, evidence capture, and cleanup — no changes to drift.py or test_drift.py in any of these):
| Checkpoint | Commit | Created | What it proves |
|---|---|---|---|
01M1TV1DRSHP9MEEEFNBX635GK |
79e3fc5 |
07:48:32 | Added this document and .artifacts/ (the graph search/impact/diff evidence, persisted rather than run-and-discarded). |
01M1TVCS9BP281QYHS1DZ894PS |
b580d85 |
07:54:44 | Fixed the setup instructions (venv needed for PEP 668 systems) and disclosed the key-fragment finding from the final secrets checklist. |
01M1TW0KSKTHPHFEE3DD11H166 |
50bdd1d |
08:05:34 | Documented --backend api as present but untested this session (no fresh key supplied), and the local-model extraction-quality demo risk found during dry-run testing. |
01M1TW3PTW9Z873JEWTYCKZ32Q |
a302f3a |
08:07:15 | Expanded this table to all checkpoints that exist (rather than undercounting at 4) and hardened the local-extraction limitation from "unverified" to "demonstrably weak" with concrete dry-run evidence. |
# requires entire 0.6.2+ and Python 3. On systems with a PEP 668-managed
# system Python (Debian/Ubuntu, current Homebrew Python - a bare `pip
# install` fails there with "externally-managed-environment"), use a venv:
python3 -m venv venv && source venv/bin/activate
pip install requests
# local backend (Ollama on localhost) is the default and sends nothing off-machine
python drift.py --repo <path-to-repo> --checkpoint <checkpoint-id>
# hosted fallback requires explicit opt-in and prints what leaves the machine
python drift.py --repo <path-to-repo> --checkpoint <checkpoint-id> \
--backend api --allow-external
# tests: stdlib only, no network, no subprocess call to `entire`
python -m unittest test_drift -vNot applicable. This build does not use Databricks. Its only two data sources
are entire checkpoint explain --transcript (session transcripts) and
entire graph snapshot (code graph), both local and deterministic.
- The timeout/test false-
droppedbug (Stage 3–4) was an extraction-assertion mismatch, not an evaluator bug: this graph never exposes Python constructor parameters or attributes as checkable symbols, so naming one insymbol_existswas structurally unwinnable. Fixed by narrowing the extraction prompt's definition of "symbol" — but this is a probabilistic mitigation on a single non-deterministic LLM call, not a guarantee. A future run could still mistarget an assertion, and evidence tiering must never be used to excuse that if it happens again. - Local-model extraction quality is demonstrably weak, not just
unverified. Three separate live runs of
gemma3:4b(the only pulled model small enough to be usable here;nemotron-3-nano:4bwas available but not exercised) against the real demo-repo checkpoint were made across this build: one produced malformed, repetition-looped JSON; two later ones (in the final dry-run pass) completed but hallucinatedEXTRACTION_SYSTEM's own few-shot examples — literally "build a CLI tool called drift" and "add retry with exponential backoff" — as if they were real requirements from the HTTP-client session, and in one run fabricated a nonexistent symbol name (exponential_backoff) to check against. Also,OLLAMA_MODELdefaults to"llama3.1"indrift.py, which was never pulled on this machine — the literal default backend fails outright unlessOLLAMA_MODELis exported to a model that's actually installed. All of today's positive verification (the corrected evidence table, the timeout/test diagnosis, the passing end-to-end tests) used previously recorded hosted-API output or mocked extraction, never a completed, correct real local-model run.--backend local's argument handling, URL, and degradation behavior are verified; what a real local model actually extracts, on this machine, is not demo-ready. Stated plainly: local extraction on this hardware, on a real-length transcript, has produced hallucinated content — including reproducing its own prompt's few-shot examples as if they were real extracted requirements — rather than degrading gracefully. The graph verification layer is unaffected by this: it correctly reported these fabricated symbols asabsentrather thanlanded, because it never trusts the extraction output as ground truth. See.artifacts/graph_verification_capture.txtfor a control-case capture of that layer running correctly on both a real symbol and a mistargeted one, independent of any extraction call — this is the output shown for the live demo, not a fresh extraction run. entire graph impactcannot see calls made through theASSERTION_HANDLERSdict-dispatch table — a load-bearing part of the verdict/evidence path shows zero callers under static impact analysis. Anyone adding a sixth assertion type should know graph tooling won't catch broken wiring there.partialis a documented verdict (CLAUDE.md: "symbol exists,has_inbound_edgefails") that was never implemented as a distinct row inmain()— it is not currently produced. Left untouched as out of scope for this checkpoint, but it is a real, pre-existing gap between documented and actual behavior.--backend apiis present and code-complete (schema-constrained hosted fallback viaclaude-haiku-4-5, gated behind--allow-external) but untested this session: the old key from a prior session was confirmed absent from this environment (unset, not in any shell rc file or.env), and no fresh key was supplied to test a live call against it before submission. Do not read the earlier checkpoints' recorded--backend apiruns as proof this session's code path was re-verified live — it wasn't.--backend localis what was demonstrated today.- Found while auditing this submission, from a session predating today's
work: the
last-promptpreview field of two earlier checkpoints (01M1TNGEWC61CPW6SRDVFBJXMM,01M1TQ54CDJMHNHTPCR34RVHDY) contains an 18-character fragment of what appears to be a real Anthropic API key, truncated by the harness's own preview at that length — only a few characters beyond the public key-format prefix were exposed. It surfaces becauseentire checkpoint explain <id> --transcript— the same command drift.py itself runs — returns it. Not introduced by today's checkpoint (confirmed absent from both of today's checkpoints); not remediated here, since rewriting already-pushed, already-merged checkpoint/git history is a destructive, shared-history operation outside this checkpoint's scope and requires the repo owner's explicit decision. Recommendation: rotate that key.