96 lines
5.4 KiB
Python
96 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare the source LinuxCNC XYZAC oracle with the WASM worker trace."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
NATIVE_REPORT = ROOT / "coverage/xyzac-native-tick-trace.report.json"
|
|
WASM_REPORT = ROOT / "coverage/xyzac-wasm-tick-trace.report.json"
|
|
REPORT = ROOT / "coverage/xyzac-native-wasm-tick-parity.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((value if isinstance(value, bytes) else canonical(value).encode())).hexdigest()
|
|
|
|
|
|
def first_difference(left: Any, right: Any, path: str = "$") -> str | None:
|
|
if type(left) is not type(right):
|
|
return path
|
|
if isinstance(left, dict):
|
|
for key in sorted(set(left) | set(right)):
|
|
if key not in left or key not in right:
|
|
return f"{path}.{key}"
|
|
difference = first_difference(left[key], right[key], f"{path}.{key}")
|
|
if difference:
|
|
return difference
|
|
return None
|
|
if isinstance(left, list):
|
|
for index in range(max(len(left), len(right))):
|
|
if index >= len(left) or index >= len(right):
|
|
return f"{path}[{index}]"
|
|
difference = first_difference(left[index], right[index], f"{path}[{index}]")
|
|
if difference:
|
|
return difference
|
|
return None
|
|
return None if left == right else path
|
|
|
|
|
|
def canonical_row(row: dict[str, Any]) -> dict[str, Any]:
|
|
result = dict(row)
|
|
for key in ("joint_actual", "joint_commanded", "homed", "limits"):
|
|
if isinstance(result.get(key), list):
|
|
result[key] = result[key][:5]
|
|
return result
|
|
|
|
|
|
def compare_rows(native: list[dict[str, Any]], wasm: list[dict[str, Any]]) -> dict[str, Any]:
|
|
fields = ("task_state", "task_mode", "interp_state", "exec_state", "current_line", "motion_line", "actual_pose", "commanded_pose", "joint_actual", "joint_commanded", "velocity", "queue_depth", "motion_id", "active_segment_id", "position", "homed", "limits", "kinematics_type", "motion_type", "paused")
|
|
for index in range(max(len(native), len(wasm))):
|
|
if index >= len(native) or index >= len(wasm):
|
|
return {"status": "diverged", "first_divergent_tick": index, "difference_path": f"$.rows[{index}]", "native": native[index] if index < len(native) else None, "wasm": wasm[index] if index < len(wasm) else None}
|
|
native_row = canonical_row(native[index])
|
|
wasm_row = canonical_row(wasm[index])
|
|
for field in fields:
|
|
if native_row.get(field) != wasm_row.get(field):
|
|
return {"status": "diverged", "first_divergent_tick": index, "difference_path": f"$.rows[{index}].{field}", "native": native_row.get(field), "wasm": wasm_row.get(field)}
|
|
return {"status": "verified", "first_divergent_tick": None, "difference_path": None}
|
|
|
|
|
|
def main() -> int:
|
|
native = json.loads(NATIVE_REPORT.read_text(encoding="utf-8"))
|
|
wasm_ticks = max(1, min(1000, int(native.get("row_count") or 1)))
|
|
build = subprocess.run(["node", "scripts/run_xyzac_wasm_tick_trace.mjs", "--ticks", str(wasm_ticks), "--output", str(WASM_REPORT)], cwd=ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, timeout=120)
|
|
wasm = json.loads(WASM_REPORT.read_text(encoding="utf-8")) if WASM_REPORT.is_file() else {"status": "blocked", "rows": []}
|
|
comparison = compare_rows(native.get("rows", []), wasm.get("rows", [])) if native.get("status") == "verified" and wasm.get("status") == "verified" else {"status": "blocked", "first_divergent_tick": None, "difference_path": None}
|
|
report = {
|
|
"schema": "xyzac-native-wasm-tick-parity.v1",
|
|
"status": comparison["status"],
|
|
"candidate_eligible": comparison["status"] == "verified",
|
|
"case": native.get("case"),
|
|
"native": {"report": str(NATIVE_REPORT.relative_to(ROOT)), "report_sha256": native.get("report_sha256"), "trace_sha256": native.get("trace_sha256"), "row_count": native.get("row_count")},
|
|
"wasm": {"report": str(WASM_REPORT.relative_to(ROOT)), "report_sha256": wasm.get("report_sha256"), "trace_sha256": wasm.get("trace_sha256"), "row_count": wasm.get("row_count"), "runner_returncode": build.returncode, "runner_output_sha256": digest(build.stdout)},
|
|
"comparison": comparison,
|
|
"first_divergent_tick": comparison.get("first_divergent_tick"),
|
|
"difference_path": comparison.get("difference_path"),
|
|
"known_boundaries": ["native public stat does not expose TP segment geometry", "WASM public snapshot does not expose acceleration/active blend geometry", "verified oracle availability is not a full TP numerical parity claim"],
|
|
"release_status": "STOP_RELEASE",
|
|
}
|
|
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
|
|
REPORT.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps({"schema": report["schema"], "status": report["status"], "first_divergent_tick": report["first_divergent_tick"], "difference_path": report["difference_path"], "report_sha256": report["report_sha256"]}, ensure_ascii=False))
|
|
return 0 if report["status"] == "verified" else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|