Implement AXIS parity UI and execution baseline
This commit is contained in:
137
scripts/generate_gui_source_inventory.py
Normal file
137
scripts/generate_gui_source_inventory.py
Normal file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Index LinuxCNC GUI sources, simulation descriptors and visual assets.
|
||||
|
||||
This is an audit input for FE-M4. It does not claim that a desktop GUI file is
|
||||
already implemented in Web; each row retains its source hash and adaptation
|
||||
boundary for later React/Worker tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
REPORT = ROOT / "coverage/gui-source-inventory-fe-m4.report.json"
|
||||
CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1"
|
||||
|
||||
SOURCE_ROOTS = (
|
||||
Path("linuxcnc/src/emc/usr_intf/axis"),
|
||||
Path("linuxcnc/src/emc/usr_intf/gmoccapy"),
|
||||
Path("linuxcnc/src/emc/usr_intf/gscreen"),
|
||||
Path("linuxcnc/src/emc/usr_intf/touchy"),
|
||||
Path("linuxcnc/src/emc/usr_intf/qtvcp"),
|
||||
Path("linuxcnc/src/emc/usr_intf/qtplasmac"),
|
||||
Path("linuxcnc/src/emc/usr_intf/pyui"),
|
||||
Path("linuxcnc/src/emc/usr_intf/gremlin"),
|
||||
Path("linuxcnc/lib/python/qtvcp"),
|
||||
Path("linuxcnc/lib/python/gmoccapy"),
|
||||
Path("linuxcnc/lib/python/gscreen"),
|
||||
Path("linuxcnc/lib/python/touchy"),
|
||||
Path("linuxcnc/lib/python/gladevcp"),
|
||||
Path("linuxcnc/lib/python/plasmac"),
|
||||
Path("linuxcnc/lib/python/vismach.py"),
|
||||
)
|
||||
SIM_ROOT = Path("linuxcnc/configs/sim")
|
||||
GUI_FAMILIES = ("axis", "gmoccapy", "gscreen", "touchy", "qtvcp", "qtdragon", "qtplasmac", "pyvcp", "gladevcp", "ngcgui", "vismach", "plasma", "laser")
|
||||
ASSET_SUFFIXES = {".png", ".gif", ".svg", ".jpg", ".jpeg", ".ui", ".glade", ".gladep", ".xml", ".pref", ".halscope", ".halshow"}
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for block in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def canonical(value: Any) -> str:
|
||||
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
|
||||
def family(path: str) -> str:
|
||||
lower = path.lower()
|
||||
for name in GUI_FAMILIES:
|
||||
if f"/{name}/" in lower or lower.endswith(f"/{name}") or name in Path(lower).name:
|
||||
return name
|
||||
return "shared"
|
||||
|
||||
|
||||
def adaptation(path: str) -> str:
|
||||
lower = path.lower()
|
||||
if "/src/emc/usr_intf/" in lower and any(token in lower for token in ("pncconf", "stepconf")):
|
||||
return "REAL-EXCLUDED/WEB-EQUIV"
|
||||
if lower.endswith((".glade", ".gladep", ".ui", ".xml")):
|
||||
return "WEB-EQUIV/DESCRIPTOR"
|
||||
if "/python/" in lower or lower.endswith(".py"):
|
||||
return "PY-WASM/WEB-EQUIV"
|
||||
if any(lower.endswith(suffix) for suffix in ASSET_SUFFIXES):
|
||||
return "WEB-EQUIV/SOURCE-ASSET"
|
||||
return "WEB-EQUIV/SOURCE-MAP"
|
||||
|
||||
|
||||
def rows() -> list[dict[str, Any]]:
|
||||
selected: dict[str, Path] = {}
|
||||
for root in SOURCE_ROOTS:
|
||||
path = ROOT / root
|
||||
if path.is_file():
|
||||
selected[root.as_posix()] = path
|
||||
elif path.is_dir():
|
||||
for item in path.rglob("*"):
|
||||
if item.is_file() and "__pycache__" not in item.parts and item.suffix != ".pyc":
|
||||
selected[item.relative_to(ROOT).as_posix()] = item
|
||||
for item in (ROOT / SIM_ROOT).rglob("*"):
|
||||
if not item.is_file() or "__pycache__" in item.parts or item.suffix == ".pyc":
|
||||
continue
|
||||
relative = item.relative_to(ROOT).as_posix()
|
||||
lower = relative.lower()
|
||||
if any(f"/{name}/" in lower for name in GUI_FAMILIES) or item.suffix.lower() in ASSET_SUFFIXES:
|
||||
selected[relative] = item
|
||||
result = []
|
||||
for relative, path in sorted(selected.items()):
|
||||
result.append({"source_id": f"gui:{relative}", "path": relative, "family": family(relative), "kind": "asset" if path.suffix.lower() in ASSET_SUFFIXES else "source", "adaptation": adaptation(relative), "bytes": path.stat().st_size, "sha256": sha256(path), "status": "planned"})
|
||||
return result
|
||||
|
||||
|
||||
def generate() -> dict[str, Any]:
|
||||
records = rows()
|
||||
manifest_path = ROOT / "coverage/manifest.jsonl"
|
||||
manifest_sources: set[str] = set()
|
||||
if manifest_path.is_file():
|
||||
for line in manifest_path.read_text(encoding="utf-8").splitlines():
|
||||
try:
|
||||
value = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if isinstance(value, dict):
|
||||
source = value.get("source")
|
||||
if isinstance(source, str): manifest_sources.add(source.removeprefix("config:").removeprefix("source:").removeprefix("doc:"))
|
||||
for record in records:
|
||||
record["manifest_indexed"] = record["path"] in manifest_sources
|
||||
family_counts = Counter(record["family"] for record in records)
|
||||
kind_counts = Counter(record["kind"] for record in records)
|
||||
report: dict[str, Any] = {
|
||||
"schema": "gui-source-inventory-fe-m4-report.v1",
|
||||
"task_id": "FE-M4-GUI-01",
|
||||
"context_id": CONTEXT_ID,
|
||||
"status": "verified" if records and all(record["sha256"] for record in records) else "blocked",
|
||||
"implementation_status": "audited",
|
||||
"scope": {"source_roots": [path.as_posix() for path in SOURCE_ROOTS], "simulation_root": SIM_ROOT.as_posix(), "families": list(GUI_FAMILIES)},
|
||||
"counts": {"records": len(records), "families": dict(sorted(family_counts.items())), "kinds": dict(sorted(kind_counts.items())), "manifest_indexed": sum(record["manifest_indexed"] for record in records), "manifest_unindexed": sum(not record["manifest_indexed"] for record in records)},
|
||||
"records": records,
|
||||
"checks": {"source_hashes": {"status": "verified" if all(record["sha256"] for record in records) else "blocked"}, "exact_paths": {"status": "verified" if all((ROOT / record["path"]).is_file() for record in records) else "blocked"}, "manifest_reverse_index": {"status": "verified" if all(record["manifest_indexed"] for record in records) else "blocked", "unindexed": [record["path"] for record in records if not record["manifest_indexed"]][:100]}},
|
||||
"boundaries": ["desktop GTK/Qt/Tk windows are not copied into Web", "Python/VCP files require PY-WASM and capability checks", "REAL-EXCLUDED hardware configuration editors remain explicit", "full GUI acceptance requires Worker workflow, source oracle and browser evidence per family"],
|
||||
}
|
||||
report["report_sha256"] = hashlib.sha256(canonical(report).encode("utf-8")).hexdigest()
|
||||
REPORT.parent.mkdir(parents=True, exist_ok=True)
|
||||
REPORT.write_text(json.dumps(report, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
report = generate()
|
||||
print(f"gui_source_inventory={report['status']} records={report['counts']['records']} families={len(report['counts']['families'])} unindexed={report['counts']['manifest_unindexed']} report_sha256={report['report_sha256']}")
|
||||
Reference in New Issue
Block a user