Files
FreeCADWasmDev/tools/generate_feature_capability_registry.py
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

1330 lines
89 KiB
Python

#!/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 '<a href={url}' not in workbench_text:
return set()
adapter_text = adapter.read_text(encoding="utf-8")
if 'export function bimExternalUrl' not in adapter_text or 'native.status !== "exposed"' not in adapter_text:
return set()
script_text = scripts.read_text(encoding="utf-8")
discovered = set()
for command, module in (("BIM_Help", "BimHelp"), ("BIM_Examples", "BimExamples")):
row = re.search(r'\{"' + command + r'",\s*BimDesktopCommandRoute::ExternalUrl,\s*(\w+)\s*\}', registry_text)
if not row:
continue
body = re.search(r'static\s+const\s+char\s*\*\s*' + re.escape(row[1]) + r'\s*=\s*R"PY\((.*?)\)PY";', script_text, re.DOTALL)
if not body:
continue
expected = f"from bimcommands.{module} import {command}\n_result = {{'command': '{command}', 'url': {command}().getUrl()}}"
try:
if ast.dump(ast.parse(body[1])) != ast.dump(ast.parse(expected)):
continue
source = ROOT / f"freecad-source/src/Mod/BIM/bimcommands/{module}.py"
tree = ast.parse(source.read_text(encoding="utf-8"))
cls = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == command)
method = next(node for node in cls.body if isinstance(node, ast.FunctionDef) and node.name == "getUrl")
if len(method.body) != 1 or not isinstance(method.body[0], ast.Return):
continue
destination = ast.literal_eval(method.body[0].value)
if not isinstance(destination, str) or not destination.startswith("https://"):
continue
except (OSError, SyntaxError, StopIteration, ValueError, TypeError):
continue
discovered.add(command)
return discovered
def implemented_desktop_commands(text: str) -> 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 "<TestMdiViewHost" in workbench_text
and "registerTestMdiHost" in mdi_host.read_text(encoding="utf-8"))
if route and route.get("worker") == "test.worker.ts" and mdi_connected \
and "test-desktop-routes.json" in adapter.read_text(encoding="utf-8") \
and all(token in worker.read_text(encoding="utf-8") for token in ("isTestRequest", "dispatchTest")) \
and all(token in facade.read_text(encoding="utf-8") for token in ("test.inspectRegistry", "desktop-command", "fc_web_test_desktop")) \
and all(token in dispatcher.read_text(encoding="utf-8") for token in ('testDesktopRoutes[record.command', 'this.router.request("Test", endpoint')):
return {**route, "worker": "test.worker.ts", "executionModule": "Test", "transport": "direct-command",
"nativeRegistration": native_desktop_sources.get("Test")}
if module == "BIM" and command in {
"BIM_Glue", "BIM_IfcElements", "BIM_IfcExplorer", "BIM_IfcProperties",
"BIM_TDView", "BIM_Text", "BIM_TogglePanels", "BIM_Trash", "BIM_Report",
"BIM_ResetCloneColors", "BIM_Setup", "BIM_TDPage", "BIM_Tutorial", "BIM_Unclone",
"BIM_Ungroup", "BIM_Views", "BIM_Welcome", "BIM_Windows", "IFC_UpdateIOS",
}:
registry = PROBE_ROOT / "BIMDesktopCommandRegistry.h"
native = PROBE_ROOT / "BIMDesktopCommands.h"
runtime = PROBE_ROOT / "BimRuntimeProbe.cpp"
adapter = ROOT / "web/src/bim-desktop-adapter.ts"
worker = ROOT / "web/src/bim.worker.ts"
shell = ROOT / "web/src/workbench-shell.tsx"
if all(path.is_file() for path in (registry, native, runtime, adapter, worker, shell)):
registry_text = registry.read_text(encoding="utf-8")
native_text = native.read_text(encoding="utf-8")
runtime_text = runtime.read_text(encoding="utf-8")
adapter_text = adapter.read_text(encoding="utf-8")
shell_text = shell.read_text(encoding="utf-8")
if command == "BIM_Windows":
task = ROOT / "web/src/bim-windows-task.tsx"
if (not task.is_file() or '<BimWindowsTask' not in shell_text
or 'validBimWindowsResult' not in adapter_text
or not all(token in task.read_text(encoding="utf-8") for token in (
'selection, changes', 'freecad-bim-focus', 'value.windows'))
or 'setattr(obj, prop, value)' not in native_text):
return None
if command in {"BIM_Ungroup", "BIM_Unclone"}:
task = ROOT / "web/src/bim-relationship-task.tsx"
helpers = re.search(r'selected_document_helper = _panel_ifc and _command in \((.*?)\)', runtime_text, re.DOTALL)
if (not task.is_file() or command not in task.read_text(encoding="utf-8")
or '<BimRelationshipTask' not in shell_text or not helpers
or command not in ast.literal_eval('(' + helpers[1] + ')')):
return None
entry = re.search(r'\{"' + re.escape(command) + r'",\s*BimDesktopCommandRoute::(?:Document|ExternalUrl),\s*(\w+)(?:\.c_str\(\))?\s*\}', registry_text)
facade = ROOT / "web/src/bim-python-family-batch-facade.ts"
route = web_routes.get(("BIM", "bim.pythonFamilyBatch"))
shell_connected = command in shell_text
if command == "IFC_UpdateIOS":
# The updater uses the mounted shell's generic BIM dispatch.
# It has no command-specific task component in that shell.
shell_connected = all(token in shell_text for token in (
'prepareBimDesktopArgs(desktopRecord, desktopArgs, bimNudge)',
'desktopCommands.execute(desktopRecord, { ...args,',
'...(["Material", "BIM"].includes(desktopRecord.module) ? preparedDesktopArgs : {})',
))
if (route and entry and entry[1] in native_text and command in adapter_text
and shell_connected and facade.is_file()
and 'bimDesktopCommandImplementation(nameValue.c_str())->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 "<WebStandardTask" in workbench_text):
return {**route, "worker": owner["worker"], "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_CommandLine", "Std_RecentFiles", "Std_RecentMacros", "Std_ReloadStyleSheet"
}:
owner = routes["owners"]["PartBox"]
route = web_routes.get(("Part", owner["endpoint"]))
paths = {name: ROOT / "web/src" / name for name in (
"web-recent-standard-adapter.ts", "web-standard-task.tsx",
"web-desktop-adapter.ts", "web-standard-command-contract.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")
bridge_text = bridge.read_text(encoding="utf-8")
behavior = {
"Std_CommandLine": ("console_code.InteractiveConsole(namespace)", "console.push(line)"),
"Std_RecentFiles": ("execute_recent_item(args, document_id, 'Std_RecentFiles')", "App.openDocument(path)"),
"Std_RecentMacros": ("execute_recent_item(args, document_id, 'Std_RecentMacros')", "exec(compile(stream.read(), path, 'exec'), namespace, namespace)"),
"Std_ReloadStyleSheet": ("with open(path, encoding='utf-8') as stream:", "content = stream.read()"),
}[command]
if (f"def execute_{command}(args, document_id):" in native_text
and f"'{command}': execute_{command}" in native_text
and all(token in native_text for token in behavior)
and "value = native_standard_handlers[name](args, document_id)" in native_text
and f'"{command}"' in bridge_text and "executeDocumentCommand(input.constData(), output)" in bridge_text
and "return executeRecentStandard(router, command, args, documentId, applySelection)" in texts["web-desktop-adapter.ts"]
and all(token in texts["web-recent-standard-adapter.ts"] for token in (
'router.request("Part", viewEndpoint(owner)', 'freecad-web/document-view-batch/v1',
'readDocumentFile(filename)', 'parameters.source = macro.source',
'style.textContent = value.styleSheet'))
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 "<WebStandardTask" in workbench_text):
return {**route, "worker": owner["worker"], "executionModule": "Part", "transport": "direct-command",
"canonicalId": f"Web:desktop:{command}", "nativeRegistration": str(native.relative_to(ROOT))}
if command in routes.get("websiteCommands", []) and command in {
"Std_DevHandbook", "Std_FreeCADDonation", "Std_FreeCADForum", "Std_FreeCADUserHub",
"Std_FreeCADWebsite", "Std_OnlineHelp", "Std_OnlineHelpWebsite", "Std_ReportBug"}:
owner = routes["owners"]["PartBox"]
route = web_routes.get(("Part", owner["endpoint"]))
paths = {name: ROOT / "web/src" / name for name in (
"web-website-adapter.ts", "web-website-task.tsx", "web-desktop-adapter.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()}
if (f'"{command}"' in bridge.read_text(encoding="utf-8")
and f"'{command}'" in native.read_text(encoding="utf-8")
and 'const website = executeWebsite(router, command, args, documentId)' in texts["web-desktop-adapter.ts"]
and 'if (website) return website;' in texts["web-desktop-adapter.ts"]
and all(token in texts["web-website-adapter.ts"] for token in (
'routes.websiteCommands', 'router.request("Part", viewEndpoint(owner)',
'freecad-web/document-view-batch/v1'))
and all('dispatchDocumentView' in texts[name] for name in ("part.worker.ts", "part-save.worker.ts"))
and 'validStdWorkflow' in texts["part-view-facade.ts"]
and 'websiteResult(response, command, documentId, owner)' in texts["web-website-task.tsx"]
and '<a href={url}' in texts["web-website-task.tsx"] and '<WebWebsiteTask' in workbench_text):
return {**route, "worker": owner["worker"], "executionModule": "Part", "transport": "direct-command",
"canonicalId": f"Web:desktop:{command}", "nativeRegistration": str(bridge.relative_to(ROOT))}
if command in {"Std_LinkActions", "Std_LinkImport", "Std_LinkImportAll", "Std_LinkMake", "Std_LinkMakeGroup", "Std_LinkMakeRelative", "Std_LinkReplace", "Std_LinkUnlink"}:
owner = routes["owners"]["PartBox"]
route = web_routes.get(("Part", owner["endpoint"]))
native = PROBE_ROOT / "WebViewCommands.h"
bridge = PROBE_ROOT / "NativeDocumentView.h"
adapter = ROOT / "web/src/web-desktop-adapter.ts"
task = ROOT / "web/src/workbench-shell.tsx"
dispatcher = ROOT / "web/src/desktop-command-dispatcher.ts"
if route and all(path.is_file() for path in (native, bridge, adapter, task)):
native_text = native.read_text(encoding="utf-8")
adapter_text = adapter.read_text(encoding="utf-8")
if command in {"Std_LinkMakeRelative", "Std_LinkReplace", "Std_LinkUnlink"}:
mutation = PROBE_ROOT / "WebLinkMutationCommands.h"
controls = ROOT / "web/src/web-link-mutation-task.tsx"
if (not mutation.is_file() or not controls.is_file()
or '<WebLinkMutationTask' not in workbench_text
or command not in controls.read_text(encoding="utf-8")
or 'executeWebLinkMutation(input, output)' not in native_text
or not all(token in mutation.read_text(encoding="utf-8") for token in (
command, 'Data::noElementName', 'CopyOnLinkReplace(parent, obj, replacement)', 'app.commitTransaction(tid)'))):
return None
if (f'"{command}"' in native_text
and f'"{command}"' in bridge.read_text(encoding="utf-8")
and 'statefulCommandDoc' in adapter_text and 'command.startsWith("Std_Link")' in adapter_text
and 'src/Gui/CommandLink.cpp' in adapter_text
and 'const documentViewCommand = isNativeLinkCommand(command)' in adapter_text
and 'router.request("Part", viewEndpoint("PartBox")' in adapter_text
and 'validLinkResult(reply.result, p.command, p.document)' in (ROOT / "web/src/part-view-facade.ts").read_text(encoding="utf-8")
and 'executeWebDesktop(this.router, record.command' in dispatcher.read_text(encoding="utf-8")):
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("standardCommands", []) and command in {"Std_About", "Std_AboutQt", "Std_AnnotationLabel", "Std_DlgParameter", "Std_DlgPreferences", "Std_DlgCustomize", "Std_PythonHelp"}:
owner = routes["owners"]["PartBox"]
route = web_routes.get(("Part", owner["endpoint"]))
native = PROBE_ROOT / "WebViewCommands.h"
bridge = PROBE_ROOT / "NativeDocumentView.h"
files = {name: ROOT / "web/src" / name for name in (
"web-owner-standard-adapter.ts", "web-desktop-adapter.ts", "part-view-facade.ts",
"part.worker.ts", "part-save.worker.ts", "web-standard-task.tsx")}
if route and all(path.is_file() for path in [native, bridge, *files.values()]):
texts = {name: path.read_text(encoding="utf-8") for name, path in files.items()}
if (f'def execute_{command}(' in native.read_text(encoding="utf-8")
and f'"{command}"' in bridge.read_text(encoding="utf-8")
and re.search(r'\bexecuteOwnerStandard\s*\(\s*router\s*,\s*command\s*,\s*args\s*,\s*documentId\s*\)', texts["web-desktop-adapter.ts"])
and all(token in texts["web-owner-standard-adapter.ts"] for token in (
'routes.standardCommands.includes(command)', 'router.request("Part", viewEndpoint(owner)',
'validStandardResult', 'freecad-web/document-view-batch/v1'))
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 '<WebStandardTask' in workbench_text
and 'batch.records?.find(item => 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 '<WebOpenTask' not in workbench_text:
return None
if (not all(token in native.read_text(encoding="utf-8") for token in (
'App.openDocument(path)', 'App.getImportType()', 'module.open(path)', 'App.closeDocument(name)'))
or 'executeWebOpenCommand(input.constData(), output)' not in (PROBE_ROOT / "NativeDocumentView.h").read_text(encoding="utf-8")
or 'executeOpenDocument(router, args, documentId, applySelection)' not in (ROOT / "web/src/web-desktop-adapter.ts").read_text(encoding="utf-8")
or 'router.request("Part", viewEndpoint(owner)' not in files[0].read_text(encoding="utf-8")
or 'validOpenResult(reply.result, p.document)' not in (ROOT / "web/src/part-view-facade.ts").read_text(encoding="utf-8")):
return None
if command in {"Std_Part", "Std_VarSet"}:
native = PROBE_ROOT / "WebStructureCommands.h"
files = [ROOT / "web/src" / name for name in ("web-structure-adapter.ts", "web-structure-contract.ts", "web-structure-task.tsx")]
if not all(path.is_file() for path in [native, *files]) or '<WebStructureTask' not in workbench_text:
return None
if (not all(token in native.read_text(encoding="utf-8") for token in (
"App::Part", "App::VarSet", "doc.addObject(type_id, name)", "target.setExpression", "doc.commitTransaction()"))
or 'executeWebStructureCommand(input.constData(), output)' not in (PROBE_ROOT / "NativeDocumentView.h").read_text(encoding="utf-8")
or 'executeStructure(router, command, args, documentId, applySelection)' not in (ROOT / "web/src/web-desktop-adapter.ts").read_text(encoding="utf-8")
or 'router.request("Part", viewEndpoint(owner)' not in files[0].read_text(encoding="utf-8")
or 'validStructureResult(reply.result, p.command, p.document)' not in (ROOT / "web/src/part-view-facade.ts").read_text(encoding="utf-8")):
return None
if command in {"Std_ToggleFreeze", "Std_ToggleSkipRecompute"}:
native = (PROBE_ROOT / "NativeDocumentView.h").read_text(encoding="utf-8")
task = (ROOT / "web/src/web-commanddoc-task.tsx").read_text(encoding="utf-8")
required_native = "obj->unfreeze(); else obj->freeze();" if command == "Std_ToggleFreeze" else "doc->setStatus(App::Document::SkipRecompute, checked)"
if required_native not in native or f'"{command}"' not in task or '<WebCommandDocTask' not in workbench_text:
return None
if command in {"Std_SaveAs", "Std_SaveCopy", "Std_SelectAll", "Std_Undo"}:
native = (PROBE_ROOT / "WebViewCommands.h").read_text(encoding="utf-8")
adapter = (ROOT / "web/src/web-desktop-adapter.ts").read_text(encoding="utf-8")
task = (ROOT / "web/src/web-commanddoc-task.tsx").read_text(encoding="utf-8")
required_native = {"Std_SaveAs": "target.saveAs(destination)", "Std_SaveCopy": "target.saveCopy(destination)",
"Std_SelectAll": "[identity(obj) for obj in doc.Objects]", "Std_Undo": "doc.undo()"}
if (required_native[command] not in native or f'"{command}"' not in task
or "<WebCommandDocTask" not in workbench_text
or 'router.request("Part", viewEndpoint("PartBox")' not in adapter
or 'writeDocumentFile' not in adapter):
return None
if command == "Std_Group":
group_paths = [ROOT / "web/src" / name for name in ("web-group-adapter.ts", "web-group-task.tsx", "web-group-contract.ts")]
group_native = PROBE_ROOT / "WebGroupCommand.h"
if not all(path.is_file() for path in [*group_paths, group_native]) or '<WebGroupTask' not in workbench_text:
return None
if not all(token in group_native.read_text(encoding="utf-8") for token in (
"App::DocumentObjectGroup", "parent.allowObject(created)", "doc.commitTransaction()")):
return None
if ('executeGroup(router, args, documentId)' not in (ROOT / "web/src/web-desktop-adapter.ts").read_text(encoding="utf-8")
or 'executeWebGroupCommand(input.constData(), output)' not in (PROBE_ROOT / "NativeDocumentView.h").read_text(encoding="utf-8")
or 'router.request("Part", viewEndpoint(owner)' not in group_paths[0].read_text(encoding="utf-8")
or 'validGroupResult' not in (ROOT / "web/src/part-view-facade.ts").read_text(encoding="utf-8")):
return None
owner = routes["owners"]["PartBox"]
route = web_routes.get(("Part", owner["endpoint"]))
required = [ROOT / "web/src" / name for name in ("part-view-facade.ts", "part.worker.ts", "web-desktop-adapter.ts", "web-view-task.tsx")]
bridge = PROBE_ROOT / "NativeDocumentView.h"
if route and all(path.is_file() for path in [*required, bridge]) \
and "document-view-routes.json" in required[0].read_text(encoding="utf-8") \
and "dispatchDocumentView" in required[1].read_text(encoding="utf-8") \
and 'router.request("Part", viewEndpoint(owner)' in required[2].read_text(encoding="utf-8") \
and "executeWebDesktop" in required[3].read_text(encoding="utf-8") \
and "<WebViewTask" in workbench_text \
and f'"{command}"' in bridge.read_text(encoding="utf-8") \
and 'nativeDocumentView(object, QStringLiteral("PartBox"))' in (PROBE_ROOT / "PartBoxProbe.cpp").read_text(encoding="utf-8"):
return {**route, "worker": owner["worker"], "executionModule": "Part", "transport": "direct-command",
"canonicalId": f"Web:desktop:{command}", "nativeRegistration": str(bridge.relative_to(ROOT))}
view_table = ROOT / "web/src/scene-display-commands.json"
view_host = ROOT / "web/src/scene-command-host.ts"
view_adapter = ROOT / "web/src/scene-command-adapter.ts"
view_bridge = ROOT / "web/src/import-render-bridge.ts"
dispatcher = ROOT / "web/src/desktop-command-dispatcher.ts"
if (all(path.is_file() for path in (view_table, view_host, view_adapter, view_bridge, dispatcher))
and command in json.loads(view_table.read_text(encoding="utf-8"))
and "registerSceneCommandHost" in view_host.read_text(encoding="utf-8")
and "attachSceneCommands" in view_bridge.read_text(encoding="utf-8")
and "executeSceneDisplayCommand" in dispatcher.read_text(encoding="utf-8")):
return {"command": command, "worker": None, "executionModule": "Web", "transport": "browser-ui",
"nativeRegistration": None, "browserHandler": "web/src/scene-command-adapter.ts"}
table = ROOT / "web/src/dock-overlay-commands.json"
adapter = ROOT / "web/src/dock-overlay-adapter.ts"
host = ROOT / "web/src/dock-overlay-host.tsx"
dispatcher = ROOT / "web/src/desktop-command-dispatcher.ts"
worker = ROOT / "web/src/web-dock-overlay.worker.ts"
facade = ROOT / "web/src/web-dock-overlay-facade.ts"
native = PROBE_ROOT / "WebDockOverlayCommands.h"
route = web_routes.get(("Web", "web.dockOverlay"))
if (table.is_file() and adapter.is_file() and host.is_file() and dispatcher.is_file()
and route and all(path.is_file() for path in (worker, facade, native))
and command in json.loads(table.read_text(encoding="utf-8"))
and "registerDockOverlayHost" in host.read_text(encoding="utf-8")
and 'router.request("Web", "web.dockOverlay"' in host.read_text(encoding="utf-8")
and 'dispatchDockOverlay(queue, message, kernel)' in worker.read_text(encoding="utf-8")
and 'fc_web_dock_overlay_dispatch' in facade.read_text(encoding="utf-8")
and f'"{command}"' in native.read_text(encoding="utf-8")
and "executeDockOverlayCommand" in dispatcher.read_text(encoding="utf-8")
and re.search(r'<DockOverlayHost(?:\s|>)', 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())