Files
geointel/scripts/verify_accuracy_phase1_evidence.py
Jens faeb58ef6d
GeoIntel release gates / Compile, test, contracts and builds (push) Successful in 1m49s
GeoIntel release gates / Python and npm vulnerability policy (push) Successful in 21s
GeoIntel release gates / Production AI image, SBOM and container scan (push) Successful in 5m39s
GeoIntel release gates / Deploy exact gated revision to Unraid (push) Failing after 58m43s
Initial public release
2026-08-31 21:56:53 +02:00

129 lines
4.8 KiB
Python

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())