Phase 32: API & Service Foundation¶
Status: Complete
Date: 2026-03-05
Tests: 77 new (API tests, marked @pytest.mark.api)
Engine changes: Zero
Summary¶
Built a FastAPI service layer wrapping the simulation engine. Async run execution with WebSocket progress streaming, SQLite persistence via aiosqlite, and a documented REST API.
Delivered¶
32a: Core Scaffolding¶
api/__init__.py— package marker with versionapi/config.py—ApiSettings(pydantic-settings,SW_API_*env prefix)api/schemas.py— 25 Pydantic request/response modelsapi/dependencies.py— FastAPI DI providers (settings, db, run_manager)api/scenarios.py— Scenario/unit discovery helpers (scans base + era dirs)api/main.py— App factory, CORS, lifespan, router registrationapi/routers/meta.py—GET /health,/meta/eras,/meta/doctrines,/meta/terrain-typesapi/routers/scenarios.py—GET /scenarios,GET /scenarios/{name}api/routers/units.py—GET /units,GET /units/{type}
32b: Database & Run Execution¶
api/database.py— SQLite via aiosqlite,runs+batchestables, full CRUDapi/run_manager.py—RunManagerwith submit/cancel/progress, step-based execution loopapi/routers/runs.py— Full run lifecycle: POST, GET, DELETE, events, narrative, forces, snapshots
32c: WebSocket & Batch & Analysis¶
- WebSocket progress streaming at
WS /runs/{id}/progress(asyncio.Queue + call_soon_threadsafe) - Batch MC execution at
POST /runs/batch,GET /runs/batch/{id},WS /runs/batch/{id}/progress api/routers/analysis.py—/analysis/compare,/analysis/sweep,/analysis/tempo/{id}
Architecture¶
api/lives at repo root alongsidestochastic_warfare/(engine stays pure)- All engine imports are lazy (inside handler functions), matching MCP server pattern
- Progress streaming: engine.step() in thread -> call_soon_threadsafe -> asyncio.Queue -> WebSocket
- SQLite for single-user persistence; events/snapshots stored as JSON blobs
- API tests excluded from default test run via
@pytest.mark.api+ addopts filter
Dependencies Added¶
fastapi>=0.115(API framework)uvicorn[standard]>=0.34(ASGI server)aiosqlite>=0.20(async SQLite)pydantic-settings>=2.0(env-based config)httpx>=0.27(test client, dev extra)pytest-asyncio>=0.23(async test support, dev extra)
Test Summary¶
| Test File | Tests | Focus |
|---|---|---|
| test_database.py | 16 | SQLite CRUD for runs/batches |
| test_meta.py | 7 | Health, eras, doctrines, terrain types |
| test_scenarios.py | 14 | Scenario listing, detail, era scenarios |
| test_units.py | 13 | Unit listing, filtering, detail |
| test_runs.py | 14 | Run submit, poll, events, narrative, forces, delete |
| test_websocket.py | 3 | WS progress, nonexistent run/batch |
| test_batch.py | 5 | Batch submit, poll, metrics |
| test_analysis.py | 7 | Compare, sweep, tempo |
| Total | 77 |
API Endpoints¶
| Method | Path | Purpose |
|---|---|---|
| GET | /api/health | Service health check |
| GET | /api/meta/eras | Available eras |
| GET | /api/meta/doctrines | Doctrine templates |
| GET | /api/meta/terrain-types | Terrain type list |
| GET | /api/scenarios | List all scenarios |
| GET | /api/scenarios/{name} | Scenario detail |
| GET | /api/units | List units (filterable) |
| GET | /api/units/{type} | Unit detail |
| POST | /api/runs | Submit simulation run |
| GET | /api/runs | List runs (paginated) |
| GET | /api/runs/{id} | Run detail |
| DELETE | /api/runs/{id} | Delete run |
| GET | /api/runs/{id}/forces | Force states |
| GET | /api/runs/{id}/events | Paginated events |
| GET | /api/runs/{id}/narrative | Battle narrative |
| GET | /api/runs/{id}/snapshots | State snapshots |
| WS | /api/runs/{id}/progress | Live progress stream |
| POST | /api/runs/batch | Submit MC batch |
| GET | /api/runs/batch/{id} | Batch detail |
| WS | /api/runs/batch/{id}/progress | Batch progress |
| POST | /api/analysis/compare | A/B comparison |
| POST | /api/analysis/sweep | Parameter sweep |
| GET | /api/analysis/tempo/{id} | Tempo analysis |
Lessons Learned¶
- Scenario YAML format varies:
sidescan be a list of{side: ..., units: [...]}or a dict{side_name: {units: [...]}}.73_eastingusesblue_forces/red_forces. API must handle all formats. test_scenariois too minimal: Lacksdate,terrain,sidesrequired byCampaignScenarioConfig. Usetest_campaignfor API tests.- Step-based progress is clean:
engine.step()in a loop with periodic queue emissions gives real-time progress without any engine modifications. - Starlette TestClient for WebSocket: httpx AsyncClient doesn't support WebSocket testing; use
starlette.testclient.TestClientsynchronously.
Postmortem¶
Scope: On Target¶
All planned deliverables shipped. 23 REST + 2 WebSocket endpoints match the plan exactly. 13 source files, 77 tests (plan estimated ~105 — realistic given lower need for boundary validation tests at this stage). Extra file api/scenarios.py was added for scenario/unit scan helpers (not in plan but natural decomposition).
Quality: High¶
- Zero bare
print(), no TODOs/FIXMEs, type hints on all public functions - Test quality is solid: real scenarios, real engine execution, no mocks, cross-module integration tests
- All pydantic models used, all DB methods called, all routers registered
Integration: Fully Wired¶
- Every source file imported by at least one other module or test
- No dead modules detected
- Lifespan properly manages Database + RunManager lifecycle
Deficits: 2 new items¶
-
32-D1:
config_overridesnot applied to engine —RunSubmitRequest.config_overridesis accepted and stored in DB but never injected intocalibration_overridesbeforeScenarioLoader.load(). The MCP server's_tool_modify_parametershows the pattern (temp YAML with overrides). Low priority — the field is stored and returned correctly, just not yet used by the engine. -
32-D2: Terrain types list is hardcoded —
GET /api/meta/terrain-typesreturns a static list rather than deriving fromTerrainConfigor scanning scenario data. Very low priority — the list is stable and correct.
Documentation Gaps Fixed During Postmortem¶
mkdocs.ymlnav: added Phase 32 devlog entrydocs/index.md: updated test badge (7,307 → 7,384) and phase badge (31 → 32)docs/reference/api.md: added REST API section (endpoints, config, setup)
Performance: No Regression¶
Engine suite runs in ~115s, consistent with Phase 31 baseline.