audit: establish accuracy phase 1 baseline
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user