141 lines
6.1 KiB
Python
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())
|