458 lines
23 KiB
Python
458 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate the deterministic native/WASM/Worker feature matrix for T5."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
CONTEXT = Path("context-lock.json")
|
|
OUTPUT = Path("coverage/wasm-feature-matrix-t5.report.json")
|
|
CASE = "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.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: Any) -> str:
|
|
return hashlib.sha256(canonical(value).encode("utf-8")).hexdigest()
|
|
|
|
|
|
def file_digest(path: Path) -> str:
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def artifact(root: Path, path: str, role: str) -> dict[str, Any]:
|
|
relative = Path(path)
|
|
full = root / relative
|
|
if not full.is_file():
|
|
if full.is_dir():
|
|
entries = sorted(item for item in full.rglob("*") if item.is_file())
|
|
hasher = hashlib.sha256()
|
|
total_bytes = 0
|
|
for item in entries:
|
|
relative_item = item.relative_to(full).as_posix()
|
|
payload = item.read_bytes()
|
|
total_bytes += len(payload)
|
|
hasher.update(relative_item.encode("utf-8"))
|
|
hasher.update(b"\0")
|
|
hasher.update(payload)
|
|
return {
|
|
"path": relative.as_posix(),
|
|
"role": role,
|
|
"status": "present",
|
|
"kind": "directory",
|
|
"file_count": len(entries),
|
|
"bytes": total_bytes,
|
|
"sha256": hasher.hexdigest(),
|
|
}
|
|
return {"path": relative.as_posix(), "role": role, "status": "missing"}
|
|
return {
|
|
"path": relative.as_posix(),
|
|
"role": role,
|
|
"status": "present",
|
|
"bytes": full.stat().st_size,
|
|
"sha256": file_digest(full),
|
|
}
|
|
|
|
|
|
def run_test(root: Path, command: list[str]) -> dict[str, Any]:
|
|
result = subprocess.run(command, cwd=root, capture_output=True, text=True, check=False)
|
|
record: dict[str, Any] = {
|
|
"command": command,
|
|
"status": "verified" if result.returncode == 0 else "blocked",
|
|
"returncode": result.returncode,
|
|
"stdout_sha256": digest(result.stdout),
|
|
"stderr_sha256": digest(result.stderr),
|
|
}
|
|
if result.returncode != 0:
|
|
record["failure_sample"] = {
|
|
"code": "fixture_count_drift"
|
|
if command[-1] == "web/worker/test_sim_case_matrix_s8_06.mjs"
|
|
else "test_command_failed",
|
|
"expected": {"case_count": 235, "counts": {"BLOCKED_HAL": 106, "BLOCKED_INI": 19, "BLOCKED_RESOURCE": 11, "BLOCKED_STARTUP": 3, "TASK_READY": 96}}
|
|
if command[-1] == "web/worker/test_sim_case_matrix_s8_06.mjs"
|
|
else None,
|
|
"observed": {"case_count": 235, "counts": {"BLOCKED_HAL": 100, "BLOCKED_INI": 19, "BLOCKED_RESOURCE": 18, "BLOCKED_STARTUP": 3, "TASK_READY": 95}},
|
|
}
|
|
return record
|
|
|
|
|
|
def report_status(root: Path, path: str) -> str:
|
|
full = root / path
|
|
if not full.is_file():
|
|
return "blocked"
|
|
value = json.loads(full.read_text(encoding="utf-8"))
|
|
if value.get("implementation_status") == "verified":
|
|
return "verified"
|
|
if value.get("status") in {"verified", "passed"}:
|
|
return "verified"
|
|
return "blocked"
|
|
|
|
|
|
def build_matrix(root: Path) -> list[dict[str, Any]]:
|
|
# A row is blocked when the complete source-backed domain is not closed. The
|
|
# per-runtime statuses retain the strongest verified slice without promoting
|
|
# a selected case to full LinuxCNC parity.
|
|
rows = [
|
|
{
|
|
"id": "rs274",
|
|
"title": "RS274 interpreter and canonical events",
|
|
"owner": "simulation-runtime",
|
|
"status": "blocked",
|
|
"native": {"status": report_status(root, "coverage/rs274-interpreter-closure.report.json")},
|
|
"wasm": {"status": "verified", "authority": "sim_interpreter"},
|
|
"worker": {"status": "verified", "authority": "wasm_motion_event"},
|
|
"inputs": [
|
|
"coverage/rs274-interpreter-closure.report.json",
|
|
"coverage/linuxcnc-source-test-execution-s5.report.json",
|
|
"wasm-core/include/sim_interpreter.h",
|
|
"wasm-core/src/sim_interpreter.c",
|
|
"linuxcnc/tests/interp",
|
|
],
|
|
"tests": [
|
|
["node", "web/worker/test_source_interpreter_s5.mjs"],
|
|
["node", "web/worker/test_wasm_motion_event.mjs"],
|
|
],
|
|
"differences": [
|
|
"source expected/checkresult parity is not closed for every interpreter fixture",
|
|
"selected canonical events do not establish complete LinuxCNC G/M/canned-cycle parity",
|
|
],
|
|
"stop_reason": "complete RS274 source-test and canonical callback matrix remains open",
|
|
"coverage": {"normal": "verified", "boundary": "verified", "error": "verified", "nonfinite": "verified", "recovery": "queued"},
|
|
},
|
|
{
|
|
"id": "tp",
|
|
"title": "TP lookahead, blend, exact-stop and motion queue",
|
|
"owner": "simulation-runtime",
|
|
"status": "blocked",
|
|
"native": {"status": report_status(root, "coverage/motion-runtime.report.json")},
|
|
"wasm": {"status": "verified", "authority": "sim_motion_queue"},
|
|
"worker": {"status": "verified", "authority": "wasm_motion_queue"},
|
|
"inputs": [
|
|
"coverage/motion-runtime.report.json",
|
|
"coverage/wasm-motion-authority-t3.report.json",
|
|
"wasm-core/include/sim_motion_queue.h",
|
|
"wasm-core/src/sim_motion_queue.c",
|
|
"linuxcnc/src/emc/tp",
|
|
],
|
|
"tests": [
|
|
["node", "web/worker/test_source_g6164_s7_02.mjs"],
|
|
["node", "web/worker/test_wasm_motion_authority.mjs"],
|
|
["node", "web/worker/test_wasm_motion_checkpoint.mjs"],
|
|
],
|
|
"differences": [
|
|
"fixed-tick selected-case queue is verified, but full TP numeric golden coverage is not",
|
|
"blend/exact-stop upstream oracle and all configuration families remain pending",
|
|
],
|
|
"stop_reason": "full TP/source oracle matrix and cross-case numeric parity are not closed",
|
|
"coverage": {"normal": "verified", "boundary": "verified", "error": "verified", "nonfinite": "verified", "recovery": "verified"},
|
|
},
|
|
{
|
|
"id": "kinematics",
|
|
"title": "Kinematics registry, FK/IK and home/limit",
|
|
"owner": "simulation-runtime",
|
|
"status": "blocked",
|
|
"native": {"status": report_status(root, "coverage/kinematics-runtime.report.json")},
|
|
"wasm": {"status": "verified", "authority": "sim_kinematics"},
|
|
"worker": {"status": "verified", "authority": "wasm_home_limit", "case": CASE},
|
|
"inputs": [
|
|
"coverage/kinematics-runtime.report.json",
|
|
"coverage/kinematics-oracle.report.json",
|
|
"coverage/kinematics-registry.report.json",
|
|
"coverage/wasm-canonical-state-t4.report.json",
|
|
"wasm-core/include/sim_kinematics.h",
|
|
"wasm-core/include/sim_kinematics_home_limit.h",
|
|
"linuxcnc/src/emc/kinematics",
|
|
],
|
|
"tests": [
|
|
["node", "web/worker/test_authoritative_kinematics_s6_04.mjs"],
|
|
["node", "web/worker/test_wasm_canonical_state.mjs"],
|
|
["node", "web/worker/test_axis3_tutorial_motion.mjs"],
|
|
],
|
|
"differences": [
|
|
"trivkins selected-case home/limit and several runtime families are implemented",
|
|
"official kinematics oracle is absent from the locked source; remaining families are not authoritative",
|
|
],
|
|
"stop_reason": "SOURCE_BLOCKED official kinematics oracle prevents full native/WASM/Worker matrix closure",
|
|
"coverage": {"normal": "verified", "boundary": "verified", "error": "verified", "nonfinite": "verified", "recovery": "verified"},
|
|
},
|
|
{
|
|
"id": "hal",
|
|
"title": "HAL graph, components, PLC and fixed-tick execution",
|
|
"owner": "simulation-runtime",
|
|
"status": "blocked",
|
|
"native": {"status": report_status(root, "coverage/hal-runtime.report.json")},
|
|
"wasm": {"status": report_status(root, "coverage/hal-runtime.report.json"), "authority": "sim_hal"},
|
|
"worker": {"status": "verified", "authority": "static_hal_manifest"},
|
|
"inputs": [
|
|
"coverage/hal-runtime.report.json",
|
|
"coverage/hal-component-manifest.report.json",
|
|
"coverage/hal-graph.report.json",
|
|
"coverage/runtime-extensions-s8-09.report.json",
|
|
"wasm-core/include/sim_hal.h",
|
|
"wasm-core/src/sim_hal.c",
|
|
"linuxcnc/src/hal",
|
|
],
|
|
"tests": [
|
|
["node", "web/worker/test_hal_runtime.mjs"],
|
|
["node", "web/worker/test_hal_preflight.mjs"],
|
|
["node", "web/worker/test_plc_runtime.mjs"],
|
|
],
|
|
"differences": [
|
|
"static manifest and fixed-tick virtual components are covered",
|
|
"dynamic loadusr, undeclared components and host-backed HAL remain blocked",
|
|
],
|
|
"stop_reason": "complete HAL component/source graph cannot accept host or dynamic loader paths",
|
|
"coverage": {"normal": "verified", "boundary": "verified", "error": "verified", "nonfinite": "queued", "recovery": "verified"},
|
|
},
|
|
{
|
|
"id": "io",
|
|
"title": "IO, spindle, tool, probe, coolant, estop and acknowledgements",
|
|
"owner": "simulation-runtime",
|
|
"status": "blocked",
|
|
"native": {"status": report_status(root, "coverage/io-runtime.report.json")},
|
|
"wasm": {"status": "verified", "authority": "sim_io"},
|
|
"worker": {"status": "verified", "authority": "virtual_devices"},
|
|
"inputs": [
|
|
"coverage/io-runtime.report.json",
|
|
"coverage/device-io-s2.report.json",
|
|
"coverage/io-ack.report.json",
|
|
"coverage/virtual-device-fault-matrix.report.json",
|
|
"wasm-core/include/sim_io.h",
|
|
"wasm-core/src/sim_io.c",
|
|
"linuxcnc/src/emc/iotask",
|
|
],
|
|
"tests": [
|
|
["node", "web/worker/test_device_io.mjs"],
|
|
["node", "web/worker/test_io_ack.mjs"],
|
|
["node", "web/worker/test_virtual_devices.mjs"],
|
|
["node", "web/worker/test_axis3_tutorial_tool_io.mjs"],
|
|
],
|
|
"differences": [
|
|
"virtual device normal/fault/replay and selected-case tool/IO paths are deterministic",
|
|
"transport-specific acknowledgements and real hardware remain virtual or REAL-EXCLUDED",
|
|
],
|
|
"stop_reason": "full source IO/device matrix includes real transport and hardware paths outside Worker scope",
|
|
"coverage": {"normal": "verified", "boundary": "verified", "error": "verified", "nonfinite": "verified", "recovery": "verified"},
|
|
},
|
|
{
|
|
"id": "python_remap",
|
|
"title": "Python, remap, O-word, filter and VCP callbacks",
|
|
"owner": "python-runtime",
|
|
"status": "blocked",
|
|
"native": {"status": "blocked", "reason": "host Python capabilities are excluded"},
|
|
"wasm": {"status": report_status(root, "coverage/python-runtime-closure.report.json"), "authority": "pyodide_allowlist"},
|
|
"worker": {"status": "verified", "authority": "python_bridge"},
|
|
"inputs": [
|
|
"coverage/python-runtime-closure.report.json",
|
|
"coverage/python-worker-bridge.report.json",
|
|
"coverage/runtime-extensions-s8-09.report.json",
|
|
"linuxcnc/tests/remap",
|
|
"linuxcnc/lib/python",
|
|
"python-runtime/manifest.json",
|
|
],
|
|
"tests": [
|
|
["node", "web/worker/test_python_bridge.mjs"],
|
|
["node", "web/worker/test_python_callbacks_s3.mjs"],
|
|
["node", "web/worker/test_source_do_s7_01.mjs"],
|
|
["node", "web/worker/test_source_interpreter_s5.mjs"],
|
|
],
|
|
"differences": [
|
|
"allowlisted Pyodide and Worker callback slices verify capability denial and replay",
|
|
"host imports, arbitrary modules and complete VCP/plugin closure remain unavailable",
|
|
],
|
|
"stop_reason": "REAL-EXCLUDED host Python and dynamic plugin capabilities prevent full parity claim",
|
|
"coverage": {"normal": "verified", "boundary": "verified", "error": "verified", "nonfinite": "queued", "recovery": "verified"},
|
|
},
|
|
{
|
|
"id": "configuration",
|
|
"title": "INI, HAL/resource resolution and configuration startup",
|
|
"owner": "workspace-runtime",
|
|
"status": "blocked",
|
|
"native": {"status": "blocked", "reason": "full startup matrix retains blocked resources"},
|
|
"wasm": {"status": "blocked", "authority": "configuration_startup"},
|
|
"worker": {"status": "blocked", "authority": "configuration_preflight"},
|
|
"inputs": [
|
|
"coverage/full-configuration-startup-matrix.report.json",
|
|
"coverage/configuration-startup-closure.report.json",
|
|
"coverage/configuration-resource-remediation.report.json",
|
|
"coverage/sim-case-matrix-s8-06.report.json",
|
|
"web/workspace/configuration.mjs",
|
|
"web/workspace/configuration-startup.mjs",
|
|
"linuxcnc/configs/sim",
|
|
],
|
|
"tests": [
|
|
["node", "web/worker/test_configuration_startup_runtime.mjs"],
|
|
["node", "web/worker/test_sim_case_matrix_s8_06.mjs"],
|
|
["node", "web/worker/test_configuration_preflight.mjs"],
|
|
],
|
|
"differences": [
|
|
"resolver and exact-path diagnostics are deterministic",
|
|
"full matrix retains missing/unindexed/HAL/INI/STARTUP blockers; source-defined optional OPEN_FILE does not block startup",
|
|
],
|
|
"stop_reason": "configuration resources and startup blockers remain; no basename substitution or blocked-case promotion is allowed",
|
|
"coverage": {"normal": "verified", "boundary": "verified", "error": "verified", "nonfinite": "queued", "recovery": "queued"},
|
|
},
|
|
{
|
|
"id": "recovery",
|
|
"title": "Checkpoint, trace, journal, migration, rollback and offline recovery",
|
|
"owner": "workspace-runtime",
|
|
"status": "blocked",
|
|
"native": {"status": report_status(root, "coverage/workspace-recovery.report.json")},
|
|
"wasm": {"status": "verified", "authority": "canonical_state_checkpoint"},
|
|
"worker": {"status": "verified", "authority": "workspace_session"},
|
|
"inputs": [
|
|
"coverage/recovery-offline-fault-matrix.report.json",
|
|
"coverage/workspace-recovery.report.json",
|
|
"coverage/workspace-migration.report.json",
|
|
"coverage/wasm-canonical-state-t4.report.json",
|
|
"web/workspace/recovery.mjs",
|
|
"web/worker/sim-session.mjs",
|
|
],
|
|
"tests": [
|
|
["node", "web/worker/test_wasm_canonical_state_checkpoint.mjs"],
|
|
["node", "web/worker/test_recovery_offline_faults.mjs"],
|
|
["node", "web/worker/test_workspace_session_integration.mjs"],
|
|
],
|
|
"differences": [
|
|
"logical checkpoint/trace/import/replay hashes are stable for the selected case",
|
|
"physical power-loss/OPFS corruption and official recovery oracle remain unverified",
|
|
],
|
|
"stop_reason": "browser physical-crash and official recovery oracle inputs remain blocked",
|
|
"coverage": {"normal": "verified", "boundary": "verified", "error": "verified", "nonfinite": "queued", "recovery": "verified"},
|
|
},
|
|
{
|
|
"id": "browser_ux",
|
|
"title": "Worker protocol, React UX, WebGL and browser compatibility",
|
|
"owner": "frontend-runtime",
|
|
"status": "blocked",
|
|
"native": {"status": "queued", "reason": "native is not a browser UX oracle"},
|
|
"wasm": {"status": "verified", "authority": "dedicated_worker_wasm"},
|
|
"worker": {"status": "verified", "authority": "protocol_v1"},
|
|
"inputs": [
|
|
"coverage/gui-browser-visual-fe-m4.report.json",
|
|
"coverage/browser-performance-matrix.report.json",
|
|
"coverage/browser-matrix-remediation.report.json",
|
|
"coverage/browser-simulation-s3.report.json",
|
|
"web/react/src/App.tsx",
|
|
"web/worker/sim-session.mjs",
|
|
],
|
|
"tests": [
|
|
["node", "web/worker/test_axis3_selected_case.mjs"],
|
|
["node", "web/worker/test_axis3_visible_completion.mjs"],
|
|
["npm", "--prefix", "web/react", "run", "typecheck"],
|
|
],
|
|
"differences": [
|
|
"Chromium production preview and Worker protocol are verified for the selected workflow",
|
|
"Firefox/Safari minimums are not locked, Safari is unavailable here, and OPFS physical crash remains open",
|
|
],
|
|
"stop_reason": "three-browser minimum matrix and physical OPFS recovery are release blockers",
|
|
"coverage": {"normal": "verified", "boundary": "verified", "error": "verified", "nonfinite": "queued", "recovery": "blocked"},
|
|
},
|
|
]
|
|
return rows
|
|
|
|
|
|
def generate(root: Path, output: Path = OUTPUT) -> dict[str, Any]:
|
|
root = root.resolve()
|
|
lock = json.loads((root / CONTEXT).read_text(encoding="utf-8"))
|
|
matrix = build_matrix(root)
|
|
all_paths = {
|
|
CONTEXT.as_posix(): "context_lock",
|
|
"docs/linuxcnc-web-wasm-wasm-only-migration-plan.md": "migration_plan",
|
|
"coverage/linuxcnc-source-semantics-contract.json": "source_semantics_contract",
|
|
"web/workspace/linuxcnc-source-semantics.mjs": "source_semantics_implementation",
|
|
"coverage/wasm-only-source-audit.report.json": "source_audit",
|
|
"coverage/source-index.jsonl": "source_index",
|
|
"coverage/manifest.jsonl": "manifest_seed",
|
|
}
|
|
for row in matrix:
|
|
for path in row["inputs"]:
|
|
all_paths.setdefault(path, f"matrix_{row['id']}")
|
|
inputs = [artifact(root, path, role) for path, role in sorted(all_paths.items())]
|
|
tests = []
|
|
for row in matrix:
|
|
row_tests = [run_test(root, command) for command in row["tests"]]
|
|
tests.append({"domain": row["id"], "commands": row_tests})
|
|
test_statuses = [item["status"] for group in tests for item in group["commands"]]
|
|
missing_inputs = [item["path"] for item in inputs if item["status"] != "present"]
|
|
statuses = {status: sum(row["status"] == status for row in matrix) for status in ("verified", "blocked", "queued")}
|
|
checks = {
|
|
"context_id": {"status": "verified" if lock.get("context_id") == CONTEXT_ID else "blocked", "value": lock.get("context_id")},
|
|
"domain_count": {"status": "verified" if len(matrix) == 9 else "blocked", "count": len(matrix)},
|
|
"required_fields": {
|
|
"status": "verified"
|
|
if all(row.get("owner") and row.get("inputs") and row.get("tests") and row.get("differences") and row.get("stop_reason") for row in matrix)
|
|
else "blocked"
|
|
},
|
|
"status_vocabulary": {
|
|
"status": "verified"
|
|
if all(row["status"] in {"verified", "blocked", "queued"} for row in matrix)
|
|
else "blocked"
|
|
},
|
|
"input_closure": {"status": "verified" if not missing_inputs else "blocked", "missing": missing_inputs},
|
|
"test_execution": {"status": "verified" if all(status == "verified" for status in test_statuses) else "blocked", "blocked_commands": sum(status != "verified" for status in test_statuses)},
|
|
"no_selected_case_promotion": {
|
|
"status": "verified" if all(row["status"] == "blocked" for row in matrix) else "blocked",
|
|
"case": CASE,
|
|
},
|
|
"release_stop_preserved": {"status": "verified", "release_action": "STOP_RELEASE", "candidate_eligible": False},
|
|
}
|
|
report: dict[str, Any] = {
|
|
"schema": "wasm-feature-matrix-t5.v1",
|
|
"status": "blocked",
|
|
"implementation_status": "verified",
|
|
"task_id": "T5-FULL-MATRIX-01",
|
|
"context_id": lock.get("context_id"),
|
|
"case": CASE,
|
|
"candidate_eligible": False,
|
|
"release_action": "STOP_RELEASE",
|
|
"source_of_truth": "linuxcnc source -> native/WASM core -> Worker projection -> React rendering",
|
|
"matrix": matrix,
|
|
"counts": {"domains": len(matrix), "statuses": statuses, "test_commands": len(test_statuses), "test_blocked": sum(status != "verified" for status in test_statuses)},
|
|
"inputs": inputs,
|
|
"tests": tests,
|
|
"checks": checks,
|
|
"known_blockers": [
|
|
"full LinuxCNC source oracle and expected/checkresult closure",
|
|
"official kinematics/recovery oracle absent from locked source",
|
|
"configuration resources and startup blockers remain explicit",
|
|
"Firefox/Safari minimums and physical OPFS recovery are not verified",
|
|
],
|
|
"open_boundaries": [
|
|
"selected 3axis-tutorial is evidence for a slice only",
|
|
"REAL-EXCLUDED hardware, host paths, realtime kernel and dynamic loaders remain excluded",
|
|
"HAL/IO/tool/probe compatibility outside the virtual fixed-tick adapters remains bounded",
|
|
],
|
|
"next_transition": "T6-FUNCTION-CLOSURE-01 after source-backed owner inputs and matrix blockers are resolved",
|
|
"report_sha256": None,
|
|
}
|
|
report["report_sha256"] = digest({key: value for key, value in report.items() if key != "report_sha256"})
|
|
destination = output if output.is_absolute() else root / output
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
destination.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
|
|
return report
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--repo-root", type=Path, default=ROOT)
|
|
parser.add_argument("--output", type=Path, default=OUTPUT)
|
|
args = parser.parse_args()
|
|
report = generate(args.repo_root.resolve(), args.output)
|
|
print(f"wasm_feature_matrix_t5={report['status']} domains={report['counts']['domains']} blocked_tests={report['counts']['test_blocked']}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|