from __future__ import annotations import argparse from datetime import datetime, timezone import hashlib import json from pathlib import Path import subprocess from typing import Any REPOSITORY_ROOT = Path(__file__).resolve().parents[1] DEFAULT_EVIDENCE_ROOT = REPOSITORY_ROOT / "artifacts" / "evidence" / "accuracy" / "P1" DEFAULT_OUTPUT = DEFAULT_EVIDENCE_ROOT / "evidence-manifest.json" PROGRAM_PATHS = ( "docs/accuracy-program/00-execution-contract.md", "docs/accuracy-program/01-system-inventory.md", "docs/accuracy-program/02-data-lineage.md", "docs/accuracy-program/03-baseline-and-gaps.md", "docs/accuracy-program/04-risk-register.md", "docs/accuracy-program/05-metric-framework.md", "docs/accuracy-program/06-implementation-roadmap.md", "docs/accuracy-program/status.json", "scripts/build_accuracy_phase1_evidence_manifest.py", "scripts/collect_accuracy_phase1_inference_smoke.py", "scripts/collect_accuracy_phase1_ml_lineage.py", "scripts/collect_accuracy_phase1_runtime.py", "scripts/reproduce_accuracy_phase1_findings.py", "scripts/run_accuracy_phase1_baseline.py", "scripts/verify_accuracy_phase1_evidence.py", "tests/test_accuracy_phase1_baseline.py", ) def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def role_for(path: Path) -> str: name = path.name.lower() if name.endswith(".junit.xml"): return "test_report" if name.endswith(".sql"): return "migration_evidence" if "runtime" in name or "gpu-inference" in name: return "runtime_evidence" if "lineage" in name: return "lineage_evidence" if "reproduction" in name: return "defect_reproduction" if "ruff" in name or "lint" in name: return "lint_evidence" if "test" in name or "vitest" in name or "golden-qa" in name: return "test_evidence" if path.suffix.lower() == ".json": return "structured_inventory" return "execution_log" def record(path: Path, *, displayed_path: str, role: str) -> dict[str, Any]: return { "path": displayed_path, "role": role, "size_bytes": path.stat().st_size, "sha256": sha256_file(path), } def git_head() -> str | None: result = subprocess.run( ["git", "rev-parse", "HEAD"], cwd=REPOSITORY_ROOT, capture_output=True, text=True, check=False, ) return result.stdout.strip() if result.returncode == 0 else None def build_manifest(evidence_root: Path, output: Path) -> dict[str, Any]: if not evidence_root.is_dir(): raise FileNotFoundError(f"Evidence root does not exist: {evidence_root}") evidence_files = [ path for path in evidence_root.rglob("*") if path.is_file() and path.resolve() != output.resolve() ] evidence_records = [ record( path, displayed_path=path.relative_to(REPOSITORY_ROOT).as_posix(), role=role_for(path), ) for path in sorted(evidence_files) ] program_records = [] for relative in PROGRAM_PATHS: path = REPOSITORY_ROOT / relative if not path.is_file(): raise FileNotFoundError(f"Required Phase-1 program file is missing: {relative}") program_records.append(record(path, displayed_path=relative, role="phase1_program")) return { "schema_version": 1, "generated_at": datetime.now(timezone.utc).isoformat(), "audited_repository_head": git_head(), "claim_boundary": ( "This manifest proves retained-file identity and completeness. It does " "not establish model accuracy, human label acceptance, split independence " "or release readiness." ), "evidence_root": evidence_root.relative_to(REPOSITORY_ROOT).as_posix(), "evidence_file_count": len(evidence_records), "evidence_total_bytes": sum(item["size_bytes"] for item in evidence_records), "evidence_files": evidence_records, "program_file_count": len(program_records), "program_files": program_records, } def main() -> int: parser = argparse.ArgumentParser(description="Build the immutable GeoIntel Accuracy P1 evidence manifest.") parser.add_argument("--evidence-root", type=Path, default=DEFAULT_EVIDENCE_ROOT) parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) args = parser.parse_args() evidence_root = args.evidence_root.expanduser().resolve() output = args.output.expanduser().resolve() if output.exists(): parser.error(f"refusing to overwrite existing evidence manifest: {output}") if output.parent != evidence_root: parser.error("--output must be directly inside --evidence-root") payload = build_manifest(evidence_root, output) output.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") print( json.dumps( { "status": "created", "output": str(output), "evidence_file_count": payload["evidence_file_count"], "program_file_count": payload["program_file_count"], }, indent=2, sort_keys=True, ) ) return 0 if __name__ == "__main__": raise SystemExit(main())