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).