feat(backend): versioned stat schemas + stats storage + validation (#139) #206

Merged
claude-bot merged 1 commit from feat/139-stat-schemas into feat/v3.9-game-aware-systems 2026-07-18 16:57:57 +00:00
Contributor

Second backend issue of the v3.9.0 Game-Aware Systems pillar. Targets the integration branch. Builds on #133.

Closes #139.

What

The structured-stat foundation that #140 (editor), #142 (schema-targeted generation), and #145 (conversion wizard) build on.

  • game_system_schemas table — versioned (UNIQUE(system_id, version)), status as TEXT + CHECK (not a PG enum), definition JSONB, FK to game_systems ON DELETE CASCADE. Seeded with dnd5e v1 and pf2e v1 (npc + creature sections) loaded from app/game_systems/<key>/schema_v1.json. Migration c7d8e9f0a1b2 (chains off #133's b5c6d7e8f9a0).
  • stats JSONB envelope on lore_entries and lore_entry_versions (nullable): {system, schema_version, values, visibility}, snapshotted into version history like sidebar_fields.
  • Validation (game_system_service, hand-rolled, no new dep): validate_entry_stats resolves the campaign's linked active schema, validates values (rejects unknown fields, type mismatches — bool is not int — out-of-range ints, unknown enum choices), server-stamps system/schema_version (client values ignored, anti-spoof), defaults visibility to gm. A stats write on an unlinked/schema-less campaign or an entry type the schema doesn't cover → 400.
  • Lore CRUD accepts an optional stats envelope on create/update; explicit null clears it. Response filtering hides GM-only stat blocks from players via the existing structured-item visibility rules.
  • GET /api/game-systems/{system_id}/schema returns the active schema definition for #140's editor (404 when none, e.g. generic).
  • Self-heal: ensure_builtin_schemas mirrors #133's ensure_builtin_systems so the create_all test harness (which skips migrations) sees the seeded schemas.
  • Docs: webapp/CLAUDE.md migration list brought current (adds #133 + #139; head → c7d8e9f0a1b2).

Verification (Docker, py3.12)

  • 618 backend tests pass (was 594; +24 new in test_stat_schemas.py covering validator accept/reject, the lore-CRUD stats path, version snapshot, and the schema endpoint) — no regressions.
  • Migration against real Postgres 16: upgrade headdowngrade -1upgrade head clean; game_system_schemas seeded (dnd5e v1 / pf2e v1 active); stats columns added to both tables.

Known follow-ups (out of #139 scope)

  • Export/import does not yet carry stats — the campaign export/import bundle constructs lore dicts from an explicit key list; extending it (with an export-schema-version bump) is a natural follow-up, deliberately left out here. Flagging so a round-trip stat-block loss is tracked.
  • No bot API change; BOT_CONTRACT_VERSION stays 1.

🤖 Generated with Claude Code

Second backend issue of the **v3.9.0 Game-Aware Systems** pillar. Targets the integration branch. Builds on #133. Closes #139. ## What The structured-stat foundation that #140 (editor), #142 (schema-targeted generation), and #145 (conversion wizard) build on. - **`game_system_schemas` table** — versioned (`UNIQUE(system_id, version)`), `status` as `TEXT` + `CHECK` (not a PG enum), `definition` JSONB, FK to `game_systems` `ON DELETE CASCADE`. Seeded with **dnd5e v1** and **pf2e v1** (npc + creature sections) loaded from `app/game_systems/<key>/schema_v1.json`. Migration `c7d8e9f0a1b2` (chains off #133's `b5c6d7e8f9a0`). - **`stats` JSONB envelope** on `lore_entries` and `lore_entry_versions` (nullable): `{system, schema_version, values, visibility}`, snapshotted into version history like `sidebar_fields`. - **Validation** (`game_system_service`, hand-rolled, no new dep): `validate_entry_stats` resolves the campaign's linked active schema, validates values (rejects unknown fields, type mismatches — `bool` is not `int` — out-of-range ints, unknown enum choices), **server-stamps `system`/`schema_version`** (client values ignored, anti-spoof), defaults `visibility` to `gm`. A stats write on an unlinked/schema-less campaign or an entry type the schema doesn't cover → **400**. - **Lore CRUD** accepts an optional `stats` envelope on create/update; explicit `null` clears it. Response filtering hides GM-only stat blocks from players via the existing structured-item visibility rules. - **`GET /api/game-systems/{system_id}/schema`** returns the active schema definition for #140's editor (404 when none, e.g. `generic`). - **Self-heal**: `ensure_builtin_schemas` mirrors #133's `ensure_builtin_systems` so the `create_all` test harness (which skips migrations) sees the seeded schemas. - **Docs**: `webapp/CLAUDE.md` migration list brought current (adds #133 + #139; head → `c7d8e9f0a1b2`). ## Verification (Docker, py3.12) - **618 backend tests pass** (was 594; +24 new in `test_stat_schemas.py` covering validator accept/reject, the lore-CRUD stats path, version snapshot, and the schema endpoint) — no regressions. - Migration against real Postgres 16: `upgrade head` → `downgrade -1` → `upgrade head` clean; `game_system_schemas` seeded (dnd5e v1 / pf2e v1 active); `stats` columns added to both tables. ## Known follow-ups (out of #139 scope) - **Export/import does not yet carry `stats`** — the campaign export/import bundle constructs lore dicts from an explicit key list; extending it (with an export-schema-version bump) is a natural follow-up, deliberately left out here. Flagging so a round-trip stat-block loss is tracked. - No bot API change; `BOT_CONTRACT_VERSION` stays 1. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add a versioned, per-system stat schema registry and a validated `stats`
envelope on lore entries — the structured-stat foundation the stat-block
editor (#140), schema-targeted generation (#142), and the conversion wizard
(#145) build on.

- New game_system_schemas table (versioned, TEXT+CHECK status, JSONB
  definition, unique per system+version) seeded with dnd5e v1 and pf2e v1
  (npc + creature) loaded from app/game_systems/<key>/schema_v1.json.
  Migration c7d8e9f0a1b2.
- lore_entries.stats and lore_entry_versions.stats nullable JSONB envelopes
  {system, schema_version, values, visibility}, snapshotted into version
  history like sidebar_fields.
- game_system_service gains load_builtin_schemas / ensure_builtin_schemas
  (self-heal for the create_all test harness), get_active_schema,
  validate_stats_values, validate_entry_stats. Hand-rolled validation, no new
  dependency: rejects unknown fields, type mismatches (bool is not int),
  out-of-range ints, unknown enum choices; server stamps system/schema_version
  (client values ignored) and defaults visibility to gm.
- Lore create/update accept an optional stats envelope validated against the
  campaign's linked active schema (ValueError -> 400); a stats write on an
  unlinked/schema-less campaign or uncovered entry type is rejected. Response
  filtering hides GM-only stat blocks from players via the existing
  structured-item visibility rules.
- GET /api/game-systems/{system_id}/schema returns the active schema for the
  editor (#140), 404 when none.

Docs: bring webapp/CLAUDE.md migration list current (adds #133 + #139, head
c7d8e9f0a1b2). No bot API change; BOT_CONTRACT_VERSION stays 1.

Verified in Docker (py3.12): 618 tests pass; migration upgrade/downgrade/
upgrade clean against a real Postgres, schemas seeded and stats columns added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude-bot merged commit fbf6fe78b5 into feat/v3.9-game-aware-systems 2026-07-18 16:57:57 +00:00
Sign in to join this conversation.
No description provided.