Skip to content

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 version
  • api/config.pyApiSettings (pydantic-settings, SW_API_* env prefix)
  • api/schemas.py — 25 Pydantic request/response models
  • api/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 registration
  • api/routers/meta.pyGET /health, /meta/eras, /meta/doctrines, /meta/terrain-types
  • api/routers/scenarios.pyGET /scenarios, GET /scenarios/{name}
  • api/routers/units.pyGET /units, GET /units/{type}

32b: Database & Run Execution

  • api/database.py — SQLite via aiosqlite, runs + batches tables, full CRUD
  • api/run_manager.pyRunManager with submit/cancel/progress, step-based execution loop
  • api/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 alongside stochastic_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: sides can be a list of {side: ..., units: [...]} or a dict {side_name: {units: [...]}}. 73_easting uses blue_forces/red_forces. API must handle all formats.
  • test_scenario is too minimal: Lacks date, terrain, sides required by CampaignScenarioConfig. Use test_campaign for 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.TestClient synchronously.

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

  1. 32-D1: config_overrides not applied to engineRunSubmitRequest.config_overrides is accepted and stored in DB but never injected into calibration_overrides before ScenarioLoader.load(). The MCP server's _tool_modify_parameter shows the pattern (temp YAML with overrides). Low priority — the field is stored and returned correctly, just not yet used by the engine.

  2. 32-D2: Terrain types list is hardcodedGET /api/meta/terrain-types returns a static list rather than deriving from TerrainConfig or scanning scenario data. Very low priority — the list is stable and correct.

Documentation Gaps Fixed During Postmortem

  • mkdocs.yml nav: added Phase 32 devlog entry
  • docs/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.