#!/usr/bin/env python3 """Freeze the source and exact bitmap inventory for the AXIS parity track.""" from __future__ import annotations import hashlib import json import re from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] REPORT = ROOT / "coverage/axis-source-inventory.report.json" CONTEXT_ID = "linuxcnc-2.10.0-pre0-wasm-m1-py-c1" AXIS_TCL = Path("linuxcnc/share/axis/tcl/axis.tcl") SOURCE_FILES = ( AXIS_TCL, Path("linuxcnc/share/axis/tcl/support.tcl"), Path("linuxcnc/share/axis/tcl/accel.tcl"), Path("linuxcnc/share/axis/tcl/combobox.tcl"), Path("linuxcnc/share/axis/tcl/sb.tcl"), Path("linuxcnc/share/axis/tcl/dialog.tcl"), Path("linuxcnc/src/emc/usr_intf/axis/README"), Path("linuxcnc/lib/python/gremlin.py"), Path("linuxcnc/lib/python/gremlin_view.py"), Path("linuxcnc/lib/python/rs274/glcanon.py"), ) TOOLBAR_ASSETS = ( "tool_estop.gif", "tool_power.gif", "tool_open.gif", "tool_reload.gif", "tool_run.gif", "tool_step.gif", "tool_pause.gif", "resume_inhibit.gif", "tool_stop.gif", "tool_blockdelete.gif", "tool_optpause.gif", "tool_zoomin.gif", "tool_zoomout.gif", "tool_axis_z.gif", "tool_axis_z2.gif", "tool_axis_x.gif", "tool_axis_y.gif", "tool_axis_y2.png", "tool_axis_p.gif", "tool_rotate.gif", "tool_clear.gif", ) IMAGE_ROOT = Path("linuxcnc/share/axis/images") CASE_ROOT = Path("linuxcnc/configs/sim/axis") SUPPORT_ASSETS = ( "axis-16x16.png", "axis-24x24.png", "axis-32x32.png", "axis-48x48.png", "banner.gif", "spindle_ccw.gif", "spindle_cw.gif", "std_error.gif", "std_info.gif", "std_warning.gif", ) DECLARATION_KEYS = ("DISPLAY", "PYVCP", "GLADEVCP", "NGCGUI", "EMBED_TAB_NAME", "EMBED_TAB_COMMAND", "HALFILE", "POSTGUI_HALFILE") 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 source_records() -> list[dict[str, Any]]: records: list[dict[str, Any]] = [] for relative in SOURCE_FILES: path = ROOT / relative records.append({ "path": relative.as_posix(), "bytes": path.stat().st_size if path.is_file() else None, "sha256": sha256(path) if path.is_file() else None, "status": "indexed" if path.is_file() else "missing", }) return records def parse_axis_tcl() -> dict[str, Any]: text = (ROOT / AXIS_TCL).read_text(encoding="utf-8") menu_items = re.findall(r"setup_menu_accel\s+[^\n]+\s+\[_\s+([^\n]+)", text) widgets = re.findall(r"^(?:Button|button|checkbutton|radiobutton|combobox|scale|label|text|listbox)\s+([^\s\\]+)", text, re.MULTILINE) notebooks = re.findall(r"NoteBook\s+([^\s\\]+)", text) procedures = re.findall(r"^proc\s+([^\s{]+)", text, re.MULTILINE) bindings = re.findall(r"^bind\s+[^\s]+\s+<([^>]+)>", text, re.MULTILINE) toolbar_assets = re.findall(r"\[load_image\s+([^\]\s]+)", text) return { "menu_accelerator_entries": len(menu_items), "menu_labels_sample": menu_items[:12], "widget_declarations": len(widgets), "widget_names_sample": widgets[:24], "notebooks": notebooks, "procedures": procedures, "procedure_count": len(procedures), "bindings": sorted(set(bindings)), "binding_count": len(set(bindings)), "toolbar_load_image_names": toolbar_assets, "toolbar_load_image_count": len(toolbar_assets), "line_count": len(text.splitlines()), } def asset_records() -> list[dict[str, Any]]: result: list[dict[str, Any]] = [] names = list(dict.fromkeys((*TOOLBAR_ASSETS, *SUPPORT_ASSETS))) for name in names: path = ROOT / IMAGE_ROOT / name result.append({ "name": name, "role": "toolbar" if name in TOOLBAR_ASSETS else "support", "path": (IMAGE_ROOT / name).as_posix(), "bytes": path.stat().st_size if path.is_file() else None, "sha256": sha256(path) if path.is_file() else None, "status": "indexed" if path.is_file() else "missing", }) return result def case_records() -> list[dict[str, Any]]: records: list[dict[str, Any]] = [] root = ROOT / CASE_ROOT for path in sorted(root.rglob("*.ini")): declarations: dict[str, str] = {} section = "" for raw in path.read_text(encoding="utf-8", errors="replace").splitlines(): line = raw.split("#", 1)[0].strip() if not line: continue if line.startswith("[") and line.endswith("]"): section = line[1:-1].strip().upper() continue if "=" not in line or section != "DISPLAY": continue key, value = (part.strip() for part in line.split("=", 1)) key = key.upper() if key in DECLARATION_KEYS: declarations[key] = value records.append({ "path": path.relative_to(ROOT).as_posix(), "family": "axis", "source_sha256": sha256(path), "declarations": declarations, "status": "indexed", }) return records def generate() -> dict[str, Any]: sources = source_records() assets = asset_records() cases = case_records() axis = parse_axis_tcl() report: dict[str, Any] = { "schema": "axis-source-inventory.v1", "task_id": "AXIS-INV-01", "context_id": CONTEXT_ID, "status": "verified" if all(row["status"] == "indexed" for row in sources + assets + cases) else "blocked", "implementation_status": "source_frozen_and_inventory", "completed_tasks": ["AXIS-INV-01", "AXIS-INV-02", "AXIS-INV-03", "AXIS-INV-04"], "source": sources, "assets": assets, "cases": cases, "axis_tcl": axis, "anchors": [ {"id": "menus", "path": AXIS_TCL.as_posix(), "lines": "33-482"}, {"id": "toolbar", "path": AXIS_TCL.as_posix(), "lines": "484-787"}, {"id": "notebooks", "path": AXIS_TCL.as_posix(), "lines": "790-1312"}, {"id": "status_and_program", "path": AXIS_TCL.as_posix(), "lines": "1314-1709"}, {"id": "state_gating", "path": AXIS_TCL.as_posix(), "lines": "1715-1887"}, {"id": "keyboard_and_defaults", "path": AXIS_TCL.as_posix(), "lines": "1941-2048"}, {"id": "dynamic_help", "path": AXIS_TCL.as_posix(), "lines": "2250-2270"}, ], "checks": { "source_paths": {"status": "verified" if all(row["status"] == "indexed" for row in sources) else "blocked"}, "toolbar_assets": {"status": "verified" if all(row["status"] == "indexed" for row in assets if row["role"] == "toolbar") else "blocked", "expected": len(TOOLBAR_ASSETS), "indexed": sum(row["status"] == "indexed" and row["role"] == "toolbar" for row in assets)}, "support_assets": {"status": "verified" if all(row["status"] == "indexed" for row in assets if row["role"] == "support") else "blocked", "expected": len(SUPPORT_ASSETS), "indexed": sum(row["status"] == "indexed" and row["role"] == "support" for row in assets)}, "case_paths": {"status": "verified" if cases and all(row["status"] == "indexed" for row in cases) else "blocked", "count": len(cases), "expected_baseline": 115}, "case_display_declarations": {"status": "verified" if cases and all(row["declarations"].get("DISPLAY", "axis").lower() in {"axis", "profile_axis"} for row in cases) else "blocked", "allowed": ["axis", "profile_axis"]}, "source_line_count": {"status": "verified" if axis["line_count"] == 2300 else "blocked", "expected": 2300, "actual": axis["line_count"]}, "toolbar_names_declared": {"status": "verified" if all(name.removesuffix(".gif").removesuffix(".png") in axis["toolbar_load_image_names"] for name in TOOLBAR_ASSETS if name != "tool_axis_y2.png") else "blocked"}, }, "boundary": "inventory_only; no runtime or protocol mutation", } 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__": result = generate() print(f"axis_source_inventory={result['status']} sources={len(result['source'])} assets={len(result['assets'])} report_sha256={result['report_sha256']}")