API Contracts

This page tracks stable request and response expectations for operational endpoints. All request and response bodies are validated through Pydantic v2 models with extra="forbid" semantics, meaning unknown fields cause immediate 422 rejections. The API surface is split into three logical tiers: read-only content queries (progressions, spells, wiki), stateless computation (level-up preview/apply/validate), and stateful persistence (character CRUD and snapshot management).

Progression and Spell APIs

  • GET /api/tools/progression/manifest: returns progression manifest payload.
  • GET /api/tools/progression/{entity_type}/{edition}/{index}: returns manifest entry, progression JSON, canonical source (if present), and review document.
  • GET /api/tools/spells?edition={2014|2024}: returns an edition-scoped spell catalog used by the UI and decision flows.

These endpoints are pure reads against generated artifacts in progressions/ and the SQLite source database at @file:data/5e-database.sqlite. The manifest (@file:progressions/manifest.json) acts as the authoritative catalog—no progression file is served unless it has a corresponding manifest entry. Entity identity (entity_type + srd_version + index) is the compound key that ties manifest entries, file paths, and API lookups together. The spell catalog endpoint queries the source database directly rather than generated artifacts, since spell data is already normalized at ingestion time.

Homebrew and Review APIs

  • PUT /api/tools/homebrew/{entity_type}/{index}: writes canonical class/subclass document, path index must match payload.
  • GET|PUT /api/tools/reviews/{entity_type}/{edition}/{index}: reads/writes structured review docs.

The homebrew system maintains two parallel representations: canonical JSON documents in homebrew/classes/ or homebrew/subclasses/, and per-level review documents under homebrew/reviews/. The PUT endpoint enforces that the index in the URL path matches the payload body, preventing accidental cross-entity writes. Review documents use a ReviewDocumentModel with per-level status tracking (pending, approved, flagged), enabling a granular editorial workflow without requiring the entire entity to be reviewed atomically.

Rebuild API

  • POST /api/tools/rebuild
  • Scope all triggers full refresh + generation.
  • Scope entity requires entity_type, edition, index.

The rebuild endpoint orchestrates the same pipeline that the CLI commands db-refresh and generate-progressions run, but scoped to either the entire dataset or a single entity. A full all rebuild re-ingests every SRD and homebrew source into SQLite, then regenerates all progression JSON artifacts. An entity-scoped rebuild targets a single class or subclass, which is significantly faster during iterative homebrew authoring. Both scopes update the manifest atomically at the end of the pipeline, so partial failures never leave the manifest pointing at stale or missing progression files.

Operational workflow is documented in Rebuild Runbook.

SRD Level-Up APIs

  • POST /api/tools/srd-level-up/preview: returns deterministic changes, required/optional choices, and canonical validation issues/warnings.
  • POST /api/tools/srd-level-up/apply: returns canonical decision log and blocks with structured issues plus unresolved_choice_ids when invalid.
  • POST /api/tools/srd-level-up/validate: returns valid, error issues, and warnings.
  • POST /api/characters/{character_id}/level-up/apply: persists only fully valid apply results.

These endpoints are the core of the level-up workflow. The three stateless endpoints (preview, apply, validate) accept a full SrdCharacterState in the request body and compute results without touching the database. This makes them deterministic and testable in isolation (see @file:tests/test_srd_level_up_api.py). The character-scoped /level-up/apply endpoint bridges the gap between stateless computation and persistence: it loads the saved character state from the app database, runs the apply logic, and only persists a new snapshot if there are zero validation issues and no unresolved required choices.

Detailed schema and behavior rules are tracked in SRD Level-Up Contracts.

Character Persistence APIs

  • GET /api/characters: lists saved characters.
  • POST /api/characters: creates a new saved character snapshot lineage.
  • GET /api/characters/{character_id}: returns the current saved character record.
  • DELETE /api/characters/{character_id}: deletes a character and all associated snapshots and decision plans.
  • GET /api/characters/{character_id}/snapshots: returns immutable snapshot history for that character.
  • POST /api/characters/{character_id}/level-up/preview: runs level-up preview against the saved character state.
  • POST /api/characters/{character_id}/level-up/apply: persists a new snapshot only when the apply result is fully valid. Uses optimistic locking on current_level to prevent concurrent level-up conflicts (returns HTTP 409 on conflict).
  • PUT /api/characters/{character_id}/decision-plan: stores a reusable decision-plan payload for the character.
  • GET /api/characters/{character_id}/decision-plan: reads the current decision-plan record, if any.
  • GET /api/characters/{character_id}/feature-catalog: returns a dictionary of feature descriptions (name + description) for all features the character has accumulated through their current level.
  • GET /api/characters/{character_id}/progression/levels/{level}/asi-feat-choice: returns the current ASI/feat choice state at a specific level, including ASI and feat option lists.
  • POST /api/characters/{character_id}/progression/levels/{level}/asi-feat-swap: swaps the ASI/feat decision at a specific level by replaying the level-up from the prior snapshot with the new choice.

