from __future__ import annotations import argparse import hashlib import json from pathlib import Path from typing import Any REPOSITORY_ROOT = Path(__file__).resolve().parents[1] DEFAULT_MANIFEST = ( REPOSITORY_ROOT / "artifacts" / "evidence" / "accuracy" / "P1" / "evidence-manifest.json" ) REQUIRED_DOCUMENTS = tuple( REPOSITORY_ROOT / "docs" / "accuracy-program" / f"{index:02d}-{name}.md" for index, name in ( (0, "execution-contract"), (1, "system-inventory"), (2, "data-lineage"), (3, "baseline-and-gaps"), (4, "risk-register"), (5, "metric-framework"), (6, "implementation-roadmap"), ) ) 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 verify_record(item: dict[str, Any]) -> list[str]: errors: list[str] = [] relative = item.get("path") if not isinstance(relative, str): return ["manifest record has no string path"] path = REPOSITORY_ROOT / relative if not path.is_file(): return [f"missing file: {relative}"] expected_size = item.get("size_bytes") if path.stat().st_size != expected_size: errors.append( f"size mismatch for {relative}: expected {expected_size}, got {path.stat().st_size}" ) expected_hash = item.get("sha256") actual_hash = sha256_file(path) if actual_hash != expected_hash: errors.append( f"sha256 mismatch for {relative}: expected {expected_hash}, got {actual_hash}" ) return errors def main() -> int: parser = argparse.ArgumentParser(description="Verify GeoIntel Accuracy P1 evidence and program hashes.") parser.add_argument("--manifest", type=Path, default=DEFAULT_MANIFEST) args = parser.parse_args() manifest_path = args.manifest.expanduser().resolve() if not manifest_path.is_file(): parser.error(f"evidence manifest does not exist: {manifest_path}") payload = json.loads(manifest_path.read_text(encoding="utf-8")) errors: list[str] = [] evidence_records = payload.get("evidence_files") program_records = payload.get("program_files") if not isinstance(evidence_records, list) or not isinstance(program_records, list): errors.append("manifest must contain evidence_files and program_files arrays") evidence_records = [] program_records = [] for item in [*evidence_records, *program_records]: if not isinstance(item, dict): errors.append("manifest file record is not an object") continue errors.extend(verify_record(item)) evidence_root = REPOSITORY_ROOT / str(payload.get("evidence_root") or "") listed = { str(item.get("path")) for item in evidence_records if isinstance(item, dict) and isinstance(item.get("path"), str) } current = { path.relative_to(REPOSITORY_ROOT).as_posix() for path in evidence_root.rglob("*") if path.is_file() and path.resolve() != manifest_path } for relative in sorted(current - listed): errors.append(f"unlisted evidence file: {relative}") for relative in sorted(listed - current): errors.append(f"listed evidence file no longer exists: {relative}") for document in REQUIRED_DOCUMENTS: if not document.is_file() or document.stat().st_size == 0: errors.append(f"required document missing or empty: {document.relative_to(REPOSITORY_ROOT)}") status_path = REPOSITORY_ROOT / "docs" / "accuracy-program" / "status.json" if not status_path.is_file(): errors.append("required status.json is missing") else: status = json.loads(status_path.read_text(encoding="utf-8")) if status.get("phase1", {}).get("status") != "complete": errors.append("status.json must mark Phase 1 complete") if status.get("release", {}).get("status") != "blocked": errors.append("status.json must keep release blocked") if status.get("release", {}).get("promotion_allowed") is not False: errors.append("status.json must keep promotion disallowed") if status.get("scope", {}).get("national_building_validation") is not False: errors.append("status.json must not claim national building validation") summary = { "schema_version": 1, "status": "passed" if not errors else "failed", "manifest": manifest_path.relative_to(REPOSITORY_ROOT).as_posix(), "evidence_files_checked": len(evidence_records), "program_files_checked": len(program_records), "errors": errors, } print(json.dumps(summary, indent=2, sort_keys=True)) return 0 if not errors else 1 if __name__ == "__main__": raise SystemExit(main())