#!/usr/bin/env bash # P3-J: env key audit — code readers vs reference vs .env.example. # Default: report-only (exit 0). P3J_AUDIT_STRICT=1: fail on undoc code keys. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" export P3J_AUDIT_STRICT="${P3J_AUDIT_STRICT:-0}" python3 - <<'PY' from __future__ import annotations import os import re import subprocess from pathlib import Path STRICT = os.environ.get("P3J_AUDIT_STRICT", "0").strip() in ("1", "true", "yes", "on") ref_text = Path("docs/config/reference.md").read_text(encoding="utf-8") env_lines = Path(".env.example").read_text(encoding="utf-8").splitlines() ref_exact: set[str] = set() ref_prefixes: list[str] = [] for line in ref_text.splitlines(): if not re.match(r"\s*\| `BUTLER_", line): continue for tok in re.findall(r"BUTLER_[A-Z0-9_*]+", line): if tok.endswith("*"): ref_prefixes.append(tok[:-1]) else: ref_exact.add(tok) def ref_covers(key: str) -> bool: if key in ref_exact: return True return any(key.startswith(p) for p in ref_prefixes) WHITELIST = ( re.compile(r"^BUTLER_NONEXISTENT_KEY"), re.compile(r"^BUTLER_SMOKE_"), re.compile(r"^BUTLER_HOOK_"), re.compile(r"^BUTLER_TEST_"), re.compile(r"^BUTLER_WECHAT_.*_SIM$"), re.compile(r"^BUTLER_MODEL$"), ) IGNORE_EXACT = frozenset({ "BUTLER_PY", "BUTLER_PYTHON", "BUTLER_NAME", "BUTLER_ONLY", "BUTLER_BLOCKED_PROJECT_TOOLS", "BUTLER_FOO", "BUTLER_HOOK_EVENT", "BUTLER_HOOK_INPUT", "BUTLER_HOOK_TOOL", "BUTLER_NOTIFY_URLS", "BUTLER_RUNTIME_RUN_CONSISTENCY", "BUTLER_RUNTIME_SMOKE_PUSH", "BUTLER_RUN_REAL_API_SMOKE", "BUTLER_WORKFLOW_HANDOFF_ONLY", "BUTLER_DISABLE_EXPERIMENTAL_CACHE", }) def audit_whitelisted(key: str) -> bool: if key in IGNORE_EXACT: return True if key.endswith("_"): return True return any(p.match(key) for p in WHITELIST) env_keys: set[str] = set() for line in env_lines: stripped = line.strip() if not stripped.startswith("#") and not stripped.startswith("BUTLER_"): continue if stripped.startswith("#") and "=" not in stripped: continue m = re.search(r"(BUTLER_[A-Z0-9_]+)", stripped) if m: env_keys.add(m.group(1)) rg = subprocess.run( ["rg", "-o", r"BUTLER_[A-Z0-9_]+", "butler/", "scripts/"], capture_output=True, text=True, cwd=Path("."), ) code_keys = sorted( { m.group(0) for line in rg.stdout.splitlines() for m in [re.search(r"BUTLER_[A-Z0-9_]+", line)] if m } ) undoc_code = [] for k in code_keys: if audit_whitelisted(k): continue if not ref_covers(k): undoc_code.append(k) undoc_code.sort() ref_no_code = sorted( k for k in ref_exact if k not in IGNORE_EXACT and subprocess.run( ["rg", "-qF", k, "butler/", "scripts/"], capture_output=True, ).returncode != 0 ) example_missing = sorted(k for k in ref_exact if k not in env_keys and k not in IGNORE_EXACT) print("=== P3-J env audit ===") print(f"reference_exact_keys={len(ref_exact)}") print(f"reference_prefixes={len(ref_prefixes)}") print(f"env_example_keys={len(env_keys)}") print(f"code_butler_script_keys={len(code_keys)}") print(f"strict={STRICT}") print("") print("(a) Code-read BUTLER_* not covered by reference.md:") if undoc_code: for k in undoc_code: print(f" {k}") else: print(" (none)") print("") print("(b) Reference table keys with no butler/ or scripts/ occurrence (info):") if ref_no_code: for k in ref_no_code[:40]: print(f" {k}") if len(ref_no_code) > 40: print(f" ... +{len(ref_no_code) - 40} more") else: print(" (none)") print("") print("(c) Reference keys missing from .env.example (info):") if example_missing: for k in example_missing[:40]: print(f" {k}") if len(example_missing) > 40: print(f" ... +{len(example_missing) - 40} more") else: print(" (none)") if STRICT and undoc_code: raise SystemExit(1) PY echo "p3j-env-audit: OK"