Server

server/__init__.py defines the FastAPI app and operational endpoints. server/wiki.py defines the engineering wiki API.

This is distinct from the public static wiki in site/: the static site is a documentation frontend, while FastAPI owns the live API contracts and wiki diagnostics.

API Surface Areas

  • Progressions: manifest + entity retrieval routes. GET /api/tools/progression/manifest returns the full manifest catalog, while GET /api/tools/progression/{entity_type}/{edition}/{index} returns the manifest entry, generated progression JSON, canonical homebrew record (if any), and level-by-level review document as a composite response. This bundling avoids multiple round-trips when the UI needs to display a full entity view.
  • Spells: edition-scoped spell catalog route (GET /api/tools/spells?edition=...) for UI and tooling lookup. The route uses a fallback strategy—if a 2024 spell table doesn't exist, it falls back to the 2014 merged view. This handles the incremental 2024 data availability without requiring the client to negotiate editions.
  • SRD Level-Up: stateless /api/tools/srd-level-up/* routes (preview, apply, validate) operate on raw SrdCharacterState without any persistence. The persisted /api/characters/{id}/level-up/* routes wrap the same stateless engine with character identity lookup and snapshot creation.
  • Characters: create/list/get/delete routes, snapshot history, feature catalog, ASI/feat choice inspection and swapping, and saved decision-plan persistence. All character data lives in lorewright-app.sqlite, separate from the SRD content database. The decision-plan is a client-managed document that records intended level-up choices before they are applied. The level-up apply route uses optimistic locking on current_level to prevent concurrent modification conflicts, and the store layer normalizes duplicate snapshot levels so each character retains a single current snapshot per level.
  • Homebrew: canonical record write path (PUT /api/tools/homebrew/{entity_type}/{index}). Validates that the payload index and type fields match the URL parameters before writing to disk. The canonical JSON files in homebrew/ are the source of truth for homebrew content.
  • Reviews: level-by-level review read/write path. Review documents default to all 20 levels at pending status. On read, stored levels are merged over the default template so that missing levels are always present. This avoids schema migration when new review fields are added.
  • Rebuild: pipeline execution trigger for entity or all. Always runs refresh_sqlite_database first to ensure the SQLite source data is current before generating progressions, and returns either the manifest for scope: "all" or the regenerated entity payload for scope: "entity".
  • Wiki API-only: manifest, rendered pages, health report, source view, commit history, TOC payloads, backlink/source metadata, and stale-source reporting. The wiki router is mounted as an APIRouter with the /api/wiki prefix. Frontmatter parsing is implemented with stdlib only (no PyYAML dependency).

See API Contracts for endpoint-level expectations.

Public Static Wiki vs FastAPI Wiki API

  • site/ renders public pages from markdown during the Next.js build and publishes them to Vercel.
  • server/wiki.py serves /api/wiki/* for local engineering use and automated checks.
  • API-only wiki capabilities include health, commit history, source metadata/staleness, historical source viewing, and page TOC data.
  • Current implementation detail: the static site reads top-level wiki/*.md files, while the FastAPI wiki API recursively walks wiki/. The docs call out this difference without changing behavior in this pass.

Route Families Worth Keeping in Sync

  • /api/tools/progression/* and /api/tools/spells
  • /api/tools/homebrew/*, /api/tools/reviews/*, and /api/tools/rebuild
  • /api/tools/srd-level-up/*
  • /api/characters, /api/characters/{id}, /api/characters/{id}/snapshots
  • /api/characters/{id}/level-up/* and /api/characters/{id}/decision-plan
  • /api/characters/{id}/feature-catalog and /api/characters/{id}/progression/levels/{level}/asi-feat-*
  • /api/wiki/*

Rebuild Coupling

POST /api/tools/rebuild executes the same pipeline modules used by CLI commands; see Rebuild Runbook. The API handler calls refresh_sqlite_database followed by either generate_all_progressions or generate_selected_progression depending on the request scope. This means a rebuild triggered from the API has the same side effects as running poetry run lorewright db-refresh && poetry run lorewright generate-progressions from the CLI—the only difference is that the API collapses both steps into a single request.

Design Rationale

The server is intentionally a thin routing layer. Route handlers perform argument validation, delegate to engine functions, and translate engine exceptions into HTTP status codes. There is no business logic in server/__init__.py beyond request/response shaping—all domain computation lives in engine/.

The dual-database architecture (5e-database.sqlite for SRD content, lorewright-app.sqlite for character state) reflects a separation of concerns: SRD data is regenerable from source JSON files, while character data is user-created and must be preserved across rebuilds. The server opens SQLite connections per-request rather than maintaining a connection pool, which is appropriate for SQLite's single-writer concurrency model and avoids cross-thread connection sharing issues.

Pydantic models defined inline in server/__init__.py (like RebuildRequestModel, ReviewDocumentModel, SpellCatalogItemModel) are server-specific request/response shapes that don't belong in the engine's schema layer. They exist only to validate API input at the routing boundary.

The choice to serve the wiki API from the same FastAPI app (rather than a separate service) keeps the deployment footprint minimal. Since the wiki API primarily reads files from the wiki/ directory and shells out to git for commit history, it has no shared state or resource contention with the main API routes.

Assumptions & Constraints

  • Single-process deployment: The server assumes a single uvicorn process. There is no session affinity, distributed caching, or inter-process coordination. This is sufficient for the current use case (local dev + small team).
  • SQLite file existence: Routes that read from 5e-database.sqlite gracefully handle a missing database (returning empty results), but character routes require lorewright-app.sqlite to exist. The app database is created on first character write.
  • No authentication: All API routes are unauthenticated. The server is designed for local or trusted-network use only.
  • Manifest as index: The progression API relies entirely on progressions/manifest.json to locate entity files. If the manifest is out of sync with the filesystem, routes will return 404 errors even if the progression file exists on disk.
  • Homebrew write path is file-based: PUT requests to the homebrew and review routes write directly to the filesystem. There is no locking or conflict detection—concurrent writes to the same entity will last-write-wins.

Conceptual Model

The server mediates between three distinct storage backends:

  1. SRD content database (5e-database.sqlite): read-only from the server's perspective. Populated by the ingest pipeline. Queried for spell catalogs and as input to level-up preview.
  2. Progression artifacts (progressions/): read-only JSON files generated by the pipeline. Served directly by entity retrieval routes. The manifest acts as an in-memory index loaded on each request.
  3. App state database (lorewright-app.sqlite): read-write. Owns character records, snapshots, and decision plans. This is the only store with user-created data.

The rebuild route is the only mutation path for stores (1) and (2), and it atomically regenerates both. Store (3) is never affected by rebuilds—character data persists independently.

Test Coverage

Primary API behavior checks are cataloged in Testing Strategy, especially:

  • tests/test_progression_tool_api.py
  • tests/test_srd_level_up_api.py
  • tests/test_wiki_api.py