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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user