feat(backend): schema-targeted stat-block generation (#142) #210

Merged
claude-bot merged 1 commit from feat/142-schema-targeted-statblock into feat/v3.9-game-aware-systems 2026-07-18 17:55:57 +00:00
Contributor

Backend issue of the v3.9.0 Game-Aware Systems pillar. Targets the integration branch. A bolt-on to #130, using #139's schemas + validator.

Closes #142.

What

When a campaign links a system whose active schema covers the entry's type, the existing generate_statblock draft now targets that schema instead of producing free-form prose:

  • generate_lore_entry_statblock_targeted prompts with the schema's field list and asks for a JSON values object, validates it via #139's validate_stats_values, retries once with the validation error appended as guidance, then falls back to #130's free-form generator if it still fails. sidebar_fields + the markdown body are derived from the same validated values so the infobox, body, and structured envelope can't disagree.
  • Worker takes the targeted path only when a covering schema exists (else #130 unchanged, no wasted LLM call); 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.
  • Approval re-validates the staged stats against the current schema before writing entry.stats (skips + logs if the schema changed / system unlinked since generation); the version snapshot captures stats. current_stats is surfaced on LoreDraftResponse for review parity with current_sidebar_fields.
  • Also resolves #137's flagged 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-after-retry is exactly #130's free-form behaviour (current_stats NULL, no regression).

Verification (Docker, py3.12)

  • 641 backend tests pass (was 632; +9 in test_statblock_schema_targeting.py covering targeted generation, retry-then-fallback, approval writing entry.stats, and the no-schema fallback) — no regressions.
  • Migration d8e9f0a1b2c3: upgrade headdowngrade -1upgrade head clean against real Postgres; current_stats column added.
  • webapp/CLAUDE.md migration chain updated (head → d8e9f0a1b2c3).

Known follow-up (out of scope)

  • patch_draft doesn't yet accept current_stats edits, so a GM editing a staged sidebar row in review won't retro-sync the structured envelope. Worth a small follow-up if inline stat editing during review is wanted. No bot API change; BOT_CONTRACT_VERSION stays 1.

🤖 Generated with Claude Code

Backend issue of the **v3.9.0 Game-Aware Systems** pillar. Targets the integration branch. A bolt-on to #130, using #139's schemas + validator. Closes #142. ## What When a campaign links a system whose active schema covers the entry's type, the existing `generate_statblock` draft now **targets that schema** instead of producing free-form prose: - **`generate_lore_entry_statblock_targeted`** prompts with the schema's field list and asks for a JSON `values` object, validates it via #139's `validate_stats_values`, **retries once** with the validation error appended as guidance, then **falls back to #130's free-form generator** if it still fails. `sidebar_fields` + the markdown body are **derived from the same validated `values`** so the infobox, body, and structured envelope can't disagree. - **Worker** takes the targeted path only when a covering schema exists (else #130 unchanged, no wasted LLM call); 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. - **Approval** re-validates the staged stats against the *current* schema before writing `entry.stats` (skips + logs if the schema changed / system unlinked since generation); the version snapshot captures stats. `current_stats` is surfaced on `LoreDraftResponse` for review parity with `current_sidebar_fields`. - **Also resolves #137's flagged 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-after-retry is **exactly #130's free-form behaviour** (`current_stats` NULL, no regression). ## Verification (Docker, py3.12) - **641 backend tests pass** (was 632; +9 in `test_statblock_schema_targeting.py` covering targeted generation, retry-then-fallback, approval writing `entry.stats`, and the no-schema fallback) — no regressions. - Migration `d8e9f0a1b2c3`: `upgrade head` → `downgrade -1` → `upgrade head` clean against real Postgres; `current_stats` column added. - `webapp/CLAUDE.md` migration chain updated (head → `d8e9f0a1b2c3`). ## Known follow-up (out of scope) - `patch_draft` doesn't yet accept `current_stats` edits, so a GM editing a staged sidebar row in review won't retro-sync the structured envelope. Worth a small follow-up if inline stat editing during review is wanted. No bot API change; `BOT_CONTRACT_VERSION` stays 1. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
claude-bot merged commit 76443f1117 into feat/v3.9-game-aware-systems 2026-07-18 17:55:57 +00:00
Sign in to join this conversation.
No description provided.