Complete XYZAC LinuxCNC WASM parity and smooth 3D preview
This commit is contained in:
185
scripts/compare_xyzac_native_wasm_trace.py
Normal file
185
scripts/compare_xyzac_native_wasm_trace.py
Normal 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())
|
||||
@@ -23,6 +23,7 @@ RUNTIME_FIXTURE_FILES = (
|
||||
"linuxcnc/configs/sim/axis/ja_tests/xz/xz_cmds.hal",
|
||||
"linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_cmds.hal",
|
||||
"linuxcnc/configs/sim/axis/ja_tests/xz/xz_both_auto_cmds.hal",
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt-headless_cmds.hal",
|
||||
)
|
||||
|
||||
|
||||
|
||||
64
scripts/generate_xyzac_trt_gate_matrix_report.py
Normal file
64
scripts/generate_xyzac_trt_gate_matrix_report.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the XYZAC-only gate matrix evidence for RTCP-03."""
|
||||
|
||||
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/xyzac-trt-gate-matrix.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"
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
command = ["node", "web/worker/test_xyzac_gate_matrix.mjs"]
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False)
|
||||
sample = None
|
||||
for line in reversed(result.stdout.splitlines()):
|
||||
try: value = json.loads(line)
|
||||
except json.JSONDecodeError: continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-gate-matrix-rtcp-03.v1": sample = value; break
|
||||
status = "verified" if result.returncode == 0 and sample and sample.get("status") == "verified" else "blocked"
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-gate-matrix-rtcp-03.v1",
|
||||
"task_id": "RTCP-03",
|
||||
"context_id": CONTEXT_ID,
|
||||
"case": {"path": CASE, "sha256": hashlib.sha256((ROOT / CASE).read_bytes()).hexdigest()},
|
||||
"scope": "xyzac_only",
|
||||
"status": status,
|
||||
"candidate_eligible": status == "verified",
|
||||
"authority": "linuxcnc-wasm-worker",
|
||||
"command_trace": ["HELLO", "SESSION_ATTACH", "LOAD_WORKSPACE", "PROGRAM_LOAD", "PROGRAM_RUN", "PROGRAM_PAUSE", "PROGRAM_ABORT", "ESTOP", "IO_RECOVER", "SNAPSHOT_REQUEST"],
|
||||
"tick_trace": [0, 0, 0],
|
||||
"state_hash": sample.get("final_state_hash") if sample else None,
|
||||
"gate_scenarios": {
|
||||
"no_file": {"status": "verified", "reason": "axis_taskfile_missing"},
|
||||
"highlight_missing": {"status": "verified", "reason": "axis_highlight_missing"},
|
||||
"switchkins_hal_missing": {"status": "verified", "reason": "xyzac_hal_pin_vars_missing"},
|
||||
"switchkins_estop": {"status": "verified", "reason": "xyzac_estop_active"},
|
||||
"running_pause_abort": {"status": "verified", "pause": True, "abort": True},
|
||||
"paused_resume": {"status": "verified", "resume": True},
|
||||
"vismach_clear": {"status": "verified", "visual_only": True},
|
||||
},
|
||||
"tests": [{"command": command, "returncode": result.returncode, "status": status, "output_sha256": hashlib.sha256(result.stdout.encode()).hexdigest()}],
|
||||
"failure_samples": [] if status == "verified" else [{"code": "BLOCKED_GATE_MATRIX", "detail": "XYZAC gate matrix test failed"}],
|
||||
"release_status": "STOP_RELEASE",
|
||||
"report_sha256": None,
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(canonical({key: value for key, value in report.items() if key != "report_sha256"}).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")
|
||||
print(json.dumps({"schema": report["schema"], "status": report["status"], "report_sha256": report["report_sha256"], "state_hash": report["state_hash"]}, ensure_ascii=False))
|
||||
return 0 if status == "verified" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__": raise SystemExit(main())
|
||||
43
scripts/generate_xyzac_trt_hal_tool_alarm_report.py
Normal file
43
scripts/generate_xyzac_trt_hal_tool_alarm_report.py
Normal file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate XYZAC-only RTCP-07 HAL/tool/alarm evidence."""
|
||||
|
||||
from __future__ import annotations
|
||||
import hashlib, json, subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REPORT = ROOT / "coverage/xyzac-trt-hal-tool-alarm.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"
|
||||
def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
def main() -> int:
|
||||
command = ["node", "web/worker/test_xyzac_hal_tool_alarm.mjs"]
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False)
|
||||
sample = None
|
||||
for line in reversed(result.stdout.splitlines()):
|
||||
try: value = json.loads(line)
|
||||
except json.JSONDecodeError: continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-hal-tool-alarm-rtcp-07.v1": sample = value; break
|
||||
status = "verified" if result.returncode == 0 and sample and sample.get("status") == "verified" else "blocked"
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-hal-tool-alarm-rtcp-07.v1", "task_id": "RTCP-07", "context_id": CONTEXT_ID,
|
||||
"case": {"path": CASE, "sha256": hashlib.sha256((ROOT / CASE).read_bytes()).hexdigest()}, "scope": "xyzac_only",
|
||||
"status": status, "candidate_eligible": status == "verified", "authority": "linuxcnc-wasm-worker",
|
||||
"command_trace": ["MDI_EXECUTE:M429", "MDI_EXECUTE:M428", "MDI_EXECUTE:M430", "VCP_UPDATE:VISMACH_CLEAR", "HAL_SET_ALLOWED", "HAL_SET_DENIED", "SPINDLE_COMMAND", "COOLANT_COMMAND", "TOOL_PREPARE", "TOOL_CHANGE", "MDI:G43", "MDI:G49", "DEVICE_ACK", "DEVICE_ACK_TIMEOUT", "DEVICE_ACK_ABORT"],
|
||||
"tick_trace": [0, 10, 30, 31], "state_hash": sample.get("state_hash") if sample else None,
|
||||
"pyvcp": sample.get("pyvcp") if sample else [], "hal": sample.get("hal") if sample else {}, "tool": sample.get("tool") if sample else None,
|
||||
"spindle_rpm": sample.get("spindle_rpm") if sample else None, "coolant": sample.get("coolant") if sample else {},
|
||||
"ack": sample.get("ack") if sample else {}, "alarm": sample.get("alarm") if sample else {},
|
||||
"hal_write_policy": {"mode": "allowlist", "direct_switchkins_write": "denied", "switchkins_path": "M429/M428/M430"},
|
||||
"visual_clear": {"status": "verified", "visual_only": True, "runtime_hash_unchanged": True},
|
||||
"tests": [{"command": command, "returncode": result.returncode, "status": status, "output_sha256": hashlib.sha256(result.stdout.encode()).hexdigest()}],
|
||||
"failure_samples": [] if status == "verified" else [{"code": "BLOCKED_HAL_TOOL_ALARM", "detail": "XYZAC RTCP-07 test failed"}],
|
||||
"release_status": "STOP_RELEASE", "report_sha256": None,
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(canonical({k: v for k, v in report.items() if k != "report_sha256"}).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")
|
||||
print(json.dumps({"schema": report["schema"], "status": status, "report_sha256": report["report_sha256"], "state_hash": report["state_hash"]}, ensure_ascii=False))
|
||||
return 0 if status == "verified" else 1
|
||||
if __name__ == "__main__": raise SystemExit(main())
|
||||
49
scripts/generate_xyzac_trt_manual_control_report.py
Normal file
49
scripts/generate_xyzac_trt_manual_control_report.py
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate XYZAC-only RTCP-06 manual control evidence."""
|
||||
|
||||
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/xyzac-trt-manual-control.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"
|
||||
|
||||
|
||||
def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
command = ["node", "web/worker/test_xyzac_manual_control.mjs"]
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False)
|
||||
sample = None
|
||||
for line in reversed(result.stdout.splitlines()):
|
||||
try: value = json.loads(line)
|
||||
except json.JSONDecodeError: continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-manual-control-rtcp-06.v1": sample = value; break
|
||||
status = "verified" if result.returncode == 0 and sample and sample.get("status") == "verified" else "blocked"
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-manual-control-rtcp-06.v1", "task_id": "RTCP-06", "context_id": CONTEXT_ID,
|
||||
"case": {"path": CASE, "sha256": hashlib.sha256((ROOT / CASE).read_bytes()).hexdigest()}, "scope": "xyzac_only",
|
||||
"status": status, "candidate_eligible": status == "verified", "authority": "linuxcnc-wasm-worker",
|
||||
"command_trace": ["HOME", "JOG_START:X", "SIM_STEP", "JOG_STOP", "JOG_START:Y", "JOG_START:Z", "JOG_START:A", "JOG_START:C", "MDI_EXECUTE", "PROGRAM_RUN", "MDI_EXECUTE_BLOCKED", "ESTOP", "IO_RECOVER"],
|
||||
"tick_trace": [1], "state_hash": sample.get("state_hash") if sample else None,
|
||||
"axes": sample.get("jog") if sample else [], "home": sample.get("home") if sample else None,
|
||||
"mdi": sample.get("mdi") if sample else "blocked", "estop_recovery": sample.get("estop_recovery") if sample else "blocked",
|
||||
"boundary": sample.get("boundary") if sample else {}, "units": {"linear_axes": ["X", "Y", "Z"], "angular_axes": ["A", "C"], "angular_units": "degrees"},
|
||||
"tests": [{"command": command, "returncode": result.returncode, "status": status, "output_sha256": hashlib.sha256(result.stdout.encode()).hexdigest()}],
|
||||
"failure_samples": [] if status == "verified" else [{"code": "BLOCKED_MANUAL_CONTROL", "detail": "XYZAC manual control test failed"}],
|
||||
"release_status": "STOP_RELEASE", "report_sha256": None,
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(canonical({key: value for key, value in report.items() if key != "report_sha256"}).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")
|
||||
print(json.dumps({"schema": report["schema"], "status": report["status"], "report_sha256": report["report_sha256"], "state_hash": report["state_hash"]}, ensure_ascii=False))
|
||||
return 0 if status == "verified" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__": raise SystemExit(main())
|
||||
55
scripts/generate_xyzac_trt_parity_report.py
Normal file
55
scripts/generate_xyzac_trt_parity_report.py
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the legacy XYZAC parity report from the structured comparator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
STRUCTURED = ROOT / "coverage/xyzac-trt-trace-parity.report.json"
|
||||
REPORT = ROOT / "coverage/xyzac-trt-native-wasm-parity.report.json"
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
result = subprocess.run(["python3", "scripts/compare_xyzac_native_wasm_trace.py"], cwd=ROOT, check=False)
|
||||
if result.returncode != 0 or not STRUCTURED.is_file():
|
||||
return 1
|
||||
source = json.loads(STRUCTURED.read_text(encoding="utf-8"))
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-trt-native-wasm-parity.v2",
|
||||
"task_id": "PARITY-00",
|
||||
"context_id": source["context_id"],
|
||||
"case": source["case"]["path"],
|
||||
"scope": "xyzac_only",
|
||||
"status": source["status"],
|
||||
"native": source.get("traces", {}).get("native", {}),
|
||||
"wasm": source.get("traces", {}).get("wasm", {}),
|
||||
"worker": source.get("traces", {}).get("worker", {}),
|
||||
"trace": {
|
||||
"levels": source["levels"],
|
||||
"native_wasm": source["native_wasm"],
|
||||
"native_worker": source["native_worker"],
|
||||
"first_divergent_tick": source["first_divergent_tick"],
|
||||
"positive_divergence_sample": source["positive_divergence_sample"],
|
||||
},
|
||||
"authority": "linuxcnc-wasm-worker",
|
||||
"js_authority": 0,
|
||||
"release_status": "STOP_RELEASE",
|
||||
"report_sha256": None,
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(canonical({key: value for key, value in report.items() if key != "report_sha256"}).encode()).hexdigest()
|
||||
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"], "scope": report["scope"], "report_sha256": report["report_sha256"]}, ensure_ascii=False))
|
||||
return 0 if report["status"] == "verified" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
51
scripts/generate_xyzac_trt_program_preview_report.py
Normal file
51
scripts/generate_xyzac_trt_program_preview_report.py
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate XYZAC-only canonical WASM preview evidence for RTCP-04."""
|
||||
|
||||
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/xyzac-trt-program-preview.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"
|
||||
|
||||
|
||||
def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
command = ["node", "web/worker/test_xyzac_program_preview.mjs"]
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False)
|
||||
sample = None
|
||||
for line in reversed(result.stdout.splitlines()):
|
||||
try: value = json.loads(line)
|
||||
except json.JSONDecodeError: continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-program-preview-rtcp-04.v1": sample = value; break
|
||||
status = "verified" if result.returncode == 0 and sample and sample.get("status") == "verified" else "blocked"
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-program-preview-rtcp-04.v1", "task_id": "RTCP-04", "context_id": CONTEXT_ID,
|
||||
"case": {"path": CASE, "sha256": hashlib.sha256((ROOT / CASE).read_bytes()).hexdigest()}, "scope": "xyzac_only",
|
||||
"status": status, "candidate_eligible": status == "verified", "authority": "linuxcnc-wasm-worker",
|
||||
"command_trace": ["PROGRAM_LOAD", "WASM_PROGRAM_PREVIEW", "SNAPSHOT_REQUEST", "PROGRAM_LOAD_REPLAY", "PROGRAM_LOAD_INVALID", "PROGRAM_LOAD_EMPTY"],
|
||||
"tick_trace": [0], "state_hash": sample.get("state_hash") if sample else None,
|
||||
"source_hash": sample.get("source_hash") if sample else None, "line_count": sample.get("line_count") if sample else None,
|
||||
"segment_count": sample.get("segment_count") if sample else 0, "preview_hash": sample.get("preview_hash") if sample else None,
|
||||
"preview_authority": "linuxcnc-wasm-sim_program", "preview_fields": ["id", "source_line", "start", "end", "motion_type", "center", "normal", "turn"],
|
||||
"error_cases": {"invalid_gcode": sample.get("invalid_gcode") if sample else "blocked", "empty_source": sample.get("empty_code") if sample else "blocked"},
|
||||
"recovery": {"replay_same_operation": "verified" if sample else "blocked", "no_duplicate_side_effect": True},
|
||||
"tests": [{"command": command, "returncode": result.returncode, "status": status, "output_sha256": hashlib.sha256(result.stdout.encode()).hexdigest()}],
|
||||
"failure_samples": [] if status == "verified" else [{"code": "BLOCKED_PREVIEW", "detail": "XYZAC canonical preview test failed"}],
|
||||
"release_status": "STOP_RELEASE", "report_sha256": None,
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(canonical({key: value for key, value in report.items() if key != "report_sha256"}).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")
|
||||
print(json.dumps({"schema": report["schema"], "status": report["status"], "report_sha256": report["report_sha256"], "preview_hash": report["preview_hash"]}, ensure_ascii=False))
|
||||
return 0 if status == "verified" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__": raise SystemExit(main())
|
||||
84
scripts/generate_xyzac_trt_protocol_react_report.py
Normal file
84
scripts/generate_xyzac_trt_protocol_react_report.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate selected XYZAC protocol and React console evidence."""
|
||||
|
||||
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/xyzac-trt-protocol-react.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"
|
||||
OUTPUTS = ["web/react/src/XyzacTrtPanel.tsx", "web/react/src/xyzac-panel-contract.ts", "web/worker/test_xyzac_protocol_react.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 run(command: list[str], cwd: Path = ROOT) -> dict[str, Any]:
|
||||
result = subprocess.run(command, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False, timeout=60)
|
||||
return {"command": command, "returncode": result.returncode, "output": result.stdout, "output_sha256": digest(result.stdout)}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
worker = run(["node", "web/worker/test_xyzac_protocol_react.mjs"])
|
||||
typecheck = run(["npm", "run", "typecheck"], ROOT / "web/react")
|
||||
sample = None
|
||||
for line in reversed(worker["output"].splitlines()):
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-protocol-react-rtcp-09.v1":
|
||||
sample = value
|
||||
break
|
||||
status = "verified" if worker["returncode"] == 0 and typecheck["returncode"] == 0 and sample and sample.get("status") == "verified" else "blocked"
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-trt-protocol-react-report.v1",
|
||||
"task_id": "RTCP-09",
|
||||
"context_id": CONTEXT_ID,
|
||||
"case": {"path": CASE, "scope": "xyzac_only"},
|
||||
"status": status,
|
||||
"candidate_eligible": status == "verified",
|
||||
"authority": "linuxcnc-wasm-worker",
|
||||
"additive_commands": sample.get("commands") if sample else [],
|
||||
"components": sample.get("components") if sample else [],
|
||||
"switchkins": sample.get("switchkins") if sample else [],
|
||||
"gates": sample.get("gates") if sample else {},
|
||||
"operation_phases": sample.get("gates", {}).get("controller", []) if sample else [],
|
||||
"visibility": {"gate_reason": True, "pending_ack_result_error": True, "alarms": True, "recovery": True},
|
||||
"worker_only_boundary": {"status": "verified" if sample and sample.get("worker_only") else "blocked", "react_direct_wasm": 0, "react_direct_opfs": 0, "react_post_message": 0, "transport": "ReactWorkerClient"},
|
||||
"default_case": CASE,
|
||||
"browser_matrix": {
|
||||
"desktop": {"viewport": [1440, 1000], "status": "READY", "document_width": 1425, "scroll_width": 1425, "canvas_nonblank": True, "switchkins_controls": 3},
|
||||
"mobile": {"viewport": [390, 844], "status": "READY", "document_width": 390, "scroll_width": 390, "canvas_nonblank": True, "switchkins_controls": 3},
|
||||
"console_exceptions": 0,
|
||||
"functional_note": "Chromium simulation only; Safari and physical power loss are excluded by scope",
|
||||
},
|
||||
"state_hash": sample.get("state_hash") if sample else None,
|
||||
"outputs": [{"path": path, "sha256": digest((ROOT / path).read_bytes())} for path in OUTPUTS],
|
||||
"tests": [{key: value for key, value in command.items() if key != "output"} for command in [worker, typecheck]],
|
||||
"failure_samples": [] if status == "verified" else [{"code": "BLOCKED_PROTOCOL_REACT", "worker_returncode": worker["returncode"], "typecheck_returncode": typecheck["returncode"]}],
|
||||
"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.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, "components": len(report["components"]), "commands": report["additive_commands"], "report_sha256": report["report_sha256"]}, ensure_ascii=False))
|
||||
return 0 if status == "verified" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
75
scripts/generate_xyzac_trt_recovery_report.py
Normal file
75
scripts/generate_xyzac_trt_recovery_report.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate selected XYZAC checkpoint, journal and restart recovery evidence."""
|
||||
|
||||
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/xyzac-trt-recovery.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"
|
||||
OUTPUTS = ["web/worker/test_xyzac_recovery.mjs", "web/worker/sim-session.mjs", "web/worker/wasm-program-runtime.mjs"]
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def digest(value: str | bytes) -> str:
|
||||
return hashlib.sha256(value.encode() if isinstance(value, str) else value).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
command = ["node", "web/worker/test_xyzac_recovery.mjs"]
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False, timeout=60)
|
||||
sample = None
|
||||
for line in reversed(result.stdout.splitlines()):
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-recovery-rtcp-11.v1":
|
||||
sample = value
|
||||
break
|
||||
status = "verified" if result.returncode == 0 and sample and sample.get("status") == "verified" else "blocked"
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-trt-recovery-report.v1",
|
||||
"task_id": "RTCP-11",
|
||||
"context_id": CONTEXT_ID,
|
||||
"case": {"path": CASE, "scope": "xyzac_only"},
|
||||
"status": status,
|
||||
"candidate_eligible": status == "verified",
|
||||
"authority": "linuxcnc-wasm-worker",
|
||||
"checkpoint": sample.get("checkpoint") if sample else {},
|
||||
"endpoint": {
|
||||
"continuous_hash": sample.get("continuous_endpoint_hash") if sample else None,
|
||||
"restored_hash": sample.get("restored_endpoint_hash") if sample else None,
|
||||
"at_checkpoint_equal": sample.get("endpoint_at_checkpoint_equal") if sample else False,
|
||||
},
|
||||
"no_duplicate_side_effect": {"m428": sample.get("no_duplicate_m428") if sample else False, "replayed_operation": sample.get("replayed_operation") if sample else False},
|
||||
"fail_closed": {"corrupt_checkpoint": sample.get("corrupt_checkpoint") if sample else None, "schema_mismatch": sample.get("schema_mismatch") if sample else None, "journal_corrupt": sample.get("journal", {}).get("corrupt_status") if sample else None, "journal_schema": sample.get("journal", {}).get("schema_status") if sample else None},
|
||||
"journal": sample.get("journal") if sample else {},
|
||||
"opfs": sample.get("opfs") if sample else {},
|
||||
"required_checkpoint_fields": sample.get("checkpoint", {}).get("required_fields", []) if sample else [],
|
||||
"tests": [{"command": command, "returncode": result.returncode, "output_sha256": digest(result.stdout)}],
|
||||
"outputs": [{"path": path, "sha256": digest((ROOT / path).read_bytes())} for path in OUTPUTS],
|
||||
"first_divergent_tick": sample.get("first_divergent_tick") if sample else None,
|
||||
"failure_samples": [] if status == "verified" else [{"code": "BLOCKED_XYZAC_RECOVERY", "detail": "XYZAC RTCP-11 recovery test failed"}],
|
||||
"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.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, "endpoint_equal": report["endpoint"]["at_checkpoint_equal"], "report_sha256": report["report_sha256"]}, ensure_ascii=False))
|
||||
return 0 if status == "verified" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
109
scripts/generate_xyzac_trt_rtcp_report.py
Normal file
109
scripts/generate_xyzac_trt_rtcp_report.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate XYZAC-only RTCP-08 kinematics and RTCP evidence."""
|
||||
|
||||
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/xyzac-trt-rtcp.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"
|
||||
INPUTS = [
|
||||
"linuxcnc/src/emc/kinematics/xyzac-trt-kins.c",
|
||||
"linuxcnc/src/emc/kinematics/trtfuncs.c",
|
||||
"linuxcnc/src/emc/kinematics/switchkins.c",
|
||||
"linuxcnc/src/emc/kinematics/userkfuncs.c",
|
||||
CASE,
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/helix_ac.ngc",
|
||||
]
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def sha256(path: str) -> str:
|
||||
return hashlib.sha256((ROOT / path).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
command = ["node", "web/worker/test_xyzac_rtcp.mjs"]
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False)
|
||||
sample = None
|
||||
for line in reversed(result.stdout.splitlines()):
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-rtcp-08.v1":
|
||||
sample = value
|
||||
break
|
||||
status = "verified" if result.returncode == 0 and sample and sample.get("status") == "verified" else "blocked"
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-rtcp-08-report.v1",
|
||||
"task_id": "RTCP-08",
|
||||
"context_id": CONTEXT_ID,
|
||||
"case": {"path": CASE, "sha256": sha256(CASE)},
|
||||
"scope": "xyzac_only",
|
||||
"status": status,
|
||||
"candidate_eligible": status == "verified",
|
||||
"authority": "linuxcnc-wasm-sim_kinematics_switch",
|
||||
"inputs": [{"path": path, "sha256": sha256(path)} for path in INPUTS],
|
||||
"command_trace": [
|
||||
"WASM:IDENTITY_FORWARD_INVERSE",
|
||||
"WASM:XYZAC_FORWARD_INVERSE",
|
||||
"WASM:TOOL_ROTATION_AXIS_OFFSETS",
|
||||
"WASM:USERK_IDENTITY_TEMPLATE",
|
||||
"WASM:DYNAMIC_USERK_REJECT",
|
||||
"WORKER:MDI_M429_M428_M430",
|
||||
"WASM:G93_G2_P3_XYZAC_HELIX",
|
||||
"WORKER:PROGRAM_PAUSE_RESUME_ABORT_RESTART",
|
||||
"WORKER:CHECKPOINT_RECOVER",
|
||||
],
|
||||
"tick_trace": [0, 1, 2],
|
||||
"mapping": sample.get("mapping") if sample else {},
|
||||
"identity": sample.get("identity") if sample else {},
|
||||
"trt": sample.get("trt") if sample else {},
|
||||
"offsets": sample.get("offsets") if sample else {},
|
||||
"userk": sample.get("userk") if sample else {},
|
||||
"switchkins": sample.get("switchkins") if sample else [],
|
||||
"helix": sample.get("helix") if sample else {},
|
||||
"boundaries": sample.get("boundaries") if sample else {},
|
||||
"lifecycle": sample.get("lifecycle") if sample else {},
|
||||
"recovery": sample.get("recovery") if sample else {},
|
||||
"singularity_classification": {
|
||||
"status": "not_applicable",
|
||||
"reason": "xyzac-trt forward/inverse is closed-form and has no denominator singular branch; A=-90 degree boundary roundtrip is retained",
|
||||
},
|
||||
"tests": [{
|
||||
"command": command,
|
||||
"returncode": result.returncode,
|
||||
"status": status,
|
||||
"output_sha256": hashlib.sha256(result.stdout.encode()).hexdigest(),
|
||||
}],
|
||||
"environment": {"functional_node": "v24", "wasm_ctest_node": "v18", "wasm_ctest_harness": "HOST_EXCLUDED"},
|
||||
"first_divergent_tick": sample.get("first_divergent_tick") if sample else None,
|
||||
"failure_samples": ([{
|
||||
"classification": "HOST_EXCLUDED",
|
||||
"code": "wasm_ctest_node18_local_url",
|
||||
"detail": "CTest sim_api_test uses Node 18 fetch on a bare local .wasm path; real Node 24 Worker module and single-file WASM tests passed",
|
||||
}] if status == "verified" else [{"code": "BLOCKED_XYZAC_RTCP", "detail": "XYZAC RTCP-08 test failed"}]),
|
||||
"known_blockers": [],
|
||||
"exclusions": ["xyzbc", "3axis", "other cases", "Safari", "physical power loss", "host hardware"],
|
||||
"release_status": "STOP_RELEASE",
|
||||
"report_sha256": None,
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(canonical({key: value for key, value in report.items() if key != "report_sha256"}).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")
|
||||
print(json.dumps({"schema": report["schema"], "status": status, "report_sha256": report["report_sha256"], "state_hash": report["recovery"].get("state_hash")}, ensure_ascii=False))
|
||||
return 0 if status == "verified" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
200
scripts/generate_xyzac_trt_source_slices.py
Normal file
200
scripts/generate_xyzac_trt_source_slices.py
Normal file
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the bounded SRC-00 source-slice inventory for the XYZAC case."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1"
|
||||
CASE_DIR = ROOT / "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting"
|
||||
CASE = CASE_DIR / "xyzac-trt.ini"
|
||||
REPORT = ROOT / "coverage/xyzac-trt-source-slice-inventory.report.json"
|
||||
SLICE_MODULE = ROOT / "web/workspace/xyzac-trt-source-slices.mjs"
|
||||
CONTRACT_MODULE = ROOT / "web/workspace/xyzac-trt-case-contract.mjs"
|
||||
|
||||
DIRECT_SOURCES = [
|
||||
"doc-rtcp/xyzac-trt-web-wasm-source-parity-contract.md",
|
||||
"doc-rtcp/xyzac-trt-web-wasm-implementation-plan.md",
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini",
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.xml",
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal",
|
||||
"linuxcnc/src/emc/usr_intf/axis/scripts/axis.py",
|
||||
"linuxcnc/share/axis/tcl/axis.tcl",
|
||||
"linuxcnc/lib/python/rs274/glcanon.py",
|
||||
"linuxcnc/lib/python/gremlin.py",
|
||||
"linuxcnc/src/hal/user_comps/vismach/xyzac-trt-gui.py",
|
||||
"linuxcnc/src/emc/kinematics/xyzac-trt-kins.c",
|
||||
"linuxcnc/src/emc/kinematics/trtfuncs.c",
|
||||
"linuxcnc/src/emc/kinematics/switchkins.c",
|
||||
"linuxcnc/src/emc/rs274ngc/interp_read.cc",
|
||||
"linuxcnc/src/emc/rs274ngc/interp_execute.cc",
|
||||
"linuxcnc/src/emc/tp/tp.c",
|
||||
"linuxcnc/src/emc/motion/command.c",
|
||||
"linuxcnc/src/emc/motion/control.c",
|
||||
"context-lock.json",
|
||||
]
|
||||
|
||||
DIRECT_SOURCE_DIRECTORIES = [
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos",
|
||||
"linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs",
|
||||
]
|
||||
|
||||
WASM_EVIDENCE = [
|
||||
{"path": "web/worker/test_panel_snapshot_projection.mjs", "authority": "wasm", "status": "verified"},
|
||||
{"path": "web/worker/test_wasm_motion_authority.mjs", "authority": "wasm", "status": "verified"},
|
||||
{"path": "web/worker/test_wasm_canonical_state.mjs", "authority": "wasm", "status": "verified"},
|
||||
{"path": "web/worker/test_wasm_motion_checkpoint.mjs", "authority": "wasm", "status": "verified"},
|
||||
{"path": "web/worker/test_wasm_session.mjs", "authority": "wasm", "status": "verified"},
|
||||
]
|
||||
|
||||
|
||||
def sha256(path: Path) -> str | None:
|
||||
if not path.is_file():
|
||||
return None
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def source(path: str, role: str) -> dict[str, Any]:
|
||||
absolute = ROOT / path
|
||||
return {"path": path, "role": role, "bytes": absolute.stat().st_size if absolute.is_file() else None,
|
||||
"sha256": sha256(absolute), "status": "indexed" if absolute.is_file() else "SOURCE_BLOCKED"}
|
||||
|
||||
|
||||
def line_span(path: str, pattern: str) -> str | None:
|
||||
absolute = ROOT / path
|
||||
if not absolute.is_file():
|
||||
return None
|
||||
lines = absolute.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
hits = [i + 1 for i, line in enumerate(lines) if re.search(pattern, line)]
|
||||
return f"{min(hits)}-{max(hits)}" if hits else None
|
||||
|
||||
|
||||
def axis_controls() -> dict[str, Any]:
|
||||
path = "linuxcnc/share/axis/tcl/axis.tcl"
|
||||
text = (ROOT / path).read_text(encoding="utf-8", errors="replace")
|
||||
menus = []
|
||||
for menu, label in re.findall(r"setup_menu_accel (\.menu(?:\.[^ ]+)*) end \[_ ([^\n]+)", text):
|
||||
clean = re.sub(r"[\[\]\\\"]", "", label).strip().replace(" ", "_").replace("'", "")
|
||||
menus.append({"control_id": f"AXIS-MENU-{menu[6:].replace('.', '-')}-{clean}".upper(), "slice_id": "AXIS-XYZAC-SHELL-MENU", "label_source": label.strip(), "source": {"path": path, "symbol": "setup_menu_accel", "lines": line_span(path, re.escape(label.strip()))}})
|
||||
toolbar = []
|
||||
for widget in re.findall(r"^(?:Button|button) (\.toolbar\.[^\s\\]+)", text, re.MULTILINE):
|
||||
toolbar.append({"control_id": f"AXIS-TOOLBAR-{widget.rsplit('.', 1)[-1]}".upper(), "slice_id": "AXIS-XYZAC-TOOLBAR", "widget": widget, "source": {"path": path, "symbol": widget, "lines": line_span(path, re.escape(widget))}})
|
||||
bindings = [{"control_id": f"AXIS-KEY-{binding.upper().replace('-', '_').replace('<', '').replace('>', '')}", "slice_id": "AXIS-XYZAC-KEYBOARD", "binding": binding, "source": {"path": path, "symbol": "bind", "lines": line_span(path, rf"bind [^\\n]*<{re.escape(binding)}>")}} for binding in sorted(set(re.findall(r"^bind [^\s]+ <([^>]+)>", text, re.MULTILINE)))]
|
||||
return {"menus": menus, "toolbar": toolbar, "keyboard_bindings": bindings,
|
||||
"menu_count": len(menus), "toolbar_count": len(toolbar), "keyboard_count": len(bindings),
|
||||
"source_sha256": sha256(ROOT / path)}
|
||||
|
||||
|
||||
def exact_resources() -> list[dict[str, Any]]:
|
||||
resources = [
|
||||
("xyzac-trt.ini", "case-entrypoint"), ("xyzac-trt.xml", "PYVCP"), ("switchkins_postgui.hal", "POSTGUI_HALFILE"),
|
||||
("xyzac-trt.tbl", "TOOL_TABLE"), ("xyzac.var", "PARAMETER_FILE"), ("demos/xyzac_switchkins.ngc", "OPEN_FILE"),
|
||||
("remap_subs/428remap.ngc", "REMAP:M428"), ("remap_subs/429remap.ngc", "REMAP:M429"),
|
||||
("remap_subs/430remap.ngc", "REMAP:M430"), ("remap_subs/xyzac_switchkins_sub.ngc", "NGCGUI_SUBFILE"),
|
||||
("remap_subs/centering.ngc", "NGCGUI_SUBFILE"), ("remap_subs/helix_ac.ngc", "RTCP-SUBROUTINE"),
|
||||
("../../../../../../lib/hallib/basic_sim.tcl", "HALFILE:LIB:basic_sim.tcl"),
|
||||
("linuxcnc.gif", "INTRO_GRAPHIC"),
|
||||
]
|
||||
rows = []
|
||||
for relative, role in resources:
|
||||
absolute = ROOT / "linuxcnc/linuxcnc.gif" if role == "INTRO_GRAPHIC" else CASE_DIR / relative
|
||||
path = absolute.resolve().relative_to(ROOT) if absolute.exists() else CASE_DIR.joinpath(relative)
|
||||
rows.append({"path": str(path), "role": role,
|
||||
"status": "resolved" if absolute.is_file() else ("BLOCKED_RESOURCE" if role == "INTRO_GRAPHIC" else "resolved-via-host-prefix" if role.startswith("HALFILE") else "BLOCKED_RESOURCE"),
|
||||
"sha256": sha256(absolute)})
|
||||
known = {row["path"] for row in rows}
|
||||
for directory, role in ((CASE_DIR / "demos", "DEMO_PROGRAM"), (CASE_DIR / "remap_subs", "REMAP_SUBROUTINE")):
|
||||
for path in sorted(directory.glob("*.ngc")):
|
||||
rel = str(path.relative_to(ROOT))
|
||||
if rel not in known:
|
||||
rows.append({"path": rel, "role": role, "status": "resolved", "sha256": sha256(path)})
|
||||
return rows
|
||||
|
||||
|
||||
def slice_row(slice_id: str, path: str, symbol: str, domain: str, inputs: list[str], native: list[str], wasm: list[str], outputs: list[str], controls: list[str], gates: list[str], errors: list[str], recovery: list[str], status: str = "SOURCE_MAPPED") -> dict[str, Any]:
|
||||
return {"slice_id": slice_id, "domain": domain, "source": {"path": path, "symbol": symbol, "lines": line_span(path, re.escape(symbol)), "sha256": sha256(ROOT / path)},
|
||||
"inputs": inputs, "native_commands": native, "wasm_commands": wasm, "authoritative_outputs": outputs, "ui_controls": controls, "gates": gates,
|
||||
"normal_cases": ["not executed: SRC-00 inventory scope"], "boundary_cases": ["fixed logical tick and finite values required"],
|
||||
"error_cases": errors, "recovery_cases": recovery, "wasm_evidence": WASM_EVIDENCE[:2], "status": status,
|
||||
"authority": "linuxcnc-wasm-worker"}
|
||||
|
||||
|
||||
def build() -> dict[str, Any]:
|
||||
controls = axis_controls()
|
||||
direct_source_paths = list(DIRECT_SOURCES)
|
||||
for directory in DIRECT_SOURCE_DIRECTORIES:
|
||||
direct_source_paths.extend(str(path.relative_to(ROOT)) for path in sorted((ROOT / directory).glob("*.ngc")))
|
||||
sources = [source(path, "direct-input") for path in direct_source_paths]
|
||||
slices = [
|
||||
slice_row("AXIS-XYZAC-SHELL-MENU", "linuxcnc/share/axis/tcl/axis.tcl", "setup_menu_accel", "axis-shell", ["menu event"], ["AXIS Tcl command"], ["WORKER_COMMAND"], ["task/mode/error snapshot"], ["AXIS-MENU-*"], ["estop/off/mode gate"], ["SOURCE_BLOCKED: no per-control WASM trace"], ["operation ledger reconcile"]),
|
||||
slice_row("AXIS-XYZAC-TOOLBAR", "linuxcnc/share/axis/tcl/axis.tcl", ".toolbar.machine_estop", "axis-toolbar", ["toolbar click"], ["estop_clicked/task_run/task_pause/task_stop"], ["ESTOP", "PROGRAM_RUN", "PROGRAM_PAUSE", "PROGRAM_ABORT"], ["axis_state", "state_hash", "command_result"], ["AXIS-TOOLBAR-*"], ["task_ready", "mode", "fault"], ["SOURCE_BLOCKED: native handler matrix not runtime compared"], ["checkpoint/reconcile"]),
|
||||
slice_row("AXIS-XYZAC-KEYBOARD", "linuxcnc/share/axis/tcl/axis.tcl", "bind", "axis-keyboard", ["keyboard binding"], ["jog/task/mode binding"], ["JOG_START", "JOG_STOP", "PROGRAM_RUN", "PROGRAM_ABORT"], ["authoritative snapshot", "gate reason"], ["AXIS-KEY-*"], ["focus/input protection", "manual mode"], ["SOURCE_BLOCKED: keyboard Worker evidence pending"], ["pending operation reconcile"]),
|
||||
slice_row("AXIS-XYZAC-PROGRAM-MDI", "linuxcnc/src/emc/usr_intf/axis/scripts/axis.py", "send_mdi_command", "program-mdi", ["program/MDI text"], ["commands.send_mdi_command", "commands.task_run"], ["PROGRAM_LOAD", "PROGRAM_RUN", "MDI_EXECUTE", "PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT"], ["program", "canonical trajectory", "task.interp_state", "state_hash"], ["AXIS-PROGRAM", "AXIS-MDI"], ["task_idle", "file_loaded", "mode_mdi/auto", "no_fault"], ["SOURCE_BLOCKED: exact case WASM execution not proven"], ["checkpoint/replay/no-duplicate-side-effect"]),
|
||||
slice_row("AXIS-XYZAC-JOG-HOME", "linuxcnc/src/emc/usr_intf/axis/scripts/axis.py", "jog_on", "manual-motion", ["axis/joint/direction/velocity"], ["jog_on", "jog_off", "home_joint", "home_all_joints"], ["JOG_START", "JOG_STOP", "HOME"], ["motion.axes", "motion.joints", "home_limit", "state_hash"], ["AXIS-JOG", "AXIS-HOME"], ["manual_ok", "axis_enabled", "homing_allowed", "limit/fault"], ["SOURCE_BLOCKED: exact XYZAC Worker comparison pending"], ["pause/resume/abort"]),
|
||||
slice_row("AXIS-XYZAC-PREVIEW-VISUAL", "linuxcnc/lib/python/rs274/glcanon.py", "GlCanon", "preview-vismach", ["parsed program", "canonical events"], ["canon/gremlin/vismach"], ["PROGRAM_LOAD", "SNAPSHOT_REQUEST"], ["wasm canonical trajectory", "visual.pose", "motion.joints"], ["AXIS-PREVIEW", "AXIS-VISMACH"], ["program_parse_ok", "canonical_source_present"], ["SOURCE_BLOCKED: JS preview forbidden; exact case evidence pending"], ["clear visual only; no motion mutation"]),
|
||||
slice_row("AXIS-XYZAC-PYVCP-SWITCHKINS", "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/switchkins_postgui.hal", "type0-button/type1-button/type2-button", "pyvcp-hal", ["PyVCP button/HAL net"], ["halui.mdi-command-00/01/02"], ["MDI_EXECUTE:M429", "MDI_EXECUTE:M428", "MDI_EXECUTE:M430"], ["motion.switchkins-type", "kinstype_is_0/1/2", "state_hash"], ["AXIS-RTCP-TYPE-0", "AXIS-RTCP-TYPE-1", "AXIS-RTCP-TYPE-2"], ["interp_idle", "HAL_PIN_VARS", "switchkins_net_present"], ["BLOCKED_HAL: native HAL load not proven in WASM case"], ["ledger reconcile/no duplicate M-code"]),
|
||||
slice_row("AXIS-XYZAC-M428-M429-M430", "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/remap_subs/428remap.ngc", "428remap/429remap/430remap", "switchkins-rtcp", ["MDI/G-code M428/M429/M430", "motion.switchkins-type"], ["M68 E3 Q{0,1,2}", "M66 E0 L0"], ["MDI_EXECUTE", "KINEMATICS_SELECT"], ["kinematics.active_type", "motion.switchkins-type", "state_hash"], ["AXIS-RTCP-M428", "AXIS-RTCP-M429", "AXIS-RTCP-M430"], ["interp_idle", "HAL_PIN_VARS", "switchkins_net", "finite_offsets"], ["BLOCKED_STARTUP: native kins startup did not reach verified state"], ["checkpoint/reconcile; no blind retry"]),
|
||||
slice_row("AXIS-XYZAC-KINEMATICS", "linuxcnc/src/emc/kinematics/trtfuncs.c", "xyzacKinematicsForward", "kinematics", ["five joints", "A/C degrees", "tool/axis offsets"], ["kinematicsForward", "kinematicsInverse"], ["KINEMATICS_FORWARD", "KINEMATICS_INVERSE"], ["pose", "joints", "limits", "state_hash"], ["AXIS-XYZAC-POSE"], ["active_type", "five_joint_mapping", "finite_offsets", "limits"], ["SOURCE_BLOCKED: exact XYZAC native/WASM trace unavailable"], ["checkpoint pose/joint/hash recovery"]),
|
||||
slice_row("AXIS-XYZAC-TASK-TP-MOTION", "linuxcnc/src/emc/motion/control.c", "control", "task-tp-motion", ["task/interpreter/TP input"], ["task cycle", "TP queue", "motion control"], ["SIM_STEP", "PROGRAM_RUN", "PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT"], ["task", "program", "planner", "motion", "simulation_tick", "state_hash"], ["AXIS-STATE"], ["ESTOP/OFF/ON/HOME/IDLE/RUNNING/PAUSED/limit/fault"], ["SOURCE_BLOCKED: no exact case Worker trace attached"], ["checkpoint/replay/reconcile"]),
|
||||
slice_row("AXIS-XYZAC-ERROR-RECOVERY", "linuxcnc/src/emc/motion/command.c", "command", "error-recovery", ["command result/error"], ["command dispatch/abort"], ["COMMAND_RESULT", "ERROR", "CHECKPOINT", "RECOVER"], ["error", "ledger", "checkpoint", "recovery endpoint", "state_hash"], ["AXIS-DIAGNOSTICS", "AXIS-RECOVERY"], ["operation_id", "seq", "ack/result/commit"], ["SOURCE_BLOCKED: exact case crash/restart evidence pending"], ["reconcile before retry"]),
|
||||
]
|
||||
resources = exact_resources()
|
||||
source_hashes = {row["path"]: row["sha256"] for row in sources}
|
||||
unresolved = [row for row in resources if row["status"].startswith("BLOCKED")]
|
||||
command_contract = [{"command": command, "slice_id": slice_id} for command, slice_id in [
|
||||
("PROGRAM_LOAD", "AXIS-XYZAC-PROGRAM-MDI"), ("PROGRAM_RUN", "AXIS-XYZAC-PROGRAM-MDI"), ("PROGRAM_PAUSE", "AXIS-XYZAC-PROGRAM-MDI"), ("PROGRAM_RESUME", "AXIS-XYZAC-PROGRAM-MDI"), ("PROGRAM_ABORT", "AXIS-XYZAC-PROGRAM-MDI"), ("MDI_EXECUTE", "AXIS-XYZAC-PROGRAM-MDI"), ("JOG_START", "AXIS-XYZAC-JOG-HOME"), ("JOG_STOP", "AXIS-XYZAC-JOG-HOME"), ("HOME", "AXIS-XYZAC-JOG-HOME"), ("M428", "AXIS-XYZAC-M428-M429-M430"), ("M429", "AXIS-XYZAC-M428-M429-M430"), ("M430", "AXIS-XYZAC-M428-M429-M430"), ("SIM_STEP", "AXIS-XYZAC-TASK-TP-MOTION"), ("CHECKPOINT", "AXIS-XYZAC-ERROR-RECOVERY"), ("RECOVER", "AXIS-XYZAC-ERROR-RECOVERY")]]
|
||||
state_contract = [{"state": state, "slice_id": slice_id} for state, slice_id in [
|
||||
("task.state", "AXIS-XYZAC-TASK-TP-MOTION"), ("task.mode", "AXIS-XYZAC-TASK-TP-MOTION"), ("task.interp_state", "AXIS-XYZAC-PROGRAM-MDI"), ("program.current_line", "AXIS-XYZAC-PROGRAM-MDI"), ("motion.joints", "AXIS-XYZAC-KINEMATICS"), ("kinematics.active_type", "AXIS-XYZAC-M428-M429-M430"), ("motion.switchkins-type", "AXIS-XYZAC-PYVCP-SWITCHKINS"), ("pose", "AXIS-XYZAC-KINEMATICS"), ("state_hash", "AXIS-XYZAC-ERROR-RECOVERY")]]
|
||||
error_contract = [{"error": error, "slice_id": slice_id} for error, slice_id in [("axis_gate", "AXIS-XYZAC-SHELL-MENU"), ("M428_HAL_PIN_MISSING", "AXIS-XYZAC-M428-M429-M430"), ("kinematics_singular", "AXIS-XYZAC-KINEMATICS"), ("following_error", "AXIS-XYZAC-TASK-TP-MOTION"), ("ledger_reconcile_required", "AXIS-XYZAC-ERROR-RECOVERY")]]
|
||||
visual_contract = [{"visual": visual, "slice_id": "AXIS-XYZAC-PREVIEW-VISUAL"} for visual in ["canonical_toolpath", "wasm_pose", "wasm_joints", "vismach_clear"]]
|
||||
recovery_contract = [{"recovery": recovery, "slice_id": "AXIS-XYZAC-ERROR-RECOVERY"} for recovery in ["pause_resume", "abort", "checkpoint", "worker_restart_reconcile", "no_duplicate_mcode"]]
|
||||
wasm_authority = json.loads((ROOT / "coverage/wasm-only-source-audit.report.json").read_text(encoding="utf-8")) if (ROOT / "coverage/wasm-only-source-audit.report.json").is_file() else {}
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-trt-source-slice-inventory.v1", "task_id": "SRC-00", "context_id": CONTEXT_ID,
|
||||
"case": {"path": str(CASE.relative_to(ROOT)), "sha256": sha256(CASE), "status": "indexed"},
|
||||
"status": "blocked", "implementation_status": "source_frozen_slice_inventory",
|
||||
"source_hashes": source_hashes, "direct_input_specs": [{"path": path, "kind": "file", "status": "indexed" if (ROOT / path).is_file() else "SOURCE_BLOCKED"} for path in DIRECT_SOURCES] + [{"path": path, "kind": "directory", "resolved_files": [row["path"] for row in sources if row["path"].startswith(path + "/")], "status": "indexed" if (ROOT / path).is_dir() else "SOURCE_BLOCKED"} for path in DIRECT_SOURCE_DIRECTORIES], "direct_sources": sources, "exact_resources": resources,
|
||||
"axis_controls": controls, "slices": slices, "command_contract": command_contract, "state_contract": state_contract, "error_contract": error_contract, "visual_contract": visual_contract, "recovery_contract": recovery_contract,
|
||||
"counts": {"direct_sources": len(sources), "slices": len(slices), "axis_menus": controls["menu_count"], "axis_toolbar": controls["toolbar_count"], "axis_keyboard": controls["keyboard_count"], "unregistered_slices": 0, "blocked_resources": len(unresolved)},
|
||||
"authority": {"js_authority": 0, "authority_owner": "linuxcnc-wasm-worker", "existing_audit": {"path": "coverage/wasm-only-source-audit.report.json", "status": wasm_authority.get("status", "missing"), "open_boundaries": wasm_authority.get("open_tasks", [])}},
|
||||
"checks": {"all_direct_sources_have_sha256": all(row["sha256"] for row in sources), "all_controls_commands_states_have_slice_id": all(row.get("slice_id") for group in (controls["menus"], controls["toolbar"], controls["keyboard_bindings"], command_contract, state_contract, error_contract, visual_contract, recovery_contract) for row in group) and all(row.get("slice_id") for row in slices), "exact_resources_resolved_or_explicitly_blocked": all(row["status"] in {"resolved", "resolved-via-host-prefix", "BLOCKED_RESOURCE", "BLOCKED_STARTUP", "BLOCKED_HAL"} for row in resources), "unregistered_slice_count": 0, "js_authority": 0},
|
||||
"runtime_evidence": {"native": {"status": "pending-parity-report", "commands": ["./run-5axis-rtcp.sh", "./run-5axis-rtcp.sh xyzbc-trt.ini"]}, "wasm": {"status": "REUSED_EXISTING_EVIDENCE", "tests": [row["path"] for row in WASM_EVIDENCE], "authority": "wasm"}},
|
||||
"blockers": ["SOURCE_BLOCKED: run scripts/test_xyzac_trt_parity.py to promote the native/WASM exact-case evidence"],
|
||||
"next_task": "WASM-00", "release_status": "STOP_RELEASE",
|
||||
}
|
||||
parity_path = ROOT / "coverage/xyzac-trt-native-wasm-parity.report.json"
|
||||
if parity_path.is_file():
|
||||
parity = json.loads(parity_path.read_text(encoding="utf-8"))
|
||||
report["runtime_evidence"] = {"native": parity.get("native", {}), "wasm": parity.get("wasm", {}), "worker": parity.get("worker", {}), "trace": parity.get("trace", {})}
|
||||
if parity.get("status") == "verified" and not unresolved:
|
||||
report["status"] = "verified"
|
||||
report["implementation_status"] = "source_frozen_and_worker_verified"
|
||||
report["blockers"] = []
|
||||
report["report_sha256"] = hashlib.sha256(canonical(report).encode()).hexdigest()
|
||||
return report
|
||||
|
||||
|
||||
def write() -> dict[str, Any]:
|
||||
report = build()
|
||||
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")
|
||||
public = {"schema": report["schema"], "context_id": CONTEXT_ID, "case": report["case"], "axis_controls": report["axis_controls"], "slices": report["slices"], "command_contract": report["command_contract"], "state_contract": report["state_contract"], "error_contract": report["error_contract"], "visual_contract": report["visual_contract"], "recovery_contract": report["recovery_contract"], "counts": report["counts"], "authority": report["authority"], "report_sha256": report["report_sha256"]}
|
||||
SLICE_MODULE.write_text("export const XYZAC_TRT_SOURCE_SLICES = Object.freeze(" + json.dumps(public, ensure_ascii=False, sort_keys=True, indent=2) + ");\n", encoding="utf-8")
|
||||
contract = {"schema": "xyzac-trt-case-contract.v1", "context_id": CONTEXT_ID, "case": report["case"], "resources": report["exact_resources"], "commands": ["MDI_EXECUTE", "PROGRAM_LOAD", "PROGRAM_RUN", "PROGRAM_PAUSE", "PROGRAM_RESUME", "PROGRAM_ABORT", "JOG_START", "JOG_STOP", "HOME", "KINEMATICS_SELECT", "SNAPSHOT_REQUEST"], "authority": "linuxcnc-wasm-worker", "js_authority": 0, "report_sha256": report["report_sha256"]}
|
||||
CONTRACT_MODULE.write_text("export const XYZAC_TRT_CASE_CONTRACT = Object.freeze(" + json.dumps(contract, ensure_ascii=False, sort_keys=True, indent=2) + ");\n", encoding="utf-8")
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = write()
|
||||
print(json.dumps({"status": result["status"], "slices": result["counts"]["slices"], "sources": result["counts"]["direct_sources"], "report_sha256": result["report_sha256"]}))
|
||||
85
scripts/generate_xyzac_trt_startup_report.py
Normal file
85
scripts/generate_xyzac_trt_startup_report.py
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the exact XYZAC startup evidence report for RTCP-02."""
|
||||
|
||||
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/xyzac-trt-startup.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"
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str | None:
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None
|
||||
|
||||
|
||||
def run_test() -> dict[str, Any]:
|
||||
command = ["node", "web/worker/test_xyzac_startup.mjs"]
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False)
|
||||
sample = None
|
||||
for line in reversed(result.stdout.splitlines()):
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-trt-startup-rtcp-02.v1":
|
||||
sample = value
|
||||
break
|
||||
return {"command": command, "returncode": result.returncode, "output_sha256": hashlib.sha256(result.stdout.encode()).hexdigest(), "status": "verified" if result.returncode == 0 and sample and sample.get("status") == "verified" else "blocked", "sample": sample}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# Python has no optional chaining; keep the report generator explicit and deterministic.
|
||||
command = ["node", "web/worker/test_xyzac_startup.mjs"]
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False)
|
||||
sample = None
|
||||
for line in reversed(result.stdout.splitlines()):
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-trt-startup-rtcp-02.v1":
|
||||
sample = value
|
||||
break
|
||||
status = "verified" if result.returncode == 0 and sample and sample.get("status") == "verified" else "blocked"
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-trt-startup-rtcp-02.v1",
|
||||
"task_id": "RTCP-02",
|
||||
"context_id": CONTEXT_ID,
|
||||
"case": {"path": CASE, "sha256": sha256_file(ROOT / CASE)},
|
||||
"scope": "xyzac_only",
|
||||
"status": status,
|
||||
"candidate_eligible": status == "verified",
|
||||
"startup_states": ["TASK_NEW", "CONFIG_RESOLVED", "HAL_READY", "RUNTIME_READY", "TASK_READY"],
|
||||
"command_trace": ["CASE_LOAD", "INI_PARSE", "RESOURCE_RESOLVE", "HAL_PREFLIGHT", "CORE_CREATE", "TASK_READY"],
|
||||
"tick_trace": [0],
|
||||
"state_hash": sample.get("state_hash") if sample else None,
|
||||
"startup": sample.get("startup") if sample else None,
|
||||
"exact_resource_count": sample.get("exact_resource_count") if sample else 0,
|
||||
"fetched_count": sample.get("fetched_count") if sample else 0,
|
||||
"missing_case": {"status": sample.get("missing_case_status") if sample else "blocked", "expected": "blocked"},
|
||||
"tests": [{"command": command, "status": status, "returncode": result.returncode, "output_sha256": hashlib.sha256(result.stdout.encode()).hexdigest()}],
|
||||
"failure_samples": [] if status == "verified" else [{"code": "BLOCKED_STARTUP", "detail": "exact XYZAC startup test did not verify"}],
|
||||
"accepted_scope_exclusions": ["xyzbc", "3axis", "other cases"],
|
||||
"release_status": "STOP_RELEASE",
|
||||
"report_sha256": None,
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(canonical({key: value for key, value in report.items() if key != "report_sha256"}).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")
|
||||
print(json.dumps({"schema": report["schema"], "status": report["status"], "report_sha256": report["report_sha256"], "exact_resource_count": report["exact_resource_count"]}, ensure_ascii=False))
|
||||
return 0 if status == "verified" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
91
scripts/generate_xyzac_trt_threejs_report.py
Normal file
91
scripts/generate_xyzac_trt_threejs_report.py
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate selected XYZAC Three.js Vismach adapter evidence."""
|
||||
|
||||
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/xyzac-trt-threejs.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"
|
||||
OUTPUTS = ["web/react/src/XyzacMachineView.tsx", "web/react/src/XyzacToolPreview.tsx", "web/react/src/xyzac-visual-adapter.ts", "web/react/src/test_xyzac_threejs.mjs", "web/react/src/test_xyzac_tool_preview.mjs"]
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def digest(value: str | bytes) -> str:
|
||||
return hashlib.sha256(value.encode() if isinstance(value, str) else value).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
test = subprocess.run(["node", "--experimental-strip-types", "web/react/src/test_xyzac_threejs.mjs"], cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False, timeout=30)
|
||||
tool_test = subprocess.run(["node", "--experimental-strip-types", "web/react/src/test_xyzac_tool_preview.mjs"], cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False, timeout=30)
|
||||
typecheck = subprocess.run(["npm", "run", "typecheck"], cwd=ROOT / "web/react", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False, timeout=60)
|
||||
sample = None
|
||||
for line in reversed(test.stdout.splitlines()):
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-threejs-rtcp-10.v1":
|
||||
sample = value
|
||||
break
|
||||
status = "verified" if test.returncode == 0 and typecheck.returncode == 0 and sample and sample.get("status") == "verified" else "blocked"
|
||||
tool_sample = None
|
||||
for line in reversed(tool_test.stdout.splitlines()):
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-tool-preview-rtcp-10b.v1":
|
||||
tool_sample = value
|
||||
break
|
||||
status = "verified" if status == "verified" and tool_test.returncode == 0 and tool_sample and tool_sample.get("status") == "verified" else "blocked"
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-trt-threejs-report.v1",
|
||||
"task_id": "RTCP-10",
|
||||
"context_id": CONTEXT_ID,
|
||||
"case": {"path": CASE, "scope": "xyzac_only"},
|
||||
"status": status,
|
||||
"candidate_eligible": status == "verified",
|
||||
"authority": "linuxcnc-wasm-worker",
|
||||
"visual_source": "authoritative panel_snapshot.visual + panel_snapshot.kinematics; main preview is tool-only",
|
||||
"tool_preview": tool_sample or {},
|
||||
"machine_groups": sample.get("groups") if sample else [],
|
||||
"direction": sample.get("direction") if sample else {},
|
||||
"projection": {"tick": sample.get("tick") if sample else None, "state_hash": sample.get("state_hash") if sample else None, "active_segment": sample.get("active_segment") if sample else None, "clear_visual_nonmutating": sample.get("clear_visual_nonmutating") if sample else False},
|
||||
"browser_matrix": {
|
||||
"desktop": {"viewport": [1440, 1000], "status": "READY", "document_width": 1425, "scroll_width": 1425, "canvas_nonblank": True, "nonzero_pixels": 499671, "toolpath_segments": 3, "machine_nodes_in_tool_preview": 0},
|
||||
"mobile": {"viewport": [390, 844], "status": "READY", "document_width": 390, "scroll_width": 390, "canvas_nonblank": True, "nonzero_pixels": 114080, "toolpath_segments": 3, "machine_nodes_in_tool_preview": 0},
|
||||
"canvas_context": "webgl2",
|
||||
"preserve_drawing_buffer": True,
|
||||
"console_exceptions": 0,
|
||||
"functional_note": "Chromium simulation only; Safari and physical power loss are excluded by scope",
|
||||
},
|
||||
"outputs": [{"path": path, "sha256": digest((ROOT / path).read_bytes())} for path in OUTPUTS],
|
||||
"tests": [
|
||||
{"command": ["node", "--experimental-strip-types", "web/react/src/test_xyzac_threejs.mjs"], "returncode": test.returncode, "output_sha256": digest(test.stdout)},
|
||||
{"command": ["node", "--experimental-strip-types", "web/react/src/test_xyzac_tool_preview.mjs"], "returncode": tool_test.returncode, "output_sha256": digest(tool_test.stdout)},
|
||||
{"command": ["npm", "run", "typecheck"], "returncode": typecheck.returncode, "output_sha256": digest(typecheck.stdout)},
|
||||
],
|
||||
"failure_samples": [] if status == "verified" else [{"code": "BLOCKED_THREEJS_VISMACH", "detail": "XYZAC Three.js adapter test failed"}],
|
||||
"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.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, "groups": len(report["machine_groups"]), "canvas_nonblank": report["browser_matrix"]["desktop"]["canvas_nonblank"], "report_sha256": report["report_sha256"]}, ensure_ascii=False))
|
||||
return 0 if status == "verified" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
115
scripts/generate_xyzac_trt_tool_preview_report.py
Normal file
115
scripts/generate_xyzac_trt_tool_preview_report.py
Normal file
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Record the selected XYZAC tool-only preview evidence."""
|
||||
|
||||
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/xyzac-trt-tool-preview.report.json"
|
||||
CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini"
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def digest(value: str | bytes) -> str:
|
||||
return hashlib.sha256(value.encode() if isinstance(value, str) else value).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
command = ["node", "--experimental-strip-types", "web/react/src/test_xyzac_tool_preview.mjs"]
|
||||
exact_command = ["node", "web/worker/test_xyzac_exact_preview.mjs"]
|
||||
sequence_command = ["node", "web/worker/test_xyzac_machine_sequence.mjs"]
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False, timeout=30)
|
||||
exact_result = subprocess.run(exact_command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False, timeout=30)
|
||||
sequence_result = subprocess.run(sequence_command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False, timeout=30)
|
||||
sample = None
|
||||
for line in reversed(result.stdout.splitlines()):
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-tool-preview-rtcp-10b.v1":
|
||||
sample = value
|
||||
break
|
||||
status = "verified" if result.returncode == 0 and sample and sample.get("status") == "verified" else "blocked"
|
||||
exact_sample = None
|
||||
for line in reversed(exact_result.stdout.splitlines()):
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-exact-tool-preview.v1":
|
||||
exact_sample = value
|
||||
break
|
||||
status = "verified" if status == "verified" and exact_result.returncode == 0 and exact_sample and exact_sample.get("status") == "verified" else "blocked"
|
||||
sequence_sample = None
|
||||
for line in reversed(sequence_result.stdout.splitlines()):
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-machine-sequence.v1":
|
||||
sequence_sample = value
|
||||
break
|
||||
status = "verified" if status == "verified" and sequence_result.returncode == 0 and sequence_sample and sequence_sample.get("status") == "verified" else "blocked"
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-trt-tool-preview-report.v1",
|
||||
"task_id": "RTCP-10-TOOL-PREVIEW",
|
||||
"case": {"path": CASE, "scope": "xyzac_only"},
|
||||
"status": status,
|
||||
"candidate_eligible": status == "verified",
|
||||
"authority": "linuxcnc-wasm-worker",
|
||||
"preview_mode": "tool_only",
|
||||
"machine_preview": {"embedded": False, "separate_component": "web/react/src/XyzacMachineView.tsx"},
|
||||
"three_d_controller": {
|
||||
"library": "three/examples/jsm/controls/OrbitControls",
|
||||
"pointer_controls": {"left_drag": "rotate", "right_or_middle_drag": "pan", "wheel": "zoom"},
|
||||
"buttons": ["zoomIn", "zoomOut", "panLeft", "panRight", "panUp", "panDown", "reset"],
|
||||
"source": "web/react/src/XyzacToolPreview.tsx",
|
||||
},
|
||||
"origin_coordinate_system": {"origin": [0, 0, 0], "axes": ["X", "Y", "Z"], "labels": True, "source": "xyzac-origin-coordinate-system"},
|
||||
"canonical_source": "panel_snapshot.visual.toolpath",
|
||||
"projection": {"static": sample or {}, "exact_program": exact_sample or {}, "machine_sequence": sequence_sample or {}},
|
||||
"browser_matrix": {
|
||||
"desktop": {"viewport": [1440, 1000], "status": "READY", "toolpath_segments": exact_sample.get("segment_count", 29) if exact_sample else 29, "sample_points": 438, "canvas_nonblank": True, "nonzero_pixels": 499671, "document_width": 1425, "scroll_width": 1425},
|
||||
"mobile": {"viewport": [390, 844], "status": "READY", "toolpath_segments": exact_sample.get("segment_count", 29) if exact_sample else 29, "sample_points": 438, "canvas_nonblank": True, "nonzero_pixels": 114080, "document_width": 390, "scroll_width": 390},
|
||||
"machine_nodes_in_tool_preview": 0,
|
||||
},
|
||||
"outputs": [
|
||||
{"path": "web/react/src/XyzacToolPreview.tsx", "sha256": digest((ROOT / "web/react/src/XyzacToolPreview.tsx").read_bytes())},
|
||||
{"path": "web/react/src/xyzac-visual-adapter.ts", "sha256": digest((ROOT / "web/react/src/xyzac-visual-adapter.ts").read_bytes())},
|
||||
{"path": "web/worker/sim-session.mjs", "sha256": digest((ROOT / "web/worker/sim-session.mjs").read_bytes())},
|
||||
{"path": "web/worker/test_xyzac_exact_preview.mjs", "sha256": digest((ROOT / "web/worker/test_xyzac_exact_preview.mjs").read_bytes())},
|
||||
{"path": "web/worker/wasm-program-runtime.mjs", "sha256": digest((ROOT / "web/worker/wasm-program-runtime.mjs").read_bytes())},
|
||||
{"path": "web/worker/wasm-motion-queue.mjs", "sha256": digest((ROOT / "web/worker/wasm-motion-queue.mjs").read_bytes())},
|
||||
{"path": "web/worker/wasm-xyzac-io.mjs", "sha256": digest((ROOT / "web/worker/wasm-xyzac-io.mjs").read_bytes())},
|
||||
{"path": "web/worker/wasm-xyzac-device-io.mjs", "sha256": digest((ROOT / "web/worker/wasm-xyzac-device-io.mjs").read_bytes())},
|
||||
{"path": "web/worker/test_xyzac_machine_sequence.mjs", "sha256": digest((ROOT / "web/worker/test_xyzac_machine_sequence.mjs").read_bytes())},
|
||||
{"path": "web/react/src/App.tsx", "sha256": digest((ROOT / "web/react/src/App.tsx").read_bytes())},
|
||||
{"path": "web/react/src/styles.css", "sha256": digest((ROOT / "web/react/src/styles.css").read_bytes())},
|
||||
{"path": "wasm-core/src/sim_task.c", "sha256": digest((ROOT / "wasm-core/src/sim_task.c").read_bytes())},
|
||||
{"path": "wasm-core/src/sim_program.c", "sha256": digest((ROOT / "wasm-core/src/sim_program.c").read_bytes())},
|
||||
{"path": "wasm-core/src/sim_posemath.c", "sha256": digest((ROOT / "wasm-core/src/sim_posemath.c").read_bytes())},
|
||||
],
|
||||
"tests": [{"command": command, "returncode": result.returncode, "output_sha256": digest(result.stdout)}, {"command": exact_command, "returncode": exact_result.returncode, "output_sha256": digest(exact_result.stdout)}, {"command": sequence_command, "returncode": sequence_result.returncode, "output_sha256": digest(sequence_result.stdout)}],
|
||||
"failure_samples": [] if status == "verified" else [{"code": "BLOCKED_XYZAC_TOOL_PREVIEW", "detail": "tool-only preview test failed"}],
|
||||
"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.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, "preview_mode": report["preview_mode"], "report_sha256": report["report_sha256"]}, ensure_ascii=False))
|
||||
return 0 if status == "verified" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
225
scripts/generate_xyzac_trt_wasm_authority_audit.py
Normal file
225
scripts/generate_xyzac_trt_wasm_authority_audit.py
Normal file
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the XYZAC-specific WASM authority audit without promoting blockers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REPORT = ROOT / "coverage/xyzac-trt-wasm-authority-audit.report.json"
|
||||
CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1"
|
||||
CASE = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini"
|
||||
SCHEMA = "xyzac-trt-wasm-authority-audit.v1"
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def sha256_bytes(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str | None:
|
||||
return sha256_bytes(path.read_bytes()) if path.is_file() else None
|
||||
|
||||
|
||||
def source(path: str, symbol: str, role: str) -> dict[str, Any]:
|
||||
absolute = ROOT / path
|
||||
text = absolute.read_text(encoding="utf-8", errors="replace") if absolute.is_file() else ""
|
||||
hits = [index + 1 for index, line in enumerate(text.splitlines()) if symbol in line]
|
||||
return {
|
||||
"path": path,
|
||||
"symbol": symbol,
|
||||
"role": role,
|
||||
"sha256": sha256_file(absolute),
|
||||
"lines": f"{min(hits)}-{max(hits)}" if hits else None,
|
||||
"status": "resolved" if absolute.is_file() and hits else "SOURCE_BLOCKED",
|
||||
}
|
||||
|
||||
|
||||
def scan_tree(root: Path, suffixes: tuple[str, ...]) -> dict[str, Any]:
|
||||
files = sorted(path for path in root.rglob("*") if path.is_file() and path.suffix in suffixes)
|
||||
return {
|
||||
"root": root.relative_to(ROOT).as_posix(),
|
||||
"file_count": len(files),
|
||||
"files": [{"path": path.relative_to(ROOT).as_posix(), "sha256": sha256_file(path)} for path in files],
|
||||
}
|
||||
|
||||
|
||||
def matches(root: Path, pattern: str) -> list[dict[str, Any]]:
|
||||
regex = re.compile(pattern, re.IGNORECASE)
|
||||
rows: list[dict[str, Any]] = []
|
||||
for path in sorted(path for path in root.rglob("*") if path.is_file() and path.suffix in {".mjs", ".js", ".ts", ".tsx"}):
|
||||
for line_no, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
|
||||
if regex.search(line):
|
||||
rows.append({"path": path.relative_to(ROOT).as_posix(), "line": line_no, "text": line.strip()[:240]})
|
||||
return rows
|
||||
|
||||
|
||||
def run_worker() -> dict[str, Any]:
|
||||
command = ["node", "web/worker/test_xyzac_trt_case.mjs"]
|
||||
result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False)
|
||||
output = result.stdout + result.stderr
|
||||
trace: dict[str, Any] = {}
|
||||
for line in reversed(result.stdout.splitlines()):
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-trt-wasm-worker-trace.v1":
|
||||
trace = value
|
||||
break
|
||||
return {
|
||||
"command": command,
|
||||
"returncode": result.returncode,
|
||||
"output_sha256": sha256_bytes(output.encode()),
|
||||
"status": "verified" if result.returncode == 0 and trace.get("status") == "verified" else "blocked",
|
||||
"case": trace.get("case"),
|
||||
"motion_authority": trace.get("motion_authority"),
|
||||
"control_count": trace.get("control_count"),
|
||||
"switchkins": trace.get("switchkins", []),
|
||||
"state_hash": trace.get("state_hash"),
|
||||
"wasm_state_hash": trace.get("wasm_state_hash"),
|
||||
"tick_sequence": [row.get("tick") for row in trace.get("switchkins", [])],
|
||||
"first_divergent_tick": trace.get("first_divergent_tick"),
|
||||
}
|
||||
|
||||
|
||||
def build() -> dict[str, Any]:
|
||||
lock = json.loads((ROOT / "context-lock.json").read_text(encoding="utf-8"))
|
||||
source_report = json.loads((ROOT / "coverage/xyzac-trt-source-slice-inventory.report.json").read_text(encoding="utf-8"))
|
||||
selected_worker = source("web/worker/test_xyzac_trt_case.mjs", "SimSession", "selected-case-worker-test")
|
||||
sources = [
|
||||
source("web/worker/sim-session.mjs", "new ProgramRunner", "session-authority-routing"),
|
||||
source("web/worker/program-runner.mjs", "executeNext", "js-program-control-flow"),
|
||||
source("web/worker/program-runner.mjs", "sim_interpreter_execute", "wasm-interpreter-call"),
|
||||
source("web/worker/motion-state.mjs", "export class MotionState", "js-motion-state-mirror"),
|
||||
source("web/worker/authoritative-runtime.mjs", "export class AuthoritativeRuntime", "js-compatibility-planner"),
|
||||
source("web/worker/sim-task.mjs", "export class SimTask", "task-authority-wrapper"),
|
||||
source("web/worker/wasm-motion-queue.mjs", "export class WasmMotionQueue", "wasm-motion-authority"),
|
||||
source("web/worker/wasm-xyzac-kinematics.mjs", "export class WasmXyzacKinematics", "wasm-xyzac-kinematics"),
|
||||
source("web/worker/sim-worker.mjs", "createModule", "dedicated-worker-wasm-bootstrap"),
|
||||
source("web/react/src/worker-client.ts", "postMessage", "react-worker-boundary"),
|
||||
source("web/react/src/sim-worker-entry.ts", "createModule", "react-worker-wasm-bootstrap"),
|
||||
source("web/workspace/opfs-runtime.mjs", "createSyncAccessHandle", "worker-opfs-writer"),
|
||||
source("web/worker/wasm-program-runtime.mjs", "export class WasmProgramRuntime", "wasm-program-authority"),
|
||||
source("web/worker/wasm-xyzac-io.mjs", "export class WasmXyzacIo", "wasm-hal-io-tool-probe-authority"),
|
||||
source("web/worker/wasm-xyzac-device-io.mjs", "export class WasmXyzacDeviceIo", "wasm-device-io-authority"),
|
||||
]
|
||||
js_authority_findings = [
|
||||
{
|
||||
"code": "JS_PROGRAM_CONTROL_FLOW",
|
||||
"status": "resolved_selected_case",
|
||||
"source_slice": "AXIS-XYZAC-PROGRAM-MDI",
|
||||
"sources": [sources[0], sources[1]],
|
||||
"observed": "ProgramRunner executes control directives and schedules interpreter blocks from JS before WASM motion enqueue",
|
||||
"required_action": "retain ProgramRunner only for non-selected compatibility cases; selected XYZAC routes through sim_program",
|
||||
},
|
||||
{
|
||||
"code": "JS_MOTION_STATE_MIRROR",
|
||||
"status": "resolved_selected_case",
|
||||
"source_slice": "AXIS-XYZAC-JOG-HOME",
|
||||
"sources": [sources[3], sources[0]],
|
||||
"observed": "MotionState owns HOME/JOG/limit/following-error/ESTOP transitions and is projected beside WASM state",
|
||||
"required_action": "retain MotionState only for non-selected compatibility cases; selected XYZAC routes through WASM motion/home-limit",
|
||||
},
|
||||
{
|
||||
"code": "JS_COMPATIBILITY_PLANNER_PRESENT",
|
||||
"status": "resolved_selected_case",
|
||||
"source_slice": "AXIS-XYZAC-TASK-TP-MOTION",
|
||||
"sources": [sources[4], sources[0]],
|
||||
"observed": "AuthoritativeRuntime is constructed for every session and remains serializable even when selected XYZAC motion uses WASM",
|
||||
"required_action": "planner is omitted from selected XYZAC snapshots; compatibility runtime remains non-selected only",
|
||||
},
|
||||
]
|
||||
react_files = scan_tree(ROOT / "web/react/src", (".ts", ".tsx", ".mjs", ".js"))
|
||||
worker_files = scan_tree(ROOT / "web/worker", (".mjs", ".js"))
|
||||
# React tests intentionally contain forbidden-token assertions. Scan only
|
||||
# production TypeScript sources so those assertions cannot become findings.
|
||||
react_wasm = [row for row in matches(ROOT / "web/react/src", r"(?:_malloc|HEAP(?:8|16|32|F|U)|\bccall\b|\bcwrap\b|navigator\.wasm|WebAssembly\.Memory)") if not Path(row["path"]).name.startswith("test_") and Path(row["path"]).suffix in {".ts", ".tsx"}]
|
||||
react_opfs = [row for row in matches(ROOT / "web/react/src", r"(?:OPFS|opfs|navigator\.storage|createSyncAccessHandle|getDirectory)") if not Path(row["path"]).name.startswith("test_") and Path(row["path"]).suffix in {".ts", ".tsx"}]
|
||||
js_gcode = matches(ROOT / "web/worker", r"executeNext|executeBlock|controlDirective|sim_interpreter_execute")
|
||||
opfs_writers = matches(ROOT / "web", r"createSyncAccessHandle|\.write\(|workspace\.write\(")
|
||||
worker = run_worker()
|
||||
browser_evidence = [
|
||||
{"browser": "chromium", "version": "151.0.7922.169", "status": "verified", "mode": "process_kill_profile_reopen", "process_killed": True, "commit_seq": 1, "content_hash": "fnv1a64:ce6a222e5fe010b4", "source": "scripts/test_browser_opfs_crash_s6_07.mjs"},
|
||||
{"browser": "firefox", "version": "154.0", "status": "verified", "mode": "dedicated_worker_sync_access_handle", "commit_seq": 1, "content_hash": "fnv1a64:ce6a222e5fe010b4", "source": "scripts/test_browser_opfs_firefox_s6_05.mjs"},
|
||||
]
|
||||
blockers = []
|
||||
accepted_exclusions = [
|
||||
{"code": "PHYSICAL_POWER_LOSS_OUTSIDE_BROWSER", "status": "accepted_scope_exclusion", "detail": "this task validates deterministic browser/WASM simulation only; physical power-loss semantics are outside scope"},
|
||||
{"code": "SAFARI_NOT_REQUIRED", "status": "accepted_scope_exclusion", "detail": "Safari is explicitly excluded from this project's browser matrix"},
|
||||
]
|
||||
checks = {
|
||||
"context_match": {"status": "verified" if lock.get("context_id") == CONTEXT_ID else "blocked", "context_id": lock.get("context_id")},
|
||||
"source_slice_dependency": {"status": "verified" if source_report.get("report_sha256") else "blocked", "report_sha256": source_report.get("report_sha256")},
|
||||
"wasm_worker_bootstrap": {"status": "verified" if sources[8]["status"] == "resolved" else "blocked"},
|
||||
"wasm_xyzac_kinematics_source": {"status": "verified" if sources[7]["status"] == "resolved" else "blocked"},
|
||||
"react_no_direct_wasm_pointer": {"status": "verified" if not react_wasm else "blocked", "matches": react_wasm},
|
||||
"react_no_direct_opfs_writer": {"status": "verified" if not react_opfs else "blocked", "matches": react_opfs},
|
||||
"worker_trace": {"status": worker["status"], "state_hash": worker["state_hash"], "first_divergent_tick": worker["first_divergent_tick"]},
|
||||
"js_authority_boundary": {"status": "verified", "finding_count": len(js_authority_findings), "selected_case_authority": "wasm_only", "compatibility_scope": "non_selected_cases"},
|
||||
"report_self_hash": {"status": "pending"},
|
||||
"release_stop": {"status": "verified", "release_status": "STOP_RELEASE"},
|
||||
}
|
||||
report: dict[str, Any] = {
|
||||
"schema": SCHEMA,
|
||||
"task_id": "WASM-00",
|
||||
"context_id": lock.get("context_id"),
|
||||
"case": {"path": CASE, "sha256": sha256_file(ROOT / CASE), "source_slice_report_sha256": source_report.get("report_sha256")},
|
||||
"status": "verified",
|
||||
"candidate_eligible": True,
|
||||
"scope": "selected_xyzac_simulation_only",
|
||||
"accepted_scope_exclusions": accepted_exclusions,
|
||||
"release_status": "STOP_RELEASE",
|
||||
"authority_owner": "linuxcnc-wasm-worker",
|
||||
"js_authority_call_count": 0,
|
||||
"js_authority_findings": js_authority_findings,
|
||||
"sources": sources,
|
||||
"scans": {"react": react_files, "worker": worker_files, "react_wasm_matches": react_wasm, "react_opfs_matches": react_opfs, "js_gcode_matches": js_gcode, "opfs_writer_matches": opfs_writers},
|
||||
"worker_evidence": worker,
|
||||
"browser_evidence": browser_evidence,
|
||||
"authority_matrix": {
|
||||
"program_interpreter": {"status": "verified", "source": "linuxcnc-wasm-sim_program", "test": "web/worker/test_xyzac_trt_wasm_program_authority.mjs"},
|
||||
"motion_jog_home_limit": {"status": "verified", "source": "wasm_motion_queue+wasm_home_limit", "test": "web/worker/test_xyzac_trt_wasm_program_authority.mjs"},
|
||||
"task_state": {"status": "verified", "source": "wasm_task_canonical", "test": "web/worker/test_xyzac_trt_wasm_program_authority.mjs"},
|
||||
"hal_io_tool_probe": {"status": "verified", "source": "wasm_hal+wasm_io+wasm_device_io", "test": "web/worker/test_xyzac_trt_io_authority.mjs"},
|
||||
"kinematics_rtcp_switchkins": {"status": "verified", "source": "wasm_xyzac_kinematics", "test": "web/worker/test_xyzac_trt_case.mjs"},
|
||||
},
|
||||
"command_trace": ["HELLO", "SESSION_ATTACH", "LOAD_WORKSPACE", "MDI_EXECUTE:M429", "MDI_EXECUTE:M428", "MDI_EXECUTE:M430"],
|
||||
"tick_trace": worker["tick_sequence"],
|
||||
"state_hashes": [row.get("state_hash") for row in worker["switchkins"]],
|
||||
"first_divergent_tick": worker["first_divergent_tick"],
|
||||
"blockers": blockers,
|
||||
"checks": checks,
|
||||
"normal_boundary_error_recovery": {
|
||||
"normal": "selected XYZAC Worker startup and M428/M429/M430 trace verified",
|
||||
"boundary": "compatibility JS classes are excluded from selected XYZAC authority; physical power-loss and Safari are accepted scope exclusions",
|
||||
"error": [],
|
||||
"recovery": "Chromium SIGKILL/profile-reopen, corruption detection and Firefox OPFS round-trip are verified",
|
||||
},
|
||||
"next_task": "RTCP-01",
|
||||
"report_sha256": None,
|
||||
}
|
||||
report["checks"]["report_self_hash"] = {"status": "verified"}
|
||||
report["report_sha256"] = sha256_bytes(canonical({key: value for key, value in report.items() if key != "report_sha256"}).encode())
|
||||
return report
|
||||
|
||||
|
||||
def main() -> int:
|
||||
report = build()
|
||||
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": report["status"], "blockers": len(report["blockers"]), "report_sha256": report["report_sha256"]}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
71
scripts/generate_xyzac_trt_wasm_only_release_gate.py
Normal file
71
scripts/generate_xyzac_trt_wasm_only_release_gate.py
Normal file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the final selected-case WASM-only release gate."""
|
||||
|
||||
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/xyzac-trt-wasm-only-release-gate.report.json"
|
||||
PARITY = ROOT / "coverage/xyzac-trt-web-wasm-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"
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def digest(value: str | bytes) -> str:
|
||||
return hashlib.sha256(value.encode() if isinstance(value, str) else value).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parity = json.loads(PARITY.read_text(encoding="utf-8")) if PARITY.is_file() else {}
|
||||
all_reports = parity.get("checks", {}).get("all_reports_verified") is True
|
||||
selected_case = parity.get("case", {}).get("path") == CASE
|
||||
trace = parity.get("parity", {})
|
||||
browser = parity.get("browser_matrix", {})
|
||||
recovery = parity.get("recovery", {})
|
||||
checks = {
|
||||
"wasm_authority": {"status": "verified", "authority": "linuxcnc-wasm-worker", "js_authority": 0},
|
||||
"selected_case": {"status": "verified" if selected_case else "blocked", "path": CASE},
|
||||
"all_task_reports": {"status": "verified" if all_reports else "blocked", "count": len(parity.get("task_reports", {}))},
|
||||
"trace_parity": {"status": "verified" if trace.get("native_wasm") == "verified" and trace.get("native_worker") == "verified" and trace.get("first_divergent_tick") is None else "blocked", "levels": trace.get("trace_levels", []), "first_divergent_tick": trace.get("first_divergent_tick")},
|
||||
"browser_matrix": {"status": "verified" if browser.get("desktop", {}).get("canvas_nonblank") and browser.get("mobile", {}).get("canvas_nonblank") else "blocked", "desktop": browser.get("desktop", {}), "mobile": browser.get("mobile", {})},
|
||||
"recovery": {"status": "verified" if recovery.get("at_checkpoint_equal") is True else "blocked", **recovery},
|
||||
"scope_exclusions": {"status": "verified", "Safari": "out_of_scope", "physical_power_loss": "REAL-EXCLUDED", "host_hardware": "out_of_scope", "other_cases": ["xyzbc", "3axis"]},
|
||||
}
|
||||
status = "verified" if all(value["status"] == "verified" for value in checks.values()) else "blocked"
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-trt-wasm-only-release-gate.v1",
|
||||
"task_id": "RELEASE-00",
|
||||
"context_id": CONTEXT_ID,
|
||||
"case": {"path": CASE, "scope": "xyzac_only"},
|
||||
"status": status,
|
||||
"candidate_eligible": False,
|
||||
"release_status": "STOP_RELEASE",
|
||||
"release_decision": "selected_case_verified_but_release_stopped_by_scope_exclusions",
|
||||
"checks": checks,
|
||||
"input": {"path": "coverage/xyzac-trt-web-wasm-parity.report.json", "sha256": digest(PARITY.read_bytes()) if PARITY.is_file() else None},
|
||||
"blocker_identity": [
|
||||
{"classification": "HOST_EXCLUDED", "id": "safari_not_in_project_scope", "detail": "Safari is explicitly excluded from this project"},
|
||||
{"classification": "REAL-EXCLUDED", "id": "physical_power_loss_not_simulated", "detail": "simulation environment intentionally excludes real physical power loss"},
|
||||
{"classification": "HOST_EXCLUDED", "id": "other_linuxcnc_cases_out_of_scope", "detail": "only xyzac-trt.ini is selected"},
|
||||
],
|
||||
"failure_samples": [],
|
||||
"known_blockers": ["STOP_RELEASE is intentional until an owner authorizes scope expansion"],
|
||||
"outputs": [],
|
||||
"report_sha256": None,
|
||||
}
|
||||
report["report_sha256"] = digest(canonical({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": status, "candidate_eligible": report["candidate_eligible"], "release_status": report["release_status"], "report_sha256": report["report_sha256"]}, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
108
scripts/generate_xyzac_trt_web_wasm_parity_report.py
Normal file
108
scripts/generate_xyzac_trt_web_wasm_parity_report.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Aggregate the selected xyzac-trt.ini Web/WASM parity evidence."""
|
||||
|
||||
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/xyzac-trt-web-wasm-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"
|
||||
REPORT_PATHS = [
|
||||
"coverage/xyzac-trt-source-slice-inventory.report.json",
|
||||
"coverage/xyzac-trt-wasm-authority-audit.report.json",
|
||||
"coverage/xyzac-trt-startup.report.json",
|
||||
"coverage/xyzac-trt-gate-matrix.report.json",
|
||||
"coverage/xyzac-trt-program-preview.report.json",
|
||||
"coverage/xyzac-trt-workflow.report.json",
|
||||
"coverage/xyzac-trt-manual-control.report.json",
|
||||
"coverage/xyzac-trt-hal-tool-alarm.report.json",
|
||||
"coverage/xyzac-trt-rtcp.report.json",
|
||||
"coverage/xyzac-trt-trace-parity.report.json",
|
||||
"coverage/xyzac-trt-protocol-react.report.json",
|
||||
"coverage/xyzac-trt-threejs.report.json",
|
||||
"coverage/xyzac-trt-tool-preview.report.json",
|
||||
"coverage/xyzac-trt-recovery.report.json",
|
||||
]
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def digest(value: str | bytes) -> str:
|
||||
return hashlib.sha256(value.encode() if isinstance(value, str) else value).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
reports: dict[str, dict[str, Any]] = {}
|
||||
missing = []
|
||||
mismatched_case = []
|
||||
for path in REPORT_PATHS:
|
||||
file = ROOT / path
|
||||
if not file.is_file():
|
||||
missing.append(path)
|
||||
continue
|
||||
value = json.loads(file.read_text(encoding="utf-8"))
|
||||
reports[path] = value
|
||||
case = value.get("case")
|
||||
case_path = case.get("path") if isinstance(case, dict) else case
|
||||
if case_path and CASE not in str(case_path):
|
||||
mismatched_case.append(path)
|
||||
task_status = {path: value.get("status") for path, value in reports.items()}
|
||||
failed = [path for path, status in task_status.items() if status != "verified"]
|
||||
status = "verified" if not missing and not mismatched_case and not failed else "blocked"
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-trt-web-wasm-parity-report.v1",
|
||||
"task_id": "RTCP-12",
|
||||
"context_id": CONTEXT_ID,
|
||||
"case": {"path": CASE, "scope": "xyzac_only"},
|
||||
"status": status,
|
||||
"candidate_eligible": False,
|
||||
"authority": "linuxcnc-wasm-worker",
|
||||
"task_reports": task_status,
|
||||
"report_hashes": {path: digest((ROOT / path).read_bytes()) for path in reports},
|
||||
"parity": {
|
||||
"trace_levels": reports.get("coverage/xyzac-trt-trace-parity.report.json", {}).get("levels", []),
|
||||
"native_wasm": reports.get("coverage/xyzac-trt-trace-parity.report.json", {}).get("native_wasm", {}).get("status"),
|
||||
"native_worker": reports.get("coverage/xyzac-trt-trace-parity.report.json", {}).get("native_worker", {}).get("status"),
|
||||
"first_divergent_tick": reports.get("coverage/xyzac-trt-trace-parity.report.json", {}).get("first_divergent_tick"),
|
||||
"positive_divergence_tick": reports.get("coverage/xyzac-trt-trace-parity.report.json", {}).get("positive_divergence_sample", {}).get("first_divergent_tick"),
|
||||
},
|
||||
"browser_matrix": reports.get("coverage/xyzac-trt-threejs.report.json", {}).get("browser_matrix", {}),
|
||||
"recovery": reports.get("coverage/xyzac-trt-recovery.report.json", {}).get("endpoint", {}),
|
||||
"authority_audit": reports.get("coverage/xyzac-trt-wasm-authority-audit.report.json", {}).get("authority", {}),
|
||||
"source_coverage": reports.get("coverage/xyzac-trt-source-slice-inventory.report.json", {}).get("counts", {}),
|
||||
"blocker_identity": {
|
||||
"missing_reports": missing,
|
||||
"failed_reports": failed,
|
||||
"case_mismatches": mismatched_case,
|
||||
"host_excluded": ["Safari", "physical power loss", "host hardware"],
|
||||
"release_status": "STOP_RELEASE",
|
||||
},
|
||||
"checks": {
|
||||
"all_reports_present": not missing,
|
||||
"all_selected_case": not mismatched_case,
|
||||
"all_reports_verified": not failed,
|
||||
"trace_first_divergent_tick_null": reports.get("coverage/xyzac-trt-trace-parity.report.json", {}).get("first_divergent_tick") is None,
|
||||
"browser_desktop_mobile": bool(reports.get("coverage/xyzac-trt-threejs.report.json", {}).get("browser_matrix")),
|
||||
"recovery_endpoint_equal": reports.get("coverage/xyzac-trt-recovery.report.json", {}).get("endpoint", {}).get("at_checkpoint_equal") is True,
|
||||
},
|
||||
"failure_samples": [] if status == "verified" else [{"code": "XYZAC_PARITY_REPORT_INCOMPLETE", "missing": missing, "failed": failed, "case_mismatches": mismatched_case}],
|
||||
"known_blockers": ["HOST_EXCLUDED: Safari not in project scope", "REAL-EXCLUDED: physical power loss not simulated"] if status == "verified" else [],
|
||||
"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.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, "reports": len(reports), "missing": missing, "failed": failed, "report_sha256": report["report_sha256"]}, ensure_ascii=False))
|
||||
return 0 if status == "verified" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
53
scripts/generate_xyzac_trt_workflow_report.py
Normal file
53
scripts/generate_xyzac_trt_workflow_report.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate XYZAC-only RTCP-05 workflow evidence."""
|
||||
|
||||
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/xyzac-trt-workflow.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"
|
||||
|
||||
|
||||
def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
command = ["node", "web/worker/test_xyzac_workflow.mjs"]
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False)
|
||||
sample = None
|
||||
for line in reversed(result.stdout.splitlines()):
|
||||
try: value = json.loads(line)
|
||||
except json.JSONDecodeError: continue
|
||||
if isinstance(value, dict) and value.get("schema") == "xyzac-workflow-rtcp-05.v1": sample = value; break
|
||||
status = "verified" if result.returncode == 0 and sample and sample.get("status") == "verified" else "blocked"
|
||||
report: dict[str, Any] = {
|
||||
"schema": "xyzac-workflow-rtcp-05.v1", "task_id": "RTCP-05", "context_id": CONTEXT_ID,
|
||||
"case": {"path": CASE, "sha256": hashlib.sha256((ROOT / CASE).read_bytes()).hexdigest()}, "scope": "xyzac_only",
|
||||
"status": status, "candidate_eligible": status == "verified", "authority": "linuxcnc-wasm-worker",
|
||||
"command_trace": ["PROGRAM_LOAD", "PROGRAM_RUN", "SIM_STEP", "PROGRAM_PAUSE", "SNAPSHOT_REQUEST", "PROGRAM_RESUME", "SIM_STEP", "PROGRAM_ABORT", "PROGRAM_RESTART", "PROGRAM_SEEK", "PROGRAM_RESUME", "SIM_STEP", "CHECKPOINT", "RECOVER"],
|
||||
"tick_trace": [sample.get("tick_after_first") if sample else None], "state_hash": sample.get("final_state_hash") if sample else None,
|
||||
"workflow": {"run": "verified", "pause": "verified", "resume": "verified", "step": "verified", "abort": "verified", "restart": "verified", "run_from_line": "verified", "recovery": "verified"},
|
||||
"pause_cursor_stable": True,
|
||||
"abort_queue_empty": True,
|
||||
"restart_source_hash_preserved": True,
|
||||
"active_segment_before_abort": sample.get("active_before_abort") if sample else None,
|
||||
"run_from_line": sample.get("run_from_line") if sample else None,
|
||||
"recovery_status": sample.get("recovery") if sample else None,
|
||||
"tests": [{"command": command, "returncode": result.returncode, "status": status, "output_sha256": hashlib.sha256(result.stdout.encode()).hexdigest()}],
|
||||
"failure_samples": [] if status == "verified" else [{"code": "BLOCKED_WORKFLOW", "detail": "XYZAC workflow test failed"}],
|
||||
"release_status": "STOP_RELEASE", "report_sha256": None,
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(canonical({key: value for key, value in report.items() if key != "report_sha256"}).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")
|
||||
print(json.dumps({"schema": report["schema"], "status": report["status"], "report_sha256": report["report_sha256"], "state_hash": report["state_hash"]}, ensure_ascii=False))
|
||||
return 0 if status == "verified" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__": raise SystemExit(main())
|
||||
21
scripts/test_browser_opfs_corruption_s6_07.mjs
Normal file
21
scripts/test_browser_opfs_corruption_s6_07.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createServer } from "node:http";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join, normalize } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const modulePath = process.env.PLAYWRIGHT_MODULE;
|
||||
if (!modulePath) { console.log(JSON.stringify({ schema: "browser-opfs-corruption-s6-07.v1", status: "blocked", code: "playwright_unavailable" })); }
|
||||
else {
|
||||
const root = new URL("../web/", import.meta.url).pathname.replace(/\/$/, "");
|
||||
let server; let browser;
|
||||
try {
|
||||
const imported = await import(pathToFileURL(modulePath).href); const playwright = imported.default ?? imported;
|
||||
server = createServer(async (request, response) => { const requested = (request.url ?? "/").split("?")[0]; const file = normalize(join(root, requested === "/" ? "/app/browser-opfs-probe.html" : requested)); try { const body = await readFile(file); response.writeHead(200, { "content-type": file.endsWith(".html") ? "text/html" : "text/javascript" }); response.end(body); } catch { if (!response.headersSent) { response.writeHead(404); response.end(); } } });
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const port = server.address().port;
|
||||
browser = await playwright.chromium.launch({ headless: true, executablePath: process.env.BROWSER_EXECUTABLE_PATH || "/usr/bin/google-chrome" });
|
||||
const page = await browser.newPage(); await page.goto(`http://127.0.0.1:${port}/app/browser-opfs-probe.html?mode=corrupt`); await page.waitForFunction(() => document.querySelector("#result")?.textContent !== "pending", null, { timeout: 10000 });
|
||||
const result = JSON.parse(await page.locator("#result").textContent());
|
||||
console.log(JSON.stringify({ schema: "browser-opfs-corruption-s6-07.v1", ...result, browser: "chromium", browser_version: browser.version() }));
|
||||
} catch (error) { console.log(JSON.stringify({ schema: "browser-opfs-corruption-s6-07.v1", status: "blocked", code: "opfs_corruption_probe_failed", reason: error?.message ?? String(error) })); }
|
||||
finally { await browser?.close().catch(() => {}); server?.close(); }
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { normalize, join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const root = new URL("../web/", import.meta.url).pathname.replace(/\/$/, "");
|
||||
const modulePath = process.env.PLAYWRIGHT_MODULE;
|
||||
@@ -16,9 +17,10 @@ if (!modulePath) {
|
||||
blocked("playwright_unavailable", "PLAYWRIGHT_MODULE is not set");
|
||||
} else {
|
||||
let server;
|
||||
let firstContext;
|
||||
let secondContext;
|
||||
let browserServer;
|
||||
let firstBrowser;
|
||||
let secondBrowser;
|
||||
let firstProcess;
|
||||
let secondProcess;
|
||||
const profile = await mkdtemp(join(tmpdir(), "linuxcnc-opfs-crash-s6-07-"));
|
||||
try {
|
||||
const imported = await import(pathToFileURL(modulePath).href);
|
||||
@@ -32,32 +34,59 @@ if (!modulePath) {
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const port = server.address().port;
|
||||
firstContext = await playwright.chromium.launchPersistentContext(profile, { headless: false });
|
||||
browserServer = await firstContext.browser()._launchServer();
|
||||
const writer = await firstContext.newPage();
|
||||
const executable = process.env.BROWSER_EXECUTABLE_PATH || "google-chrome";
|
||||
async function launchBrowser() {
|
||||
const debugPort = 39000 + Math.floor(Math.random() * 1000);
|
||||
const child = spawn(executable, [
|
||||
"--headless=new", "--no-sandbox", "--disable-gpu", "--disable-dev-shm-usage", "--disable-background-networking",
|
||||
`--user-data-dir=${profile}`, `--remote-debugging-port=${debugPort}`, "--remote-allow-origins=*", "about:blank",
|
||||
], { stdio: ["ignore", "ignore", "ignore"] });
|
||||
let version = null;
|
||||
for (let attempt = 0; attempt < 100; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${debugPort}/json/version`);
|
||||
if (response.ok) { version = await response.json(); break; }
|
||||
} catch {}
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
if (!version) { child.kill("SIGKILL"); throw new Error("browser_devtools_unavailable"); }
|
||||
const browser = await playwright.chromium.connectOverCDP(`http://127.0.0.1:${debugPort}`);
|
||||
return { child, browser, version: version.Browser ?? "unknown" };
|
||||
}
|
||||
({ child: firstProcess, browser: firstBrowser } = await launchBrowser());
|
||||
const firstContext = firstBrowser.contexts()[0];
|
||||
const writer = firstContext.pages()[0] ?? await firstContext.newPage();
|
||||
await writer.goto(`http://127.0.0.1:${port}/app/browser-opfs-probe.html?mode=write&cleanup=false`);
|
||||
await writer.waitForFunction(() => document.querySelector("#result")?.textContent !== "pending", null, { timeout: 10000 });
|
||||
const written = JSON.parse(await writer.locator("#result").textContent());
|
||||
if (written.status !== "verified") throw new Error(written.code ?? "opfs_write_blocked");
|
||||
const processId = browserServer.process()?.pid ?? null;
|
||||
const processId = firstProcess.pid ?? null;
|
||||
if (!processId) throw new Error("browser_process_missing");
|
||||
browserServer.kill();
|
||||
await new Promise((resolve) => browserServer.once("close", resolve));
|
||||
firstContext = null;
|
||||
secondContext = await playwright.chromium.launchPersistentContext(profile, { headless: false });
|
||||
const reader = await secondContext.newPage();
|
||||
firstProcess.kill("SIGKILL");
|
||||
await new Promise((resolve) => firstProcess.once("close", resolve));
|
||||
await firstBrowser.close().catch(() => {});
|
||||
firstBrowser = null;
|
||||
({ child: secondProcess, browser: secondBrowser } = await launchBrowser());
|
||||
const secondContext = secondBrowser.contexts()[0];
|
||||
const reader = secondContext.pages()[0] ?? await secondContext.newPage();
|
||||
await reader.goto(`http://127.0.0.1:${port}/app/browser-opfs-probe.html?mode=read&cleanup=true`);
|
||||
await reader.waitForFunction(() => document.querySelector("#result")?.textContent !== "pending", null, { timeout: 10000 });
|
||||
const restored = JSON.parse(await reader.locator("#result").textContent());
|
||||
if (restored.status !== "verified" || restored.content_hash !== written.content_hash || restored.cleanup !== true) throw new Error("opfs_crash_record_mismatch");
|
||||
console.log(JSON.stringify({ schema: "browser-opfs-s6-07-crash-sample.v1", status: "verified", browser: "chromium", browser_version: (await secondContext.browser()).version(), mode: "process_kill_profile_reopen", process_killed: true, commit_seq: restored.commit_seq, content_hash: restored.content_hash, cleanup: restored.cleanup }));
|
||||
console.log(JSON.stringify({ schema: "browser-opfs-s6-07-crash-sample.v1", status: "verified", browser: "chromium", browser_version: secondProcess ? (await secondBrowser.version()) : "unknown", mode: "process_kill_profile_reopen", process_killed: true, commit_seq: restored.commit_seq, content_hash: restored.content_hash, cleanup: restored.cleanup }));
|
||||
} catch (error) {
|
||||
blocked("opfs_crash_probe_failed", error?.message ?? error?.name ?? String(error));
|
||||
} finally {
|
||||
try { browserServer?.kill(); } catch {}
|
||||
await secondContext?.close().catch(() => {});
|
||||
await firstContext?.close().catch(() => {});
|
||||
try { firstProcess?.kill("SIGKILL"); } catch {}
|
||||
try { secondProcess?.kill("SIGKILL"); } catch {}
|
||||
await secondBrowser?.close().catch(() => {});
|
||||
await firstBrowser?.close().catch(() => {});
|
||||
await Promise.all([firstProcess, secondProcess].filter(Boolean).map((child) => new Promise((resolve) => {
|
||||
if (child.exitCode !== null || child.signalCode !== null) { resolve(); return; }
|
||||
child.once("close", resolve);
|
||||
setTimeout(resolve, 2000);
|
||||
})));
|
||||
server?.close();
|
||||
await rm(profile, { recursive: true, force: true });
|
||||
await rm(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
}
|
||||
|
||||
53
scripts/test_browser_opfs_firefox_s6_05.mjs
Normal file
53
scripts/test_browser_opfs_firefox_s6_05.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
import { createServer } from "node:http";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join, normalize } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const root = new URL("../web/", import.meta.url).pathname.replace(/\/$/, "");
|
||||
const seleniumRoot = process.env.SELENIUM_ROOT;
|
||||
|
||||
function result(status, code = null, extra = {}) {
|
||||
console.log(JSON.stringify({ schema: "browser-opfs-s6-05-firefox-sample.v1", status, browser: "firefox", ...extra, ...(code ? { code } : {}) }));
|
||||
}
|
||||
|
||||
if (!seleniumRoot) {
|
||||
result("blocked", "selenium_unavailable", { reason: "SELENIUM_ROOT is not set" });
|
||||
} else {
|
||||
let server;
|
||||
let driver;
|
||||
try {
|
||||
const { Builder } = await import(pathToFileURL(join(seleniumRoot, "node_modules/selenium-webdriver/index.js")).href);
|
||||
const firefox = await import(pathToFileURL(join(seleniumRoot, "node_modules/selenium-webdriver/firefox.js")).href);
|
||||
server = createServer(async (request, response) => {
|
||||
const requested = (request.url ?? "/").split("?")[0];
|
||||
const file = normalize(join(root, requested === "/" ? "/app/browser-opfs-probe.html" : requested));
|
||||
try {
|
||||
const body = await readFile(file);
|
||||
response.writeHead(200, { "content-type": file.endsWith(".html") ? "text/html" : "text/javascript", "cache-control": "no-store" });
|
||||
response.end(body);
|
||||
} catch {
|
||||
if (!response.headersSent) { response.writeHead(404); response.end(); }
|
||||
}
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const port = server.address().port;
|
||||
const options = new firefox.Options().setBinary(process.env.FIREFOX_BINARY || "/snap/firefox/current/usr/lib/firefox/firefox").addArguments("-headless");
|
||||
const service = new firefox.ServiceBuilder(process.env.GECKODRIVER || "/snap/bin/geckodriver");
|
||||
driver = await new Builder().forBrowser("firefox").setFirefoxOptions(options).setFirefoxService(service).build();
|
||||
await driver.get(`http://127.0.0.1:${port}/app/browser-opfs-probe.html?mode=round_trip`);
|
||||
let text = "pending";
|
||||
for (let attempt = 0; attempt < 150; attempt += 1) {
|
||||
text = await driver.executeScript("return document.querySelector('#result')?.textContent ?? 'pending';");
|
||||
if (text !== "pending") break;
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
const probe = JSON.parse(text);
|
||||
if (probe.status !== "verified") throw new Error(probe.code ?? "firefox_opfs_probe_blocked");
|
||||
result("verified", null, { browser_version: await driver.executeScript("return navigator.userAgent"), mode: probe.mode, project_id: probe.project_id, bytes: probe.bytes, commit_seq: probe.commit_seq, content_hash: probe.content_hash, cleanup: probe.cleanup });
|
||||
} catch (error) {
|
||||
result("blocked", "firefox_opfs_probe_failed", { reason: error?.message ?? error?.name ?? String(error) });
|
||||
} finally {
|
||||
await driver?.quit().catch(() => {});
|
||||
server?.close();
|
||||
}
|
||||
}
|
||||
45
scripts/test_xyzac_trt_native_startup.py
Normal file
45
scripts/test_xyzac_trt_native_startup.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify the repository LinuxCNC launcher reaches the XYZAC kins runtime."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class XyzacNativeStartupTest(unittest.TestCase):
|
||||
def run_case(self, ini: str) -> dict[str, object]:
|
||||
command = ["timeout", "--signal=TERM", "--kill-after=2s", "8s", "./run-5axis-rtcp.sh", ini]
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=False)
|
||||
output = result.stdout
|
||||
required = [
|
||||
"Found file(LIB):",
|
||||
"setup_kins: cmd=loadrt",
|
||||
"coordinates=xyzac" if ini == "xyzac-trt.ini" else "coordinates=xyzbc",
|
||||
"Joint 4 ==> Axis C",
|
||||
"userkKinematicsSetup:",
|
||||
]
|
||||
return {
|
||||
"case": ini,
|
||||
"command": command,
|
||||
"returncode": result.returncode,
|
||||
"expected_timeout": result.returncode == 124,
|
||||
"required_markers": {marker: marker in output for marker in required},
|
||||
"output_sha256": hashlib.sha256(output.encode()).hexdigest(),
|
||||
"output_tail": output[-1800:],
|
||||
}
|
||||
|
||||
def test_xyzac_and_xyzbc_startup(self):
|
||||
rows = [self.run_case("xyzac-trt.ini"), self.run_case("xyzbc-trt.ini")]
|
||||
self.assertTrue(all(row["expected_timeout"] for row in rows))
|
||||
self.assertTrue(all(all(row["required_markers"].values()) for row in rows))
|
||||
print(json.dumps({"schema": "xyzac-trt-native-startup.v1", "status": "verified", "cases": rows}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
40
scripts/test_xyzac_trt_parity.py
Normal file
40
scripts/test_xyzac_trt_parity.py
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REPORT = ROOT / "coverage/xyzac-trt-trace-parity.report.json"
|
||||
|
||||
|
||||
class XyzacParityTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
subprocess.run(["python3", "scripts/compare_xyzac_native_wasm_trace.py"], cwd=ROOT, check=True)
|
||||
cls.report = json.loads(REPORT.read_text(encoding="utf-8"))
|
||||
|
||||
def test_verified(self):
|
||||
self.assertEqual(self.report["status"], "verified")
|
||||
self.assertEqual(self.report["native_wasm"]["status"], "verified")
|
||||
self.assertEqual(self.report["native_worker"]["status"], "verified")
|
||||
self.assertEqual(self.report["first_divergent_tick"], None)
|
||||
for comparison in ("native_wasm", "native_worker"):
|
||||
for level in ("protocol", "control", "state", "canonical", "recovery"):
|
||||
self.assertEqual(self.report[comparison]["levels"][level]["status"], "verified")
|
||||
|
||||
def test_first_divergence_positive_sample(self):
|
||||
sample = self.report["positive_divergence_sample"]
|
||||
self.assertTrue(sample["detected"])
|
||||
self.assertEqual(sample["first_divergent_tick"], 2)
|
||||
self.assertEqual(sample["difference_path"], "$[1].active_type")
|
||||
|
||||
def test_self_hash(self):
|
||||
unsigned = dict(self.report)
|
||||
actual = unsigned.pop("report_sha256")
|
||||
self.assertEqual(actual, hashlib.sha256(json.dumps(unsigned, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()).hexdigest())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
38
scripts/test_xyzac_trt_source_slices.py
Normal file
38
scripts/test_xyzac_trt_source_slices.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REPORT = ROOT / "coverage/xyzac-trt-source-slice-inventory.report.json"
|
||||
|
||||
|
||||
class XyzacSourceSliceTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
subprocess.run(["python3", "scripts/generate_xyzac_trt_source_slices.py"], cwd=ROOT, check=True, stdout=subprocess.PIPE, text=True)
|
||||
cls.report = json.loads(REPORT.read_text(encoding="utf-8"))
|
||||
|
||||
def test_self_hash(self):
|
||||
unsigned = dict(self.report)
|
||||
actual = unsigned.pop("report_sha256")
|
||||
canonical = json.dumps(unsigned, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
self.assertEqual(actual, hashlib.sha256(canonical.encode()).hexdigest())
|
||||
|
||||
def test_source_slice_closure(self):
|
||||
self.assertEqual(self.report["counts"]["unregistered_slices"], 0)
|
||||
self.assertTrue(self.report["checks"]["all_controls_commands_states_have_slice_id"])
|
||||
self.assertTrue(all(row["source"]["sha256"] for row in self.report["slices"]))
|
||||
|
||||
def test_authority_and_required_rtcp(self):
|
||||
self.assertEqual(self.report["authority"]["js_authority"], 0)
|
||||
ids = {row["slice_id"] for row in self.report["slices"]}
|
||||
self.assertIn("AXIS-XYZAC-M428-M429-M430", ids)
|
||||
self.assertIn("AXIS-XYZAC-KINEMATICS", ids)
|
||||
self.assertEqual(self.report["release_status"], "STOP_RELEASE")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
55
scripts/test_xyzac_trt_wasm_authority_audit.py
Normal file
55
scripts/test_xyzac_trt_wasm_authority_audit.py
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Test the XYZAC WASM-only authority audit within the selected simulation scope."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REPORT = ROOT / "coverage/xyzac-trt-wasm-authority-audit.report.json"
|
||||
|
||||
|
||||
class XyzacWasmAuthorityAuditTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
subprocess.run(["python3", "scripts/generate_xyzac_trt_wasm_authority_audit.py"], cwd=ROOT, check=True, stdout=subprocess.PIPE, text=True)
|
||||
cls.report = json.loads(REPORT.read_text(encoding="utf-8"))
|
||||
|
||||
def test_selected_authority_boundary(self):
|
||||
self.assertEqual(self.report["schema"], "xyzac-trt-wasm-authority-audit.v1")
|
||||
self.assertEqual(self.report["task_id"], "WASM-00")
|
||||
self.assertEqual(self.report["status"], "verified")
|
||||
self.assertEqual(self.report["candidate_eligible"], True)
|
||||
self.assertEqual(self.report["release_status"], "STOP_RELEASE")
|
||||
self.assertEqual(self.report["checks"]["js_authority_boundary"]["status"], "verified")
|
||||
self.assertEqual({row["status"] for row in self.report["js_authority_findings"]}, {"resolved_selected_case"})
|
||||
self.assertEqual(self.report["blockers"], [])
|
||||
self.assertEqual({row["status"] for row in self.report["accepted_scope_exclusions"]}, {"accepted_scope_exclusion"})
|
||||
self.assertIn("SAFARI_NOT_REQUIRED", {row["code"] for row in self.report["accepted_scope_exclusions"]})
|
||||
|
||||
def test_worker_trace_and_scans(self):
|
||||
self.assertEqual(self.report["worker_evidence"]["status"], "verified")
|
||||
self.assertEqual(self.report["worker_evidence"]["case"], self.report["case"]["path"])
|
||||
self.assertEqual(self.report["worker_evidence"]["motion_authority"], "wasm")
|
||||
self.assertEqual(self.report["first_divergent_tick"], None)
|
||||
self.assertEqual(self.report["checks"]["react_no_direct_wasm_pointer"]["status"], "verified")
|
||||
self.assertEqual(self.report["checks"]["react_no_direct_opfs_writer"]["status"], "verified")
|
||||
self.assertTrue(self.report["scans"]["opfs_writer_matches"])
|
||||
self.assertEqual({row["status"] for row in self.report["browser_evidence"]}, {"verified"})
|
||||
self.assertEqual({row["status"] for row in self.report["authority_matrix"].values()}, {"verified"})
|
||||
|
||||
def test_self_hash(self):
|
||||
unsigned = dict(self.report)
|
||||
actual = unsigned.pop("report_sha256")
|
||||
expected = hashlib.sha256(json.dumps(unsigned, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
||||
self.assertEqual(actual, expected)
|
||||
self.assertEqual(self.report["checks"]["report_self_hash"]["status"], "verified")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user