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,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']}")