fix(imports): 3 broken imports — ENTITY_MODELS path, Room→CommConversation, get_cached_mail_summary→MailService
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
+1084
File diff suppressed because it is too large
Load Diff
@@ -55,7 +55,7 @@ async def send_agent_message(
|
|||||||
|
|
||||||
# 2. Create a kommunikation message in a dedicated agent room
|
# 2. Create a kommunikation message in a dedicated agent room
|
||||||
try:
|
try:
|
||||||
from app.plugins.builtins.kommunikation.contracts import Message, Room
|
from app.plugins.builtins.kommunikation.contracts import CommConversation as Room, CommMessage as Message
|
||||||
|
|
||||||
# Find or create the agent-to-agent room
|
# Find or create the agent-to-agent room
|
||||||
room_name = f"agent:{from_agent_id}:{target_agent.id}"
|
room_name = f"agent:{from_agent_id}:{target_agent.id}"
|
||||||
|
|||||||
@@ -112,8 +112,15 @@ async def run_agent(
|
|||||||
logger.warning("Failed to collect contacts for proactive context")
|
logger.warning("Failed to collect contacts for proactive context")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from app.core.cache import get_cached_mail_summary
|
from app.services.mail_service import MailService
|
||||||
context_data["recent_mails"] = get_cached_mail_summary(agent.tenant_id) or []
|
mail_svc = MailService(db)
|
||||||
|
recent_mails = await mail_svc.list_mails(
|
||||||
|
tenant_id=agent.tenant_id,
|
||||||
|
user_id=None,
|
||||||
|
page=1,
|
||||||
|
page_size=5,
|
||||||
|
)
|
||||||
|
context_data["recent_mails"] = recent_mails.get("items", [])
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Failed to collect mails for proactive context")
|
logger.warning("Failed to collect mails for proactive context")
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ async def _check_entity_ownership(
|
|||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
from app.models.entity_permission import ENTITY_MODELS
|
from app.services.entity_permission_service import ENTITY_MODELS
|
||||||
|
|
||||||
model_info = ENTITY_MODELS.get(entity_type)
|
model_info = ENTITY_MODELS.get(entity_type)
|
||||||
if model_info is None:
|
if model_info is None:
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Test Marathon — Runs all trace scripts and test scripts, combines results.
|
||||||
|
|
||||||
|
Usage: python3 scripts/test_suite/marathon.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
|
SUITE_DIR = Path(__file__).resolve().parent
|
||||||
|
|
||||||
|
# All trace scripts (static analysis — find verkabelungsfehler)
|
||||||
|
TRACE_SCRIPTS = [
|
||||||
|
("trace_api_contracts", "Frontend↔Backend API Contracts"),
|
||||||
|
("trace_hooks", "Hook Registrations vs Triggers"),
|
||||||
|
("trace_functions", "Dead Functions (defined but never called)"),
|
||||||
|
("trace_stores", "Unused Store Actions/State"),
|
||||||
|
("trace_contracts", "Contract Attribute Mismatches"),
|
||||||
|
("trace_plugins", "Plugin→Manifest→Frontend Verkabelung"),
|
||||||
|
("trace_imports", "Broken/Missing Imports"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# All test scripts (runtime — find crashes and errors)
|
||||||
|
# These will be added as they are built
|
||||||
|
TEST_SCRIPTS = [
|
||||||
|
# ("test_all_endpoints", "All 224 API Endpoints"),
|
||||||
|
# ("test_all_routes", "All 62 Frontend Routes"),
|
||||||
|
# ("test_schema_vs_models", "DB Schema vs Models"),
|
||||||
|
# ("test_permission_matrix", "Permission Matrix"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def run_script(name: str, description: str) -> dict:
|
||||||
|
"""Run a single script and capture results."""
|
||||||
|
script_path = SUITE_DIR / f"{name}.py"
|
||||||
|
if not script_path.exists():
|
||||||
|
return {"name": name, "description": description, "status": "SKIP", "reason": "Script not found", "issues": 0}
|
||||||
|
|
||||||
|
print(f"\n{'─' * 70}")
|
||||||
|
print(f"RUNNING: {name} — {description}")
|
||||||
|
print(f"{'─' * 70}")
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, str(script_path)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=300,
|
||||||
|
cwd=str(ROOT),
|
||||||
|
)
|
||||||
|
elapsed = time.time() - start
|
||||||
|
|
||||||
|
# Print stdout
|
||||||
|
if result.stdout:
|
||||||
|
print(result.stdout)
|
||||||
|
if result.stderr:
|
||||||
|
print(f"STDERR: {result.stderr[:500]}")
|
||||||
|
|
||||||
|
# Try to read results JSON
|
||||||
|
results_file = SUITE_DIR / f"results_{name}.json"
|
||||||
|
issues_count = 0
|
||||||
|
if results_file.exists():
|
||||||
|
try:
|
||||||
|
with open(results_file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if "summary" in data:
|
||||||
|
issues_count = data["summary"].get("total_issues", 0)
|
||||||
|
elif "issues" in data:
|
||||||
|
issues_count = len(data["issues"])
|
||||||
|
elif "mismatches" in data:
|
||||||
|
issues_count = len(data["mismatches"])
|
||||||
|
elif "broken" in data:
|
||||||
|
issues_count = len(data["broken"])
|
||||||
|
elif "critical_dead" in data:
|
||||||
|
issues_count = len(data["critical_dead"])
|
||||||
|
elif "orphan_registrations" in data:
|
||||||
|
issues_count = len(data.get("orphan_registrations", [])) + len(data.get("orphan_triggers", []))
|
||||||
|
elif "unused" in data:
|
||||||
|
issues_count = sum(len(v) for v in data["unused"].values()) if isinstance(data["unused"], dict) else 0
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"status": "PASS" if result.returncode == 0 else "FAIL",
|
||||||
|
"exit_code": result.returncode,
|
||||||
|
"elapsed": round(elapsed, 2),
|
||||||
|
"issues": issues_count,
|
||||||
|
}
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
elapsed = time.time() - start
|
||||||
|
return {"name": name, "description": description, "status": "TIMEOUT", "elapsed": round(elapsed, 2), "issues": 0}
|
||||||
|
except Exception as e:
|
||||||
|
elapsed = time.time() - start
|
||||||
|
return {"name": name, "description": description, "status": "ERROR", "error": str(e), "elapsed": round(elapsed, 2), "issues": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("╔" + "═" * 70 + "╗")
|
||||||
|
print("║" + " LEOCRM TEST MARATHON — Vollständige System-Audit".center(70) + "║")
|
||||||
|
print("║" + f" Started: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}".center(70) + "║")
|
||||||
|
print("╚" + "═" * 70 + "╝")
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
total_start = time.time()
|
||||||
|
|
||||||
|
# Phase 1: Trace Scripts (static analysis)
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("PHASE 1: TRACE SCRIPTS — Statische Verkabelungs-Analyse")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
for name, desc in TRACE_SCRIPTS:
|
||||||
|
result = run_script(name, desc)
|
||||||
|
all_results.append(result)
|
||||||
|
|
||||||
|
# Phase 2: Test Scripts (runtime)
|
||||||
|
if TEST_SCRIPTS:
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("PHASE 2: TEST SCRIPTS — Runtime Tests")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
for name, desc in TEST_SCRIPTS:
|
||||||
|
result = run_script(name, desc)
|
||||||
|
all_results.append(result)
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
total_elapsed = time.time() - total_start
|
||||||
|
print("\n" + "╔" + "═" * 70 + "╗")
|
||||||
|
print("║" + " MARATHON SUMMARY".center(70) + "║")
|
||||||
|
print("╠" + "═" * 70 + "╣")
|
||||||
|
|
||||||
|
total_issues = 0
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
skipped = 0
|
||||||
|
|
||||||
|
for r in all_results:
|
||||||
|
status_icon = {"PASS": "✅", "FAIL": "❌", "SKIP": "⏭️", "TIMEOUT": "⏰", "ERROR": "💥"}.get(r["status"], "?")
|
||||||
|
issues = r.get("issues", 0)
|
||||||
|
total_issues += issues
|
||||||
|
if r["status"] == "PASS":
|
||||||
|
passed += 1
|
||||||
|
elif r["status"] in ("FAIL", "ERROR", "TIMEOUT"):
|
||||||
|
failed += 1
|
||||||
|
else:
|
||||||
|
skipped += 1
|
||||||
|
elapsed = r.get("elapsed", 0)
|
||||||
|
line = f" {status_icon} {r['name']:<30} {issues:>5} issues {elapsed:>6}s"
|
||||||
|
print("║" + line.ljust(70) + "║")
|
||||||
|
|
||||||
|
print("╠" + "═" * 70 + "╣")
|
||||||
|
summary_line = f" Total: {len(all_results)} scripts | ✅ {passed} passed | ❌ {failed} failed | ⏭️ {skipped} skipped"
|
||||||
|
print("║" + summary_line.ljust(70) + "║")
|
||||||
|
issues_line = f" Total issues found: {total_issues}"
|
||||||
|
print("║" + issues_line.ljust(70) + "║")
|
||||||
|
time_line = f" Total time: {round(total_elapsed, 2)}s"
|
||||||
|
print("║" + time_line.ljust(70) + "║")
|
||||||
|
print("╚" + "═" * 70 + "╝")
|
||||||
|
|
||||||
|
# Write combined results
|
||||||
|
combined_file = SUITE_DIR / "marathon_results.json"
|
||||||
|
with open(combined_file, "w") as f:
|
||||||
|
json.dump({
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"total_time": round(total_elapsed, 2),
|
||||||
|
"total_scripts": len(all_results),
|
||||||
|
"total_issues": total_issues,
|
||||||
|
"passed": passed,
|
||||||
|
"failed": failed,
|
||||||
|
"skipped": skipped,
|
||||||
|
"results": all_results,
|
||||||
|
}, f, indent=2)
|
||||||
|
print(f"\nCombined results: {combined_file}")
|
||||||
|
|
||||||
|
return total_issues
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"timestamp": "2026-08-17T00:23:01.849835",
|
||||||
|
"total_time": 6.53,
|
||||||
|
"total_scripts": 7,
|
||||||
|
"total_issues": 1112,
|
||||||
|
"passed": 1,
|
||||||
|
"failed": 6,
|
||||||
|
"skipped": 0,
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"name": "trace_api_contracts",
|
||||||
|
"description": "Frontend\u2194Backend API Contracts",
|
||||||
|
"status": "FAIL",
|
||||||
|
"exit_code": 246,
|
||||||
|
"elapsed": 0.21,
|
||||||
|
"issues": 758
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trace_hooks",
|
||||||
|
"description": "Hook Registrations vs Triggers",
|
||||||
|
"status": "FAIL",
|
||||||
|
"exit_code": 1,
|
||||||
|
"elapsed": 0.06,
|
||||||
|
"issues": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trace_functions",
|
||||||
|
"description": "Dead Functions (defined but never called)",
|
||||||
|
"status": "FAIL",
|
||||||
|
"exit_code": 3,
|
||||||
|
"elapsed": 2.94,
|
||||||
|
"issues": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trace_stores",
|
||||||
|
"description": "Unused Store Actions/State",
|
||||||
|
"status": "FAIL",
|
||||||
|
"exit_code": 67,
|
||||||
|
"elapsed": 0.08,
|
||||||
|
"issues": 323
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trace_contracts",
|
||||||
|
"description": "Contract Attribute Mismatches",
|
||||||
|
"status": "PASS",
|
||||||
|
"exit_code": 0,
|
||||||
|
"elapsed": 1.72,
|
||||||
|
"issues": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trace_plugins",
|
||||||
|
"description": "Plugin\u2192Manifest\u2192Frontend Verkabelung",
|
||||||
|
"status": "FAIL",
|
||||||
|
"exit_code": 24,
|
||||||
|
"elapsed": 0.11,
|
||||||
|
"issues": 24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trace_imports",
|
||||||
|
"description": "Broken/Missing Imports",
|
||||||
|
"status": "FAIL",
|
||||||
|
"exit_code": 4,
|
||||||
|
"elapsed": 1.4,
|
||||||
|
"issues": 4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,327 @@
|
|||||||
|
{
|
||||||
|
"contracts": [
|
||||||
|
{
|
||||||
|
"name": "ContractError",
|
||||||
|
"file": "app/plugins/builtins/contracts.py",
|
||||||
|
"attributes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PluginContract",
|
||||||
|
"file": "app/plugins/builtins/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ContractRegistry",
|
||||||
|
"file": "app/plugins/builtins/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"_instance"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "McpClientContract",
|
||||||
|
"file": "app/plugins/builtins/mcp_client/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"McpServerConfig",
|
||||||
|
"McpClient",
|
||||||
|
"McpServerExecuteRequest",
|
||||||
|
"McpServerExecuteResponse",
|
||||||
|
"McpServerToolInfo",
|
||||||
|
"McpServerToolsResponse"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ForgejoErrorReporterContract",
|
||||||
|
"file": "app/plugins/builtins/forgejo_error_reporter/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"report_error_to_forgejo",
|
||||||
|
"ReportedError"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AutomationContract",
|
||||||
|
"file": "app/plugins/builtins/automation/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"AgentDefinition",
|
||||||
|
"Automation",
|
||||||
|
"CronJob",
|
||||||
|
"AgentRun",
|
||||||
|
"AgentVersion",
|
||||||
|
"AutomationRun",
|
||||||
|
"AutomationVersion",
|
||||||
|
"AgentService",
|
||||||
|
"AutomationService",
|
||||||
|
"CronJobService",
|
||||||
|
"RunLogService",
|
||||||
|
"run_agent",
|
||||||
|
"run_automation",
|
||||||
|
"calculate_next_run",
|
||||||
|
"scheduler_tick",
|
||||||
|
"send_agent_message"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DmsContract",
|
||||||
|
"file": "app/plugins/builtins/dms/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"DmsFile",
|
||||||
|
"Folder"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MailContract",
|
||||||
|
"file": "app/plugins/builtins/mail/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"Mail"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TestContractRegistry",
|
||||||
|
"file": "app/plugins/builtins/tests/test_contracts.py",
|
||||||
|
"attributes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TestKommunikationContract",
|
||||||
|
"file": "app/plugins/builtins/tests/test_contracts.py",
|
||||||
|
"attributes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TestAIAssistantContract",
|
||||||
|
"file": "app/plugins/builtins/tests/test_contracts.py",
|
||||||
|
"attributes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TestMailContract",
|
||||||
|
"file": "app/plugins/builtins/tests/test_contracts.py",
|
||||||
|
"attributes": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TasksContract",
|
||||||
|
"file": "app/plugins/builtins/tasks/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"list_tasks",
|
||||||
|
"get_task",
|
||||||
|
"create_task",
|
||||||
|
"update_task",
|
||||||
|
"delete_task",
|
||||||
|
"assign_task",
|
||||||
|
"update_task_status",
|
||||||
|
"get_due_tasks",
|
||||||
|
"Task"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CalendarContract",
|
||||||
|
"file": "app/plugins/builtins/calendar/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"Calendar",
|
||||||
|
"CalendarEntry",
|
||||||
|
"CalendarEntryLink"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "UnifiedSearchContract",
|
||||||
|
"file": "app/plugins/builtins/unified_search/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"generate_embedding",
|
||||||
|
"hybrid_search",
|
||||||
|
"find_similar_all_types",
|
||||||
|
"get_search_registry",
|
||||||
|
"llm_analyze_query",
|
||||||
|
"BaseSearchProvider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "GraphRagContract",
|
||||||
|
"file": "app/plugins/builtins/graph_rag/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"GraphRAGSearchProvider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "ReportGeneratorContract",
|
||||||
|
"file": "app/plugins/builtins/report_generator/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"ReportTemplate",
|
||||||
|
"ReportInstance",
|
||||||
|
"generate_pdf",
|
||||||
|
"generate_print_pdf",
|
||||||
|
"generate_preset_report",
|
||||||
|
"generate_pdf_from_template_content",
|
||||||
|
"render_template_file",
|
||||||
|
"render_template_string",
|
||||||
|
"get_preset_list",
|
||||||
|
"PRESET_META",
|
||||||
|
"PRESET_TEMPLATES"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "SystemNotifContract",
|
||||||
|
"file": "app/plugins/builtins/system_notif/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"SystemParticipantHandler"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "EntityLinksContract",
|
||||||
|
"file": "app/plugins/builtins/entity_links/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"EntityLink"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "KommunikationContract",
|
||||||
|
"file": "app/plugins/builtins/kommunikation/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"parse_mentions",
|
||||||
|
"get_conversation",
|
||||||
|
"get_messages",
|
||||||
|
"send_message",
|
||||||
|
"create_plugin_room",
|
||||||
|
"get_participant_registry",
|
||||||
|
"ParticipantHandler",
|
||||||
|
"MiniAppRegistry",
|
||||||
|
"MiniAppDef",
|
||||||
|
"get_miniapp_registry",
|
||||||
|
"reset_miniapp_registry",
|
||||||
|
"post_system_message",
|
||||||
|
"CommConversation",
|
||||||
|
"CommMessage",
|
||||||
|
"CommParticipant"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AiProactiveContract",
|
||||||
|
"file": "app/plugins/builtins/ai_proactive/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"ProactiveSuggestion",
|
||||||
|
"ContextLog",
|
||||||
|
"ProactiveSettings",
|
||||||
|
"handle_context_change",
|
||||||
|
"get_active_suggestions",
|
||||||
|
"get_sse_queue",
|
||||||
|
"get_stats",
|
||||||
|
"get_user_settings",
|
||||||
|
"mark_dismissed",
|
||||||
|
"push_suggestion",
|
||||||
|
"register_context_tools",
|
||||||
|
"deep_analysis",
|
||||||
|
"heartbeat"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AIAssistantContract",
|
||||||
|
"file": "app/plugins/builtins/ai_assistant/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"get_tool_registry",
|
||||||
|
"ToolRegistry",
|
||||||
|
"AITool",
|
||||||
|
"get_default_provider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TagsContract",
|
||||||
|
"file": "app/plugins/builtins/tags/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"Tag",
|
||||||
|
"TagAssignment"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PermissionsContract",
|
||||||
|
"file": "app/plugins/builtins/permissions/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"Permission",
|
||||||
|
"ShareLink"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "McpServerContract",
|
||||||
|
"file": "app/plugins/builtins/mcp_server/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"ToolDefinitions",
|
||||||
|
"ToolHandlers",
|
||||||
|
"get_tool_definition",
|
||||||
|
"get_all_tool_names",
|
||||||
|
"McpToolDefinition",
|
||||||
|
"McpToolExecuteRequest",
|
||||||
|
"McpToolExecuteResponse",
|
||||||
|
"McpToolListResponse",
|
||||||
|
"McpToolParameter",
|
||||||
|
"McpServerConfig"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "AiUiControlContract",
|
||||||
|
"file": "app/plugins/builtins/ai_ui_control/contracts.py",
|
||||||
|
"attributes": [
|
||||||
|
"contract_name",
|
||||||
|
"AIUIControlWSManager",
|
||||||
|
"UICommand",
|
||||||
|
"UICommandCreate",
|
||||||
|
"UICommandFeedback",
|
||||||
|
"UICommandResponse",
|
||||||
|
"UICommandStatus",
|
||||||
|
"UICommandStatusResponse",
|
||||||
|
"UICommandType"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accesses": [
|
||||||
|
{
|
||||||
|
"contract": "AIAssistantContract",
|
||||||
|
"attribute": "get_tool_registry",
|
||||||
|
"file": "app/plugins/builtins/automation/agent_coordinator.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contract": "AIAssistantContract",
|
||||||
|
"attribute": "get_tool_registry",
|
||||||
|
"file": "app/plugins/builtins/automation/agent_coordinator.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contract": "GraphRagContract",
|
||||||
|
"attribute": "GraphRAGSearchProvider",
|
||||||
|
"file": "app/plugins/builtins/unified_search/provider_registry.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contract": "AutomationContract",
|
||||||
|
"attribute": "AgentRun",
|
||||||
|
"file": "app/plugins/builtins/ai_assistant/external_api.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contract": "AutomationContract",
|
||||||
|
"attribute": "AgentRun",
|
||||||
|
"file": "app/plugins/builtins/ai_assistant/external_api.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contract": "AutomationContract",
|
||||||
|
"attribute": "AgentRun",
|
||||||
|
"file": "app/plugins/builtins/ai_assistant/external_api.py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"contract": "DmsContract",
|
||||||
|
"attribute": "DmsFile",
|
||||||
|
"file": "app/plugins/builtins/permissions/public_routes.py"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mismatches": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"py_defs": 1616,
|
||||||
|
"py_dead": 629,
|
||||||
|
"ts_defs": 959,
|
||||||
|
"ts_dead": 442,
|
||||||
|
"critical_dead": [
|
||||||
|
{
|
||||||
|
"name": "seed_default_workspace",
|
||||||
|
"file": "app/services/workspace_service.py",
|
||||||
|
"line": 560,
|
||||||
|
"lang": "py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "register_default_history_hooks",
|
||||||
|
"file": "app/core/history_hooks.py",
|
||||||
|
"line": 128,
|
||||||
|
"lang": "py"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "register_default_entities",
|
||||||
|
"file": "app/core/restore_registry.py",
|
||||||
|
"line": 114,
|
||||||
|
"lang": "py"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"total_imports": 2274,
|
||||||
|
"broken": [
|
||||||
|
{
|
||||||
|
"file": "app/routes/entity_permissions.py",
|
||||||
|
"module": "app.models.entity_permission",
|
||||||
|
"name": "ENTITY_MODELS",
|
||||||
|
"line": 105,
|
||||||
|
"issue": "Name 'ENTITY_MODELS' not found in module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "app/core/auth.py",
|
||||||
|
"module": "app.models.session",
|
||||||
|
"name": "SessionModel",
|
||||||
|
"line": 261,
|
||||||
|
"issue": "Name 'SessionModel' not found in module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "app/plugins/builtins/automation/agent_comm.py",
|
||||||
|
"module": "app.plugins.builtins.kommunikation.contracts",
|
||||||
|
"name": "Room",
|
||||||
|
"line": 58,
|
||||||
|
"issue": "Name 'Room' not found in module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "app/plugins/builtins/automation/agent_runner.py",
|
||||||
|
"module": "app.core.cache",
|
||||||
|
"name": "get_cached_mail_summary",
|
||||||
|
"line": 115,
|
||||||
|
"issue": "Name 'get_cached_mail_summary' not found in module"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,701 @@
|
|||||||
|
{
|
||||||
|
"stores": {
|
||||||
|
"calendarStore": [
|
||||||
|
"setVisibleWeek",
|
||||||
|
"toggleCalendarVisibility",
|
||||||
|
"goToNextDay",
|
||||||
|
"goToToday",
|
||||||
|
"setVisibleMonth",
|
||||||
|
"setActiveCalendarId",
|
||||||
|
"goToPrevDay",
|
||||||
|
"goToPrevWeek",
|
||||||
|
"setRangeEnd",
|
||||||
|
"visibleMonth",
|
||||||
|
"rangeStart",
|
||||||
|
"visibleWeek",
|
||||||
|
"selectedEntry",
|
||||||
|
"activeCalendarId",
|
||||||
|
"rangeEnd",
|
||||||
|
"setVisibleDay",
|
||||||
|
"viewMode",
|
||||||
|
"goToNextMonth",
|
||||||
|
"visibleDay",
|
||||||
|
"date",
|
||||||
|
"setViewMode",
|
||||||
|
"entry",
|
||||||
|
"visibleCalendarIds",
|
||||||
|
"setRangeStart",
|
||||||
|
"mode",
|
||||||
|
"goToPrevMonth",
|
||||||
|
"setSelectedEntry",
|
||||||
|
"calendars",
|
||||||
|
"goToNextWeek",
|
||||||
|
"setCalendars"
|
||||||
|
],
|
||||||
|
"aiUIControlStore": [
|
||||||
|
"setAIActive",
|
||||||
|
"value",
|
||||||
|
"section",
|
||||||
|
"entity",
|
||||||
|
"pendingSettings",
|
||||||
|
"settings",
|
||||||
|
"clearPending",
|
||||||
|
"setActiveCommand",
|
||||||
|
"status",
|
||||||
|
"activeTab",
|
||||||
|
"pendingFilter",
|
||||||
|
"setConnected",
|
||||||
|
"activeModal",
|
||||||
|
"lastFeedback",
|
||||||
|
"connected",
|
||||||
|
"setLastFeedback",
|
||||||
|
"feedback",
|
||||||
|
"command_id",
|
||||||
|
"setPendingSettings",
|
||||||
|
"commandHistory",
|
||||||
|
"setActiveModal",
|
||||||
|
"active",
|
||||||
|
"addCommandToHistory",
|
||||||
|
"activeCommand",
|
||||||
|
"action",
|
||||||
|
"filter",
|
||||||
|
"setPendingFilter",
|
||||||
|
"aiActive",
|
||||||
|
"setActiveTab",
|
||||||
|
"modal"
|
||||||
|
],
|
||||||
|
"onboardingStore": [
|
||||||
|
"completed",
|
||||||
|
"isActive",
|
||||||
|
"goToStep",
|
||||||
|
"prev",
|
||||||
|
"start",
|
||||||
|
"skipped",
|
||||||
|
"complete",
|
||||||
|
"reset",
|
||||||
|
"next",
|
||||||
|
"skip",
|
||||||
|
"data",
|
||||||
|
"step"
|
||||||
|
],
|
||||||
|
"windowStore": [
|
||||||
|
"windows",
|
||||||
|
"minimizeWindow",
|
||||||
|
"toggleAiChat",
|
||||||
|
"nextZIndex",
|
||||||
|
"openWindow",
|
||||||
|
"const",
|
||||||
|
"restoreWindow",
|
||||||
|
"updateWindowSize",
|
||||||
|
"height",
|
||||||
|
"newWindow",
|
||||||
|
"zIndex",
|
||||||
|
"aiChatVisible",
|
||||||
|
"config",
|
||||||
|
"type",
|
||||||
|
"title",
|
||||||
|
"position",
|
||||||
|
"componentProps",
|
||||||
|
"setActiveWindow",
|
||||||
|
"updateWindowPosition",
|
||||||
|
"closeWindow",
|
||||||
|
"activeWindowId",
|
||||||
|
"null",
|
||||||
|
"component",
|
||||||
|
"size",
|
||||||
|
"toggleFullscreen",
|
||||||
|
"width"
|
||||||
|
],
|
||||||
|
"commandPaletteStore": [
|
||||||
|
"close",
|
||||||
|
"toggle",
|
||||||
|
"isOpen",
|
||||||
|
"open"
|
||||||
|
],
|
||||||
|
"pluginToolbarStore": [
|
||||||
|
"updateItem",
|
||||||
|
"value",
|
||||||
|
"setActivePlugin",
|
||||||
|
"label",
|
||||||
|
"registerItems",
|
||||||
|
"plugin",
|
||||||
|
"unregisterPlugin",
|
||||||
|
"updates",
|
||||||
|
"query",
|
||||||
|
"activePlugin",
|
||||||
|
"null",
|
||||||
|
"items",
|
||||||
|
"onClick"
|
||||||
|
],
|
||||||
|
"commStore": [
|
||||||
|
"file_type",
|
||||||
|
"sort_order",
|
||||||
|
"locked_by",
|
||||||
|
"is_direct",
|
||||||
|
"conversation_id",
|
||||||
|
"file_size",
|
||||||
|
"sender_id",
|
||||||
|
"content",
|
||||||
|
"participants",
|
||||||
|
"addMessage",
|
||||||
|
"role",
|
||||||
|
"setActiveConversation",
|
||||||
|
"setTyping",
|
||||||
|
"setConversations",
|
||||||
|
"created_at",
|
||||||
|
"participant_id",
|
||||||
|
"typingUsers",
|
||||||
|
"userIds",
|
||||||
|
"file_name",
|
||||||
|
"reply_to_id",
|
||||||
|
"participant_type",
|
||||||
|
"setMessages",
|
||||||
|
"conv",
|
||||||
|
"title",
|
||||||
|
"blocks",
|
||||||
|
"edited_at",
|
||||||
|
"activeConversationId",
|
||||||
|
"sender_type",
|
||||||
|
"file_id",
|
||||||
|
"last_msg_at",
|
||||||
|
"updateConversation",
|
||||||
|
"msgs",
|
||||||
|
"display_name",
|
||||||
|
"count",
|
||||||
|
"is_archived",
|
||||||
|
"reactions",
|
||||||
|
"loading",
|
||||||
|
"setLoading",
|
||||||
|
"created_by_type",
|
||||||
|
"attachments",
|
||||||
|
"created_by",
|
||||||
|
"last_msg_preview",
|
||||||
|
"setUnread",
|
||||||
|
"block_data",
|
||||||
|
"file_source",
|
||||||
|
"convId",
|
||||||
|
"is_pinned",
|
||||||
|
"messages",
|
||||||
|
"convs",
|
||||||
|
"content_format",
|
||||||
|
"unread_count",
|
||||||
|
"conversations",
|
||||||
|
"thumbnail_path",
|
||||||
|
"is_locked",
|
||||||
|
"last_msg_sender_type",
|
||||||
|
"block_type",
|
||||||
|
"unreadCounts",
|
||||||
|
"metadata"
|
||||||
|
],
|
||||||
|
"pluginStore": [
|
||||||
|
"permission",
|
||||||
|
"is_core",
|
||||||
|
"getDetailTabsForEntity",
|
||||||
|
"default_value",
|
||||||
|
"entity",
|
||||||
|
"setManifests",
|
||||||
|
"loaded",
|
||||||
|
"badge_key",
|
||||||
|
"error",
|
||||||
|
"entityType",
|
||||||
|
"parent",
|
||||||
|
"row_span",
|
||||||
|
"custom_fields",
|
||||||
|
"detail_tabs",
|
||||||
|
"dashboard_widgets",
|
||||||
|
"col_span",
|
||||||
|
"reset",
|
||||||
|
"order",
|
||||||
|
"getCustomFieldsForEntity",
|
||||||
|
"getAllDashboardWidgets",
|
||||||
|
"display_name",
|
||||||
|
"protected",
|
||||||
|
"version",
|
||||||
|
"label",
|
||||||
|
"required",
|
||||||
|
"path",
|
||||||
|
"getAllMenuItems",
|
||||||
|
"loading",
|
||||||
|
"getAllSettingsPages",
|
||||||
|
"setLoading",
|
||||||
|
"field_type",
|
||||||
|
"group",
|
||||||
|
"options",
|
||||||
|
"component",
|
||||||
|
"page_routes",
|
||||||
|
"entity_type",
|
||||||
|
"manifests",
|
||||||
|
"label_key",
|
||||||
|
"settings_pages",
|
||||||
|
"getAllPageRoutes",
|
||||||
|
"menu_items",
|
||||||
|
"icon",
|
||||||
|
"setError"
|
||||||
|
],
|
||||||
|
"themeStore": [
|
||||||
|
"borderRadius",
|
||||||
|
"applyTheme",
|
||||||
|
"toggleDarkMode",
|
||||||
|
"config",
|
||||||
|
"loadFromStorage",
|
||||||
|
"accentColor",
|
||||||
|
"DEFAULT_THEME",
|
||||||
|
"primaryColor",
|
||||||
|
"target",
|
||||||
|
"darkMode",
|
||||||
|
"amount",
|
||||||
|
"result",
|
||||||
|
"setTheme",
|
||||||
|
"saveToStorage",
|
||||||
|
"base",
|
||||||
|
"fontFamily",
|
||||||
|
"scales"
|
||||||
|
],
|
||||||
|
"uiStore": [
|
||||||
|
"message",
|
||||||
|
"suggestionSidebarOpen",
|
||||||
|
"setAISidebarCollapsed",
|
||||||
|
"openAISidebarProactive",
|
||||||
|
"locale",
|
||||||
|
"theme",
|
||||||
|
"clearToasts",
|
||||||
|
"toggleAISidebar",
|
||||||
|
"setLocale",
|
||||||
|
"open",
|
||||||
|
"type",
|
||||||
|
"setTheme",
|
||||||
|
"collapsed",
|
||||||
|
"toggleSidebar",
|
||||||
|
"setAISidebarTab",
|
||||||
|
"aiSidebarCollapsed",
|
||||||
|
"clearNotifications",
|
||||||
|
"setSidebarOpen",
|
||||||
|
"setMessageSidebarCollapsed",
|
||||||
|
"removeToast",
|
||||||
|
"notifications",
|
||||||
|
"toggleSuggestionSidebar",
|
||||||
|
"toggleMessageSidebar",
|
||||||
|
"toasts",
|
||||||
|
"messageSidebarCollapsed",
|
||||||
|
"toast",
|
||||||
|
"removeNotification",
|
||||||
|
"index",
|
||||||
|
"addToast",
|
||||||
|
"sidebarOpen",
|
||||||
|
"aiSidebarTab"
|
||||||
|
],
|
||||||
|
"workspaceStore": [
|
||||||
|
"is_visible",
|
||||||
|
"widgets",
|
||||||
|
"context",
|
||||||
|
"height",
|
||||||
|
"config",
|
||||||
|
"modules",
|
||||||
|
"visibleModuleKeys",
|
||||||
|
"workspaces",
|
||||||
|
"workspace_id",
|
||||||
|
"setActiveWorkspace",
|
||||||
|
"menu_order",
|
||||||
|
"position_x",
|
||||||
|
"is_active",
|
||||||
|
"reset",
|
||||||
|
"description",
|
||||||
|
"isModuleVisible",
|
||||||
|
"position_y",
|
||||||
|
"widget_key",
|
||||||
|
"isLoading",
|
||||||
|
"activeWorkspaceId",
|
||||||
|
"moduleKey",
|
||||||
|
"loading",
|
||||||
|
"setLoading",
|
||||||
|
"setMyWorkspaces",
|
||||||
|
"module_key",
|
||||||
|
"setContext",
|
||||||
|
"is_default",
|
||||||
|
"myWorkspaces",
|
||||||
|
"width",
|
||||||
|
"hasWorkspaces",
|
||||||
|
"icon"
|
||||||
|
],
|
||||||
|
"authStore": [
|
||||||
|
"currentTenant",
|
||||||
|
"setUser",
|
||||||
|
"setAuthenticated",
|
||||||
|
"first_name",
|
||||||
|
"is_system_admin",
|
||||||
|
"role",
|
||||||
|
"isAuthenticated",
|
||||||
|
"field_permissions",
|
||||||
|
"error",
|
||||||
|
"setError",
|
||||||
|
"perms",
|
||||||
|
"fieldPerms",
|
||||||
|
"isLoading",
|
||||||
|
"tenant",
|
||||||
|
"loading",
|
||||||
|
"setLoading",
|
||||||
|
"slug",
|
||||||
|
"permissions",
|
||||||
|
"last_name",
|
||||||
|
"user",
|
||||||
|
"isSystemAdmin",
|
||||||
|
"authed",
|
||||||
|
"setPermissions",
|
||||||
|
"avatar_url",
|
||||||
|
"tenants",
|
||||||
|
"setTenant",
|
||||||
|
"email",
|
||||||
|
"logout"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"usage": {},
|
||||||
|
"unused": {
|
||||||
|
"calendarStore": [
|
||||||
|
"setVisibleWeek",
|
||||||
|
"toggleCalendarVisibility",
|
||||||
|
"goToNextDay",
|
||||||
|
"goToToday",
|
||||||
|
"setVisibleMonth",
|
||||||
|
"setActiveCalendarId",
|
||||||
|
"goToPrevDay",
|
||||||
|
"goToPrevWeek",
|
||||||
|
"setRangeEnd",
|
||||||
|
"visibleMonth",
|
||||||
|
"rangeStart",
|
||||||
|
"visibleWeek",
|
||||||
|
"selectedEntry",
|
||||||
|
"activeCalendarId",
|
||||||
|
"rangeEnd",
|
||||||
|
"setVisibleDay",
|
||||||
|
"viewMode",
|
||||||
|
"goToNextMonth",
|
||||||
|
"visibleDay",
|
||||||
|
"date",
|
||||||
|
"setViewMode",
|
||||||
|
"entry",
|
||||||
|
"visibleCalendarIds",
|
||||||
|
"setRangeStart",
|
||||||
|
"mode",
|
||||||
|
"goToPrevMonth",
|
||||||
|
"setSelectedEntry",
|
||||||
|
"calendars",
|
||||||
|
"goToNextWeek",
|
||||||
|
"setCalendars"
|
||||||
|
],
|
||||||
|
"aiUIControlStore": [
|
||||||
|
"setAIActive",
|
||||||
|
"value",
|
||||||
|
"section",
|
||||||
|
"entity",
|
||||||
|
"pendingSettings",
|
||||||
|
"settings",
|
||||||
|
"clearPending",
|
||||||
|
"setActiveCommand",
|
||||||
|
"status",
|
||||||
|
"activeTab",
|
||||||
|
"pendingFilter",
|
||||||
|
"setConnected",
|
||||||
|
"activeModal",
|
||||||
|
"lastFeedback",
|
||||||
|
"connected",
|
||||||
|
"setLastFeedback",
|
||||||
|
"feedback",
|
||||||
|
"command_id",
|
||||||
|
"setPendingSettings",
|
||||||
|
"commandHistory",
|
||||||
|
"setActiveModal",
|
||||||
|
"active",
|
||||||
|
"addCommandToHistory",
|
||||||
|
"activeCommand",
|
||||||
|
"action",
|
||||||
|
"filter",
|
||||||
|
"setPendingFilter",
|
||||||
|
"aiActive",
|
||||||
|
"setActiveTab",
|
||||||
|
"modal"
|
||||||
|
],
|
||||||
|
"onboardingStore": [
|
||||||
|
"completed",
|
||||||
|
"isActive",
|
||||||
|
"goToStep",
|
||||||
|
"prev",
|
||||||
|
"start",
|
||||||
|
"skipped",
|
||||||
|
"complete",
|
||||||
|
"reset",
|
||||||
|
"next",
|
||||||
|
"skip",
|
||||||
|
"data",
|
||||||
|
"step"
|
||||||
|
],
|
||||||
|
"windowStore": [
|
||||||
|
"windows",
|
||||||
|
"minimizeWindow",
|
||||||
|
"toggleAiChat",
|
||||||
|
"nextZIndex",
|
||||||
|
"openWindow",
|
||||||
|
"const",
|
||||||
|
"restoreWindow",
|
||||||
|
"updateWindowSize",
|
||||||
|
"height",
|
||||||
|
"newWindow",
|
||||||
|
"zIndex",
|
||||||
|
"aiChatVisible",
|
||||||
|
"config",
|
||||||
|
"type",
|
||||||
|
"title",
|
||||||
|
"position",
|
||||||
|
"componentProps",
|
||||||
|
"setActiveWindow",
|
||||||
|
"updateWindowPosition",
|
||||||
|
"closeWindow",
|
||||||
|
"activeWindowId",
|
||||||
|
"null",
|
||||||
|
"component",
|
||||||
|
"size",
|
||||||
|
"toggleFullscreen",
|
||||||
|
"width"
|
||||||
|
],
|
||||||
|
"commandPaletteStore": [
|
||||||
|
"close",
|
||||||
|
"toggle",
|
||||||
|
"isOpen",
|
||||||
|
"open"
|
||||||
|
],
|
||||||
|
"pluginToolbarStore": [
|
||||||
|
"updateItem",
|
||||||
|
"value",
|
||||||
|
"setActivePlugin",
|
||||||
|
"label",
|
||||||
|
"registerItems",
|
||||||
|
"plugin",
|
||||||
|
"unregisterPlugin",
|
||||||
|
"updates",
|
||||||
|
"query",
|
||||||
|
"activePlugin",
|
||||||
|
"null",
|
||||||
|
"items",
|
||||||
|
"onClick"
|
||||||
|
],
|
||||||
|
"commStore": [
|
||||||
|
"file_type",
|
||||||
|
"sort_order",
|
||||||
|
"locked_by",
|
||||||
|
"is_direct",
|
||||||
|
"conversation_id",
|
||||||
|
"file_size",
|
||||||
|
"sender_id",
|
||||||
|
"content",
|
||||||
|
"participants",
|
||||||
|
"addMessage",
|
||||||
|
"role",
|
||||||
|
"setActiveConversation",
|
||||||
|
"setTyping",
|
||||||
|
"setConversations",
|
||||||
|
"created_at",
|
||||||
|
"participant_id",
|
||||||
|
"typingUsers",
|
||||||
|
"userIds",
|
||||||
|
"file_name",
|
||||||
|
"reply_to_id",
|
||||||
|
"participant_type",
|
||||||
|
"setMessages",
|
||||||
|
"conv",
|
||||||
|
"title",
|
||||||
|
"blocks",
|
||||||
|
"edited_at",
|
||||||
|
"activeConversationId",
|
||||||
|
"sender_type",
|
||||||
|
"file_id",
|
||||||
|
"last_msg_at",
|
||||||
|
"updateConversation",
|
||||||
|
"msgs",
|
||||||
|
"display_name",
|
||||||
|
"count",
|
||||||
|
"is_archived",
|
||||||
|
"reactions",
|
||||||
|
"loading",
|
||||||
|
"setLoading",
|
||||||
|
"created_by_type",
|
||||||
|
"attachments",
|
||||||
|
"created_by",
|
||||||
|
"last_msg_preview",
|
||||||
|
"setUnread",
|
||||||
|
"block_data",
|
||||||
|
"file_source",
|
||||||
|
"convId",
|
||||||
|
"is_pinned",
|
||||||
|
"messages",
|
||||||
|
"convs",
|
||||||
|
"content_format",
|
||||||
|
"unread_count",
|
||||||
|
"conversations",
|
||||||
|
"thumbnail_path",
|
||||||
|
"is_locked",
|
||||||
|
"last_msg_sender_type",
|
||||||
|
"block_type",
|
||||||
|
"unreadCounts",
|
||||||
|
"metadata"
|
||||||
|
],
|
||||||
|
"pluginStore": [
|
||||||
|
"permission",
|
||||||
|
"is_core",
|
||||||
|
"getDetailTabsForEntity",
|
||||||
|
"default_value",
|
||||||
|
"entity",
|
||||||
|
"setManifests",
|
||||||
|
"loaded",
|
||||||
|
"badge_key",
|
||||||
|
"error",
|
||||||
|
"entityType",
|
||||||
|
"parent",
|
||||||
|
"row_span",
|
||||||
|
"custom_fields",
|
||||||
|
"detail_tabs",
|
||||||
|
"dashboard_widgets",
|
||||||
|
"col_span",
|
||||||
|
"reset",
|
||||||
|
"order",
|
||||||
|
"getCustomFieldsForEntity",
|
||||||
|
"getAllDashboardWidgets",
|
||||||
|
"display_name",
|
||||||
|
"protected",
|
||||||
|
"version",
|
||||||
|
"label",
|
||||||
|
"required",
|
||||||
|
"path",
|
||||||
|
"getAllMenuItems",
|
||||||
|
"loading",
|
||||||
|
"getAllSettingsPages",
|
||||||
|
"setLoading",
|
||||||
|
"field_type",
|
||||||
|
"group",
|
||||||
|
"options",
|
||||||
|
"component",
|
||||||
|
"page_routes",
|
||||||
|
"entity_type",
|
||||||
|
"manifests",
|
||||||
|
"label_key",
|
||||||
|
"settings_pages",
|
||||||
|
"getAllPageRoutes",
|
||||||
|
"menu_items",
|
||||||
|
"icon",
|
||||||
|
"setError"
|
||||||
|
],
|
||||||
|
"themeStore": [
|
||||||
|
"borderRadius",
|
||||||
|
"applyTheme",
|
||||||
|
"toggleDarkMode",
|
||||||
|
"config",
|
||||||
|
"loadFromStorage",
|
||||||
|
"accentColor",
|
||||||
|
"DEFAULT_THEME",
|
||||||
|
"primaryColor",
|
||||||
|
"target",
|
||||||
|
"darkMode",
|
||||||
|
"amount",
|
||||||
|
"result",
|
||||||
|
"setTheme",
|
||||||
|
"saveToStorage",
|
||||||
|
"base",
|
||||||
|
"fontFamily",
|
||||||
|
"scales"
|
||||||
|
],
|
||||||
|
"uiStore": [
|
||||||
|
"message",
|
||||||
|
"suggestionSidebarOpen",
|
||||||
|
"setAISidebarCollapsed",
|
||||||
|
"openAISidebarProactive",
|
||||||
|
"locale",
|
||||||
|
"theme",
|
||||||
|
"clearToasts",
|
||||||
|
"toggleAISidebar",
|
||||||
|
"setLocale",
|
||||||
|
"open",
|
||||||
|
"type",
|
||||||
|
"setTheme",
|
||||||
|
"collapsed",
|
||||||
|
"toggleSidebar",
|
||||||
|
"setAISidebarTab",
|
||||||
|
"aiSidebarCollapsed",
|
||||||
|
"clearNotifications",
|
||||||
|
"setSidebarOpen",
|
||||||
|
"setMessageSidebarCollapsed",
|
||||||
|
"removeToast",
|
||||||
|
"notifications",
|
||||||
|
"toggleSuggestionSidebar",
|
||||||
|
"toggleMessageSidebar",
|
||||||
|
"toasts",
|
||||||
|
"messageSidebarCollapsed",
|
||||||
|
"toast",
|
||||||
|
"removeNotification",
|
||||||
|
"index",
|
||||||
|
"addToast",
|
||||||
|
"sidebarOpen",
|
||||||
|
"aiSidebarTab"
|
||||||
|
],
|
||||||
|
"workspaceStore": [
|
||||||
|
"is_visible",
|
||||||
|
"widgets",
|
||||||
|
"context",
|
||||||
|
"height",
|
||||||
|
"config",
|
||||||
|
"modules",
|
||||||
|
"visibleModuleKeys",
|
||||||
|
"workspaces",
|
||||||
|
"workspace_id",
|
||||||
|
"setActiveWorkspace",
|
||||||
|
"menu_order",
|
||||||
|
"position_x",
|
||||||
|
"is_active",
|
||||||
|
"reset",
|
||||||
|
"description",
|
||||||
|
"isModuleVisible",
|
||||||
|
"position_y",
|
||||||
|
"widget_key",
|
||||||
|
"isLoading",
|
||||||
|
"activeWorkspaceId",
|
||||||
|
"moduleKey",
|
||||||
|
"loading",
|
||||||
|
"setLoading",
|
||||||
|
"setMyWorkspaces",
|
||||||
|
"module_key",
|
||||||
|
"setContext",
|
||||||
|
"is_default",
|
||||||
|
"myWorkspaces",
|
||||||
|
"width",
|
||||||
|
"hasWorkspaces",
|
||||||
|
"icon"
|
||||||
|
],
|
||||||
|
"authStore": [
|
||||||
|
"currentTenant",
|
||||||
|
"setUser",
|
||||||
|
"setAuthenticated",
|
||||||
|
"first_name",
|
||||||
|
"is_system_admin",
|
||||||
|
"role",
|
||||||
|
"isAuthenticated",
|
||||||
|
"field_permissions",
|
||||||
|
"error",
|
||||||
|
"setError",
|
||||||
|
"perms",
|
||||||
|
"fieldPerms",
|
||||||
|
"isLoading",
|
||||||
|
"tenant",
|
||||||
|
"loading",
|
||||||
|
"setLoading",
|
||||||
|
"slug",
|
||||||
|
"permissions",
|
||||||
|
"last_name",
|
||||||
|
"user",
|
||||||
|
"isSystemAdmin",
|
||||||
|
"authed",
|
||||||
|
"setPermissions",
|
||||||
|
"avatar_url",
|
||||||
|
"tenants",
|
||||||
|
"setTenant",
|
||||||
|
"email",
|
||||||
|
"logout"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Trace API Contracts — Find Frontend↔Backend Verkabelungsfehler.
|
||||||
|
|
||||||
|
Compares every frontend API call with the backend response fields.
|
||||||
|
Mismatches = Verkabelungsfehler.
|
||||||
|
|
||||||
|
Usage: python3 scripts/test_suite/trace_api_contracts.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Project root
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
|
FRONTEND_SRC = ROOT / "frontend" / "src"
|
||||||
|
BACKEND_APP = ROOT / "app"
|
||||||
|
|
||||||
|
|
||||||
|
def find_frontend_api_calls() -> list[dict]:
|
||||||
|
"""Find all API calls in frontend source."""
|
||||||
|
calls = []
|
||||||
|
api_dir = FRONTEND_SRC / "api"
|
||||||
|
if not api_dir.exists():
|
||||||
|
return calls
|
||||||
|
|
||||||
|
for ts_file in api_dir.rglob("*.ts"):
|
||||||
|
try:
|
||||||
|
content = ts_file.read_text(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
rel_path = str(ts_file.relative_to(FRONTEND_SRC))
|
||||||
|
|
||||||
|
# Find apiGet/apiPost/apiPut/apiPatch/apiDelete calls with URL patterns
|
||||||
|
patterns = [
|
||||||
|
r"apiGet(?:<[^>]+>)?\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
|
||||||
|
r"apiPost(?:<[^>]+>)?\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
|
||||||
|
r"apiPut(?:<[^>]+>)?\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
|
||||||
|
r"apiPatch(?:<[^>]+>)?\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
|
||||||
|
r"apiDelete(?:<[^>]+>)?\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
|
||||||
|
r"\bM\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
|
||||||
|
r"\bj\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
|
||||||
|
r"\bSt\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
|
||||||
|
r"\byt\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
|
||||||
|
r"\bxt\s*\(\s*[`'\"]([^`'\"]+)[`'\"]\s*",
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
for match in re.finditer(pattern, content):
|
||||||
|
url = match.group(1)
|
||||||
|
# Normalize URL — remove template literals, query params
|
||||||
|
url_clean = re.sub(r"\$\{[^}]+\}", "{param}", url)
|
||||||
|
url_clean = url_clean.split("?")[0].split("#")[0]
|
||||||
|
# Determine method from function name
|
||||||
|
method = "GET"
|
||||||
|
if "Post" in pattern or "\bj\s*" in pattern:
|
||||||
|
method = "POST"
|
||||||
|
elif "Put" in pattern:
|
||||||
|
method = "PUT"
|
||||||
|
elif "Patch" in pattern or "\byt\s*" in pattern:
|
||||||
|
method = "PATCH"
|
||||||
|
elif "Delete" in pattern or "\bxt\s*" in pattern:
|
||||||
|
method = "DELETE"
|
||||||
|
|
||||||
|
calls.append({
|
||||||
|
"file": rel_path,
|
||||||
|
"method": method,
|
||||||
|
"url": url_clean,
|
||||||
|
"raw_url": url,
|
||||||
|
})
|
||||||
|
|
||||||
|
return calls
|
||||||
|
|
||||||
|
|
||||||
|
def find_frontend_expected_fields() -> dict[str, list[str]]:
|
||||||
|
"""Find TypeScript interfaces/types that define expected API response fields."""
|
||||||
|
fields_by_url = defaultdict(list)
|
||||||
|
api_dir = FRONTEND_SRC / "api"
|
||||||
|
if not api_dir.exists():
|
||||||
|
return fields_by_url
|
||||||
|
|
||||||
|
for ts_file in api_dir.rglob("*.ts"):
|
||||||
|
try:
|
||||||
|
content = ts_file.read_text(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Find interface definitions
|
||||||
|
for match in re.finditer(r"interface\s+(\w+)\s*\{([^}]+)\}", content):
|
||||||
|
iface_name = match.group(1)
|
||||||
|
body = match.group(2)
|
||||||
|
field_names = re.findall(r"(\w+)\s*[?:]", body)
|
||||||
|
fields_by_url[iface_name] = field_names
|
||||||
|
|
||||||
|
return fields_by_url
|
||||||
|
|
||||||
|
|
||||||
|
def find_backend_response_fields() -> list[dict]:
|
||||||
|
"""Find backend response fields from routes."""
|
||||||
|
responses = []
|
||||||
|
routes_dir = BACKEND_APP / "routes"
|
||||||
|
if not routes_dir.exists():
|
||||||
|
return responses
|
||||||
|
|
||||||
|
for py_file in routes_dir.rglob("*.py"):
|
||||||
|
try:
|
||||||
|
content = py_file.read_text(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
rel_path = str(py_file.relative_to(BACKEND_APP))
|
||||||
|
|
||||||
|
# Find response_model definitions
|
||||||
|
for match in re.finditer(r"response_model\s*=\s*(\w+)", content):
|
||||||
|
responses.append({
|
||||||
|
"file": rel_path,
|
||||||
|
"response_model": match.group(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Find content= dict definitions (inline responses)
|
||||||
|
for match in re.finditer(r"content\s*=\s*\{([^}]+)\}", content, re.DOTALL):
|
||||||
|
body = match.group(1)
|
||||||
|
fields = re.findall(r"['\"](\w+)['\"]\s*:", body)
|
||||||
|
if fields:
|
||||||
|
responses.append({
|
||||||
|
"file": rel_path,
|
||||||
|
"fields": fields,
|
||||||
|
"type": "inline",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Find return dict patterns
|
||||||
|
for match in re.finditer(r"return\s*\{([^}]+)\}", content, re.DOTALL):
|
||||||
|
body = match.group(1)
|
||||||
|
fields = re.findall(r"['\"](\w+)['\"]\s*:", body)
|
||||||
|
if fields:
|
||||||
|
responses.append({
|
||||||
|
"file": rel_path,
|
||||||
|
"fields": fields,
|
||||||
|
"type": "return_dict",
|
||||||
|
})
|
||||||
|
|
||||||
|
return responses
|
||||||
|
|
||||||
|
|
||||||
|
def find_backend_endpoints() -> list[dict]:
|
||||||
|
"""Find all backend endpoints from route decorators."""
|
||||||
|
endpoints = []
|
||||||
|
routes_dir = BACKEND_APP / "routes"
|
||||||
|
if not routes_dir.exists():
|
||||||
|
return endpoints
|
||||||
|
|
||||||
|
for py_file in routes_dir.rglob("*.py"):
|
||||||
|
try:
|
||||||
|
content = py_file.read_text(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
rel_path = str(py_file.relative_to(BACKEND_APP))
|
||||||
|
|
||||||
|
# Find router decorators
|
||||||
|
for match in re.finditer(r"@router\.(get|post|put|patch|delete)\s*\(\s*[\"']([^\"']+)[\"']", content):
|
||||||
|
method = match.group(1).upper()
|
||||||
|
path = match.group(2)
|
||||||
|
endpoints.append({
|
||||||
|
"file": rel_path,
|
||||||
|
"method": method,
|
||||||
|
"path": path,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Also find plugin routes
|
||||||
|
plugins_dir = BACKEND_APP / "plugins" / "builtins"
|
||||||
|
if plugins_dir.exists():
|
||||||
|
for py_file in plugins_dir.rglob("*.py"):
|
||||||
|
try:
|
||||||
|
content = py_file.read_text(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
rel_path = str(py_file.relative_to(BACKEND_APP))
|
||||||
|
for match in re.finditer(r"@router\.(get|post|put|patch|delete)\s*\(\s*[\"']([^\"']+)[\"']", content):
|
||||||
|
method = match.group(1).upper()
|
||||||
|
path = match.group(2)
|
||||||
|
endpoints.append({
|
||||||
|
"file": rel_path,
|
||||||
|
"method": method,
|
||||||
|
"path": path,
|
||||||
|
"is_plugin": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
return endpoints
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_path(path: str) -> str:
|
||||||
|
"""Normalize a path for comparison."""
|
||||||
|
# Remove leading /api/v1 if present
|
||||||
|
path = re.sub(r"^/api/v1/", "/", path)
|
||||||
|
# Replace {param} patterns
|
||||||
|
path = re.sub(r"\{[^}]+\}", "{param}", path)
|
||||||
|
# Remove trailing slash
|
||||||
|
path = path.rstrip("/")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def check_frontend_calls_have_backend(frontend_calls, backend_endpoints) -> list[dict]:
|
||||||
|
"""Check if every frontend API call has a matching backend endpoint."""
|
||||||
|
backend_paths = set()
|
||||||
|
for ep in backend_endpoints:
|
||||||
|
normalized = normalize_path(ep["path"])
|
||||||
|
backend_paths.add((ep["method"], normalized))
|
||||||
|
|
||||||
|
mismatches = []
|
||||||
|
for call in frontend_calls:
|
||||||
|
url = call["url"]
|
||||||
|
# Remove /api/v1 prefix if present
|
||||||
|
url_normalized = normalize_path(url)
|
||||||
|
key = (call["method"], url_normalized)
|
||||||
|
|
||||||
|
if key not in backend_paths:
|
||||||
|
# Try without method (some endpoints might have different method mapping)
|
||||||
|
found = False
|
||||||
|
for bm, bp in backend_paths:
|
||||||
|
if bp == url_normalized:
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
if not found:
|
||||||
|
mismatches.append({
|
||||||
|
"type": "frontend_call_no_backend",
|
||||||
|
"method": call["method"],
|
||||||
|
"url": url,
|
||||||
|
"file": call["file"],
|
||||||
|
"severity": "HIGH",
|
||||||
|
"message": f"Frontend calls {call['method']} {url} but no matching backend endpoint found",
|
||||||
|
})
|
||||||
|
|
||||||
|
return mismatches
|
||||||
|
|
||||||
|
|
||||||
|
def check_backend_endpoints_have_frontend(frontend_calls, backend_endpoints) -> list[dict]:
|
||||||
|
"""Check if every backend endpoint is called by the frontend."""
|
||||||
|
frontend_paths = set()
|
||||||
|
for call in frontend_calls:
|
||||||
|
url_normalized = normalize_path(call["url"])
|
||||||
|
frontend_paths.add((call["method"], url_normalized))
|
||||||
|
|
||||||
|
mismatches = []
|
||||||
|
for ep in backend_endpoints:
|
||||||
|
normalized = normalize_path(ep["path"])
|
||||||
|
key = (ep["method"], normalized)
|
||||||
|
|
||||||
|
if key not in frontend_paths:
|
||||||
|
mismatches.append({
|
||||||
|
"type": "backend_endpoint_no_frontend",
|
||||||
|
"method": ep["method"],
|
||||||
|
"path": ep["path"],
|
||||||
|
"file": ep["file"],
|
||||||
|
"severity": "LOW",
|
||||||
|
"message": f"Backend endpoint {ep['method']} {ep['path']} is never called by frontend",
|
||||||
|
})
|
||||||
|
|
||||||
|
return mismatches
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 70)
|
||||||
|
print("TRACE API CONTRACTS — Frontend ↔ Backend Verkabelungsfehler")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
print("\n[1] Scanning frontend API calls...")
|
||||||
|
frontend_calls = find_frontend_api_calls()
|
||||||
|
print(f" Found {len(frontend_calls)} API calls in frontend")
|
||||||
|
|
||||||
|
print("\n[2] Scanning backend endpoints...")
|
||||||
|
backend_endpoints = find_backend_endpoints()
|
||||||
|
print(f" Found {len(backend_endpoints)} endpoints in backend")
|
||||||
|
|
||||||
|
print("\n[3] Scanning backend response fields...")
|
||||||
|
backend_responses = find_backend_response_fields()
|
||||||
|
print(f" Found {len(backend_responses)} response definitions")
|
||||||
|
|
||||||
|
print("\n[4] Scanning frontend expected fields...")
|
||||||
|
frontend_fields = find_frontend_expected_fields()
|
||||||
|
print(f" Found {len(frontend_fields)} TypeScript interfaces")
|
||||||
|
|
||||||
|
print("\n[5] Checking frontend calls have matching backend...")
|
||||||
|
missing_backend = check_frontend_calls_have_backend(frontend_calls, backend_endpoints)
|
||||||
|
print(f" Found {len(missing_backend)} frontend calls without backend endpoint")
|
||||||
|
|
||||||
|
print("\n[6] Checking backend endpoints are called by frontend...")
|
||||||
|
missing_frontend = check_backend_endpoints_have_frontend(frontend_calls, backend_endpoints)
|
||||||
|
print(f" Found {len(missing_frontend)} backend endpoints never called by frontend")
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
all_issues = missing_backend + missing_frontend
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("SUMMARY")
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"Frontend API calls: {len(frontend_calls)}")
|
||||||
|
print(f"Backend endpoints: {len(backend_endpoints)}")
|
||||||
|
print(f"Response definitions: {len(backend_responses)}")
|
||||||
|
print(f"TS interfaces: {len(frontend_fields)}")
|
||||||
|
print(f"\nISSUES FOUND: {len(all_issues)}")
|
||||||
|
print(f" HIGH (frontend calls missing backend): {len(missing_backend)}")
|
||||||
|
print(f" LOW (backend endpoints not called): {len(missing_frontend)}")
|
||||||
|
|
||||||
|
if missing_backend:
|
||||||
|
print("\n--- HIGH SEVERITY: Frontend calls without backend ---")
|
||||||
|
for m in missing_backend[:30]:
|
||||||
|
print(f" [{m['method']}] {m['url']} (in {m['file']})")
|
||||||
|
if len(missing_backend) > 30:
|
||||||
|
print(f" ... and {len(missing_backend) - 30} more")
|
||||||
|
|
||||||
|
if missing_frontend:
|
||||||
|
print("\n--- LOW SEVERITY: Backend endpoints never called by frontend ---")
|
||||||
|
for m in missing_frontend[:30]:
|
||||||
|
print(f" [{m['method']}] {m['path']} (in {m['file']})")
|
||||||
|
if len(missing_frontend) > 30:
|
||||||
|
print(f" ... and {len(missing_frontend) - 30} more")
|
||||||
|
|
||||||
|
# Write results to JSON
|
||||||
|
results_file = ROOT / "scripts" / "test_suite" / "results_trace_api_contracts.json"
|
||||||
|
with open(results_file, "w") as f:
|
||||||
|
json.dump({
|
||||||
|
"frontend_calls": frontend_calls,
|
||||||
|
"backend_endpoints": backend_endpoints,
|
||||||
|
"issues": all_issues,
|
||||||
|
"summary": {
|
||||||
|
"total_frontend_calls": len(frontend_calls),
|
||||||
|
"total_backend_endpoints": len(backend_endpoints),
|
||||||
|
"total_issues": len(all_issues),
|
||||||
|
"high_severity": len(missing_backend),
|
||||||
|
"low_severity": len(missing_frontend),
|
||||||
|
},
|
||||||
|
}, f, indent=2, default=str)
|
||||||
|
print(f"\nResults written to {results_file}")
|
||||||
|
|
||||||
|
return len(all_issues)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Trace Contracts — Find Contract attribute mismatches (like GraphRagContract)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import ast, re, sys, json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
|
APP = ROOT / "app"
|
||||||
|
|
||||||
|
def find_contracts() -> list[dict]:
|
||||||
|
contracts = []
|
||||||
|
for py in APP.rglob("*.py"):
|
||||||
|
try: content = py.read_text(); tree = ast.parse(content)
|
||||||
|
except: continue
|
||||||
|
rel = str(py.relative_to(ROOT))
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.ClassDef):
|
||||||
|
if "Contract" in node.name or node.name.endswith("Contract"):
|
||||||
|
attrs = []
|
||||||
|
for item in node.body:
|
||||||
|
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
|
||||||
|
attrs.append(item.target.id)
|
||||||
|
elif isinstance(item, ast.Assign):
|
||||||
|
for t in item.targets:
|
||||||
|
if isinstance(t, ast.Name): attrs.append(t.id)
|
||||||
|
contracts.append({"name": node.name, "file": rel, "attributes": attrs})
|
||||||
|
return contracts
|
||||||
|
|
||||||
|
def find_contract_access() -> list[dict]:
|
||||||
|
accesses = []
|
||||||
|
for py in APP.rglob("*.py"):
|
||||||
|
try: content = py.read_text()
|
||||||
|
except: continue
|
||||||
|
rel = str(py.relative_to(ROOT))
|
||||||
|
for m in re.finditer(r'(\w+Contract)\.(\w+)', content):
|
||||||
|
accesses.append({"contract": m.group(1), "attribute": m.group(2), "file": rel})
|
||||||
|
return accesses
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 70)
|
||||||
|
print("TRACE CONTRACTS — Contract Attribute Mismatches")
|
||||||
|
print("=" * 70)
|
||||||
|
contracts = find_contracts()
|
||||||
|
accesses = find_contract_access()
|
||||||
|
contract_attrs = {c["name"]: set(c["attributes"]) for c in contracts}
|
||||||
|
mismatches = []
|
||||||
|
for acc in accesses:
|
||||||
|
cname = acc["contract"]
|
||||||
|
attr = acc["attribute"]
|
||||||
|
if cname in contract_attrs and attr not in contract_attrs[cname]:
|
||||||
|
mismatches.append(acc)
|
||||||
|
print(f"\nContracts found: {len(contracts)}")
|
||||||
|
print(f"Contract accesses: {len(accesses)}")
|
||||||
|
print(f"\nMISMATCHES: {len(mismatches)}")
|
||||||
|
if mismatches:
|
||||||
|
print("\n--- Contract attribute mismatches ---")
|
||||||
|
for m in mismatches: print(f" {m['contract']}.{m['attribute']} (in {m['file']})")
|
||||||
|
results = {"contracts": contracts, "accesses": accesses, "mismatches": mismatches}
|
||||||
|
with open(ROOT / "scripts" / "test_suite" / "results_trace_contracts.json", "w") as f:
|
||||||
|
json.dump(results, f, indent=2, default=str)
|
||||||
|
return len(mismatches)
|
||||||
|
|
||||||
|
if __name__ == "__main__": sys.exit(main())
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Trace Functions — Find dead functions (defined but never called)."""
|
||||||
|
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"
|
||||||
|
|
||||||
|
# Critical functions that MUST be called
|
||||||
|
CRITICAL_FUNCTIONS = [
|
||||||
|
"seed_admin", "seed_default_workspace", "sync_plugin_schema",
|
||||||
|
"init_permission_registry", "register_default_entities", "register_default_history_hooks",
|
||||||
|
"discover_builtins", "load_tmp_chats", "initialize_agent",
|
||||||
|
]
|
||||||
|
|
||||||
|
def find_function_defs_py() -> list[dict]:
|
||||||
|
defs = []
|
||||||
|
for py in APP.rglob("*.py"):
|
||||||
|
try: content = py.read_text(); tree = ast.parse(content)
|
||||||
|
except: continue
|
||||||
|
rel = str(py.relative_to(ROOT))
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||||
|
if not node.name.startswith("_") and len(node.name) > 3:
|
||||||
|
defs.append({"name": node.name, "file": rel, "line": node.lineno, "lang": "py"})
|
||||||
|
return defs
|
||||||
|
|
||||||
|
def find_function_calls_py() -> set[str]:
|
||||||
|
calls = set()
|
||||||
|
for py in APP.rglob("*.py"):
|
||||||
|
try: content = py.read_text(); tree = ast.parse(content)
|
||||||
|
except: continue
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.Call):
|
||||||
|
if isinstance(node.func, ast.Name):
|
||||||
|
calls.add(node.func.id)
|
||||||
|
elif isinstance(node.func, ast.Attribute):
|
||||||
|
calls.add(node.func.attr)
|
||||||
|
return calls
|
||||||
|
|
||||||
|
def find_function_defs_ts() -> list[dict]:
|
||||||
|
defs = []
|
||||||
|
for ts in FRONTEND.rglob("*.ts"):
|
||||||
|
try: content = ts.read_text()
|
||||||
|
except: continue
|
||||||
|
rel = str(ts.relative_to(ROOT))
|
||||||
|
for m in re.finditer(r'(?:export\s+)?(?:async\s+)?function\s+(\w+)', content):
|
||||||
|
name = m.group(1)
|
||||||
|
if not name.startswith("_") and len(name) > 3:
|
||||||
|
defs.append({"name": name, "file": rel, "lang": "ts"})
|
||||||
|
for m in re.finditer(r'(?:export\s+)?const\s+(\w+)\s*=', content):
|
||||||
|
name = m.group(1)
|
||||||
|
if not name.startswith("_") and len(name) > 3:
|
||||||
|
defs.append({"name": name, "file": rel, "lang": "ts"})
|
||||||
|
return defs
|
||||||
|
|
||||||
|
def find_function_calls_ts() -> set[str]:
|
||||||
|
calls = set()
|
||||||
|
for ts in FRONTEND.rglob("*.ts"):
|
||||||
|
try: content = ts.read_text()
|
||||||
|
except: continue
|
||||||
|
# Find function calls: word followed by (
|
||||||
|
for m in re.finditer(r'\b(\w+)\s*\(', content):
|
||||||
|
calls.add(m.group(1))
|
||||||
|
return calls
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 70)
|
||||||
|
print("TRACE FUNCTIONS — Dead Functions (defined but never called)")
|
||||||
|
print("=" * 70)
|
||||||
|
py_defs = find_function_defs_py()
|
||||||
|
py_calls = find_function_calls_py()
|
||||||
|
ts_defs = find_function_defs_ts()
|
||||||
|
ts_calls = find_function_calls_ts()
|
||||||
|
py_dead = [d for d in py_defs if d["name"] not in py_calls]
|
||||||
|
ts_dead = [d for d in ts_defs if d["name"] not in ts_calls]
|
||||||
|
critical_dead = [d for d in py_dead if d["name"] in CRITICAL_FUNCTIONS]
|
||||||
|
print(f"\nPython functions defined: {len(py_defs)}")
|
||||||
|
print(f"Python functions called: {len(py_calls)}")
|
||||||
|
print(f"Python dead functions: {len(py_dead)}")
|
||||||
|
print(f"TS functions defined: {len(ts_defs)}")
|
||||||
|
print(f"TS functions called: {len(ts_calls)}")
|
||||||
|
print(f"TS dead functions: {len(ts_dead)}")
|
||||||
|
print(f"\nCRITICAL dead functions: {len(critical_dead)}")
|
||||||
|
if critical_dead:
|
||||||
|
print("\n--- CRITICAL: Functions that MUST be called but aren't ---")
|
||||||
|
for d in critical_dead: print(f" {d['name']} (in {d['file']}:{d.get('line','?')})")
|
||||||
|
results = {"py_defs": len(py_defs), "py_dead": len(py_dead), "ts_defs": len(ts_defs), "ts_dead": len(ts_dead), "critical_dead": critical_dead}
|
||||||
|
with open(ROOT / "scripts" / "test_suite" / "results_trace_functions.json", "w") as f:
|
||||||
|
json.dump(results, f, indent=2, default=str)
|
||||||
|
return len(critical_dead)
|
||||||
|
|
||||||
|
if __name__ == "__main__": sys.exit(main())
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Trace Hooks — Find orphan hooks (registered but never triggered) and orphan triggers (triggered but never received)."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import re, sys, json
|
||||||
|
from pathlib import Path
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
|
APP = ROOT / "app"
|
||||||
|
|
||||||
|
def find_hook_registrations() -> list[dict]:
|
||||||
|
regs = []
|
||||||
|
for py in APP.rglob("*.py"):
|
||||||
|
try: content = py.read_text()
|
||||||
|
except: continue
|
||||||
|
rel = str(py.relative_to(ROOT))
|
||||||
|
for m in re.finditer(r'register_(?:action|filter)s?\s*\(\s*["\']([^"\']+)['"]', content):
|
||||||
|
regs.append({"file": rel, "hook_type": "action_or_filter", "hook_name": m.group(1)})
|
||||||
|
for m in re.finditer(r'register_actions_by_owner\s*\(\s*["\']([^"\']+)['"]', content):
|
||||||
|
regs.append({"file": rel, "hook_type": "action_group", "hook_name": m.group(1)})
|
||||||
|
return regs
|
||||||
|
|
||||||
|
def find_hook_triggers() -> list[dict]:
|
||||||
|
triggers = []
|
||||||
|
for py in APP.rglob("*.py"):
|
||||||
|
try: content = py.read_text()
|
||||||
|
except: continue
|
||||||
|
rel = str(py.relative_to(ROOT))
|
||||||
|
for m in re.finditer(r'do_action\s*\(\s*["\']([^"\']+)['"]', content):
|
||||||
|
triggers.append({"file": rel, "hook_name": m.group(1), "type": "action"})
|
||||||
|
for m in re.finditer(r'apply_filters?\s*\(\s*["\']([^"\']+)['"]', content):
|
||||||
|
triggers.append({"file": rel, "hook_name": m.group(1), "type": "filter"})
|
||||||
|
for m in re.finditer(r'trigger\s*\(\s*["\']([^"\']+)['"]', content):
|
||||||
|
triggers.append({"file": rel, "hook_name": m.group(1), "type": "trigger"})
|
||||||
|
return triggers
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 70)
|
||||||
|
print("TRACE HOOKS — Orphan Hooks & Triggers")
|
||||||
|
print("=" * 70)
|
||||||
|
regs = find_hook_registrations()
|
||||||
|
triggers = find_hook_triggers()
|
||||||
|
reg_names = {r["hook_name"] for r in regs}
|
||||||
|
trig_names = {t["hook_name"] for t in triggers}
|
||||||
|
orphans_reg = [r for r in regs if r["hook_name"] not in trig_names]
|
||||||
|
orphans_trig = [t for t in triggers if t["hook_name"] not in reg_names]
|
||||||
|
print(f"\nHook registrations: {len(regs)}")
|
||||||
|
print(f"Hook triggers: {len(triggers)}")
|
||||||
|
print(f"\nISSUES: {len(orphans_reg) + len(orphans_trig)}")
|
||||||
|
print(f" Registered but never triggered: {len(orphans_reg)}")
|
||||||
|
print(f" Triggered but never received: {len(orphans_trig)}")
|
||||||
|
if orphans_reg:
|
||||||
|
print("\n--- Registered but never triggered ---")
|
||||||
|
for o in orphans_reg[:20]: print(f" {o['hook_name']} (in {o['file']})")
|
||||||
|
if orphans_trig:
|
||||||
|
print("\n--- Triggered but never received ---")
|
||||||
|
for o in orphans_trig[:20]: print(f" {o['hook_name']} (in {o['file']})")
|
||||||
|
results = {"registrations": regs, "triggers": triggers, "orphan_registrations": orphans_reg, "orphan_triggers": orphans_trig}
|
||||||
|
with open(ROOT / "scripts" / "test_suite" / "results_trace_hooks.json", "w") as f:
|
||||||
|
json.dump(results, f, indent=2, default=str)
|
||||||
|
return len(orphans_reg) + len(orphans_trig)
|
||||||
|
|
||||||
|
if __name__ == "__main__": sys.exit(main())
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Trace Imports — Find broken/missing imports in backend."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import ast, sys, json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
|
APP = ROOT / "app"
|
||||||
|
|
||||||
|
def find_imports() -> list[dict]:
|
||||||
|
imports = []
|
||||||
|
for py in APP.rglob("*.py"):
|
||||||
|
try: content = py.read_text(); tree = ast.parse(content)
|
||||||
|
except: continue
|
||||||
|
rel = str(py.relative_to(ROOT))
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.ImportFrom):
|
||||||
|
if node.module and node.module.startswith("app."):
|
||||||
|
for alias in node.names:
|
||||||
|
imports.append({"file": rel, "module": node.module, "name": alias.name, "line": node.lineno})
|
||||||
|
return imports
|
||||||
|
|
||||||
|
def check_imports_exist(imports: list[dict]) -> list[dict]:
|
||||||
|
broken = []
|
||||||
|
for imp in imports:
|
||||||
|
module_path = imp["module"].replace(".", "/") + ".py"
|
||||||
|
full_path = ROOT / module_path
|
||||||
|
if not full_path.exists():
|
||||||
|
# Try as package
|
||||||
|
pkg_path = ROOT / module_path.replace(".py", "/__init__.py")
|
||||||
|
if not pkg_path.exists():
|
||||||
|
broken.append({**imp, "issue": "Module not found"})
|
||||||
|
continue
|
||||||
|
# Check if name exists in module
|
||||||
|
try:
|
||||||
|
content = full_path.read_text()
|
||||||
|
if imp["name"] not in content and imp["name"] != "__init__":
|
||||||
|
# Could be imported via __init__.py
|
||||||
|
if not full_path.name == "__init__.py":
|
||||||
|
broken.append({**imp, "issue": f"Name '{imp['name']}' not found in module"})
|
||||||
|
except: pass
|
||||||
|
return broken
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 70)
|
||||||
|
print("TRACE IMPORTS — Broken/Missing Imports")
|
||||||
|
print("=" * 70)
|
||||||
|
imports = find_imports()
|
||||||
|
broken = check_imports_exist(imports)
|
||||||
|
print(f"\nTotal imports: {len(imports)}")
|
||||||
|
print(f"Broken imports: {len(broken)}")
|
||||||
|
if broken:
|
||||||
|
print("\n--- Broken imports ---")
|
||||||
|
for b in broken[:30]: print(f" {b['file']}:{b['line']} — {b['module']}.{b['name']} — {b['issue']}")
|
||||||
|
results = {"total_imports": len(imports), "broken": broken}
|
||||||
|
with open(ROOT / "scripts" / "test_suite" / "results_trace_imports.json", "w") as f:
|
||||||
|
json.dump(results, f, indent=2, default=str)
|
||||||
|
return len(broken)
|
||||||
|
|
||||||
|
if __name__ == "__main__": sys.exit(main())
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#!/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]:
|
||||||
|
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
|
||||||
|
manifest_file = d / "manifest.py"
|
||||||
|
plugin_info = {"name": d.name, "path": str(d.relative_to(ROOT)), "has_manifest": manifest_file.exists()}
|
||||||
|
if manifest_file.exists():
|
||||||
|
try: content = manifest_file.read_text()
|
||||||
|
except: continue
|
||||||
|
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 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.py", "severity": "HIGH"})
|
||||||
|
if p.get("has_menu_items") and p["name"] not in frontend_refs:
|
||||||
|
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())
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Trace Stores — Find unused store actions/state in frontend."""
|
||||||
|
from __future__ import annotations
|
||||||
|
import re, sys, json
|
||||||
|
from pathlib import Path
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||||
|
FRONTEND = ROOT / "frontend" / "src"
|
||||||
|
|
||||||
|
def find_store_actions() -> dict[str, list[str]]:
|
||||||
|
stores = {}
|
||||||
|
store_dir = FRONTEND / "store"
|
||||||
|
if not store_dir.exists(): return stores
|
||||||
|
for ts in store_dir.glob("*.ts"):
|
||||||
|
try: content = ts.read_text()
|
||||||
|
except: continue
|
||||||
|
store_name = ts.stem
|
||||||
|
actions = []
|
||||||
|
# Find action names in zustand store definitions
|
||||||
|
for m in re.finditer(r'(\w+)\s*:', content):
|
||||||
|
name = m.group(1)
|
||||||
|
if name not in ('state', 'set', 'get', 'name', 'storage', 'partialize') and len(name) > 3:
|
||||||
|
actions.append(name)
|
||||||
|
stores[store_name] = list(set(actions))
|
||||||
|
return stores
|
||||||
|
|
||||||
|
def find_store_usage() -> dict[str, set[str]]:
|
||||||
|
usage = defaultdict(set)
|
||||||
|
for ts in FRONTEND.rglob("*.ts"):
|
||||||
|
try: content = ts.read_text()
|
||||||
|
except: continue
|
||||||
|
for store_name in ['authStore', 'uiStore', 'themeStore', 'workspaceStore', 'pluginStore', 'commStore', 'calendarStore', 'commandPaletteStore', 'onboardingStore', 'windowStore', 'aiUIControlStore', 'pluginToolbarStore']:
|
||||||
|
if store_name in content:
|
||||||
|
# Find which actions/state are accessed
|
||||||
|
for m in re.finditer(rf'{store_name}\.*(\w+)', content):
|
||||||
|
usage[store_name].add(m.group(1))
|
||||||
|
return usage
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 70)
|
||||||
|
print("TRACE STORES — Unused Store Actions/State")
|
||||||
|
print("=" * 70)
|
||||||
|
stores = find_store_actions()
|
||||||
|
usage = find_store_usage()
|
||||||
|
unused = {}
|
||||||
|
total_unused = 0
|
||||||
|
for store_name, actions in stores.items():
|
||||||
|
used = usage.get(store_name, set())
|
||||||
|
unused_actions = [a for a in actions if a not in used]
|
||||||
|
if unused_actions:
|
||||||
|
unused[store_name] = unused_actions
|
||||||
|
total_unused += len(unused_actions)
|
||||||
|
print(f"\nStores found: {len(stores)}")
|
||||||
|
for name, actions in stores.items():
|
||||||
|
used = len(usage.get(name, set()))
|
||||||
|
print(f" {name}: {len(actions)} defined, {used} used, {len(unused.get(name, []))} unused")
|
||||||
|
print(f"\nTotal unused store actions: {total_unused}")
|
||||||
|
if unused:
|
||||||
|
print("\n--- Unused store actions ---")
|
||||||
|
for store, actions in unused.items():
|
||||||
|
for a in actions[:10]: print(f" {store}.{a}")
|
||||||
|
results = {"stores": stores, "usage": {k: list(v) for k, v in usage.items()}, "unused": unused}
|
||||||
|
with open(ROOT / "scripts" / "test_suite" / "results_trace_stores.json", "w") as f:
|
||||||
|
json.dump(results, f, indent=2, default=str)
|
||||||
|
return total_unused
|
||||||
|
|
||||||
|
if __name__ == "__main__": sys.exit(main())
|
||||||
Reference in New Issue
Block a user