#!/usr/bin/env python3 """Generate the implementation-first Web capability registry. The registry counts user-callable Worker commands, not Python source symbols. It is intentionally static analysis: runtime validation belongs to the deferred validation phase or to a focused issue raised during actual use. """ from __future__ import annotations import ast import argparse import json import re import subprocess from collections import Counter from datetime import datetime, timezone from functools import lru_cache from pathlib import Path from generate_web_workbench_inventory import cpp_gui_commands ROOT = Path(__file__).resolve().parents[1] REGISTRY_SOURCE = ROOT / "web/src/module-registry.generated.ts" WORKBENCH_SOURCE = ROOT / "web/src/workbench-shell.tsx" ARTIFACT_MANIFEST = ROOT / "web/public/modules/manifest.json" WORKBENCH_MATRIX = ROOT / "docs/p6/workbench-command-matrix.json" OUTPUT = ROOT / "docs/current/feature-capability-registry.json" ROUTE_OUTPUT = ROOT / "web/public/api/desktop-command-routes.json" PROBE_ROOT = ROOT / "freecad-source/src/WebAssembly/FreeCADCoreProbe" CACHE_PATH = ROOT / "docs/current/feature-capability-cache.json" def file_stamps(paths: set[Path]) -> dict: result = {} for path in sorted(paths): try: info = path.stat() result[str(path)] = [info.st_size, info.st_mtime_ns, info.st_ctime_ns] except FileNotFoundError: result[str(path)] = None return result def generation_inputs(command_matrix: dict) -> set[Path]: paths = {Path(__file__).resolve(), ROOT / "tools/generate_web_workbench_inventory.py", REGISTRY_SOURCE, WORKBENCH_SOURCE, ARTIFACT_MANIFEST, WORKBENCH_MATRIX, ROOT / "web/src/native-source-policy.ts", ROOT / "web/tools/audit-native-route-policy.mjs"} paths.update(ROOT / "web/src" / name for name in ("dock-overlay-commands.json", "dock-overlay-adapter.ts", "dock-overlay-host.tsx", "dock-overlay.css", "desktop-command-dispatcher.ts")) paths.update(ROOT / "web/src" / name for name in ("scene-display-commands.json", "scene-command-adapter.ts", "scene-command-host.ts", "scene-command-host.css", "scene-overlap-picker.ts", "scene-camera-commands.ts", "import-render-bridge.ts", "openscad-desktop-view.tsx")) paths.update(ROOT / "web/src" / name for name in ("test-desktop-routes.json", "test-mdi-host.tsx", "test-desktop-output.tsx")) paths.update(ROOT / "web/src" / name for name in ("document-view-routes.json", "web-view-task.tsx", "web-owner-standard-adapter.ts", "web-standard-command-contract.ts", "web-standard-task.tsx", "web-website-adapter.ts", "web-website-task.tsx", "web-group-contract.ts", "web-group-adapter.ts", "web-group-task.tsx", "web-link-contract.ts", "web-source-test-adapter.ts", "bim-relationship-task.tsx", "web-structure-adapter.ts", "web-structure-contract.ts", "web-structure-task.tsx", "web-link-mutation-task.tsx", "bim-windows-task.tsx", "bim-windows-contract.ts", "web-open-adapter.ts", "web-open-contract.ts", "web-open-task.tsx")) for pattern in ("*DesktopCommands.cpp", "*Probe.cpp", "*InventoryApi.cpp", "*.h", "*.hpp"): paths.update(PROBE_ROOT.glob(pattern)) for pattern in ("*-facade.ts", "*-desktop-adapter.ts", "*.worker.ts"): paths.update((ROOT / "web/src").glob(pattern)) paths.update(ROOT / "freecad-source" / row["sourcePath"] for row in command_matrix.get("records", []) if row.get("sourcePath")) return paths ROW_RE = re.compile( r'\{ module: "([^"]+)", workers: \[(.*?)\], primaryWorker: (.*?), ' r'commands: \[(.*?)\], objectTypeCount: (\d+), pythonApiCount: (\d+), ' r'guiDependency: (true|false), formatExtensions: \[(.*?)\] \},' ) PAYLOAD_RE = re.compile(r'^\s*"([^"]+:[^"]+)"\s*:', re.MULTILINE) NATIVE_DESKTOP_COMMAND_RE = re.compile(r'FC_WEB_DESKTOP_COMMAND\("([^"]+)"\s*,') IMPLEMENTED_DESKTOP_COMMAND_RE = re.compile(r'FC_WEB_IMPLEMENTED_DESKTOP_COMMAND\("([^"]+)"\)') NATIVE_DESKTOP_LIST_RE = re.compile(r'(?:desktopCommands|family\s*==\s*QStringLiteral\("desktop-command"\).*?QStringList\s+\w+)\s*=\s*\{(.*?)\};', re.DOTALL) QSTRING_RE = re.compile(r'QStringLiteral\("([^"]+)"\)') # A prefix branch alone is not proof that every command has its own source # semantics. Only explicitly audited family handlers may claim an entire # workbench command family; unmarked branches stay implementation_required. GENERIC_DESKTOP_FAMILY_RE = re.compile(r'FC_WEB_AUDITED_DESKTOP_FAMILY\("([^"]+)"\)') def quoted_values(value: str) -> list[str]: return re.findall(r'"([^"]+)"', value) def canonical_probe_module(stem: str, known_modules: set[str]) -> str: normalized_stem = re.sub(r"[^a-z0-9]", "", stem.lower()) candidates = [ module for module in known_modules if normalized_stem.startswith(re.sub(r"[^a-z0-9]", "", module.lower())) ] if candidates: return max(candidates, key=lambda module: len(re.sub(r"[^a-z0-9]", "", module.lower()))) module = stem for suffix in ("RuntimeProbe", "KernelProbe", "Probe"): if module.endswith(suffix): return module.removesuffix(suffix) return module def bim_external_url_commands(workbench_text: str) -> set[str]: """Discover the audited URL-only entries in BIM's executable script table. Document/geometry entries still require their own implementation evidence. The table is consumed by the real dispatcher; adding a name alone is not enough. """ registry = PROBE_ROOT / "BIMDesktopCommandRegistry.h" scripts = PROBE_ROOT / "BIMDesktopCommands.h" probe = PROBE_ROOT / "BimRuntimeProbe.cpp" adapter = ROOT / "web/src/bim-desktop-adapter.ts" if not all(path.is_file() for path in (registry, scripts, probe, adapter)): return set() registry_text = registry.read_text(encoding="utf-8") probe_text = probe.read_text(encoding="utf-8") if not all(token in registry_text for token in ( '#include "BIMDesktopCommands.h"', 'std::strcmp(name, command.name)', 'return &command;', 'return command ? command->route : BimDesktopCommandRoute::Unhandled;')): return set() if not all(token in probe_text for token in ( '#include "BIMDesktopCommandRegistry.h"', 'bimDesktopCommandImplementation(nameValue.c_str())', 'bimDesktopCommandRoute(nameValue.c_str()) == BimDesktopCommandRoute::ExternalUrl', 'std::string(implementation->script)', 'runGeometryValueScript(commandScript.c_str(), commandArgs)', 'if (executed)', 'familyResultJson', '\\"status\\":\\"exposed\\"')): return set() if 'bimExternalUrl(name, response)' not in workbench_text or ' set[str]: """Require an explicit marker and a command-specific exposed branch.""" implemented: set[str] = set() def exposed_body(body: str) -> bool: """Recognize source-authored exposed status in compact JSON emitters.""" return bool( re.search( r'(?:result|record|commandResult|out|response)\.insert\(\s*(?:QStringLiteral\(\s*)?"status"\s*\)?\s*,\s*' r'(?:QStringLiteral\(\s*)?"exposed"\s*\)?\s*\)', body, ) or re.search(r'\bstatus\s*=\s*(?:QStringLiteral\(\s*)?["\']exposed["\']\s*\)?', body) or re.search(r'["\']status["\']\s*:\s*["\']exposed["\']', body) # recordsJson emitters escape the JSON quotes in the C++ string. or re.search(r'\\?"status\\?"\s*:\s*\\?"exposed\\?"', body) ) def branch_body(condition: str) -> str | None: match = re.search(rf'(?:else\s+)?if\s*\([^{{}};]*{condition}[^{{}};]*\)\s*\{{', text) if not match: return None opening = text.find("{", match.start()) depth = 0 for index in range(opening, len(text)): if text[index] == "{": depth += 1 elif text[index] == "}": depth -= 1 if depth == 0: return text[opening:index + 1] return None for command in IMPLEMENTED_DESKTOP_COMMAND_RE.findall(text): condition = ( rf'(?:FC_WEB_DESKTOP_COMMAND\("{re.escape(command)}"\)|' rf'(?:name|nameValue|command|commandName|desktopName)\s*==\s*' rf'(?:QStringLiteral\("{re.escape(command)}"\)|"{re.escape(command)}")|' rf'(?:apiId|apiIdValue)\s*==\s*' rf'(?:QStringLiteral\("[A-Za-z0-9_]+:desktop:{re.escape(command)}"\)|' rf'"[A-Za-z0-9_]+:desktop:{re.escape(command)}"))' ) branches = re.finditer(rf'(?:else\s+)?if\s*\([^{{}};]*{condition}[^{{}};]*\)\s*\{{', text) for branch in branches: opening = text.find("{", branch.start()) depth = 0 closing = None for index in range(opening, len(text)): if text[index] == "{": depth += 1 elif text[index] == "}": depth -= 1 if depth == 0: closing = index break if closing is None: continue body = text[opening:closing + 1] explicit_status = exposed_body(body) # Some native probes emit the structured JSON through an # ostringstream rather than QJsonObject::insert. Accept that # source-authored form only when the same command-specific branch # explicitly assigns status=exposed; do not accept a shared tail # or a generic marker alone. explicit_status = explicit_status or re.search( r'\bstatus\s*=\s*(?:QStringLiteral\(\s*)?["\']exposed["\']\s*\)?', body, ) or re.search(r'["\']status["\']\s*:\s*["\']exposed["\']', body) # Native adapters may centralize document creation, recompute and # status reporting in a command-specific helper. A direct return # from the marked branch is executable behavior; requiring a # duplicated status insertion here incorrectly stalls the batch. explicit_status = explicit_status or re.search( r'\breturn\s+(?:executeObjectCommand|executeDocumentCommand|executeAnalysisCommand|finishDocumentCommand)\s*\(', body ) if explicit_status: implemented.add(command) break if command in implemented: continue # Compact native dispatchers often use a single-statement branch: # `if (command == ...) return executeObjectCommand(...)`. It still # executes the marked command-specific FreeCAD path and the helper # owns the exposed status, so do not classify it as a stalled marker. single_statement = re.search( rf'if\s*\([^{{}};]*(?:name|nameValue|command|commandName|desktopName)\s*==\s*' rf'(?:QStringLiteral\("{re.escape(command)}"\)|"{re.escape(command)}")[^{{}};]*\)\s*' rf'return\s+(?:executeObjectCommand|executeDocumentCommand|executeAnalysisCommand|finishDocumentCommand)\s*\(', text, ) if single_statement: implemented.add(command) continue # A native dispatcher may normalize the command into a boolean first # (`const joints = apiId == "Module:desktop:Command"`) and dispatch # through a shared branch. The marker plus the exact canonical # comparison and an exposed status in that dispatcher is sufficient; # it is still command-specific and cannot match a generic marker. canonical = re.search( rf'(?:apiId|apiIdValue)\s*==\s*(?:QStringLiteral\("[A-Za-z0-9_]+:desktop:{re.escape(command)}"\)|"[A-Za-z0-9_]+:desktop:{re.escape(command)}")', text, ) if canonical and exposed_body(text): implemented.add(command) continue # BIM's native dispatcher normalizes a group of exact command names # into a boolean alias (`const bool nudge = nameValue == ...`) and # emits one verified exposed result from that bounded branch. Match # the exact command comparison and the alias branch; a marker alone # still cannot make an unrelated command routable. name_condition = rf'(?:name|nameValue|command|commandName|desktopName)\s*==\s*(?:QStringLiteral\("{re.escape(command)}"\)|"{re.escape(command)}")' aliases = re.findall(rf'\b(?:const\s+)?bool\s+(\w+)\s*=\s*[^;{{}}]*{name_condition}[^;{{}}]*;', text) if any((body := branch_body(rf'\b{re.escape(alias)}\b')) and exposed_body(body) for alias in aliases): implemented.add(command) continue # Draft snap modes share one bounded native branch keyed by # `snapIndex >= 0`; each marked mode is still an exact command in the # source's snap-name table and receives the branch's exposed state. if command.startswith("Draft_Snap_"): snap_branch = branch_body(r'\bsnapIndex\s*>=\s*0') if snap_branch and exposed_body(snap_branch): implemented.add(command) return implemented def category(command: str) -> str: lowered = command.lower() if "save" in lowered or "reopen" in lowered: return "persistence" if "import" in lowered or "export" in lowered: return "format" if "python" in lowered: return "python" if "inspect" in lowered or "inventory" in lowered: return "inspect" if "create" in lowered or "recompute" in lowered: return "modeling" return "service" def describe_native_gaps(records: list[dict], rejected_markers: dict[str, set[str]]) -> dict[str, int]: """Provide bounded source evidence without promoting textual matches to routes.""" pending = [row for row in records if any(gap.get("stage") == "native" for gap in row.get("implementationGaps", []))] commands = {str(row["command"]) for row in pending} locations: dict[str, list[dict]] = {command: [] for command in commands} # Exact quoted names include embedded Python dispatches and C++ tables. # A match is only a navigation anchor, never proof that the command works. literal = re.compile(r'''["']([A-Za-z][A-Za-z0-9_]*)["']''') for path in sorted(PROBE_ROOT.iterdir()): if not path.is_file() or path.suffix not in {".cpp", ".h", ".hpp"}: continue for number, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), 1): for command in set(literal.findall(line)) & commands: candidates = locations[command] if len(candidates) < 6: candidates.append({"path": str(path.relative_to(ROOT)), "line": number}) counts = Counter() for row in pending: candidates = locations[str(row["command"])] rejected = row["command"] in rejected_markers.get(row["module"], set()) kind = "registration-marker-rejected" if rejected else "native-source-candidate-unregistered" if candidates else "native-entry-not-located-in-probe-directory" counts[kind] += 1 for gap in row["implementationGaps"]: if gap.get("stage") != "native": continue gap.update(classification=kind, businessRequirement="retained", sourceCandidates=candidates, evidenceScope="Static registration discovery and exact source-name anchors only; not proof of implementation completeness or business necessity.", repair=("Inspect the existing native branch/helper at sourceCandidates and connect its executable registration; implement missing behavior before registering." if candidates else "Locate the native entry using the supplied desktop source and build ownership; absence here does not establish absent local source or unnecessary business.")) return dict(counts) @lru_cache(maxsize=64) def python_command_groups(path: Path) -> set[str]: try: tree = ast.parse(path.read_text(encoding="utf-8", errors="replace")) except (OSError, SyntaxError): return set() group_classes = { node.name for node in ast.walk(tree) if isinstance(node, ast.ClassDef) and any(isinstance(member, (ast.FunctionDef, ast.AsyncFunctionDef)) and member.name == "GetCommands" for member in node.body) } commands: set[str] = set() for node in ast.walk(tree): if not isinstance(node, ast.Call) or len(node.args) < 2: continue function_name = node.func.attr if isinstance(node.func, ast.Attribute) else node.func.id if isinstance(node.func, ast.Name) else "" if function_name != "addCommand" or not isinstance(node.args[0], ast.Constant) or not isinstance(node.args[0].value, str): continue factory = node.args[1] if not isinstance(factory, ast.Call): continue class_name = factory.func.id if isinstance(factory.func, ast.Name) else factory.func.attr if isinstance(factory.func, ast.Attribute) else "" if class_name in group_classes: commands.add(node.args[0].value) return commands def is_ui_container(row: dict[str, object]) -> bool: command = str(row.get("command") or "") if command == "Separator": return True source_path = str(row.get("sourcePath") or "") if source_path.endswith(".py"): return command in python_command_groups(ROOT / "freecad-source" / source_path) if not source_path.endswith("/Workbench.cpp"): return False path = ROOT / "freecad-source" / source_path try: lines = path.read_text(encoding="utf-8", errors="replace").splitlines() except OSError: return False source_line = max(1, int(row.get("sourceLine") or 1)) search_start = max(0, source_line - 4) search_end = min(len(lines), source_line + 3) variable = "" for line in lines[search_start:search_end]: match = re.search(r'([A-Za-z_]\w*)->setCommand\("' + re.escape(command) + r'"\)', line) if match: variable = match.group(1) break if not variable: return False tail = "\n".join(lines[source_line:min(len(lines), source_line + 24)]) return bool( re.search(rf"\*\s*{re.escape(variable)}\s*<<", tail) or re.search(rf"\badd[A-Za-z0-9_]+\s*\(\s*\*\s*{re.escape(variable)}\s*\)", tail) ) def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--force", action="store_true", help="regenerate even when all inputs and outputs match the cache") options = parser.parse_args(argv) command_matrix = json.loads(WORKBENCH_MATRIX.read_text(encoding="utf-8")) inputs = file_stamps(generation_inputs(command_matrix)) try: cached = json.loads(CACHE_PATH.read_text(encoding="utf-8")) except (OSError, ValueError): cached = {} if (not options.force and cached.get("schema") == "freecad-web-feature-capability-cache/v1" and cached.get("inputs") == inputs and OUTPUT.is_file() and ROUTE_OUTPUT.is_file() and cached.get("outputs") == file_stamps({OUTPUT, ROUTE_OUTPUT})): print(json.dumps({"status": "reused", **cached.get("counts", {})})) return 0 registry_text = REGISTRY_SOURCE.read_text(encoding="utf-8") workbench_text = WORKBENCH_SOURCE.read_text(encoding="utf-8") artifact_payload = json.loads(ARTIFACT_MANIFEST.read_text(encoding="utf-8")) known_modules = { str(row.get("module")) for row in command_matrix.get("records", []) if isinstance(row, dict) and row.get("module") } desktop_keys = { (str(row.get("module")), str(row.get("command"))) for row in command_matrix.get("records", []) if isinstance(row, dict) and row.get("kind") == "desktop-gui" } artifact_modules = artifact_payload.get("modules", {}) payloads = set(PAYLOAD_RE.findall(workbench_text)) generic_command_entry = 'id="payload-editor"' in workbench_text and "JSON.parse(payloadText)" in workbench_text native_desktop_commands: dict[str, set[str]] = {} native_desktop_sources: dict[str, str] = {} native_desktop_families: set[str] = set() rejected_desktop_markers: dict[str, set[str]] = {} for path in sorted(PROBE_ROOT.glob("*DesktopCommands.cpp")): module = path.stem.removesuffix("DesktopCommands") text = path.read_text(encoding="utf-8", errors="replace") marked_commands = set(IMPLEMENTED_DESKTOP_COMMAND_RE.findall(text)) implemented_commands = implemented_desktop_commands(text) native_desktop_commands[module] = set(NATIVE_DESKTOP_COMMAND_RE.findall(text)) | implemented_commands rejected = marked_commands - implemented_commands if rejected: rejected_desktop_markers.setdefault(module, set()).update(rejected) native_desktop_sources[module] = str(path.relative_to(ROOT)) for path in sorted(PROBE_ROOT.glob("*Probe.cpp")): module = canonical_probe_module(path.stem, known_modules) text = path.read_text(encoding="utf-8", errors="replace") for registered_module in GENERIC_DESKTOP_FAMILY_RE.findall(text): native_desktop_families.add(registered_module) native_desktop_sources.setdefault(registered_module, str(path.relative_to(ROOT))) marked_commands = set(IMPLEMENTED_DESKTOP_COMMAND_RE.findall(text)) implemented_commands = implemented_desktop_commands(text) rejected = marked_commands - implemented_commands if rejected: rejected_desktop_markers.setdefault(module, set()).update(rejected) if implemented_commands: native_desktop_commands.setdefault(module, set()).update(implemented_commands) native_desktop_sources.setdefault(module, str(path.relative_to(ROOT))) commands = { command for block in NATIVE_DESKTOP_LIST_RE.findall(text) for command in QSTRING_RE.findall(block) } if commands: native_desktop_commands.setdefault(module, set()).update(commands) native_desktop_sources.setdefault(module, str(path.relative_to(ROOT))) # Some targets keep the active dispatcher in a bounded *InventoryApi.cpp # translation unit rather than in the conventional *Probe.cpp file. It # is still native ownership: include only this probe-directory pattern so # registry discovery stays scoped and does not become a repository scan. for path in sorted(PROBE_ROOT.glob("*InventoryApi.cpp")): module = canonical_probe_module(path.stem, known_modules) text = path.read_text(encoding="utf-8", errors="replace") marked_commands = set(IMPLEMENTED_DESKTOP_COMMAND_RE.findall(text)) implemented_commands = implemented_desktop_commands(text) rejected = marked_commands - implemented_commands if rejected: rejected_desktop_markers.setdefault(module, set()).update(rejected) if implemented_commands: native_desktop_commands.setdefault(module, set()).update(implemented_commands) native_desktop_sources.setdefault(module, str(path.relative_to(ROOT))) bim_urls = bim_external_url_commands(workbench_text) if bim_urls: native_desktop_commands.setdefault("BIM", set()).update(bim_urls) native_desktop_sources.setdefault("BIM", str((PROBE_ROOT / "BimRuntimeProbe.cpp").relative_to(ROOT))) desktop_adapter_modules: set[str] = set() direct_desktop_workers: dict[str, str] = {} for module in set(native_desktop_commands) | native_desktop_families: stem = re.sub(r"[^a-z0-9]", "", module.lower()) facade_text = "\n".join( path.read_text(encoding="utf-8", errors="replace") for path in sorted((ROOT / "web/src").glob(f"{stem}*-facade.ts")) ) if "fc_web_dispatch_json" in facade_text and "pythonFamily" in facade_text: desktop_adapter_modules.add(module) for worker in sorted((ROOT / "web/src").glob(f"{stem}*.worker.ts")): worker_text = worker.read_text(encoding="utf-8", errors="replace") if "DesktopRequest" in worker_text and re.search(r"is[A-Za-z0-9]+DesktopRequest", worker_text): direct_desktop_workers[module] = worker.name break records: list[dict[str, object]] = [] modules: list[dict[str, object]] = [] module_rows: dict[str, dict[str, object]] = {} web_routes: dict[tuple[str, str], dict[str, object]] = {} for match in ROW_RE.finditer(registry_text): module, workers_text, primary_text, commands_text, objects, python_apis, gui, formats_text = match.groups() workers = quoted_values(workers_text) commands = quoted_values(commands_text) primary_values = quoted_values(primary_text) primary_worker = primary_values[0] if primary_values else None artifact_ready = module in artifact_modules interactive = 0 for command in commands: capability_id = f"{module}:{command}" payload_ready = capability_id in payloads generic_api_entry = "pythonFamilyBatch" in command or "pythonMethod" in command ui_status = "interactive" if payload_ready else "generic-api-entry" if generic_api_entry else "generic-command-entry" if generic_command_entry else "needs-parameter-ui" interactive += int(ui_status != "needs-parameter-ui") web_record = { "id": capability_id, "module": module, "command": command, "category": category(command), "worker": primary_worker, "workers": workers, "artifactReady": artifact_ready, "uiStatus": ui_status, "implementationStatus": "routed" if workers and artifact_ready else "missing-runtime-route", "validationStatus": "deferred", "implementationGaps": ([{"stage": "worker", "reason": "no Worker is registered for this module"}] if not workers else []) + ([{"stage": "artifact", "reason": "module artifact is absent from the manifest"}] if not artifact_ready else []), } web_routes[(module, command)] = web_record transport_only = command.endswith(".desktopCommand") if (module, command) not in desktop_keys and not transport_only: records.append(web_record) module_row = { "module": module, "commands": len(commands), "interactiveCommands": interactive, "needsParameterUi": len(commands) - interactive, "desktopCommands": 0, "implementationRequired": 0, "workers": workers, "artifactReady": artifact_ready, "objectTypeCount": int(objects), "sourcePythonSymbolCount": int(python_apis), "guiDependency": gui == "true", "formatExtensions": quoted_values(formats_text), } modules.append(module_row) module_rows[module] = module_row desktop_rows = [ row for row in command_matrix.get("records", []) if isinstance(row, dict) and row.get("kind") == "desktop-gui" ] @lru_cache(maxsize=None) def registered_lines(relative): path = ROOT / "freecad-source" / relative return {(name, line) for name, line, _, _ in cpp_gui_commands(path)} if path.is_file() else set() # Keep every existing capability, but prefer a real constructor over a # documentation example when multiple source anchors were inventoried. for index, row in enumerate(desktop_rows): source = str(row.get("sourcePath", "")) if not source.endswith((".cpp", ".cxx", ".cc", ".h", ".hpp")): continue identity = (row.get("command"), row.get("sourceLine")) if identity in registered_lines(source): continue for surface in row.get("uiSurfaces", []): path = str(surface.get("path", "")) if (surface.get("kind") == "registration" and path.endswith((".cpp", ".cxx", ".cc", ".h", ".hpp")) and (row.get("command"), surface.get("line")) in registered_lines(path)): desktop_rows[index] = {**row, "sourcePath": path, "sourceLine": surface["line"]} break def family_route(module: str) -> dict[str, object] | None: return next( ( route for (route_module, _), route in web_routes.items() if route_module == module and ( str(route.get("command", "")).endswith(".pythonFamilyBatch") or str(route.get("command", "")).endswith(".pythonFamily") ) ), None, ) def local_route(module: str, command: str) -> dict[str, object] | None: if module == "Web" and command in {"Std_Test5", "Std_Test6", "Std_TestCmdFuncs"}: route = web_routes.get(("Part", "part.viewCommand")) adapter = ROOT / "web/src/web-source-test-adapter.ts" native = PROBE_ROOT / "WebSourceTestCommands.h" if route and adapter.is_file() and native.is_file() and command in native.read_text(encoding="utf-8") and "executeSourceTest" in adapter.read_text(encoding="utf-8"): return {**route, "worker": "part.worker.ts", "executionModule": "Part", "transport": "direct-command", "canonicalId": f"Web:desktop:{command}", "nativeRegistration": str(native.relative_to(ROOT))} if module == "Web" and command in {"Std_DownloadOnlineHelp", "Std_RandomColor", "Std_Test5", "Std_Test6", "Std_TestCmdFuncs", "Std_ViewUndock", "Std_ViewVR"}: adapter = ROOT / "web/src/web-desktop-adapter.ts" native = PROBE_ROOT / ("WebSourceTestCommands.h" if command.startswith("Std_Test") else "WebViewCommands.h") route = web_routes.get(("Part", "part.viewCommand")) if command == "Std_DownloadOnlineHelp": route = web_routes.get(("Web", "web.standardCommand")) or route if route and adapter.is_file() and native.is_file(): at = adapter.read_text(encoding="utf-8"); nt = native.read_text(encoding="utf-8") if ((command == "Std_DownloadOnlineHelp" and "downloadOnlineHelp(args)" in at) or (command == "Std_RandomColor" and "Std_RandomColor" in at and "part.pythonFamilyBatch" in at) or (command.startswith("Std_Test") and command in nt and "executeSourceTest" in at) or (command in {"Std_ViewUndock", "Std_ViewVR"} and command in nt and "executeWebDesktopCommand" in at)): return {**route, "worker": route.get("worker") or "part.worker.ts", "executionModule": route.get("module") or "Part", "transport": "direct-command", "canonicalId": f"Web:desktop:{command}", "nativeRegistration": str(native.relative_to(ROOT))} if module == "Web" and command in {"Std_MacroStepInto", "Std_MacroStepOver", "Std_MacroStopDebug", "Std_OpenMacrosFolder", "Std_ToggleBreakpoint"}: macro = PROBE_ROOT / "WebMacroCommands.h" adapter = ROOT / "web/src/web-desktop-adapter.ts" facade = ROOT / "web/src/part-python-family-facade.ts" worker = ROOT / "web/src/part.worker.ts" route = web_routes.get(("Part", "part.pythonFamilyBatch")) if route and all(path.is_file() for path in (macro, adapter, facade, worker)): mt = macro.read_text(encoding="utf-8") if (command in mt and command in adapter.read_text(encoding="utf-8") and "dispatchPartPythonFamily" in worker.read_text(encoding="utf-8") and "PART_PYTHON_FAMILY_COMMAND" in facade.read_text(encoding="utf-8")): return {**route, "worker": "part.worker.ts", "executionModule": "Part", "transport": "family-batch", "canonicalId": f"Web:desktop:{command}", "nativeRegistration": str(macro.relative_to(ROOT))} if module == "Web" and command in native_desktop_commands.get(module, set()): catalog = ROOT / "web/src/web-macro-routes.json" if catalog.is_file(): endpoint = json.loads(catalog.read_text(encoding="utf-8")).get(command) route = web_routes.get(("Part", endpoint)) if isinstance(endpoint, str) else None files = { "adapter": ROOT / "web/src/web-desktop-adapter.ts", "facade": ROOT / "web/src/part-python-family-facade.ts", "worker": ROOT / "web/src/part.worker.ts", "dispatcher": ROOT / "web/src/desktop-command-dispatcher.ts", "native": PROBE_ROOT / "PartBoxProbe.cpp", "macro": PROBE_ROOT / "WebMacroCommands.h", "recording": PROBE_ROOT / "WebMacroRecording.h", } if route and all(path.is_file() for path in files.values()): texts = {key: path.read_text(encoding="utf-8") for key, path in files.items()} # Macro requests use the live Part owner, not the standalone # Web runtime. Resolve the same endpoint used by the adapter. required = { "adapter": ('./web-macro-routes.json', 'macroRoutes[command as keyof typeof macroRoutes]', 'router.request("Part", endpoint', 'desktop-command-commandmacro', 'documentId, storedFiles', 'freecad-macro-result'), "facade": (f'PART_PYTHON_FAMILY_COMMAND = "{endpoint}"', 'fc_web_dispatch_json', 'payload.records'), "worker": ('FreeCADWebPartBoxProbe.js', 'isPartPythonFamilyRequest(message)', 'record.family === "desktop-command"', 'dispatchPartPythonFamily(queue, message, source)'), "dispatcher": ('executeWebDesktop(this.router, record.command, args, documentId, applySelection)',), "native": ('desktop-command-commandmacro', f'QStringLiteral("{command}")', 'executeWebMacroCommand(request.constData(), nativeOutput)'), "macro": (f'"{command}"', 'exec(compile(source, path,', 'debugpy.connect((host, port))', 'recorder.recording', 'App.setActiveDocument(document)'), "recording": ('webMacroDispatch(request)', 'PyList_Append(lines, text)'), } if (all(all(token in texts[key] for token in tokens) for key, tokens in required.items()) and command in workbench_text): return {**route, "worker": "part.worker.ts", "executionModule": "Part", "transport": "family-batch", "canonicalId": f"Web:desktop:{command}", "nativeRegistration": str(files["native"].relative_to(ROOT))} if module == "Test" and command in native_desktop_commands.get(module, set()): catalog = ROOT / "web/src/test-desktop-routes.json" adapter = ROOT / "web/src/test-desktop-adapter.ts" facade = ROOT / "web/src/test-facade.ts" worker = ROOT / "web/src/test.worker.ts" dispatcher = ROOT / "web/src/desktop-command-dispatcher.ts" if all(path.is_file() for path in (catalog, adapter, facade, worker, dispatcher)): endpoint = json.loads(catalog.read_text(encoding="utf-8")).get(command) route = web_routes.get((module, endpoint)) mdi_host = ROOT / "web/src/test-mdi-host.tsx" mdi_connected = not command.startswith("Std_MDITest") or (mdi_host.is_file() and "executeTestDesktopWithHost" in workbench_text and "script' in runtime_text and 'dispatchBimPythonFamilyBatch(queue, message, kernel)' in worker.read_text(encoding="utf-8") and 'fc_web_dispatch_json' in facade.read_text(encoding="utf-8")): return {**route, "worker": "bim.worker.ts", "executionModule": "BIM", "transport": "family-batch", "canonicalId": f"BIM:desktop:{command}", "nativeRegistration": str(registry.relative_to(ROOT))} if module in {"Web", "Std"}: if command in {"Std_Test1", "Std_Test2", "Std_Test3", "Std_Test4"}: catalog = ROOT / "web/src/document-view-routes.json" files = { "desktop": ROOT / "web/src/web-desktop-adapter.ts", "adapter": ROOT / "web/src/web-owner-standard-adapter.ts", "contract": ROOT / "web/src/web-standard-command-contract.ts", "facade": ROOT / "web/src/part-view-facade.ts", "worker": ROOT / "web/src/part.worker.ts", "bridge": PROBE_ROOT / "NativeDocumentView.h", "native": PROBE_ROOT / "WebSourceTestCommands.h", } if catalog.is_file() and all(path.is_file() for path in files.values()): routes = json.loads(catalog.read_text(encoding="utf-8")) owner = routes.get("owners", {}).get("PartBox", {}) route = web_routes.get(("Part", owner.get("endpoint"))) texts = {key: path.read_text(encoding="utf-8") for key, path in files.items()} required = { "desktop": (f'"{command}"', 'executeOwnerStandard(router, command, args, documentId)'), "adapter": ('routes.standardCommands.includes(command)', 'router.request("Part", viewEndpoint(owner)'), "contract": (f'"{command}"', 'result.changed === false'), "facade": ('isStandardCommand(p.command)', 'validStandardResult(reply.result, p.command, p.document)', 'fc_web_dispatch_json'), "worker": ('dispatchDocumentView(queue, message, kernel)', '/kernel/FreeCADWebPartBoxProbe.js'), "bridge": (f'"{command}"', 'executeWebSourceTestCommand(input.constData(), output)', 'completed.value("result").toObject()'), "native": (f'"{command}"', 'App::GetApplication().getDocument', '"document_not_found"'), } if (route and command in routes.get("standardCommands", []) and all(token in texts[key] for key, tokens in required.items() for token in tokens)): return {**route, "worker": "part.worker.ts", "executionModule": "Part", "transport": "direct-command", "canonicalId": f"Web:desktop:{command}", "nativeRegistration": str(files["bridge"].relative_to(ROOT))} if command in {f"Std_Test{i}" for i in range(1, 7)}: source = ROOT / "freecad-source/src/Gui/CommandTest.cpp" adapter = ROOT / "web/src/web-source-test-adapter.ts" desktop = ROOT / "web/src/web-desktop-adapter.ts" route = web_routes.get(("Part", "part.viewCommand")) if route and all(path.is_file() for path in (source, adapter, desktop)): number = command.removeprefix("Std_Test") body = re.search(r'void FCCmdTest' + number + r'::activated\(int iMsg\)\s*\{(.*?)\n\}', source.read_text(encoding="utf-8"), re.DOTALL) expected = "Q_UNUSED(iMsg);" if number in {"1", "2"} else "Q_UNUSED(iMsg); App::Document* pcDoc = getDocument(); if (!pcDoc) { return; }" adapter_text = adapter.read_text(encoding="utf-8") if (body and re.sub(r'\s+', '', body[1]) == re.sub(r'\s+', '', expected) and f'"{command}"' in adapter_text and 'operation: "source-no-op"' in adapter_text and 'router.request("Part", viewEndpoint(owner)' in adapter_text and 'executeSourceTest(router, command, args, documentId)' in desktop.read_text(encoding="utf-8")): return {**route, "worker": "part.worker.ts", "executionModule": "Part", "transport": "direct-command", "canonicalId": f"Web:desktop:{command}", "nativeRegistration": str((PROBE_ROOT / "NativeDocumentView.h").relative_to(ROOT))} catalog = ROOT / "web/src/document-view-routes.json" if catalog.is_file(): routes = json.loads(catalog.read_text(encoding="utf-8")) if command == "Std_DescriptionMode": owner = routes["owners"]["PartBox"] route = web_routes.get(("Part", owner["endpoint"])) adapter = ROOT / "web/src/web-desktop-adapter.ts" native = PROBE_ROOT / "WebViewCommands.h" if route and native.is_file() and adapter.is_file() and "'Std_DescriptionMode'" in native.read_text(encoding="utf-8") and 'executeOwnerStandard(router, command, args, documentId)' in adapter.read_text(encoding="utf-8"): return {**route, "worker": owner["worker"], "executionModule": "Part", "transport": "direct-command", "canonicalId": f"Web:desktop:{command}", "nativeRegistration": str(native.relative_to(ROOT))} if command in {"Std_WhatsThis", "Std_Workbench"} and command in routes.get("standardCommands", []): owner = routes.get("owners", {}).get("PartBox", {}) route = web_routes.get(("Part", owner.get("endpoint"))) files = { "desktop": ROOT / "web/src/web-desktop-adapter.ts", "adapter": ROOT / "web/src/web-owner-standard-adapter.ts", "contract": ROOT / "web/src/web-standard-command-contract.ts", "facade": ROOT / "web/src/part-view-facade.ts", "worker": ROOT / "web/src/part.worker.ts", "dispatcher": ROOT / "web/src/desktop-command-dispatcher.ts", "bridge": PROBE_ROOT / "NativeDocumentView.h", "native": PROBE_ROOT / "WebViewCommands.h", } if route and all(path.is_file() for path in files.values()): texts = {key: path.read_text(encoding="utf-8") for key, path in files.items()} required = { "desktop": (f'"{command}"', 'executeOwnerStandard(router, command, args, documentId)'), "adapter": ('routes.standardCommands.includes(command)', 'router.request("Part", viewEndpoint(owner)', 'schema: "freecad-web/document-view/v1", owner, document: documentId, command', 'validStandardResult(response.result, command, documentId)'), "contract": (f'"{command}"',), "facade": ('isStandardCommand(p.command)', 'validStandardResult(reply.result, p.command, p.document)', 'fc_web_dispatch_json'), "worker": ('dispatchDocumentView(queue, message, kernel)', '/kernel/FreeCADWebPartBoxProbe.js'), "dispatcher": ('executeWebDesktop(this.router, record.command',), "bridge": (f'"{command}"', 'executeDocumentCommand(input.constData(), output)'), "native": (f"'{command}'", 'elif name in gui_standard_names:', 'if not App.GuiUp:', 'native_command.isActive()'), } behavior = ('Gui.runCommand(name, mode if name == \'Std_UserEditMode\' else 0)',) mounted = True if command == "Std_Workbench": behavior = ('list(Gui.listWorkbenches())', "args.get('operation') == 'list'", 'Gui.activateWorkbench(workbench)', 'if active_workbench != workbench:') mounted = (all(token in texts["desktop"] for token in ( 'operation: "list"', 'freecad-native-workbench-picker', 'workbench: selected, operation: "activate"')) and all(token in workbench_text for token in ( 'window.addEventListener("freecad-native-workbench-picker", receive)', 'detail.select(select.value)', 'detail.select(undefined)'))) if (mounted and all(token in texts["native"] for token in behavior) and all(token in texts[key] for key, tokens in required.items() for token in tokens)): return {**route, "worker": owner["worker"], "executionModule": "Part", "transport": "direct-command", "canonicalId": f"Web:desktop:{command}", "nativeRegistration": str(files["bridge"].relative_to(ROOT))} if command in routes.get("commandFeatCommands", []): route = web_routes.get(("Part", "part.pythonFamilyBatch")) native = PROBE_ROOT / "WebMacroCommands.h" bridge = PROBE_ROOT / "PartBoxProbe.cpp" adapter = ROOT / "web/src/web-desktop-adapter.ts" behaviors = { "Std_Recompute": ("elif record['name'] == 'Std_Recompute':", "# CommandFeat.cpp intentionally leaves this command's activation empty.\n pass"), "Std_SendToPythonConsole": ("elif record['name'] == 'Std_SendToPythonConsole':", "len(grouped) != 1", "namespace['doc'] = doc", "namespace['obj'] = obj", "namespace['shp'] = getattr(obj, property_name)", "obj.getSubObject"), } if route and command in behaviors and all(path.is_file() for path in (native, bridge, adapter)): native_text = native.read_text(encoding="utf-8") bridge_text = bridge.read_text(encoding="utf-8") adapter_text = adapter.read_text(encoding="utf-8") if (all(token in native_text for token in behaviors[command]) and f'QStringLiteral("{command}")' in bridge_text and 'family == QStringLiteral("desktop-command-commandfeat")' in bridge_text and 'executeWebMacroCommand(request.constData(), nativeOutput)' in bridge_text and all(token in adapter_text for token in ( f'"{command}"', 'router.request("Part", "part.pythonFamilyBatch"', '"desktop-command-commandfeat"', 'args: { ...args, documentId }')) and (command != "Std_SendToPythonConsole" or 'executeWebDesktop(router, "Std_CommandLine", { runtimeOwner: "PartBox" }, documentId)' in adapter_text)): return {**route, "executionModule": "Part", "transport": "direct-command", "canonicalId": f"Web:desktop:{command}", "nativeRegistration": str(native.relative_to(ROOT))} if command in routes.get("standardCommands", []) and command in { "Std_RestartInSafeMode", "Std_TextDocument", "Std_UnitsCalculator", "Std_UserEditMode" }: owner = routes["owners"]["PartBox"] route = web_routes.get(("Part", owner["endpoint"])) paths = {name: ROOT / "web/src" / name for name in ( "web-desktop-adapter.ts", "web-standard-command-contract.ts", "web-standard-task.tsx", "desktop-command-dispatcher.ts", "part-view-facade.ts", "part.worker.ts", "part-save.worker.ts")} bridge = PROBE_ROOT / "NativeDocumentView.h" native = PROBE_ROOT / "WebViewCommands.h" if route and all(path.is_file() for path in [*paths.values(), bridge, native]): texts = {name: path.read_text(encoding="utf-8") for name, path in paths.items()} native_text = native.read_text(encoding="utf-8") # These commands share the document owner's standard-command # transport. GUI activation failures remain native failures. behavior = (("elif name == 'Std_TextDocument':", "App::TextDocument") if command == "Std_TextDocument" else ( "elif name in gui_standard_names:", "if not App.GuiUp:", "native_command.isActive()", "Gui.runCommand(name, mode if name == 'Std_UserEditMode' else 0)")) if (all(token in native_text for token in behavior) and f"'{command}'" in native_text and f'"{command}"' in bridge.read_text(encoding="utf-8") and "executeDocumentCommand(input.constData(), output)" in bridge.read_text(encoding="utf-8") and all(token in texts["web-desktop-adapter.ts"] for token in ( "recomputeCommand || isStandardCommand(command)", 'router.request("Part", viewEndpoint(owner)', 'schema: "freecad-web/document-view/v1", owner, document: documentId, command', "validStandardResult(response.result, command, documentId)", "freecad-web/document-view-batch/v1")) and "executeWebDesktop(this.router, record.command" in texts["desktop-command-dispatcher.ts"] and all("dispatchDocumentView" in texts[name] for name in ("part.worker.ts", "part-save.worker.ts")) and "validStandardResult" in texts["part-view-facade.ts"] and f'"{command}"' in texts["web-standard-command-contract.ts"] and f'command === "{command}"' in texts["web-standard-task.tsx"] and " item.name === command)' in texts["web-standard-task.tsx"]): return {**route, "worker": owner["worker"], "executionModule": "Part", "transport": "direct-command", "canonicalId": f"Web:desktop:{command}", "nativeRegistration": str(bridge.relative_to(ROOT))} if command in routes.get("commands", []): if command == "Std_Open": native = PROBE_ROOT / "WebOpenCommand.h" files = [ROOT / "web/src" / name for name in ("web-open-adapter.ts", "web-open-contract.ts", "web-open-task.tsx")] if not all(path.is_file() for path in [native, *files]) or 'setStatus(App::Document::SkipRecompute, checked)" if required_native not in native or f'"{command}"' not in task or ')', workbench_text)): return {**route, "worker": worker.name, "executionModule": "Web", "transport": "direct-command", "nativeRegistration": str(native.relative_to(ROOT))} if module == "Part" and command in {"Part_Box", "Part_Box2", "Part_Box3"}: route = web_routes.get((module, "part.document")) adapter = ROOT / "web/src/part-desktop-adapter.ts" if route is not None and "part-save.worker.ts" in route.get("workers", []) and adapter.is_file() and "executePartFixedBoxDesktop" in adapter.read_text(encoding="utf-8"): return {**route, "worker": "part-save.worker.ts", "executionModule": module, "transport": "direct-command", "nativeRegistration": "freecad-source/src/WebAssembly/FreeCADCoreProbe/DocumentProbe.cpp"} native_supported = command in native_desktop_commands.get(module, set()) if native_supported and module in direct_desktop_workers: exact = web_routes.get((module, command)) if exact is None and module not in desktop_adapter_modules: # A dedicated desktop facade/worker may intentionally expose # a command that is not duplicated in module-registry.generated # (Start is the canonical example). Native ownership plus the # typed direct worker is sufficient to synthesize the route; # do not require a second per-command registry entry. exact = { "id": f"{module}:{command}", "module": module, "command": command, "worker": direct_desktop_workers[module], "workers": [], "artifactReady": module in artifact_modules, "uiStatus": "generic-command-entry", "implementationStatus": "routed", "validationStatus": "deferred", } if exact is not None: return { **exact, "worker": direct_desktop_workers[module], "executionModule": module, "transport": "direct-command", "nativeRegistration": native_desktop_sources.get(module), } if native_supported and module in desktop_adapter_modules: route = family_route(module) if route is not None: return { **route, "executionModule": module, "transport": "family-batch", "nativeRegistration": native_desktop_sources.get(module), } return None local_routes: dict[str, dict[str, object]] = {} canonical_by_command: dict[str, tuple[dict[str, object], dict[str, object]]] = {} for row in desktop_rows: module = str(row.get("module") or "") command = str(row.get("command") or "") if is_ui_container(row): continue route = local_route(module, command) if route is None: continue if not isinstance(route.get("command"), str) or not route["command"].strip(): raise ValueError(f"Invalid generated route for {module}:{command}: missing endpoint") if route.get("transport") != "browser-ui" and not route.get("worker"): raise ValueError(f"Invalid generated route for {module}:{command}: missing worker") row_id = str(row.get("id") or f"{module}:desktop:{command}") local_routes[row_id] = route current = canonical_by_command.get(command) owner_hint = command.split("_", 1)[0] if "_" in command else "" if current is None or (module == owner_hint and str(current[0].get("module")) != owner_hint): canonical_by_command[command] = (row, route) desktop_route_records: list[dict[str, object]] = [] for row in desktop_rows: module = str(row.get("module") or "") command = str(row.get("command") or "") if not module or not command: continue record_id = str(row.get("id") or f"{module}:desktop:{command}") ui_container = is_ui_container(row) portable_route = local_routes.get(record_id) canonical = canonical_by_command.get(command) if "_" in command else None canonical_row = row if portable_route is None and canonical is not None: canonical_row, portable_route = canonical implementation_ready = ui_container or portable_route is not None execution_module = str(portable_route.get("executionModule") or module) if portable_route else module execution_family_route = family_route(execution_module) transport = str(portable_route.get("transport") or "family-batch") if portable_route else "family-batch" execution_route = portable_route or execution_family_route route_payload = ({ "module": module, "command": command, "worker": None, "nativeRegistration": None, "canonicalId": record_id, "transport": "ui-container", } if ui_container else { "module": execution_module, "command": execution_route.get("command") if execution_route else portable_route.get("command"), "worker": execution_route.get("worker") if execution_route else portable_route.get("worker"), "nativeRegistration": portable_route.get("nativeRegistration"), "canonicalId": portable_route.get("canonicalId", f"{execution_module}:desktop:{command}"), "transport": transport, **({"browserHandler": portable_route["browserHandler"], "nativeOperationRequired": False} if transport == "browser-ui" else {}), }) if implementation_ready and (ui_container or portable_route) else None records.append( { "id": record_id, "module": module, "command": command, "category": "navigation" if ui_container else category(command), "kind": "desktop-gui", "uiRole": "container" if ui_container else "command", "worker": route_payload.get("worker") if route_payload else None, "workers": module_rows.get(module, {}).get("workers", []), "artifactReady": module in artifact_modules, "uiStatus": "react-navigation" if ui_container else "generic-command-entry" if implementation_ready else "needs-react-workflow", "implementationStatus": "routed" if implementation_ready else "implementation_required", "validationStatus": "deferred", "sourcePath": row.get("sourcePath"), "sourceLine": row.get("sourceLine"), "uiSurfaces": row.get("uiSurfaces", []), "capabilityReason": "FreeCAD Workbench menu/toolbar container rendered by React" if ui_container else row.get("capabilityReason"), "route": route_payload, "implementationGaps": [] if implementation_ready else [{ "stage": "native" if command not in native_desktop_commands.get(module, set()) else "transport", "reason": "no command-specific native implementation is registered" if command not in native_desktop_commands.get(module, set()) else "native command has no matching Worker/Facade route", "sourcePath": row.get("sourcePath"), "sourceLine": row.get("sourceLine"), "repair": "complete this command's real business behavior and route; registration alone is insufficient", }], } ) if route_payload: desktop_route_records.append( { "id": record_id, "appearanceModule": module, "desktopCommand": command, "sourcePath": canonical_row.get("sourcePath"), "sourceLine": canonical_row.get("sourceLine"), **route_payload, } ) if module in module_rows: module_rows[module]["desktopCommands"] = int(module_rows[module]["desktopCommands"]) + 1 module_rows[module]["implementationRequired"] = int(module_rows[module]["implementationRequired"]) + int(not implementation_ready) native_gap_kinds = describe_native_gaps(records, rejected_desktop_markers) policy_run = subprocess.run( ["node", str(ROOT / "web/tools/audit-native-route-policy.mjs")], input=json.dumps({"records": records}), text=True, capture_output=True, check=True, cwd=ROOT, timeout=30, ) blocked_routes = json.loads(policy_run.stdout)["blocked"] for row in records: if row["id"] in blocked_routes: row["runtimeRouteBlock"] = blocked_routes[row["id"]] row.setdefault("implementationGaps", []).append({"stage": "policy", "reason": blocked_routes[row["id"]]["message"], "sourcePath": "web/src/native-source-policy.ts"}) row["implementationStatus"] = "implementation_required" row["uiStatus"] = "needs-react-workflow" for module_row in modules: module_row["implementationRequired"] = sum( row["module"] == module_row["module"] and row["implementationStatus"] != "routed" for row in records ) statuses = Counter(str(row["uiStatus"]) for row in records) payload = { "schema": "freecad-web-feature-capability-registry/v1", "generatedAt": datetime.now(timezone.utc).isoformat(), "strategy": "feature-first-deferred-validation", "sources": { "routes": "web/src/module-registry.generated.ts", "workbench": "web/src/workbench-shell.tsx", "artifacts": "web/public/modules/manifest.json", "pythonSymbols": "docs/format-api-inventory.json", }, "counts": { "modules": len(modules), "commands": len(records), "routed": sum(row["implementationStatus"] == "routed" for row in records), "implementationRequired": sum(row["implementationStatus"] == "implementation_required" for row in records), "interactive": statuses["interactive"], "genericApiEntry": statuses["generic-api-entry"], "genericCommandEntry": statuses["generic-command-entry"], "reactNavigation": statuses["react-navigation"], "needsParameterUi": statuses["needs-parameter-ui"], "needsReactWorkflow": statuses["needs-react-workflow"], "artifactModules": sum(bool(row["artifactReady"]) for row in modules), "validationDeferred": len(records), }, "diagnostics": { "nativeGapKinds": native_gap_kinds, "blockedRuntimeRoutes": blocked_routes, "familyMarkersRequireExplicitCommands": sorted(native_desktop_families), "rejectedDesktopMarkers": { module: sorted(commands) for module, commands in sorted(rejected_desktop_markers.items()) }, "rejectedMarkerReason": "marker requires a command-specific branch that explicitly inserts status=exposed", }, "modules": modules, "records": records, "boundaries": [ "This registry tracks Web capabilities and routes, not Python source symbols.", "Routed or interactive is an implementation state, not a validation pass.", "Runtime issues reopen only the affected capability; they do not trigger a full API gate.", ], } OUTPUT.parent.mkdir(parents=True, exist_ok=True) temporary = OUTPUT.with_suffix(".tmp") temporary.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") temporary.replace(OUTPUT) route_payload = { "schema": "freecad-web-desktop-command-routes/v1", "generatedAt": payload["generatedAt"], "records": desktop_route_records, } ROUTE_OUTPUT.parent.mkdir(parents=True, exist_ok=True) route_temporary = ROUTE_OUTPUT.with_suffix(".tmp") route_temporary.write_text(json.dumps(route_payload, ensure_ascii=False, separators=(",", ":")) + "\n", encoding="utf-8") route_temporary.replace(ROUTE_OUTPUT) if file_stamps(generation_inputs(command_matrix)) == inputs: cache_temporary = CACHE_PATH.with_suffix(".tmp") cache_temporary.write_text(json.dumps({"schema": "freecad-web-feature-capability-cache/v1", "inputs": inputs, "outputs": file_stamps({OUTPUT, ROUTE_OUTPUT}), "counts": payload["counts"]}) + "\n", encoding="utf-8") cache_temporary.replace(CACHE_PATH) print(json.dumps({"status": "generated", **payload["counts"]}, ensure_ascii=False)) return 0 if __name__ == "__main__": raise SystemExit(main())