vep/scripts/spine_check.py
2026-08-07 19:26:49 +00:00

80 lines
2.9 KiB
Python

#!/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())