Character persistence follows an event-sourcing-inspired model where state is never mutated in place. Each level-up produces a new CharacterSnapshotRecord with a source_kind tag indicating its origin (created, level_up_apply, or import). The SavedCharacterRecord always points to the latest snapshot via current_snapshot_id, but full history is retrievable through the /snapshots endpoint. This immutability guarantee means a character can always be audited back to creation.

Decision plans provide a separate persistence channel for pre-planned level-up choices. A plan is stored as a LevelUpDecisionPlanRecord (currently at schema version srd-level-up-decision-plan.v2) and can be loaded into the preview/apply flow without the user re-entering choices. The plan upsert endpoint replaces any existing plan for the character rather than versioning plans independently. Plan schema includes a model_validator that transparently upgrades v1 payloads (which used a choices dict keyed by choice_id) to the v2 structured decisions list format.

Wiki APIs (API-only)

  • GET /api/wiki/manifest: page catalog with metadata and staleness/backlink fields.
  • GET /api/wiki/health: broken links, orphan pages, missing source refs, stale pages.
  • GET /api/wiki/pages/{slug}: rendered HTML + normalized metadata, toc, source provenance, backlink count, and staleness fields.
  • GET /api/wiki/source: file lookup with optional commit and line range for historical source viewing.
  • GET /api/wiki/commits: git history summary.

These wiki capabilities are FastAPI-only and are not part of the public static Vercel wiki surface in site/. The wiki API operates directly against the wiki/ directory on disk, parsing frontmatter with a stdlib-only YAML subset parser (no PyYAML dependency) defined in @file:server/wiki.py. Staleness detection compares page modification times against the referenced source_paths files, surfacing pages that may need a re-ingest. The health endpoint aggregates all lint-level checks (broken wiki-link targets, orphan pages, missing source_paths entries) into a single response for dashboard consumption.

Design Rationale

The API is split between a tools namespace (for content management and computation endpoints used by both the UI and engineering tooling) and a characters namespace (for saved-character CRUD). This separation reflects the fact that the tools endpoints are stateless or operate on generated artifacts, while the characters endpoints manage mutable application state in a separate SQLite database (@file:data/lorewright-app.sqlite). Keeping these concerns on different URL prefixes makes it easier to reason about which endpoints have side effects and which are pure reads.

All schemas use Pydantic extra="forbid" rather than silently dropping unknown fields. This was a deliberate choice to catch client-side schema drift early—if the frontend sends a field the backend doesn't expect, the request fails loudly rather than succeeding with silently ignored data. The trade-off is that schema evolution requires coordinated backend and frontend changes, but the project accepts this cost because TypeScript types in @file:web/src/types.ts are maintained as mirrors of the Python schemas.

Assumptions & Constraints

  • Single-edition guard: The v1 character persistence flow only supports SRD 2014. The SupportedSrdEdition type is narrowed to Literal["2014"] even though the broader schema type SrdEdition includes "2024". This means any attempt to create or level-up a character with edition: "2024" will be rejected at the schema level before reaching engine logic.
  • SQLite as the persistence layer: Both the source content database and the app state database use SQLite. This assumes single-writer access patterns and rules out horizontal scaling of the API server without an external coordination layer. For the current single-instance deployment (Vercel serverless + local dev), this is acceptable.
  • Manifest as source of truth for progression lookups: The API never reads progression files by path convention alone—it always resolves through the manifest. This means a progression file that exists on disk but isn't registered in the manifest is invisible to the API.
  • Immutable snapshots are append-only: There is no endpoint to delete or overwrite a snapshot. This simplifies audit trails but means storage grows monotonically with character activity.

Conceptual Model

The API can be understood as three concentric layers of trust:

  1. Schema validation (outermost): Pydantic models reject structurally invalid requests before any business logic runs. This catches typos, unknown fields, and type mismatches.
  2. Business rule validation: The level-up engine checks game-rule invariants (feat prerequisites, spell legality, selection counts). These produce typed ValidationIssue objects with machine-readable code fields rather than free-text error messages.
  3. Persistence guards (innermost): The character store only commits a new snapshot if the business-rule layer reports zero issues. This prevents partially-valid state from being persisted.

This layered model means that a client can call the stateless preview and apply endpoints repeatedly to iterate on decisions without any risk of corrupting persisted state. Persistence is a separate, guarded step that only succeeds when the full decision set passes validation. The boundary between computation and persistence is the key architectural seam that allows the level-up engine to be tested entirely in isolation from the database.