The drift catalog
The drift catalog names the ways a plan-suite rots (§20). It is honest about its own incompleteness: every card below is rendered from the engine export, and the last class exists because every enumeration is one element short.
C1 Wrong-owner attribution in narrative
`Plan [N != canonical].*<symbol>`
C2 Short-form slug in code blocks
`"plan-0[0-9]"[^-]` (excluding scope-exempt unit tests)
C3 Helper signature drift (1-arg / 3-arg / kw-only / dict-return)
`^async def export_*\(session,`, `-> dict\b`
C4 Title L1 vs body Version drift
`head -1` + `head -7 | grep -E "Status:|Version:"`
C5 3-arg Callable in code blocks
`Callable\[\[.*UUID, UUID\], Awaitable` (negative match)
C6 Legacy register kwargs
`register_*\([^)]*export_hook=|legacy_kwarg=`
C7 Wrong module path
grep canonical vs found path per plan
C8 Kw-only marker before UUIDs
`^\s+\*,$` near function signatures
C9 §18 stale Plan-N pin freshness
`Plan #N v3.X.Y` vs current head; **also re-run after own bumps**
C10 Legacy names in narrative
catalog of retired symbol names; flag outside HISTORICAL annotations
C11 In-body docstrings inside `async def` / `def` blocks (ship to production)
`grep -A 30 "^async def"` and scan body docstrings
C12 Adjacent-bullet enumeration ±50 lines of fixed line
for each line N you fix, scan N-50 to N+50 for sibling drift
C13 Post-bump freshness re-sweep
re-run C9 AFTER own version bumps land in same wave
C14 Version-header trinity parity
title L1 ↔ `**Plan revision:**` ↔ `**Version:**` (and `**Status:**` if used) all match
C15 Per-plan Why-narrative legacy sweep
scan ALL plans, not just plans the brief explicitly named
C16 Mandate ↔ Detection enforcement gap
for each Mandate clause, run its Detection grep + verify it actually matches what the prose says (e.g., `datetime\.now\(` regex matching `datetime\.now(UTC)`); too-tight regex misses real cases, too-broad regex false-positives on legitimate code
C17 Detection comment ↔ Detection script narrative drift
when a Detection script's regex is generalized (e.g., one token → multiple tokens), re-read the surrounding comment block + echo message + ensure they describe the new generalized behavior, not the original narrow case
C18 **Fail-open enumeration** (§6.5) — a guard, parser, sanitiser, index or CI gate defined as a denylist of the known-bad, so an unrecognised construct **passes** FAIL-OPEN ENUMERATION
Enumerate every guard, then ask the two §6.5 questions of each: (1) *what happens to a construct it does not understand?* — if "it passes", it is fail-open; (2) *is its safety oracle drawn from the same catalog as the threat?* — if yes, it is circular. Greps that surface candidates: `except <SpecificError>` with no bare fallback; literal name lists (`in {"requests", "httpx", ...}`); `if <known_bad>: reject` with no `else: reject`; any indexer that enumerates node/def types (does it drop `async def`?). **The catalog itself is a fail-open enumeration — C18's grep list is a starting point, not a closure**
Ask its two questions of every guard you ship. One: if this guard encounters a construct it does not understand, what happens? If the answer is 'it passes', the guard is fail-open. Two: is the guard's safety oracle drawn from the same catalog as the threat? If yes, it is circular and proves nothing (§6.5).