Add comprehensive rule documentation for AI-assisted development covering authentication surfaces, outbound-call safety invariants, and other project conventions to guide Claude's understanding of critical system behaviors.
57 lines
2.9 KiB
Markdown
57 lines
2.9 KiB
Markdown
---
|
|
description: composition-root lifespan, nested MCP http_app lifespan, app.state wiring, route/mount ordering, honest /health
|
|
paths:
|
|
- "main.py"
|
|
- "api/deps.py"
|
|
- "core/gateway.py"
|
|
---
|
|
|
|
# Lifespan, composition root & route ordering
|
|
|
|
[main.py](../../main.py)'s `lifespan` is the **composition root**: it builds the
|
|
gateway and every service, wires them by constructor/registration, and hangs the
|
|
long-lived ones on `app.state`. This is the one place dependencies are
|
|
assembled.
|
|
|
|
- **Build services here, inject them — nothing self-constructs its deps.** The
|
|
gateway, classifier, transcription, TTS, routing, recording, receptionist, and
|
|
notification services are all constructed in the lifespan and wired together
|
|
(e.g. the receptionist receives tts/transcription/recording/routing;
|
|
`launch_hold_slayer` is registered as the `HOLD_SLAYER` mode handler). A new
|
|
service is built here and passed in, not instantiated deep in a call path.
|
|
|
|
- **The MCP sub-app's lifespan MUST be nested.** The lifespan opens
|
|
`async with mcp_http_app.lifespan(app):` around all startup. FastMCP's
|
|
streamable-HTTP session manager is initialised inside *its* lifespan; mount the
|
|
app without entering that context and every `/mcp` request 500s
|
|
("session manager not initialised" / "Task group is not initialized"). **Keep
|
|
the nesting.** This is the same landmine across the estate's mounted-MCP
|
|
services.
|
|
|
|
- **`app.state` is the handoff to request handlers.** The lifespan sets
|
|
`app.state.gateway`, `.routing_service`, `.transcription_service`,
|
|
`.notification_service`, `.recording_service`. Dependencies in
|
|
[api/deps.py](../../api/deps.py) read these and raise `503` if not yet set. MCP
|
|
tools reach the gateway via the lazy `_get_gateway_instance` resolver. Don't
|
|
reach for module-level globals; go through `app.state`.
|
|
|
|
- **Route/mount registration order is load-bearing:**
|
|
1. `call_history` router registers **before** `calls` — both live under
|
|
`/api/v1/calls`, and `calls`' `GET /{call_id}` would otherwise swallow the
|
|
literal path `history`. Keep history first.
|
|
2. The `"/mcp"` mount and all API/WS/health routes register **before** the
|
|
`"/"` static dashboard mount — a root mount matches every path, so anything
|
|
after it is unreachable. The dashboard mount stays last, and only when
|
|
`dashboard/build/` exists.
|
|
|
|
- **`/health` is honest by construction.** `healthy` = real (non-`MockSIPEngine`)
|
|
engine **and** registered trunk **and** reachable DB; it also reports STT/TTS
|
|
last-known reachability via `_availability`. Don't relax any of these to make a
|
|
probe pass — a degraded gateway must read as `degraded`, with the reason
|
|
visible.
|
|
|
|
- **Shutdown reverses startup.** Stop notifications, stop the gateway (which
|
|
cancels tracked tasks, ends active calls, stops SIP then media), close the DB.
|
|
New long-lived resources get a matching teardown here — don't leak a task or a
|
|
client across restarts.
|