Dead API Endpoint Gate¶
On-demand reference. scripts/check_dead_api_endpoints.py runs at pre-push (locally) and in CI's lint job. It compares every frontend HTTP / WebSocket call site under web/src/ against every backend Litestar route registered through src/synthorg/api/controllers/__init__.py, the A2A registration block in src/synthorg/api/app.py, and the module-level WebSocket handler in src/synthorg/api/controllers/ws.py.
What the gate catches¶
- High severity (blocks the push): any
apiClient.<method>(...)orfetch(...)call site whose URL does not resolve to a registered backend route. These are dead endpoints from the frontend's perspective; the runtime will 404. - Informational only (never blocks): backend routes with no frontend caller. These may legitimately be CLI- or public-REST-only, so the gate prints them under
--show-infobut never fails on them.
Path normalisation¶
Backend Litestar routes use typed path-params ({name:str}, {version:int}); frontend template literals use bare ${var} substitutions wrapped in encodeURIComponent. Both sides flow through normalise_path() in scripts/_dead_api_endpoints_models.py which collapses every {anything} to {*}. Comparison is then exact-string. Trailing slashes are also collapsed so @post("/") (composed to /agents/) matches apiClient.post('/agents').
The API prefix (/api/v1 by default; configurable via --api-prefix) is stripped from backend routes since the frontend's Axios client prepends it via baseURL. The single exception is the A2A WellKnownAgentCardController at /.well-known/..., which is mounted at the app root and never carries the prefix.
Per-line opt-out¶
Use the standard suppression marker on the call-site line. The justification after -- is required.
// frontend (TypeScript)
apiClient.get('/external/api') // lint-allow: dead-api-endpoints -- third-party REST
The marker is parsed by a string-literal-aware single-line scanner; bare // lint-allow: dead-api-endpoints without -- <reason> does NOT suppress, the justification is mandatory and mirrors # lint-allow: persistence-boundary -- <reason>. Call-site suppression is only enforced for files under web/src/**/*.{ts,tsx} (the scanner's scope); a Python tokenize-based marker reader exists in _dead_api_endpoints_models.py for parity but no Python files are currently scanned, so a # lint-allow: dead-api-endpoints -- comment in .py is parsed-but-unused today.
Baseline¶
scripts/dead_api_endpoints_baseline.txt freezes pre-existing high-severity findings so the gate can ship without forcing every dead-endpoint cleanup in the same PR. Format: one entry per line as <file>:<line>:<col>:<method>:<path>, sorted deterministically. Comment lines start with # and are ignored.
- Pass: current violations ⊆ baseline.
- Fail: any new violation absent from the baseline.
- Warn (still pass): baseline entries that no longer correspond to a current violation are reported as stale; regenerate the baseline once the wider fix lands.
Regenerate via uv run python scripts/check_dead_api_endpoints.py --update-baseline. Commit the diff after manual review; the gate is enforced loud-on-growth at pre-push, so any regression past this baseline blocks the push.
Module split¶
Implementation lives across four sibling helpers under scripts/ to stay below the 800-line per-file ceiling.
_dead_api_endpoints_models.py: frozen dataclasses (RouteRecord,CallSiteRecord,Violation), suppression-marker tokenizers, and the path-normaliser. All three dataclasses validate their invariants in__post_init__so a caller forgetting to normalise a path or uppercase a method fails fast at construction time instead of silently breaking comparator matching._dead_api_endpoints_backend.py: AST walker overcontrollers/__init__.py'sBASE_CONTROLLERS/OPTIONAL_CONTROLLERS/INTEGRATION_CONTROLLERS/ALL_CONTROLLERStuples, the A2A block inapi/app.py, and the@websocket("/ws")handler incontrollers/ws.py. Conditionally-registered controllers (gated byapp_state.has_*predicates oreffective_config.<feature>.enabled) are treated as registered to avoid false-positive dead-endpoint findings on optional features._dead_api_endpoints_frontend.py: token-scanning extractor overweb/src/**/*.{ts,tsx}that finds everyapiClient.<method>(URL_EXPR)andfetch(URL_EXPR, init)call. Handles plain string literals, template literals with${var}/${encodeURIComponent(var)}, same-fileconst BASE = '/path'resolution, and Axios-base prefixes (${baseUrl},${apiClient.defaults.baseURL}). Test files (__tests__/) and type definitions (.d.ts) are skipped. Dynamic URL expressions (function calls, computed paths) are documented as a known limitation: they are silently dropped from the inventory because the gate cannot statically resolve them._dead_api_endpoints_compare.py: comparator + baseline I/O.
Common scenarios¶
- New endpoint, no frontend caller yet: the gate emits an info-severity finding under
--show-info. No action required at push time. When the frontend caller lands, the info disappears automatically. - Frontend calls an endpoint that was just deleted: the gate fails with a high-severity finding. Either restore the backend route, fix the frontend caller, or add
// lint-allow: dead-api-endpoints -- <reason>if the call is intentionally calling something outside the SynthOrg API surface. - Renaming a path: update both sides in the same PR. The gate will fail on the frontend side until the backend renames land.
- Path-param rename: safe; the comparator collapses every
{anything}to{*}, so{agent_name:str}matches${agentName}regardless of identifier. - Conditionally-registered controller: the gate treats every controller mentioned in any of the four registration tuples as "registered". You don't need to disable the gate or whitelist anything when
effective_config.integrations.enabled = Falseflips off the integration controllers.
Failure surfacing¶
The gate is conservative about silent failures: a SyntaxError or OSError in any controller file produces a one-line stderr warning (so the operator can investigate), but the file is skipped rather than aborting the run. The single exception is controllers/__init__.py itself: if that file cannot be parsed, the gate raises and exits with code 2 because every backend route would otherwise be missing and every frontend call would falsely report as a dead endpoint.
Inventory entry¶
Listed under "Convention Rollout (MANDATORY)" in CLAUDE.md. The audit-category entry is in audit-category-gate-coverage.md under "Frontend-backend API contract drift (dead endpoints)".