Files
linuxcncWebWASM/scripts/generate_sim_display_families_s8_07.py

141 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""Generate deterministic DISPLAY/VCP family metadata for real sim cases."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
SIM_ROOT = ROOT / "linuxcnc/configs/sim"
OUTPUT = ROOT / "web/app/sim-display-families.mjs"
REPORT = ROOT / "coverage/sim-display-families-s8-07.report.json"
def canonical(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
def digest(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def normalize(path: str) -> str | None:
parts: list[str] = []
for part in path.replace("\\", "/").split("/"):
if not part or part == ".":
continue
if part == "..":
if not parts:
return None
parts.pop()
else:
parts.append(part)
return "/".join(parts)
def include_path(source: Path, raw: str) -> Path | None:
value = raw.strip().strip('"\'')
if value.startswith("linuxcnc/"):
path = ROOT / value
else:
path = source.parent / value
normalized = normalize(path.relative_to(ROOT).as_posix()) if path.is_absolute() and path.exists() else None
return ROOT / normalized if normalized else None
def collect_text(root_file: Path) -> str:
queue = [root_file]
seen: set[Path] = set()
chunks: list[str] = []
while queue:
path = queue.pop(0)
if path in seen or not path.is_file():
continue
seen.add(path)
text = path.read_text(encoding="utf-8", errors="replace")
chunks.append(text)
for raw in text.splitlines():
match = raw.strip().lstrip("#").match(r"INCLUDE\s+(?:=\s*)?(.+)") if False else None
import re
include = re.match(r"^#?INCLUDE\s+(?:=\s*)?(.+)$", raw.strip(), re.IGNORECASE)
if include:
child = include_path(path, include.group(1))
if child:
queue.append(child)
return "\n".join(chunks)
def classify(path: Path, text: str) -> dict[str, Any]:
import re
section = ""
values: dict[str, str] = {}
for raw in text.splitlines():
line = raw.split("#", 1)[0].strip()
match = re.match(r"^\[([^\]]+)\]$", line)
if match:
section = match.group(1).upper()
continue
assignment = re.match(r"^([^=:#]+?)\s*(?:=|:)\s*(.*)$", line)
if assignment:
values.setdefault(f"{section}.{assignment.group(1).strip().upper()}", assignment.group(2).strip())
display = values.get("DISPLAY.DISPLAY", "")
lower = f"{display} {path.as_posix()}".lower()
if "qtvcp" in lower or "qtdragon" in lower:
family = "qtvcp"
elif "gscreen" in lower:
family = "gscreen"
elif "touchy" in lower:
family = "touchy"
elif "gladevcp" in lower or "glade" in lower:
family = "gladevcp"
elif "pyvcp" in lower:
family = "pyvcp"
elif "vismach" in lower or "millturn" in lower or "puma" in lower or "scara" in lower or "hexapod" in lower:
family = "vismach"
elif display.startswith("axis") or not display:
family = "axis"
else:
family = "other"
declarations = {key.split(".", 1)[1]: value for key, value in values.items() if key.startswith("DISPLAY.") and key.split(".", 1)[1] in {"DISPLAY", "PYVCP", "GLADEVCP", "VCP", "EMBED_TAB_NAME", "EMBED_TAB_COMMAND"}}
host_boundaries = []
if "loadusr" in text:
host_boundaries.append("loadusr")
if "PYTHON" in text or "python" in text.lower():
host_boundaries.append("python")
return {"path": path.relative_to(ROOT).as_posix(), "family": family, "display": display or None, "declarations": declarations, "host_boundaries": sorted(set(host_boundaries)), "source_sha256": digest(path.read_bytes())}
def generate() -> dict[str, Any]:
cases = [classify(path, collect_text(path)) for path in sorted(SIM_ROOT.rglob("*.ini"))]
counts = {family: sum(row["family"] == family for row in cases) for family in sorted({row["family"] for row in cases})}
payload = {"schema": "sim-display-families-s8-07.v1", "cases": cases, "counts": counts}
catalog_hash = hashlib.sha256(canonical(payload).encode()).hexdigest()
module = "// Generated by scripts/generate_sim_display_families_s8_07.py.\n"
module += f"export const SIM_DISPLAY_FAMILY_SCHEMA = {json.dumps(payload['schema'])};\n"
module += f"export const SIM_DISPLAY_FAMILY_CATALOG = Object.freeze({json.dumps(cases, ensure_ascii=False, separators=(',', ':'))});\n"
module += f"export const SIM_DISPLAY_FAMILY_COUNTS = Object.freeze({json.dumps(counts, ensure_ascii=False, separators=(',', ':'))});\n"
module += f"export const SIM_DISPLAY_FAMILY_CATALOG_HASH = {json.dumps(catalog_hash)};\n"
OUTPUT.write_text(module, encoding="utf-8")
report = {"schema": "sim-display-families-s8-07-report.v1", "task_id": "S8-07", "status": "verified", "implementation_status": "verified", "counts": counts, "case_count": len(cases), "catalog_sha256": catalog_hash, "outputs": [{"path": "web/app/sim-display-families.mjs", "sha256": digest(OUTPUT.read_bytes())}], "boundaries": ["family metadata is derived from real INI/include declarations", "host/plugin operations remain capability boundaries", "this does not promote blocked startup cases"], "inputs": [{"path": "linuxcnc/configs/sim", "case_count": len(cases)}, {"path": "web/app/app.mjs", "sha256": digest((ROOT / 'web/app/app.mjs').read_bytes())}]}
report["report_sha256"] = hashlib.sha256(canonical(report).encode()).hexdigest()
REPORT.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("--output", type=Path, default=OUTPUT)
args = parser.parse_args()
report = generate()
print(f"sim_display_families_s8_07={report['status']} cases={report['case_count']} report_sha256={report['report_sha256']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())