Files
linuxcncWebWASM/scripts/generate_full_linuxcnc_simulation_gap_audit.py

189 lines
10 KiB
Python

#!/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())