Files
linuxcncWebWASM/scripts/generate_xyzac_trt_wasm_authority_audit.py

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())