#!/usr/bin/env python3 """ governance-drift-check — report state claims in ~/CLAUDE.md that the substrate contradicts. Reports. Does not correct. Detection needs no authorization; correction does (Constitutional Constraint #1). This exists so that a stale governance document is *visible* rather than *misleading* — Constitutional Constraint #4, honest degradation, applied to the governance document itself. Every check carries a POSITIVE CONTROL in the same run: an absence is not evidence until the instrument is shown capable of detecting presence. (Ratified 2026-07-27 as an epistemic standard, jurist Q2.) Built 2026-07-27 on steward authorization. Exit code is always 0 — this is a report, not a gate. """ import json import os import re import subprocess import sys from datetime import date from pathlib import Path HOME = Path.home() CLAUDE_MD = HOME / "dotfiles" / "CLAUDE.md" MONTHS = ("january february march april may june july august september " "october november december").split() findings: list[str] = [] controls: list[tuple[str, bool]] = [] # (label, passed) def control(label: str, passed: bool) -> bool: controls.append((label, passed)) return passed # ---------------------------------------------------------------- load if not CLAUDE_MD.exists(): print(f"drift-check: CANNOT RUN — {CLAUDE_MD} not found") sys.exit(0) text = CLAUDE_MD.read_text() lines = text.split("\n") # ------------------------------------------------- 1. referenced paths # Backticked paths that look like filesystem locations. raw = set(re.findall(r"`(~[^`\s]+|/Users/[^`]+?)`", text)) paths = {p for p in raw if ("/" in p) and not p.endswith(("`",))} control("path-check instrument reaches the filesystem", CLAUDE_MD.exists()) for p in sorted(paths): expanded = Path(os.path.expanduser(p.strip().rstrip(".,;"))) if not expanded.exists(): ln = next((i + 1 for i, l in enumerate(lines) if p in l), None) findings.append(f"L{ln}: path does not resolve — {p}") # ------------------------------------- 2. named MCP tools / servers # Tool names the document instructs the executor to call. tool_names = set(re.findall(r"`(kg_\w+|diary_\w+|find_tunnels|traverse)`", text)) configured: set[str] = set() for cfg in (HOME / ".claude/settings.json", HOME / ".claude/settings.local.json", HOME / ".claude.json", HOME / ".mcp.json"): if cfg.exists(): try: configured |= set((json.loads(cfg.read_text()).get("mcpServers") or {}).keys()) except Exception: pass control("MCP config readable (>=1 server found somewhere)", bool(configured)) if tool_names and not any("mempal" in s.lower() for s in configured): ln = next((i + 1 for i, l in enumerate(lines) if "kg_query" in l), None) findings.append( f"L{ln}: {len(tool_names)} named tools do not resolve — " f"{', '.join(sorted(tool_names))} (no mempalace MCP server configured; " f"servers present: {sorted(configured) or 'none'})" ) # ------------------------------------------------- 3. hooks claimed to fire hook_claims = re.findall(r"(Stop and PreCompact hooks fire|PreCompact hook)", text) if hook_claims: live: set[str] = set() for cfg in (HOME / ".claude/settings.json", HOME / ".claude/settings.local.json"): if cfg.exists(): try: h = json.loads(cfg.read_text()).get("hooks") or {} live |= {k for k, v in h.items() if v} except Exception: pass control("hooks config readable (>=1 hook event configured)", bool(live)) missing = [e for e in ("Stop", "PreCompact") if e not in live] if missing: ln = next((i + 1 for i, l in enumerate(lines) if "PreCompact" in l), None) findings.append( f"L{ln}: document claims these hooks fire, but they are unconfigured — " f"{', '.join(missing)} (configured: {sorted(live) or 'none'})" ) # ---------------------------------------------------- 4. expired horizons today = date.today() horizon_re = re.compile(r"through\s+(?:the\s+)?end\s+of\s+(\w+)\s+(\d{4})", re.I) control("horizon regex matches a known-present phrase", bool(horizon_re.search(text)) or "through end of" not in text.lower()) for i, line in enumerate(lines, 1): for m in horizon_re.finditer(line): month, year = m.group(1).lower(), int(m.group(2)) if month in MONTHS: mi = MONTHS.index(month) + 1 if (year, mi) < (today.year, today.month): findings.append( f"L{i}: horizon expired — \"{m.group(0)}\" " f"(elapsed {(today.year - year) * 12 + today.month - mi} months)" ) # ------------------------------------------------ 5. structural integrity if not text.endswith("\n"): findings.append(f"L{len(lines)}: no terminal newline " f"(wc -l undercounts by 1 — breaks line-referenced patches)") for i, line in enumerate(lines, 1): # `||` inside a line carrying table pipes = two rows fused onto one line. # Do NOT require the line to start with `|`: the real instance (L243) began # mid-sentence, which is exactly how the fusion hides. if "||" in line and line.count("|") >= 3: findings.append(f"L{i}: table rows fused on one line (`||`) — row will not render") if re.match(r"^\s+\|", line): findings.append(f"L{i}: table row has stray leading whitespace — breaks the table") if re.match(r"^#{2,4} .*\s+$", line) and i < len(lines) and lines[i].startswith("#"): findings.append(f"L{i}: empty heading stub immediately followed by a heading " f"— orphans the section beneath it") # ------------------------------------------------------------- report failed_controls = [lbl for lbl, ok in controls if not ok] if failed_controls: print("⚠ drift-check: INSTRUMENT NOT VERIFIED — treat results as unestablished") for lbl in failed_controls: print(f" failed control: {lbl}") print() if not findings: print(f"✓ governance drift-check: CLAUDE.md clean " f"({len(controls)}/{len(controls)} controls passed, {len(paths)} paths verified)") else: print(f"⚑ governance drift-check: {len(findings)} claim(s) in ~/CLAUDE.md " f"contradicted by substrate") for f in findings: print(f" {f}") print("\n Correction requires [ESCALATE] (Constitutional Constraint #1). " "This report is detection only.") sys.exit(0)