"""Run completion protocol (Layer 16, spec 22, RC_V1). RUN_COMPLETE.json is produced ONLY after all chunks are committed, the aggregator is flushed, evidence is computed, and the independent verifier returned ACCEPTED. It contains no self-referential hashes; machine verification re-checks every hash and file from disk. """ import os import json from . import versions from .util import atomic_write_json def build_run_complete(out_root, ckpt, evidence, verifier_report_path, verifier_verdict, layer_hashes, outputs): payload = { "schema_version": versions.RUN_COMPLETE_SCHEMA_VERSION, "status": "COMPLETED", "run_id": ckpt["run_id"], "source_identity": ckpt["source_identity"], "versions": ckpt["versions"], "rows": { "parsed": ckpt["cumulative"]["rows_parsed"], "canonical": ckpt["cumulative"]["rows_canonical"], "bytes_read": ckpt["cumulative"]["bytes_read"], "malformed_total": sum(ckpt["cumulative"]["malformed"].values()), }, "malformed": ckpt["cumulative"]["malformed"], "outputs": outputs, "checkpoint": { "last_completed_chunk": ckpt["last_completed_chunk"], "next_chunk": ckpt["next_chunk"], "status": ckpt["status"], }, "hashes": { "evidence_manifest_hash": evidence.get("evidence_manifest_hash"), "layer_hashes": layer_hashes, }, "verifier_report": verifier_report_path, "verifier_verdict": verifier_verdict, "completion_ts_utc": _utc_now_iso(), } atomic_write_json(_run_complete_of(out_root), payload) return payload def _run_complete_of(out_root): return os.path.join(out_root, "RUN_COMPLETE.json") def load_run_complete(out_root): with open(_run_complete_of(out_root), "r", encoding="utf-8") as fh: return json.load(fh) def verify_run_complete(out_root, evidence_payload): """Machine re-check (spec 22): status COMPLETED and the recorded evidence manifest hash must equal the on-disk evidence manifest hash. Returns (ok, diffs).""" try: rc = load_run_complete(out_root) except Exception as e: return False, ["RUN_COMPLETE.json unreadable: %s" % e] diffs = [] if rc.get("status") != "COMPLETED": diffs.append("status != COMPLETED") if rc.get("hashes", {}).get("evidence_manifest_hash") != \ evidence_payload.get("evidence_manifest_hash"): diffs.append("evidence_manifest_hash mismatch") return (not diffs), diffs def _utc_now_iso(): import datetime return datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")