drift.py
drift.py — flag docs that describe code which just changed.
Overview
drift.py — flag docs that describe code which just changed.
The engine behind check's third gate. The anchor index says which authored page
documents which symbol; the resolver maps each anchor to its file; git says what
changed. Intersect: a documented file changed but its page didn't → the prose may now
be lying.
changed = (branch vs base) ∪ (working tree + staged)
Two tiers: DIRECT the documented *symbol's* code changed. Gates. RIPPLE the documented symbol didn't change, but it calls a symbol defined in a file that did (graph-backed, bounded). Advisory only — never gates, silent without a graph. A weaker signal shouldn't block a push.
Both tiers share ONE oracle: an AST fingerprint of the symbol's source (formatting-
invariant, literal-sensitive — see fingerprint). A sig-less anchor compares that
fingerprint between the merge-base and the working tree, so pure formatter churn and
edits to *other* symbols in the same file never flag — only the documented symbol
changing does. An anchor pinned with sig: compares the same fingerprint against an
author-verified value instead of the base, and a mismatch is DIRECT drift whose message
carries the current sig so the author can re-verify the prose and re-pin. The idea is
fiberplane/drift's AST fingerprint; the sig lives inline in the anchor, not a lock file.
git supplies the cheap pre-filter (which files differ from base) and the base blob;
the gate *decision* for sig-less anchors is the fingerprint compare, not file
membership. sym: needs the graph and degrades without it. Stdlib only.
depends on src/documate/anchors.py, src/documate/core.py, src/documate/resolve.py · used by src/documate/briefs.py, src/documate/check.py
flowchart TD changed_files --> _git
API
Ffingerprint(ctx: Context, rel: str, line_start, line_end) -> str | None
16-hex AST fingerprint of the symbol spanning lines line_start..line_end of rel.
Reads the working-tree file, slices the span, and hands it to the graph adapter,
which parses it with the indexing engine and serialises the syntax tree. The digest
is invariant to reindentation, re-wrapping, blank lines, trailing whitespace, and
spacing-only edits (x=1 == x = 1, f( a,b ) == f(a, b)), but sensitive to
signatures, control flow, operators, called names, and the exact contents of
string/char/numeric literals ("a b" != "a b", 1_000 != 1000). A comment-only
edit does NOT change it (the engine drops comment/trivia nodes by default). None when
the span can't be read or parsed — the caller degrades to a note and never gates on a
broken oracle.
Same 16-hex shape as before, so committed sig: pins keep their *format*. The
algorithm changed (was a whitespace-collapsed line hash, which both over-flagged
x=1->x = 1 and silently missed edits inside string literals), so existing pin
*values* changed once — re-pin from the sig documate --check prints.
_symbol_drifted, find_driftF_git(ctx: Context, *args: str) -> list[str]
Run git against the repo root, returning non-blank stdout lines (empty on error).
_merge_base, changed_filesF_merge_base(ctx: Context, base: str) -> str
The base<->HEAD merge-base — the "before" snapshot the change set diffs against, and where the sig-less fingerprint compare reads the symbol's old source. Falls back to HEAD when there is no merge-base (base is an ancestor of / equals HEAD).
find_drift · calls _gitF_show(ctx: Context, ref: str, rel: str) -> bytes | None
Raw bytes of rel at ref (git show ref:rel), or None when it didn't exist
there (a file newly added since base) or git errors.
_symbol_driftedFchanged_files(ctx: Context, base: str) -> set[str]
Repo-relative paths differing from base: branch delta ∪ uncommitted.
find_drift · calls _gitFdependent_files(ctx: Context, changed_rel: set[str], hops: int, cap: int) -> tuple[set[str], bool]
Files whose symbols (up to hops) call/reference a symbol defined in a changed
file — the ripple set. Bounded by hops + cap with a truncated flag. Empty + False
without a graph (ripple degrades to nothing, never blocks).
find_driftF_collapse(rows: list[dict]) -> list[dict]
Dedup drift rows to one per (page, file) pair, sorted by page.
find_driftF_symbol_drifted(ctx: Context, mb: str, base: str, rel: str, name, line, line_end, anchor: str, notes: list[str]) -> bool
Did the documented symbol's code change between mb and the working tree?
Compares the symbol's AST fingerprint on each side: the working span (by its current line range) against the same symbol located *by name* in the base blob (line-shift proof). Formatter-only churn and edits to other symbols in the file therefore don't flag. Degrades to a note (returns False) when either side can't be read/parsed — a broken oracle never gates.
find_drift · calls _show, fingerprintFfind_drift(ctx: Context, base: str, ripple_hops: int, ripple_cap: int)
Return (direct rows, ripple rows, notes, truncated), deduped by (page, file).
_collapse, _merge_base, _symbol_drifted, changed_files, dependent_files, fingerprint