226 lines
13 KiB
Python
226 lines
13 KiB
Python
#!/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())
|