#!/usr/bin/env python3 """Require math evidence (receipt or Lean change) alongside math-track edits. This script enforces the math-first contract: if a commit or PR touches files in a math-track surface, at least one evidence file must also be present. Usage: # CI mode: compare against a base ref python3 scripts/math-first/require_math_evidence.py --from-git-diff origin/main # Pre-commit mode: check staged files python3 scripts/math-first/require_math_evidence.py --staged Exits 0 if: - No math-track files are present in the changeset, OR - At least one evidence file is also in the changeset Exits 1 if math-track files are present but no evidence file accompanies them. """ from __future__ import annotations import argparse import subprocess import sys from pathlib import Path # Math-track surface prefixes: files under these paths require evidence MATH_TRACK_PREFIXES = ( "0-Core-Formalism/lean/Semantics/", "6-Documentation/docs/distilled/", "shared-data/data/stack_solidification/", ) # Evidence patterns: if any changed file matches, the requirement is satisfied EVIDENCE_PREFIXES = ( "shared-data/artifacts/deepseek_review/", "0-Core-Formalism/lean/Semantics/", ) EVIDENCE_FILES = ( "claims.yaml", ) def get_changed_files_git_diff(base_ref: str) -> list[str]: """Get changed files by diffing against a base ref.""" result = subprocess.run( ["git", "diff", "--name-only", base_ref + "...HEAD"], capture_output=True, text=True, check=False, ) if result.returncode != 0: # Fall back to two-dot diff result = subprocess.run( ["git", "diff", "--name-only", base_ref], capture_output=True, text=True, check=True, ) return [f for f in result.stdout.strip().split("\n") if f] def get_changed_files_staged() -> list[str]: """Get staged files for pre-commit hook.""" result = subprocess.run( ["git", "diff", "--cached", "--name-only"], capture_output=True, text=True, check=True, ) return [f for f in result.stdout.strip().split("\n") if f] def is_math_track(path: str) -> bool: return any(path.startswith(p) for p in MATH_TRACK_PREFIXES) def is_evidence(path: str) -> bool: if any(path.startswith(p) for p in EVIDENCE_PREFIXES): return True if path in EVIDENCE_FILES: return True return False def main() -> int: parser = argparse.ArgumentParser(description=__doc__) group = parser.add_mutually_exclusive_group(required=True) group.add_argument("--from-git-diff", metavar="BASE_REF", help="Compare HEAD against BASE_REF") group.add_argument("--staged", action="store_true", help="Check staged files (pre-commit mode)") args = parser.parse_args() if args.staged: changed = get_changed_files_staged() else: changed = get_changed_files_git_diff(args.from_git_diff) if not changed: print("No changed files") return 0 math_files = [f for f in changed if is_math_track(f)] if not math_files: print("No math-track files in changeset — skipping evidence check") return 0 evidence_files = [f for f in changed if is_evidence(f)] if evidence_files: print(f"OK: {len(math_files)} math-track file(s) with " f"{len(evidence_files)} evidence file(s)") return 0 # Math-track files present but no evidence print("FAIL: math-track files changed but no evidence accompanies them:") for f in math_files: print(f" - {f}") print("\nPlease include at least one of:") print(" - A DeepSeek review receipt under shared-data/artifacts/deepseek_review/") print(" - A Lean source file under 0-Core-Formalism/lean/Semantics/") print(" - An update to claims.yaml") return 1 if __name__ == "__main__": raise SystemExit(main())