Add backend API contract audit
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-17 20:11:36 +02:00
parent 45bbe9725d
commit 32c38423a5
8 changed files with 187 additions and 7 deletions
+11
View File
@@ -4,6 +4,17 @@ Setup-, import-, demo- en maintenance-scripts voor GeoIntel.
## Runtime verification
Audit the active backend route surface against `docs/API_CONTRACTS.md`:
```bash
python scripts/audit_api_contracts.py
```
The audit imports the FastAPI app, compares implemented `GET`/`POST`/`PATCH`/
`DELETE` routes with active API contract headings and tracks the explicit
non-envelope exceptions (`/health` and export downloads). It fails when a route
exists without docs or when docs claim an endpoint that is not implemented.
Verify the browser-facing Docker/LAN runtime:
```bash
+73
View File
@@ -0,0 +1,73 @@
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BACKEND = ROOT / "backend"
DOCS = ROOT / "docs" / "API_CONTRACTS.md" # docs/API_CONTRACTS.md
ALLOWED_NON_ENVELOPE_ENDPOINTS = {
("GET", "/health"),
("GET", "/api/v1/exports/{export_id}/download"),
}
IGNORED_OPENAPI_PATHS = {
"/openapi.json",
"/docs",
"/docs/oauth2-redirect",
"/redoc",
}
def _load_app_routes() -> set[tuple[str, str]]:
sys.path.insert(0, str(BACKEND))
from app.main import create_app
app = create_app()
routes: set[tuple[str, str]] = set()
for route in app.routes:
path = getattr(route, "path", None)
methods = getattr(route, "methods", set())
if not path or path in IGNORED_OPENAPI_PATHS:
continue
for method in sorted(methods - {"HEAD", "OPTIONS"}):
routes.add((method, path))
return routes
def _load_documented_routes() -> set[tuple[str, str]]:
docs = DOCS.read_text(encoding="utf-8")
return set(re.findall(r"###\s+(GET|POST|PATCH|DELETE)\s+`([^`]+)`", docs))
def main() -> None:
implemented_routes = _load_app_routes()
documented_routes = _load_documented_routes()
missing_docs = sorted(implemented_routes - documented_routes)
stale_docs = sorted(documented_routes - implemented_routes)
missing_non_envelope = sorted(ALLOWED_NON_ENVELOPE_ENDPOINTS - implemented_routes)
errors: list[str] = []
for method, path in missing_docs:
errors.append(f"Missing documented API route: {method} {path}")
for method, path in stale_docs:
errors.append(f"Documented API route is not implemented: {method} {path}")
for method, path in missing_non_envelope:
errors.append(f"Allowed non-envelope endpoint is not implemented: {method} {path}")
if errors:
raise SystemExit("\n".join(errors))
print(
"API contract audit OK: "
f"{len(implemented_routes)} implemented routes match docs; "
f"{len(ALLOWED_NON_ENVELOPE_ENDPOINTS)} explicit non-envelope endpoints tracked."
)
if __name__ == "__main__":
main()
+1
View File
@@ -32,6 +32,7 @@ bash scripts/check_repo_structure.sh
${PYTHON_BIN} scripts/smoke_docs.py
${PYTHON_BIN} scripts/validate_fixtures.py
${PYTHON_BIN} scripts/smoke_contracts.py
${PYTHON_BIN} scripts/audit_api_contracts.py
${PYTHON_BIN} scripts/preimplementation_audit.py
${PYTHON_BIN} scripts/validate_m13_codex_assets.py
${PYTHON_BIN} scripts/validate_m14_launch_assets.py