chore(release): v3.9.0 — Game-Aware Systems #212

Merged
claude-bot merged 18 commits from feat/v3.9-game-aware-systems into main 2026-07-18 19:39:56 +00:00
Contributor

Merges the complete v3.9.0 Game-Aware Systems pillar (milestone: 7/7 issues) into main. This is the single, final merge of the pillar — each issue landed as its own reviewed PR onto the feat/v3.9-game-aware-systems integration branch first (PRs #205–#211).

The pillar

Campaigns can link to a first-class registry of TTRPG systems instead of only free text — unlocking richer AI context, versioned schema-validated stat blocks, a typed editor, schema-targeted generation, and an opt-in wizard to backfill structured stats. Fully opt-in and reversible: free-text campaigns behave exactly as before. Foundation for the FoundryVTT pillar.

# Issue PR
#133 GameSystem registry + campaign linkage (backend) #205
#139 Versioned stat schemas + stats storage + validation #206
#135 System selector UI + free-text coexistence + nudge #207
#137 System-aware LLM prompt context #208
#140 Stat-block editor + infobox projection #209
#142 Schema-targeted stat-block generation #210
#145 Opt-in conversion wizard (backend + frontend) #211

Migrations (4)

b5c6d7e8f9a0 (game_systems + campaigns.game_system_id) → c7d8e9f0a1b2 (game_system_schemas + lore_entries/versions.stats) → d8e9f0a1b2c3 (lore_entry_drafts.current_stats) → e9f0a1b2c3d4 (convert_stats enum + current_stats_notes). Head: e9f0a1b2c3d4. Each verified upgrade → downgrade → upgrade against real Postgres 16.

Verification

Every issue was verified in Docker before merge:

  • Backend: 654 tests pass (py3.12), up from 594 at the start of the pillar — no regressions across the whole branch.
  • Frontend: 338 tests pass (32 files) + production build succeeds (node:20-alpine).
  • All four migrations apply and reverse cleanly.

Compatibility

  • No bot API change — BOT_CONTRACT_VERSION stays 1. The bot package is untouched; bot payloads send the resolved effective system name with no shape change.
  • Existing free-text campaigns are behaviourally unchanged (verified by existing tests passing unmodified).
  • A latent #139 storage-semantics bug (stats stored as JSON 'null' vs SQL NULL) was found and fixed during #145 (none_as_null=True).

Known follow-ups (non-blocking, out of scope)

  • patch_draft doesn't yet accept inline current_stats/value edits during draft review (#142/#145) — GM edits sidebar rows, stats re-derive on approve.
  • Campaign export/import doesn't yet carry the stats envelope (flagged in #139) — a stat block is lost on export→re-import; candidate for a small follow-up with an export-schema-version bump.

Merging this to main cuts v3.9.0.

🤖 Generated with Claude Code

Merges the complete **v3.9.0 Game-Aware Systems** pillar (milestone: 7/7 issues) into `main`. This is the single, final merge of the pillar — each issue landed as its own reviewed PR onto the `feat/v3.9-game-aware-systems` integration branch first (PRs #205–#211). ## The pillar Campaigns can link to a first-class registry of TTRPG systems instead of only free text — unlocking richer AI context, versioned schema-validated stat blocks, a typed editor, schema-targeted generation, and an opt-in wizard to backfill structured stats. **Fully opt-in and reversible: free-text campaigns behave exactly as before.** Foundation for the FoundryVTT pillar. | # | Issue | PR | |---|-------|-----| | #133 | GameSystem registry + campaign linkage (backend) | #205 | | #139 | Versioned stat schemas + `stats` storage + validation | #206 | | #135 | System selector UI + free-text coexistence + nudge | #207 | | #137 | System-aware LLM prompt context | #208 | | #140 | Stat-block editor + infobox projection | #209 | | #142 | Schema-targeted stat-block generation | #210 | | #145 | Opt-in conversion wizard (backend + frontend) | #211 | ## Migrations (4) `b5c6d7e8f9a0` (game_systems + campaigns.game_system_id) → `c7d8e9f0a1b2` (game_system_schemas + lore_entries/versions.stats) → `d8e9f0a1b2c3` (lore_entry_drafts.current_stats) → `e9f0a1b2c3d4` (convert_stats enum + current_stats_notes). Head: `e9f0a1b2c3d4`. Each verified `upgrade → downgrade → upgrade` against real Postgres 16. ## Verification Every issue was verified in Docker before merge: - **Backend: 654 tests pass** (py3.12), up from 594 at the start of the pillar — no regressions across the whole branch. - **Frontend: 338 tests pass** (32 files) + production build succeeds (`node:20-alpine`). - All four migrations apply and reverse cleanly. ## Compatibility - **No bot API change — `BOT_CONTRACT_VERSION` stays 1.** The bot package is untouched; bot payloads send the resolved effective system name with no shape change. - Existing free-text campaigns are behaviourally unchanged (verified by existing tests passing unmodified). - A latent #139 storage-semantics bug (`stats` stored as JSON `'null'` vs SQL `NULL`) was found and fixed during #145 (`none_as_null=True`). ## Known follow-ups (non-blocking, out of scope) - `patch_draft` doesn't yet accept inline `current_stats`/value edits during draft review (#142/#145) — GM edits sidebar rows, stats re-derive on approve. - Campaign export/import doesn't yet carry the `stats` envelope (flagged in #139) — a stat block is lost on export→re-import; candidate for a small follow-up with an export-schema-version bump. Merging this to `main` cuts **v3.9.0**. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Introduce a GameSystem registry (game_systems table, seeded with D&D 5e,
Pathfinder 2e, and a system-agnostic Generic at fixed UUIDs) and an optional
campaigns.game_system_id FK (ON DELETE SET NULL). A campaign can link to a
registry system without disturbing its existing free-text game_system column;
a resolution helper decides the effective display name (registry short_name
when linked, else the free text).

- New: models/game_system.py, services/game_system_service.py (resolution,
  validation, idempotent builtin self-heal for the create_all test harness),
  routers/game_systems.py (GET /api/game-systems), Alembic b5c6d7e8f9a0.
- CampaignCreate/Update accept game_system_id (validated against active
  systems; 400 on unknown/inactive). CampaignResponse/Summary gain
  game_system_ref and return the resolved effective name; responses now built
  via campaign_service.to_campaign_response() with game_system_ref eager-loaded.
- Bot payloads (SessionTimeslotsResponse, NextSessionResponse) send the
  resolved name; field shape unchanged, BOT_CONTRACT_VERSION stays 1.

Foundational issue for the v3.9.0 Game-Aware Systems pillar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the #133 game-system registry into the campaign create/settings forms:
a new GameSystemSelect dropdown (registry systems + "Free text / Other",
defaulting to free text) sits alongside the existing free-text input on both
Dashboard's create form and CampaignDetail's edit form, threading
game_system_id through create/update payloads without ever removing the
free-text fallback. Cards/header show a subtle "Linked" badge when
game_system_ref is set. CampaignDetail also adds a GM-only, dismissible
suggested-match nudge: on load, if the campaign is unlinked and its free-text
game_system matches a registry system's short_name/name/aliases, offer to
link it (PATCH game_system_id only, free text untouched); dismissal persists
per-campaign in localStorage.

Files:
- api/gameSystems.js (new): fetchGameSystems() client for GET /api/game-systems
- components/GameSystemSelect.jsx (new): shared dropdown + free-text control
- pages/Dashboard.jsx: create form selector, Linked badge on cards
- pages/CampaignDetail.jsx: edit form selector, Linked badge, suggested-match
  nudge (findGameSystemMatch, accept/dismiss handlers)
- pages/Dashboard.test.jsx, pages/CampaignDetail.test.jsx: coverage for both
  modes, create/save payloads, and nudge show/hide/accept/dismiss

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Add game_system_service.resolve_system_prompt_context(db, campaign): for a
campaign linked to the game system registry, resolve the linked system's
prompt_hint (falling back to short_name) instead of the bare free-text
game_system column; unlinked campaigns keep byte-identical prompts.

Wire it into the three LLM prompt-building call sites: session summarise
(audio_service.summarise, called from reminder_tasks.process_audio and
routers/sessions.py select_canonical_name), name-option generation
(audio_service.generate_name_options, called from routers/sessions.py,
routers/campaigns.py get_cached_name_options, and
tasks/planning_tasks.prefetch_name_options — each now also passes the
linked system's key as system_key), and lore extraction
(lore_service.generate_session_lore_proposals).

generate_name_options gains an optional system_key param plus a
per-system category-hint override map (pf2e player_character/npc example)
that only applies when a campaign is linked to that system.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consume the #139 stat-schema/stats contract in the wiki article view.

- gameSystems.js: add fetchGameSystemSchema(systemId) → GET
  /api/game-systems/{id}/schema (404 = no typed editor).
- New StatBlockEditor component: schema-driven grouped form for npc/creature
  entries (GM, edit mode) whose campaign links a system with a section for the
  entry type. Per-field-type inputs (int/str/text/enum/bool/list[str]/list[text]),
  prefilled from entry.stats.values, visibility control (default gm). Saves by
  PATCHing the lore entry {stats:{values,visibility}}; server 400s surface inline.
  Read-only edge states: version mismatch (Update-to-vN re-opens the editor
  against the current schema; no auto-migration) and system mismatch / unlinked
  campaign (badge + Delete stat block → PATCH stats:null).
- WikiArticle: fetch the linked system schema, render the editor in edit mode,
  and project entry.stats.values as infobox rows alongside sidebar_fields
  (schema field labels, else raw key). Server already hides GM-only stats.
- Tests: StatBlockEditor.test.jsx (form render, save PATCH, validation error,
  both read-only edge states, helpers) and WikiArticle.test.jsx additions
  (infobox projection, editor-in-page).

Frontend only; backend contract from #139 unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Schema min/max render as native number-input hints, but native HTML form
validation blocked submit on an out-of-range value before it reached the
server — contradicting the server-authoritative design (#139) and preventing
a server error (or a value the server would accept) from ever surfacing. Add
`noValidate` to the form; add a call assertion to the error-surfacing test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Upgrade the #130 generate_statblock draft to target a linked system's active
schema: when a campaign links a system whose schema covers the entry's type,
generate a schema-validated `values` object instead of free-form markdown, and
write it to the entry's stats envelope on approval.

- audio_service.generate_lore_entry_statblock_targeted: prompts with the schema
  field list, asks for JSON `values`, validates via validate_stats_values,
  retries once with the error as guidance, then falls back to #130's free-form
  generator. sidebar_fields + markdown body are derived from the same validated
  values for coherence.
- Worker (run_lore_entry_draft_generation) picks the targeted path only when a
  covering schema exists (else #130 unchanged); stages the server-stamped stats
  envelope on the new lore_entry_drafts.current_stats column (migration
  d8e9f0a1b2c3). Eager-loads game_system_ref to avoid async lazy loads.
- approve_draft re-validates the staged stats against the current schema before
  writing entry.stats (skips + logs if the schema changed); the version snapshot
  captures stats. current_stats surfaced on LoreDraftResponse for review.
- Also resolves #137's follow-up: the flesh-out backstory/statblock prompts now
  use resolve_system_prompt_context instead of the raw game_system column.

Fallback for no-linked-system / generic / uncovered-type / validation-fails is
exactly #130's free-form behaviour (current_stats NULL). No bot API change;
BOT_CONTRACT_VERSION stays 1.

Verified in Docker (py3.12): 641 tests pass; migration upgrade/downgrade/upgrade
clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a convert_stats draft mode that LLM-maps an entry's existing free-form data
(body + loose sidebar_fields) into schema-validated structured stats for GM
review, entering the existing draft rail. Approval writes only entry.stats
(append-only — original prose and sidebar rows untouched), reusing #142's
current_stats staging + approval path.

- New LoreDraftMode.convert_stats + lore_entry_drafts.current_stats_notes
  (review-only per-field confidence/source). Migration e9f0a1b2c3d4 adds the
  enum value (via Alembic autocommit_block) + the notes column.
- audio_service.convert_lore_entry_stats: map-not-invent prompt (emit values
  only for stats present in the source), per-field confidence/source, validate
  via #139, retry once, else fail (no free-form fallback — structured mapping
  is the point).
- draft_service.start_convert_stats_draft (title/body/sidebar unchanged); worker
  branch requires a covering schema; approval reuses #142's stats write.
- Router: convert_stats gate (npc/creature + schema coverage) + a GM-only
  "review all" queue endpoint (GET .../wiki/convert-stats/queue).
- Fix a latent #139 storage bug the queue's `stats IS NULL` filter exposed:
  LoreEntry/LoreEntryVersion.stats now use none_as_null=True so a Python None is
  stored as SQL NULL rather than the JSON scalar 'null' (also fixes #140's
  "delete stat block" round-trip).

Verified in Docker (py3.12): 654 tests pass; migration up/down/up clean (enum
value + notes column). No bot API change; BOT_CONTRACT_VERSION stays 1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CampaignDetail: a GM-only "Stat conversion" wizard card (shown when the
  campaign links a schema-bearing system) that fetches the eligible-entry queue
  and starts a convert_stats draft per entry, navigating to draft review.
- WikiDraftReview: a convert_stats branch that shows the article prose read-only
  (unchanged) and the proposed mapped stat block from current_stats.values with
  per-field confidence/source (from current_stats_notes), schema-labelled;
  approve writes stats append-only. Other draft modes untouched.
- api: fetchConvertStatsQueue; startLoreDraft forwards mode "convert_stats".
- Scope the #135 Linked-badge test to the unique "Linked" badge (the wizard now
  also renders the system name on screen).

Verified in Docker (node:20-alpine): 338 frontend tests pass; production build
succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
chore(release): v3.9.0 — Game-Aware Systems
Some checks failed
CI / Backend lint (ruff) (pull_request) Failing after 47s
CI / Docker image build (pull_request) Successful in 36s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m5s
CI / Bot tests and audit (pull_request) Successful in 2m17s
CI / Backend migration, tests, and audit (pull_request) Successful in 4m50s
9540d5250c
Bump APP_VERSION to 3.9.0 and consolidate the v3.9.0 changelog for the
Game-Aware Systems pillar (#133, #135, #137, #139, #140, #142, #145).
BOT_CONTRACT_VERSION stays 1 (no bot API change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
style(backend): apply ruff format to v3.9.0 files
All checks were successful
CI / Docker image build (pull_request) Successful in 19s
CI / Backend lint (ruff) (pull_request) Successful in 52s
CI / Bot tests and audit (pull_request) Successful in 1m58s
CI / Frontend tests, audit, and build (pull_request) Successful in 4m2s
CI / Backend migration, tests, and audit (pull_request) Successful in 4m39s
2bc611cab6
CI's lint-backend job (ruff format --check, ruff 0.4.4) flagged 12 files added
across the Game-Aware Systems pillar. Formatting only — no behaviour change.
(CI runs only on main-targeting PRs, so the per-issue PRs onto the integration
branch never exercised it.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
claude-bot scheduled this pull request to auto merge when all checks succeed 2026-07-18 19:36:00 +00:00
claude-bot deleted branch feat/v3.9-game-aware-systems 2026-07-18 19:39:57 +00:00
Sign in to join this conversation.
No description provided.