#!/usr/bin/env python3 """Audit the complete LinuxCNC simulation plan against current evidence.""" from __future__ import annotations import argparse import hashlib import json import re from pathlib import Path from typing import Any CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1" PLAN = Path("docs/linuxcnc-web-wasm-complete-simulation-plan.md") SOURCE_MAP = Path("docs/linuxcnc-web-wasm-source-map.md") LOCK = Path("context-lock.json") EVIDENCE = { "P0-03": "coverage/sim-contract.report.json", "P1-01": "coverage/sim-task.report.json", "P1-02": "coverage/sim-scheduler.report.json", "P1-03": "coverage/sim-ledger.report.json", "P1-04": "coverage/sim-runner.report.json", "P2-01": "coverage/workspace-runtime.report.json", "P2-02": "coverage/ini-parser.report.json", "P2-03": "coverage/resource-resolver.report.json", "P2-04": "coverage/configuration-startup-runtime.report.json", "P3-01": "coverage/rs274-interpreter-closure.report.json", "P3-02": "coverage/o-word-runtime.report.json", "P3-03": "coverage/tp-blend.report.json", "P3-04": "coverage/task-program-state.report.json", "P3-05": "coverage/motion-state.report.json", "P3-06": "coverage/kinematics-base.report.json", "P3-07": "coverage/advanced-kinematics.report.json", "P4-01": "coverage/hal-graph.report.json", "P4-02": "coverage/component-capability.report.json", "P4-03": "coverage/plc-runtime.report.json", "P4-04": "coverage/device-twin.report.json", "P4-05": "coverage/io-ack.report.json", "P5-01": "coverage/python-runtime-closure.report.json", "P5-02": "coverage/python-api.report.json", "P5-03": "coverage/python-callbacks.report.json", "P6-01": "coverage/trace-export.report.json", "P6-02": "coverage/reconciliation.report.json", "P6-03": "coverage/offline-migration.report.json", "P7-01": "coverage/worker-protocol.report.json", "P7-02": "coverage/editor-workflow.report.json", "P7-03": "coverage/authoritative-panels.report.json", "P7-04": "coverage/machine-view.report.json", "P7-05": "coverage/recovery-ux.report.json", "P8-01": "coverage/native-oracle-closure.report.json", "P8-02": "coverage/startup-matrix-closure.report.json", "P8-03": "coverage/determinism.report.json", "P8-04": "coverage/browser-matrix-closure.report.json", "P8-05": "coverage/release-closure.report.json", } S5_REPORTS = ( "coverage/linuxcnc-source-test-s5.report.json", "coverage/linuxcnc-source-test-execution-s5.report.json", "coverage/linuxcnc-source-unit-execution-s5.report.json", "coverage/linuxcnc-source-interp-s5.report.json", "coverage/linuxcnc-source-hal-s5.report.json", "coverage/linuxcnc-source-io-s5.report.json", "coverage/linuxcnc-source-config-s5.report.json", "coverage/linuxcnc-source-parity-s5.report.json", ) GAP_LEDGER = ( { "gap_id": "GAP-TASK-TICK-INTEGRATION", "domain": "authoritative_runtime", "status": "OPEN", "evidence": ["docs/linuxcnc-web-wasm-system-flow-and-execution-baseline.md", "coverage/sim-task.report.json", "coverage/linuxcnc-source-parity-s5.report.json"], "reason": "interpreter/TP/motion/kinematics/HAL/IO modules have isolated evidence, but the full authoritative sim_task fixed-tick chain is not yet proven against LinuxCNC source behavior", "next_task": "S6-02", }, { "gap_id": "GAP-OFFICIAL-NATIVE-ORACLE", "domain": "oracle", "status": "SOURCE_BLOCKED", "evidence": ["coverage/native-oracle-closure.report.json", "coverage/linuxcnc-source-parity-s5.report.json"], "reason": "locked source lacks the official kinematics/recovery oracle and desktop native unit environment", "next_task": "OWNER/CONTEXT INPUT REQUIRED", }, { "gap_id": "GAP-CONFIG-RESOURCE-CLOSURE", "domain": "configuration", "status": "OPEN_BLOCKED", "evidence": ["coverage/linuxcnc-source-config-s5.report.json", "coverage/full-configuration-startup-matrix.report.json"], "reason": "981 resource rows and 208 configuration entries remain blocked by missing, unindexed or unregistered dependencies", "next_task": "S6-03", }, { "gap_id": "GAP-SOURCE-ADAPTER-PENDING", "domain": "source_parity", "status": "OPEN_BLOCKED", "evidence": ["coverage/linuxcnc-source-parity-s5.report.json"], "reason": "G64/DO, full canonical logger, custom .comp, startup tool tables, ClassicLadder and motion-logger paths remain explicitly pending or excluded", "next_task": "S6-04", }, { "gap_id": "GAP-BROWSER-OPFS-CLOSURE", "domain": "recovery_browser", "status": "OPEN_BLOCKED", "evidence": ["coverage/recovery-offline-fault-matrix.report.json", "coverage/browser-matrix-closure.report.json"], "reason": "logical Worker recovery is verified, while browser OPFS crash, Safari/performance and physical browser oracle remain incomplete", "next_task": "S6-05", }, ) def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) def digest_bytes(value: bytes) -> str: return hashlib.sha256(value).hexdigest() def artifact(root: Path, path: Path) -> dict[str, Any]: value = root / path return {"path": path.as_posix(), "status": "present", "bytes": value.stat().st_size, "sha256": digest_bytes(value.read_bytes())} if value.is_file() else {"path": path.as_posix(), "status": "missing"} def plan_tasks(root: Path) -> list[dict[str, Any]]: text = (root / PLAN).read_text(encoding="utf-8") tasks = [] pattern = re.compile(r"^\| `?(P\d-\d+)`? \| (.*?) \| (.*?) \| (.*?) \| (.*?) \|$", re.MULTILINE) for task_id, title, depends, outputs, acceptance in pattern.findall(text): evidence_path = EVIDENCE.get(task_id) evidence = None evidence_state = "NO_DIRECT_REPORT" if evidence_path and (root / evidence_path).is_file(): value = json.loads((root / evidence_path).read_text(encoding="utf-8")) evidence = {"path": evidence_path, "task_id": value.get("task_id"), "schema": value.get("schema"), "status": value.get("status"), "implementation_status": value.get("implementation_status"), "sha256": digest_bytes((root / evidence_path).read_bytes())} if value.get("status") in {"verified", "passed"} and value.get("implementation_status", "verified") in {"verified", None}: evidence_state = "EVIDENCE_VERIFIED" elif value.get("implementation_status") == "verified": evidence_state = "IMPLEMENTED_WITH_BLOCKERS" else: evidence_state = "EVIDENCE_BLOCKED" tasks.append({"task_id": task_id, "title": title, "depends_on": depends, "planned_outputs": outputs, "acceptance": acceptance, "evidence_state": evidence_state, "evidence": evidence}) return tasks def generate(root: Path, output: Path) -> dict[str, Any]: tasks = plan_tasks(root) task_counts: dict[str, int] = {} for task in tasks: task_counts[task["evidence_state"]] = task_counts.get(task["evidence_state"], 0) + 1 source_artifacts = [artifact(root, Path(path)) for path in (PLAN, SOURCE_MAP, LOCK, *map(Path, S5_REPORTS))] source_reports = [json.loads((root / path).read_text(encoding="utf-8")) for path in S5_REPORTS] source_summary = { "tasks": [report.get("task_id") for report in source_reports], "all_verified": all(report.get("status") == "verified" for report in source_reports), "s5_parity_report_sha256": next((artifact["sha256"] for artifact in source_artifacts if artifact["path"] == "coverage/linuxcnc-source-parity-s5.report.json"), None), "configuration_entries": next((report.get("counts", {}).get("total") for report in source_reports if report.get("task_id") == "S5-07"), None), } report: dict[str, Any] = { "schema": "full-linuxcnc-simulation-gap-audit.v1", "task_id": "S6-01", "context_id": CONTEXT_ID, "status": "verified" if tasks and source_summary["all_verified"] else "blocked", "audit_scope": {"plan": PLAN.as_posix(), "p0_p8_task_count": len(tasks), "source_test_track": "S5-01..S5-08", "code_domains": ["workspace", "task", "interpreter", "TP/motion", "kinematics", "HAL/PLC/IO", "Python/remap/VCP", "recovery", "Web/UI"]}, "task_ledger": {"counts": task_counts, "tasks": tasks}, "source_test_summary": source_summary, "gap_ledger": list(GAP_LEDGER), "next_execution": {"task_id": "S6-02", "status": "queued", "title": "integrate authoritative sim_task fixed tick across interpreter/TP/motion/kinematics/HAL/IO", "depends_on": ["S6-01"], "direct_gaps": ["GAP-TASK-TICK-INTEGRATION"], "required_evidence": ["native/WASM/Worker task snapshot parity", "first divergent tick or explicit blocked reason", "pause/resume/abort/recovery endpoint"]}, "inputs": source_artifacts, "boundaries": ["verified module reports are not equivalent to full LinuxCNC behavior parity", "official oracle, source/resource blockers and browser/OPFS blockers remain explicit", "no ABI/protocol/schema/manifest changes are made by this audit"], } report["report_sha256"] = digest_bytes(canonical({key: value for key, value in report.items() if key != "report_sha256"}).encode("utf-8")) output.parent.mkdir(parents=True, exist_ok=True) output.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(description=__doc__) parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1]) parser.add_argument("--report", type=Path, default=Path("coverage/full-linuxcnc-simulation-gap-audit.report.json")) args = parser.parse_args() report = generate(args.repo_root.resolve(), args.repo_root.resolve() / args.report) print(f"full_linuxcnc_simulation_gap_audit={report['status']} tasks={report['audit_scope']['p0_p8_task_count']} gaps={len(report['gap_ledger'])} next={report['next_execution']['task_id']}") return 0 if report["status"] == "verified" else 1 if __name__ == "__main__": raise SystemExit(main())