fix(d5): Marathon-Scanner-Triage — trace_api_contracts 859→218 (-75%, Router-Präfixe/Multi-Router/leere Pfade/Template-Literals gefixt), trace_plugins 27→0 (-100%, Inline-Manifest-Konvention erkannt); 371 HIGH-Fehlalarme eliminiert (OpenAPI-verifiziert); ~12 echte API-Bugs als Follow-up dokumentiert (ai/sessions ×5, policies ×4, mail ×4)

This commit is contained in:
Agent Zero
2026-08-24 12:43:41 +02:00
parent c0e8e4ecfd
commit 5cc5a3fa6a
11 changed files with 2702 additions and 7360 deletions
+49 -32
View File
@@ -138,50 +138,62 @@ def find_backend_response_fields() -> list[dict]:
return responses
def _router_prefixes(content: str) -> dict[str, str]:
"""Extract all router variable definitions with their APIRouter prefixes.
Modules may define multiple routers (e.g. ``router``, ``calendar_router``,
``resource_router``) each with its own prefix.
"""
prefixes: dict[str, str] = {}
for m in re.finditer(
r"(\w+)\s*=\s*APIRouter\([^)]*?prefix\s*=\s*[\"']([^\"']+)[\"']", content
):
prefixes[m.group(1)] = m.group(2).rstrip("/")
return prefixes
def find_backend_endpoints() -> list[dict]:
"""Find all backend endpoints from route decorators."""
"""Find all backend endpoints from route decorators.
Includes the module's APIRouter prefix so paths are comparable with
frontend calls (which use full /api/v1/... URLs).
"""
endpoints = []
routes_dir = BACKEND_APP / "routes"
if not routes_dir.exists():
return endpoints
scan_dirs = [
BACKEND_APP / "routes",
BACKEND_APP / "plugins" / "builtins",
]
for py_file in routes_dir.rglob("*.py"):
try:
content = py_file.read_text(encoding="utf-8")
except Exception:
for scan_dir in scan_dirs:
if not scan_dir.exists():
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"):
is_plugin_dir = scan_dir.name == "builtins"
for py_file in scan_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({
prefixes = _router_prefixes(content)
# Variable-agnostic with per-variable prefix mapping: modules may
# define multiple routers (e.g. ``calendar_router``) each with its
# own prefix. Path may be an empty string for collection routes
# (e.g. ``@router.get("")``).
for match in re.finditer(r"@(\w+)\.(get|post|put|patch|delete)\s*\(\s*[\"']([^\"']*)[\"']", content):
var_name = match.group(1)
method = match.group(2).upper()
prefix = prefixes.get(var_name, "")
path = prefix + match.group(3)
ep = {
"file": rel_path,
"method": method,
"path": path,
"is_plugin": True,
})
}
if is_plugin_dir:
ep["is_plugin"] = True
endpoints.append(ep)
return endpoints
@@ -190,9 +202,14 @@ 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 JS template-literal interpolations (${qs}, ${id}, ...) like params;
# ensure a '/' separator so '/webhooks${params}' -> '/webhooks/{param}'
path = re.sub(r"([^/])\$\{[^}]*\}", r"\1/{param}", path)
path = re.sub(r"^\$\{[^}]*\}", "{param}", path)
# Replace {param} patterns
path = re.sub(r"\{[^}]+\}", "{param}", path)
# Remove trailing slash
# Collapse duplicate slashes and remove trailing slash
path = re.sub(r"//+", "/", path)
path = path.rstrip("/")
return path