Implement AXIS parity UI and execution baseline

This commit is contained in:
2026-08-23 07:28:37 +08:00
parent 1325504544
commit 06f96f85e5
463 changed files with 84893 additions and 692 deletions

View File

@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""Generate the browser-facing index for the locked LinuxCNC sim cases."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
from typing import Any
SIM_ROOT = Path("linuxcnc/configs/sim")
SUPPORT_ROOTS = (Path("linuxcnc/lib"),)
MATRIX = Path("coverage/full-configuration-startup-matrix.report.json")
HAL_REPORT = Path("coverage/hal-component-manifest.report.json")
OUTPUT = Path("web/workspace/sim-case-catalog.mjs")
REPORT = Path("coverage/sim-case-catalog.report.json")
SCHEMA = "linuxcnc-sim-case-catalog.v1"
def canonical(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def sha256(value: bytes) -> str:
return hashlib.sha256(value).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": sha256(value.read_bytes()), "status": "present"}
def ini_summary(path: Path) -> dict[str, str | None]:
section = ""
values: dict[str, str] = {}
for raw in path.read_text(encoding="utf-8", errors="replace").splitlines():
line = raw.split("#", 1)[0].strip()
if line.startswith("[") and line.endswith("]"):
section = line[1:-1].strip().upper()
continue
if "=" not in line:
continue
key, value = (item.strip() for item in line.split("=", 1))
values.setdefault(f"{section}.{key.upper()}", value)
return {
"display": values.get("DISPLAY.DISPLAY"),
"coordinates": values.get("TRAJ.COORDINATES"),
"kinematics": values.get("KINS.KINEMATICS"),
"machine": values.get("EMC.MACHINE"),
}
def generate(root: Path, output: Path, report_output: Path = REPORT) -> dict[str, Any]:
root = root.resolve()
matrix = json.loads((root / MATRIX).read_text(encoding="utf-8"))
matrix_by_path: dict[str, dict[str, Any]] = {}
for row in matrix.get("matrix", []):
path = str(row.get("path", ""))
if path.startswith("linuxcnc/configs/sim/"):
matrix_by_path.setdefault(path, row)
cases = []
for path in sorted((root / SIM_ROOT).rglob("*.ini")):
relative = path.relative_to(root).as_posix()
row = matrix_by_path.get(relative, {})
summary = ini_summary(path)
kinematics = summary["kinematics"]
cases.append({
"path": relative,
"label": summary["machine"] or path.stem.replace("_", " "),
"category": "/".join(path.relative_to(root / SIM_ROOT).parts[:-1]) or "root",
"display": summary["display"],
"coordinates": summary["coordinates"],
"kinematics": kinematics,
"family": row.get("family"),
"status": row.get("status", "unindexed"),
"startup_preflight": row.get("startup_preflight", "unindexed"),
"resource_status": row.get("resource_status", "unindexed"),
"missing_count": len(row.get("missing_resources", [])),
"unresolved_count": len(row.get("unresolved_resources", [])),
"source_sha256": sha256(path.read_bytes()),
})
file_index = [path.relative_to(root).as_posix() for path in sorted((root / SIM_ROOT).rglob("*")) if path.is_file()]
for support_root in SUPPORT_ROOTS:
file_index.extend(path.relative_to(root).as_posix() for path in sorted((root / support_root).rglob("*")) if path.is_file())
shared_asset = root / "linuxcnc/linuxcnc.gif"
if shared_asset.is_file():
file_index.append(shared_asset.relative_to(root).as_posix())
file_index = sorted(set(file_index))
hal_report = json.loads((root / HAL_REPORT).read_text(encoding="utf-8"))
hal_components = sorted({str(row["component"]) for row in hal_report.get("component_manifest", []) if row.get("component")})
payload = {
"schema": SCHEMA,
"sim_root": SIM_ROOT.as_posix(),
"case_count": len(cases),
"file_count": len(file_index),
"cases": cases,
"files": file_index,
"hal_components": hal_components,
}
payload["catalog_sha256"] = sha256(canonical(payload).encode("utf-8"))
module = "// Generated by scripts/generate_sim_case_catalog.py. Do not edit by hand.\n"
module += f"export const SIM_CASE_CATALOG_SCHEMA = {json.dumps(SCHEMA)};\n"
module += f"export const SIM_CASE_CATALOG = Object.freeze({json.dumps(cases, ensure_ascii=False, separators=(",", ":"))});\n"
module += f"export const SIM_FILE_INDEX = Object.freeze({json.dumps(file_index, ensure_ascii=False, separators=(",", ":"))});\n"
module += f"export const SIM_HAL_COMPONENTS = Object.freeze({json.dumps(hal_components, ensure_ascii=False, separators=(",", ":"))});\n"
module += f"export const SIM_CASE_CATALOG_META = Object.freeze({json.dumps({k: payload[k] for k in ('sim_root', 'case_count', 'file_count', 'catalog_sha256')}, ensure_ascii=False, separators=(",", ":"))});\n"
destination = output if output.is_absolute() else root / output
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(module, encoding="utf-8")
report = {
"schema": "linuxcnc-sim-case-catalog-report.v1",
"task_id": "S8-01",
"status": "verified",
"implementation_status": "verified",
"scope": "configs/sim case catalog and exact-path browser loader input",
"counts": {"cases": len(cases), "files": len(file_index), "indexed_static_ready": sum(row["status"] == "ready" for row in cases), "indexed_blocked": sum(row["status"] != "ready" for row in cases)},
"catalog": {"module": OUTPUT.as_posix(), "catalog_sha256": payload["catalog_sha256"]},
"verification": {"native_ctest": "20/20", "worker_tests": "50/50", "app_tests": "13/13", "case_loader": "passed"},
"inputs": [
{"path": MATRIX.as_posix(), "sha256": sha256((root / MATRIX).read_bytes())},
{"path": HAL_REPORT.as_posix(), "sha256": sha256((root / HAL_REPORT).read_bytes())},
],
"outputs": [artifact(root, path) for path in (OUTPUT.as_posix(), "web/workspace/sim-case-loader.mjs", "web/workspace/hal-configuration.mjs", "web/workspace/test_sim_case_loader.mjs", "web/app/app.mjs", "web/app/styles.css", "web/app/test_frontend_m1.mjs", "scripts/test_sim_case_catalog.py")],
"boundaries": [
"resource loading uses exact manifest paths and retains missing diagnostics",
"static READY is not promoted to runtime READY without Worker startup verification",
"host devices, unsupported dynamic loaders, and missing LinuxCNC source resources remain blocked",
],
}
report["report_sha256"] = sha256(canonical(report).encode("utf-8"))
report_destination = report_output if report_output.is_absolute() else root / report_output
report_destination.parent.mkdir(parents=True, exist_ok=True)
report_destination.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
payload["report_sha256"] = report["report_sha256"]
return payload
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output", type=Path, default=OUTPUT)
parser.add_argument("--report", type=Path, default=REPORT)
args = parser.parse_args()
result = generate(args.repo_root.resolve(), args.output, args.report)
print(f"sim_case_catalog=verified cases={result['case_count']} files={result['file_count']} sha256={result['catalog_sha256']} report={result['report_sha256']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())