Research-Stack/scripts/math-first/require_math_evidence.py
Devin AI c233d648e6 chore(infra): remove rs-surface from flake.nix (Garnix shutdown) and add math-first CI scripts
- Remove rsSurface and rsSurfaceImage Nix package definitions (Garnix is
  being shut down; build rs-surface directly with cargo when needed)
- Add scripts/math-first/ Python validators (receipt validation, claims
  registry validation, math evidence gate + tests) required by CI workflows

Co-Authored-By: Allaun Silverfox <bigdataiscoming+9i37y6j2@protonmail.com>
2026-06-16 01:06:01 +00:00

122 lines
3.8 KiB
Python
Executable file

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