#!/usr/bin/env python3 """VEP spine-check — merge gate (commitment #5). For every file changed in the diff that looks like code (.py/.sql/.yml/.yaml/.js/.ts), require evidence that it is justified by the spine: 1. references a JL id (e.g. JL-013 or #2) in the file (JL justification) 2. has a corresponding test entry OR is itself a test (value test) 3. names an owning agent/actor (from contracts: Owner, Builder, T0..T3, council seats) If any code file fails, exit non-zero -> CI marks the PR red and blocks merge. Usage: python scripts/spine_check.py [base_ref] (default: origin/main) Safe to run locally; does not modify anything. """ from __future__ import annotations import os import re import subprocess import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CODE_EXTS = (".py", ".sql", ".yml", ".yaml", ".js", ".ts") JL_RE = re.compile(r"\b(JL-\d{3}|#[0-9]{1,2})\b") AGENT_RE = re.compile( r"\b(Owner|Builder|T0|T1|T2|T3|admin|actor|human|digital|tenant|analyst|advisor|finance|ops|commercial|people|data)\b", re.IGNORECASE, ) TEST_DIR_RE = re.compile(r"(^|/)tests?/", re.IGNORECASE) TEST_FILE_RE = re.compile(r"(test_|_test\.|conftest)", re.IGNORECASE) def changed_files(base: str) -> list[str]: try: out = subprocess.check_output( ["git", "diff", "--name-only", f"{base}...HEAD"], cwd=ROOT, text=True ) except subprocess.CalledProcessError: # fall back to working tree out = subprocess.check_output( ["git", "diff", "--name-only"], cwd=ROOT, text=True ) return [f.strip() for f in out.splitlines() if f.strip()] def main() -> int: base = sys.argv[1] if len(sys.argv) > 1 else "origin/main" files = changed_files(base) code_files = [f for f in files if f.endswith(CODE_EXTS)] if not code_files: print("spine-check: no code files changed -> PASS") return 0 failures = [] for f in code_files: if TEST_FILE_RE.search(f) or TEST_DIR_RE.search(f): continue # tests are the value-proof themselves path = os.path.join(ROOT, f) if not os.path.exists(path): continue with open(path, encoding="utf-8", errors="replace") as fh: text = fh.read() problems = [] if not JL_RE.search(text): problems.append("no JL justification (no JL-### or #n reference)") if not AGENT_RE.search(text): problems.append("no owning agent/actor named") if problems: failures.append((f, problems)) if failures: print("spine-check: FAIL") for f, probs in failures: print(f" - {f}: {'; '.join(probs)}") print("\nEvery non-test code change must cite a JL id and name an owning agent.") return 1 print("spine-check: PASS (all changed code justified + agent-bound)") return 0 if __name__ == "__main__": sys.exit(main())