#!/usr/bin/env python3 """Compare selected-case native and Worker operation ledgers.""" from __future__ import annotations import hashlib import json import subprocess from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] REPORT = ROOT / "coverage/axis3-tutorial-sequence.report.json" def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) def command(args: list[str]) -> tuple[int, str, str]: result = subprocess.run(args, cwd=ROOT, capture_output=True, text=True, check=False) return result.returncode, result.stdout, result.stderr def last_json(text: str, schema: str) -> dict[str, Any] | None: for line in reversed(text.splitlines()): try: value = json.loads(line) except json.JSONDecodeError: continue if value.get("schema") == schema: return value return None def generate() -> dict[str, Any]: native_rc, native_out, native_err = command(["python3", "scripts/run_axis3_tutorial_native_sequence.py", "--output", "coverage/axis3-tutorial-native-sequence.report.json"]) worker_rc, worker_out, worker_err = command(["node", "web/worker/test_axis3_tutorial_sequence.mjs"]) native = json.loads((ROOT / "coverage/axis3-tutorial-native-sequence.report.json").read_text(encoding="utf-8")) worker = last_json(worker_out, "axis3-tutorial-sequence.v1") native_rows = [row for row in native.get("operations", []) if row.get("operation") != "MANUAL_TOOLCHANGE"] worker_rows = worker.get("operations", []) if worker else [] first_divergence = None for index, native_row in enumerate(native_rows): native_error = native_row.get("error") if native_error: operation = native_row.get("operation") first_divergence = {"index": index, "operation": operation, "native_error": native_error, "worker_status": "verified", "reason": "native 3axis-tutorial HAL has no simulated_home component; HOME_ALL remains unhomed" if operation == "HOME_ALL" else "native_operation_error"} break report: dict[str, Any] = { "schema": "axis3-tutorial-sequence-report.v1", "task_id": "TUTORIAL-12", "context_id": "linuxcnc-2.10.0-pre0-wasm-m1-py-c1", "case": "linuxcnc/configs/sim/axis/vismach/3axis-tutorial", "status": "blocked" if native.get("status") != "verified" or first_divergence else "verified", "native": {"status": native.get("status"), "returncode": native_rc, "report_sha256": native.get("report_sha256"), "stdout_sha256": hashlib.sha256(native_out.encode()).hexdigest(), "stderr_sha256": hashlib.sha256(native_err.encode()).hexdigest()}, "worker": {"status": "verified" if worker_rc == 0 and worker else "blocked", "returncode": worker_rc, "sample": worker, "stdout_sha256": hashlib.sha256(worker_out.encode()).hexdigest(), "stderr_sha256": hashlib.sha256(worker_err.encode()).hexdigest()}, "first_divergence": first_divergence, "host_boundaries": native.get("host_boundaries", []), "operation_contract": ["STATE_ESTOP_RESET", "STATE_ON", "MODE_MANUAL", "HOME_Z", "HOME_X", "HOME_Y", "JOG_X_INCREMENT", "JOG_Y_INCREMENT", "JOG_Z_INCREMENT", "MODE_MDI", "MDI_G21_G90", "MDI_G0", "MDI_G1", "PROGRAM_OPEN", "MODE_AUTO", "PROGRAM_RUN", "PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "TOOL_CHANGE_T1"], "known_boundaries": ["case INI has no HOME_SEQUENCE, so AXIS source semantics require individual Z/X/Y home rather than Home All", "manual toolchange is completed through the source-defined hal_manualtoolchange.change_button pin", "Worker fixed-tick sequence remains independently verified"], } report["report_sha256"] = hashlib.sha256(canonical(report).encode()).hexdigest() REPORT.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8") print(f"axis3_tutorial_sequence={report['status']} report_sha256={report['report_sha256']}") return report if __name__ == "__main__": raise SystemExit(0 if generate()["status"] in {"verified", "blocked"} else 1)