Files
linuxcncWebWASM/scripts/generate_axis_release_evidence.py

78 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Generate the auditable AXIS parity evidence index and release decision."""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
REPORT = ROOT / "coverage/axis-release-evidence.report.json"
def canonical(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def load(path: str) -> dict[str, Any]:
return json.loads((ROOT / path).read_text(encoding="utf-8"))
def sha(path: str) -> str:
return hashlib.sha256((ROOT / path).read_bytes()).hexdigest()
def entry(path: str) -> dict[str, Any]:
value = load(path)
return {"path": path, "schema": value.get("schema"), "task_id": value.get("task_id"), "status": value.get("status"), "implementation_status": value.get("implementation_status"), "report_sha256": value.get("report_sha256"), "file_sha256": sha(path)}
def generate() -> dict[str, Any]:
reports = [
"coverage/axis-source-inventory.report.json",
"coverage/axis-contract.report.json",
"coverage/axis-control.report.json",
"coverage/axis-view-edit.report.json",
"coverage/axis-case-parity.report.json",
"coverage/axis-test-matrix.report.json",
"coverage/gui-browser-visual-fe-m4.report.json",
]
gate = load("coverage/release-candidate-gate.report.json")
entries = [entry(path) for path in reports]
counts = {status: sum(row["status"] == status for row in entries) for status in ("verified", "partial", "blocked")}
report: dict[str, Any] = {
"schema": "axis-release-evidence-report.v1",
"task_id": "AXIS-REL-01/02",
"context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1",
"status": "partial",
"implementation_status": "evidence_index_complete_release_blocked",
"reports": entries,
"summary": {"total_reports": len(entries), "verified": counts["verified"], "partial": counts["partial"], "blocked": counts["blocked"], "chromium_authorized": True, "verified_case_count": 5, "blocked_case_count": 6},
"release_decision": {
"status": "blocked",
"release_action": gate.get("release_action", "STOP_RELEASE"),
"candidate_eligible": gate.get("candidate_eligible", False),
"global_blocked_count": gate.get("blocked_count"),
"reasons": [
"Firefox/Safari minimum browser matrix remains TBD/unavailable",
"OPFS worker timeout and crash/restart Playwright evidence unavailable",
"lathe/gantry/VisMach-5-axis exact resource closure remains blocked",
"official kinematics oracle remains separately blocked",
],
},
"ownership": {"source_oracle": "locked LinuxCNC 2.10.0~pre0", "runtime_authority": "Dedicated Worker/WASM", "visual_authority": "Chromium authorized slice only", "physical_safety": "REAL-EXCLUDED"},
"integrity": {"all_report_files_present": all((ROOT / path).is_file() for path in reports), "all_report_hashes_present": all(row["report_sha256"] for row in entries), "release_gate_preserved": gate.get("release_action") == "STOP_RELEASE" and gate.get("candidate_eligible") is False},
"boundaries": ["index does not promote partial reports", "Chromium authorization is scoped to DOM/visual/performance evidence", "release gate remains STOP_RELEASE until all blockers are separately closed"],
}
report["report_sha256"] = hashlib.sha256(canonical(report).encode()).hexdigest()
REPORT.parent.mkdir(parents=True, exist_ok=True)
REPORT.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
return report
if __name__ == "__main__":
result = generate()
print(f"axis_release_evidence={result['status']} reports={result['summary']['total_reports']} release={result['release_decision']['release_action']} report_sha256={result['report_sha256']}")