#!/usr/bin/env python3 """Generate the deterministic K8 milestone closeout record. Closeout completion means the evidence package was audited and frozen with an explicit release stop. It does not turn the blocked package into a candidate. """ from __future__ import annotations import argparse import hashlib import json from pathlib import Path from typing import Any CONTEXT = Path("context-lock.json") AUDIT = Path("coverage/post-release-audit.report.json") GATE = Path("coverage/release-candidate-gate.report.json") CLOSURE = Path("coverage/release-closure.report.json") INDEX = Path("coverage/release-evidence-index.json") OUTPUT = Path("coverage/k8-milestone-closeout.report.json") def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) def digest(value: Any) -> str: return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest() def file_digest(path: Path) -> str | None: if not path.is_file(): return None handle = hashlib.sha256() with path.open("rb") as stream: for block in iter(lambda: stream.read(1024 * 1024), b""): handle.update(block) return handle.hexdigest() def read_json(root: Path, path: Path) -> dict[str, Any]: value = json.loads((root / path).read_text(encoding="utf-8")) if not isinstance(value, dict): raise ValueError(f"{path} must contain an object") return value def artifact(root: Path, path: Path, role: str) -> dict[str, Any]: full = root / path sha = file_digest(full) if sha is None: return {"path": path.as_posix(), "role": role, "status": "missing"} return {"path": path.as_posix(), "role": role, "status": "present", "bytes": full.stat().st_size, "sha256": sha} def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]: root = root.resolve() errors: dict[str, str] = {} documents: dict[Path, dict[str, Any]] = {} for path in (CONTEXT, AUDIT, GATE, CLOSURE, INDEX): try: documents[path] = read_json(root, path) except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc: documents[path] = {} errors[path.as_posix()] = str(exc) lock = documents[CONTEXT] audit = documents[AUDIT] gate = documents[GATE] closure = documents[CLOSURE] index = documents[INDEX] context_id = lock.get("context_id") hash_rows = [] for path in (CONTEXT, AUDIT, GATE, CLOSURE, INDEX): actual = file_digest(root / path) hash_rows.append({"path": path.as_posix(), "sha256": actual, "status": "verified" if actual else "blocked"}) hash_ok = all(row["status"] == "verified" for row in hash_rows) audit_report_hash = audit.get("report_sha256") audit_report_hash_ok = bool(audit_report_hash) and audit_report_hash == digest({key: value for key, value in audit.items() if key != "report_sha256"}) stop_ok = ( gate.get("release_action") == "STOP_RELEASE" and gate.get("release_decision") == "blocked_not_release_candidate" and gate.get("candidate_eligible") is False and gate.get("blocked_count") == 6 and audit.get("release_action") == "STOP_RELEASE" and audit.get("candidate_eligible") is False and audit.get("blocked_count") == 6 ) context_ok = context_id and all( report.get("context_id") in {None, context_id} for report in (audit, gate, closure, index) ) schema_versions = lock.get("schema_versions", {}) schema_ok = bool(schema_versions) and all(value == 1 for value in schema_versions.values()) audit_checks = audit.get("checks", {}) audit_checks_ok = all(value.get("status") == "verified" for value in audit_checks.values()) blocker_ids = {row.get("id") for row in audit.get("blockers", [])} blocker_ok = len(blocker_ids) == 6 and all(row.get("status") == "blocked" for row in audit.get("blockers", [])) checks = { "input_hashes": {"status": "verified" if hash_ok else "blocked", "artifacts": hash_rows}, "audit_self_hash": {"status": "verified" if audit_report_hash_ok else "blocked", "declared": audit_report_hash}, "context_consistency": {"status": "verified" if context_ok else "blocked", "context_id": context_id}, "locked_schema_contracts": {"status": "verified" if schema_ok else "blocked", "schema_versions": schema_versions}, "audit_checks": {"status": "verified" if audit_checks_ok else "blocked", "checks": {key: value.get("status") for key, value in audit_checks.items()}}, "stop_decision": {"status": "verified" if stop_ok else "blocked", "blocked_count": gate.get("blocked_count")}, "blocker_preservation": {"status": "verified" if blocker_ok else "blocked", "count": len(audit.get("blockers", [])), "ids": sorted(str(value) for value in blocker_ids)}, } checks_ok = all(value["status"] == "verified" for value in checks.values()) and not errors report: dict[str, Any] = { "schema": "k8-milestone-closeout.v1", "context_id": context_id, "milestone": "K8", "task_id": "K8-REL-06", "status": "complete" if checks_ok else "blocked", "milestone_status": "closed_with_release_blockers" if checks_ok else "closeout_blocked", "release_decision": "blocked_not_release_candidate", "release_action": "STOP_RELEASE", "candidate_eligible": False, "source_baseline": { "linuxcnc_version": lock.get("linuxcnc_version"), "linuxcnc_source_sha": lock.get("linuxcnc_source_sha"), "context_lock": artifact(root, CONTEXT, "context_lock"), }, "inputs": [ artifact(root, AUDIT, "post_release_audit"), artifact(root, GATE, "release_candidate_gate"), artifact(root, CLOSURE, "release_closure"), artifact(root, INDEX, "release_evidence_index"), artifact(root, CONTEXT, "context_lock"), ], "checks": checks, "blockers": audit.get("blockers", []), "blocked_count": len(audit.get("blockers", [])), "rebuild": { "commands": [ "python3 scripts/generate_release_closure.py", "python3 scripts/generate_post_release_audit.py", "python3 scripts/generate_k8_milestone_closeout.py", "node web/worker/test_replay.mjs", "node web/workspace/test_recovery.mjs", "node web/workspace/test_migration.mjs", "python3 -m unittest discover -s scripts -p 'test_*.py' -v", ], "deterministic_inputs": [CONTEXT.as_posix(), AUDIT.as_posix(), GATE.as_posix(), CLOSURE.as_posix(), INDEX.as_posix()], }, "failure_policy": { "on_missing_input": "STOP_RELEASE", "on_hash_mismatch": "STOP_RELEASE", "forbidden": ["delete_samples", "lower_checks", "reclassify_as_REAL-EXCLUDED", "promote_blocked_to_accepted"], }, "errors": errors, "report_sha256": None, } report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"}) destination = output if output.is_absolute() else root / output destination.parent.mkdir(parents=True, exist_ok=True) destination.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8") return report def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1]) parser.add_argument("--output", type=Path, default=OUTPUT) args = parser.parse_args() report = generate(args.repo_root, args.output) print("k8_closeout=" + report["status"] + " action=" + report["release_action"] + " blockers=" + str(report["blocked_count"])) return 0 if __name__ == "__main__": raise SystemExit(main())