Complete XYZAC LinuxCNC WASM parity and smooth 3D preview

This commit is contained in:
2026-08-28 06:24:50 +08:00
parent e8fe323aab
commit 1a7995fa9b
181 changed files with 36854 additions and 215 deletions

View File

@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""Compare selected XYZAC native, WASM and Worker traces by semantic level."""
from __future__ import annotations
import copy
import hashlib
import json
import subprocess
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
REPORT = ROOT / "coverage/xyzac-trt-trace-parity.report.json"
CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini"
CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1"
LEVELS = ("protocol", "control", "state", "canonical", "recovery")
COMMANDS = {
"native": ["./build-native/sim_xyzac_trace_test"],
"wasm": ["node", "build-wasm/sim_xyzac_trace_wasm_test.js"],
"worker": ["node", "web/worker/test_xyzac_trace_parity.mjs"],
}
def canonical(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: str | bytes) -> str:
data = value.encode() if isinstance(value, str) else value
return hashlib.sha256(data).hexdigest()
def first_path(left: Any, right: Any, path: str = "$") -> str:
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}"
if left[key] != right[key]:
return first_path(left[key], right[key], f"{path}.{key}")
return path
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}]"
if left[index] != right[index]:
return first_path(left[index], right[index], f"{path}[{index}]")
return path
return path
def row_tick(left: Any, right: Any, index: int) -> int:
for value in (left, right):
if isinstance(value, dict) and isinstance(value.get("tick"), int):
return value["tick"]
return index + 1
def compare_level(left: list[Any], right: list[Any]) -> dict[str, Any]:
for index in range(max(len(left), len(right))):
lhs = left[index] if index < len(left) else None
rhs = right[index] if index < len(right) else None
if lhs != rhs:
return {
"status": "diverged",
"first_divergent_tick": row_tick(lhs, rhs, index),
"difference_path": first_path(lhs, rhs, f"$[{index}]"),
"left": lhs,
"right": rhs,
}
return {"status": "verified", "first_divergent_tick": None, "difference_path": None}
def compare_traces(left: dict[str, Any], right: dict[str, Any]) -> dict[str, Any]:
comparisons = {level: compare_level(left["levels"][level], right["levels"][level]) for level in LEVELS}
divergent = [value["first_divergent_tick"] for value in comparisons.values() if value["first_divergent_tick"] is not None]
return {
"status": "verified" if not divergent else "diverged",
"levels": comparisons,
"first_divergent_tick": min(divergent) if divergent else None,
}
def validate_trace(value: Any) -> dict[str, Any]:
if not isinstance(value, dict) or value.get("schema") != "xyzac-structured-trace.v1" or value.get("case") != "xyzac-trt.ini":
raise ValueError("xyzac_trace_schema")
levels = value.get("levels")
if not isinstance(levels, dict) or any(not isinstance(levels.get(level), list) for level in LEVELS):
raise ValueError("xyzac_trace_levels")
return value
def run_trace(name: str) -> tuple[dict[str, Any], dict[str, Any]]:
result = subprocess.run(COMMANDS[name], cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False, timeout=30)
trace = None
for line in reversed(result.stdout.splitlines()):
try:
candidate = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(candidate, dict) and candidate.get("schema") == "xyzac-structured-trace.v1":
trace = validate_trace(candidate)
break
if result.returncode != 0 or trace is None or trace.get("status") != "verified":
raise RuntimeError(f"{name}_trace_failed:{result.returncode}")
evidence = {"command": COMMANDS[name], "returncode": result.returncode, "output_sha256": digest(result.stdout), "trace_sha256": digest(canonical(trace["levels"]))}
return trace, evidence
def main() -> int:
build_commands = [
["cmake", "--build", "build-native", "--target", "sim_xyzac_trace_test"],
["cmake", "--build", "build-wasm", "--target", "sim_xyzac_trace_wasm_test"],
]
build_evidence = []
failure_samples: list[dict[str, Any]] = []
traces: dict[str, dict[str, Any]] = {}
trace_evidence: dict[str, dict[str, Any]] = {}
status = "verified"
try:
for command in build_commands:
result = subprocess.run(command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False, timeout=60)
build_evidence.append({"command": command, "returncode": result.returncode, "output_sha256": digest(result.stdout)})
if result.returncode != 0:
raise RuntimeError(f"build_failed:{command[-1]}")
for name in COMMANDS:
traces[name], trace_evidence[name] = run_trace(name)
native_wasm = compare_traces(traces["native"], traces["wasm"])
native_worker = compare_traces(traces["native"], traces["worker"])
mutated = copy.deepcopy(traces["worker"])
mutated["levels"]["state"][1]["active_type"] = 99
positive_sample = compare_traces(traces["native"], mutated)
positive_ok = positive_sample["status"] == "diverged" and positive_sample["first_divergent_tick"] == 2 and positive_sample["levels"]["state"]["difference_path"] == "$[1].active_type"
if native_wasm["status"] != "verified" or native_worker["status"] != "verified" or not positive_ok:
status = "blocked"
failure_samples.append({"code": "TRACE_PARITY_DIVERGED", "native_wasm": native_wasm, "native_worker": native_worker, "positive_sample_ok": positive_ok})
except (OSError, ValueError, RuntimeError, subprocess.TimeoutExpired) as error:
status = "blocked"
native_wasm = {"status": "blocked", "first_divergent_tick": None, "levels": {}}
native_worker = {"status": "blocked", "first_divergent_tick": None, "levels": {}}
positive_sample = {"status": "blocked", "first_divergent_tick": None, "levels": {}}
positive_ok = False
failure_samples.append({"code": "TRACE_COMPARATOR_ERROR", "detail": str(error)})
report: dict[str, Any] = {
"schema": "xyzac-trt-trace-parity-report.v1",
"task_id": "PARITY-00",
"context_id": CONTEXT_ID,
"case": {"path": CASE, "scope": "xyzac_only"},
"status": status,
"candidate_eligible": status == "verified",
"levels": list(LEVELS),
"native_wasm": native_wasm,
"native_worker": native_worker,
"positive_divergence_sample": {
"mutation": "state[1].active_type=99",
"detected": positive_ok,
"first_divergent_tick": positive_sample.get("first_divergent_tick"),
"difference_path": positive_sample.get("levels", {}).get("state", {}).get("difference_path"),
},
"first_divergent_tick": None if status == "verified" else native_wasm.get("first_divergent_tick") or native_worker.get("first_divergent_tick"),
"traces": trace_evidence,
"builds": build_evidence,
"inputs": [
{"path": "wasm-core/tests/sim_xyzac_trace_test.c", "sha256": digest((ROOT / "wasm-core/tests/sim_xyzac_trace_test.c").read_bytes())},
{"path": "web/worker/test_xyzac_trace_parity.mjs", "sha256": digest((ROOT / "web/worker/test_xyzac_trace_parity.mjs").read_bytes())},
{"path": "coverage/xyzac-trt-rtcp.report.json", "sha256": digest((ROOT / "coverage/xyzac-trt-rtcp.report.json").read_bytes())},
],
"failure_samples": failure_samples,
"known_blockers": [],
"exclusions": ["xyzbc", "3axis", "other cases", "Safari", "physical power loss", "host hardware"],
"release_status": "STOP_RELEASE",
"report_sha256": None,
}
report["report_sha256"] = digest(canonical({key: value for key, value in report.items() if key != "report_sha256"}))
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")
print(json.dumps({"schema": report["schema"], "status": status, "levels": report["levels"], "first_divergent_tick": report["first_divergent_tick"], "positive_sample_tick": report["positive_divergence_sample"]["first_divergent_tick"], "report_sha256": report["report_sha256"]}, ensure_ascii=False))
return 0 if status == "verified" else 1
if __name__ == "__main__":
raise SystemExit(main())