#!/usr/bin/env python3 """Trace Plugins — Find plugin→manifest→frontend verkabelungsfehler.""" from __future__ import annotations import ast, re, sys, json from pathlib import Path ROOT = Path(__file__).resolve().parent.parent.parent APP = ROOT / "app" FRONTEND = ROOT / "frontend" / "src" def find_plugin_manifests() -> list[dict]: """Find plugin manifests. Project convention: manifests are defined inline in plugin.py via ``PluginManifest(...)`` — no separate manifest.py file exists. """ plugins = [] builtins = APP / "plugins" / "builtins" if not builtins.exists(): return plugins for d in builtins.iterdir(): if not d.is_dir() or d.name.startswith("_"): continue plugin_file = d / "plugin.py" if not plugin_file.exists(): # Not a plugin directory (e.g. migrations/, tests/) — skip silently continue has_manifest = False plugin_info = {"name": d.name, "path": str(d.relative_to(ROOT)), "has_manifest": False} if plugin_file.exists(): try: content = plugin_file.read_text() except: continue has_manifest = bool(re.search(r'PluginManifest\s*\(', content)) plugin_info["has_manifest"] = has_manifest for m in re.finditer(r'menu_items\s*[=:]\s*\[', content): plugin_info["has_menu_items"] = True for m in re.finditer(r'page_routes\s*[=:]\s*\[', content): plugin_info["has_page_routes"] = True for m in re.finditer(r'settings_pages\s*[=:]\s*\[', content): plugin_info["has_settings_pages"] = True for m in re.finditer(r'detail_tabs\s*[=:]\s*\[', content): plugin_info["has_detail_tabs"] = True plugins.append(plugin_info) return plugins def find_frontend_plugin_refs() -> dict[str, list[str]]: refs = {} for ts in FRONTEND.rglob("*.ts"): try: content = ts.read_text() except: continue for m in re.finditer(r'@/plugins/([\w-]+)', content): refs.setdefault(m.group(1), []).append(str(ts.relative_to(ROOT))) return refs def find_plugin_routes() -> list[dict]: routes = [] builtins = APP / "plugins" / "builtins" if not builtins.exists(): return routes for py in builtins.rglob("*.py"): try: content = py.read_text() except: continue rel = str(py.relative_to(ROOT)) for m in re.finditer(r'@router\.(get|post|put|patch|delete)\s*\(\s*["\']([^"\']+)["\']', content): routes.append({"method": m.group(1).upper(), "path": m.group(2), "file": rel}) return routes def _has_dynamic_consumption() -> bool: """True if the frontend consumes plugin manifests dynamically. The frontend fetches menu_items/page_routes via the active-manifests API (frontend/src/api/pluginManifests.ts) and renders them through pluginStore — static '@/plugins/' imports are therefore not required for wiring. """ return (FRONTEND / "api" / "pluginManifests.ts").exists() def main(): print("=" * 70) print("TRACE PLUGINS — Plugin→Manifest→Frontend Verkabelungsfehler") print("=" * 70) plugins = find_plugin_manifests() frontend_refs = find_frontend_plugin_refs() plugin_routes = find_plugin_routes() issues = [] for p in plugins: if not p["has_manifest"]: issues.append({"plugin": p["name"], "issue": "No manifest (PluginManifest in plugin.py expected)", "severity": "HIGH"}) # NOTE: menu_items are consumed dynamically by the frontend via the # active-manifests API (frontend/src/api/pluginManifests.ts + pluginStore), # NOT via static '@/plugins/' imports — a missing static import is # therefore not a wiring error. if p.get("has_menu_items") and p["name"] not in frontend_refs and not _has_dynamic_consumption(): issues.append({"plugin": p["name"], "issue": "Has menu_items but no frontend reference", "severity": "MEDIUM"}) print(f"\nPlugins found: {len(plugins)}") print(f"Plugin routes: {len(plugin_routes)}") print(f"Frontend plugin refs: {len(frontend_refs)}") print(f"\nISSUES: {len(issues)}") if issues: print("\n--- Plugin issues ---") for i in issues: print(f" [{i['severity']}] {i['plugin']}: {i['issue']}") results = {"plugins": plugins, "frontend_refs": frontend_refs, "plugin_routes": plugin_routes, "issues": issues} with open(ROOT / "scripts" / "test_suite" / "results_trace_plugins.json", "w") as f: json.dump(results, f, indent=2, default=str) return len(issues) if __name__ == "__main__": sys.exit(main())