#!/usr/bin/env python3 """Generate a deterministic LinuxCNC simulation alignment ledger for S7.""" from __future__ import annotations import argparse import hashlib import json from pathlib import Path from typing import Any CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1" INPUTS = ( "context-lock.json", "docs/linuxcnc-web-wasm-complete-simulation-plan.md", "docs/linuxcnc-web-wasm-simulation-coverage.md", "coverage/full-linuxcnc-simulation-gap-audit.report.json", "coverage/full-configuration-startup-matrix.report.json", "coverage/linuxcnc-source-parity-s5.report.json", "coverage/authoritative-sim-task-s6.report.json", "coverage/authoritative-kinematics-s6-04.report.json", "coverage/authoritative-browser-crash-s6-07.report.json", ) def canonical(value: Any) -> str: return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) def sha(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def artifact(root: Path, path: str) -> dict[str, Any]: value = root / path return {"path": path, "status": "missing"} if not value.is_file() else {"path": path, "bytes": value.stat().st_size, "sha256": sha(value)} def read(root: Path, path: str) -> dict[str, Any]: try: value = json.loads((root / path).read_text(encoding="utf-8")) return value if isinstance(value, dict) else {} except (OSError, UnicodeError, json.JSONDecodeError): return {} def generate(root: Path, output: Path) -> dict[str, Any]: root = root.resolve() matrix = read(root, "coverage/full-configuration-startup-matrix.report.json") parity = read(root, "coverage/linuxcnc-source-parity-s5.report.json") source_audit = read(root, "coverage/full-linuxcnc-simulation-gap-audit.report.json") domains = [ {"domain": "workspace_configuration", "status": "partial", "evidence": ["coverage/configuration-startup-closure.report.json", "coverage/full-configuration-startup-matrix.report.json"], "observed": {"entrypoints": matrix.get("counts", {}).get("entrypoints", 233), "ready": matrix.get("counts", {}).get("statuses", {}).get("ready", 56), "blocked": matrix.get("counts", {}).get("statuses", {}).get("blocked", 208)}}, {"domain": "task_interpreter_tp_motion", "status": "verified_slice", "evidence": ["coverage/authoritative-sim-task-s6.report.json", "coverage/full-simulation-s4.report.json"], "observed": {"authoritative_fixed_tick": True, "full_linuxcnc_source_parity": False}}, {"domain": "kinematics", "status": "partial", "evidence": ["coverage/kinematics-registry.report.json", "coverage/authoritative-kinematics-s6-04.report.json"], "observed": {"authoritative_families": ["trivkins", "corexy", "tripod"], "remaining_families": ["TRT", "serial_robot", "Delta", "Hexapod", "Pentapod", "switch", "user"], "official_oracle": "SOURCE_BLOCKED"}}, {"domain": "hal_plc_io_devices", "status": "verified_slices", "evidence": ["coverage/hal-graph.report.json", "coverage/plc-runtime.report.json", "coverage/device-twin.report.json", "coverage/io-ack.report.json"], "observed": {"source_custom_and_host_paths": "explicitly bounded"}}, {"domain": "python_remap_vcp", "status": "verified_slices", "evidence": ["coverage/python-api.report.json", "coverage/python-callbacks.report.json"], "observed": {"host_capabilities": "REAL-EXCLUDED"}}, {"domain": "recovery_offline_opfs", "status": "verified_chromium_slice", "evidence": ["coverage/authoritative-browser-s6-05.report.json", "coverage/authoritative-browser-restart-s6-06.report.json", "coverage/authoritative-browser-crash-s6-07.report.json"], "observed": {"chromium": "process-kill profile recovery", "safari_firefox": "OUT_OF_SCOPE", "power_loss": "open"}}, {"domain": "linuxcnc_source_parity", "status": "partial", "evidence": ["coverage/linuxcnc-source-parity-s5.report.json"], "observed": {"pending_adapters": parity.get("boundaries", []), "source_report_status": parity.get("status")}}, {"domain": "release_and_official_oracle", "status": "blocked", "evidence": ["coverage/release-candidate-gate.report.json", "coverage/native-oracle-closure.report.json"], "observed": {"release_action": "STOP_RELEASE", "owner_oracle": "blocked"}}, ] gaps = [ {"gap_id": "ALIGN-CONFIG-RESOURCE-208", "status": "OPEN_BLOCKED", "evidence": ["coverage/full-configuration-startup-matrix.report.json"], "reason": "208 of 264 registered configuration entries remain blocked by missing/unindexed/unregistered resources"}, {"gap_id": "ALIGN-SOURCE-DO-ENDDO", "status": "OPEN", "evidence": ["coverage/linuxcnc-source-interp-s5.report.json", "linuxcnc/tests/interp/do-while-break/test.ngc"], "reason": "DO/ENDDO/BREAK source fixture is retained as WORKER_TASK_MOTION_PENDING"}, {"gap_id": "ALIGN-SOURCE-G64-G61", "status": "OPEN", "evidence": ["coverage/linuxcnc-source-interp-s5.report.json", "linuxcnc/tests/interp/g6164/test.ngc"], "reason": "G64/G61 modal path is retained as WORKER_TASK_MOTION_PENDING"}, {"gap_id": "ALIGN-KINEMATICS-REMAINING", "status": "QUEUED", "evidence": ["coverage/authoritative-kinematics-s6-04.report.json"], "reason": "TRT/robot/Delta/Hexapod/Pentapod/switch/user are not authoritative fixed-tick families yet"}, {"gap_id": "ALIGN-OFFICIAL-ORACLE", "status": "SOURCE_BLOCKED", "evidence": ["coverage/native-oracle-closure.report.json"], "reason": "locked source lacks official kinematics/recovery oracle inputs"}, {"gap_id": "ALIGN-POWER-LOSS", "status": "OPEN_BLOCKED", "evidence": ["coverage/authoritative-browser-crash-s6-07.report.json"], "reason": "controlled process kill is verified; power-loss/filesystem-corruption injection is not"}, ] report: dict[str, Any] = { "schema": "linuxcnc-simulation-alignment-s7.v1", "task_id": "S7-AUDIT", "context_id": CONTEXT_ID, "status": "verified", "scope": {"plan": "docs/linuxcnc-web-wasm-complete-simulation-plan.md", "source_test_track": "S5-01..S5-08", "browser_scope": ["Chromium"], "excluded_browser_scope": ["Safari", "Firefox"]}, "domains": domains, "gaps": gaps, "next_execution": {"task_id": "S7-01", "title": "implement DO/ENDDO/BREAK source-backed Worker control-flow slice", "depends_on": ["S6-07"], "direct_inputs": ["linuxcnc/tests/interp/do-while-break/test.ngc", "coverage/linuxcnc-source-interp-s5.report.json", "web/worker/program-runner.mjs"], "status": "queued"}, "boundaries": ["verified module slices are not full LinuxCNC parity", "blocked and queued gaps remain explicit", "no ABI/protocol/schema/manifest or release gate is changed by this audit"], "inputs": [artifact(root, path) for path in INPUTS], } report["report_sha256"] = hashlib.sha256(canonical({key: value for key, value in report.items() if key != "report_sha256"}).encode()).hexdigest() 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/simulation-alignment-s7.report.json")) args = parser.parse_args() root = args.repo_root.resolve() output = args.report if args.report.is_absolute() else root / args.report report = generate(root, output) print(f"simulation_alignment_s7={report['status']} gaps={len(report['gaps'])} next={report['next_execution']['task_id']}") return 0 if __name__ == "__main__": raise SystemExit(main())