[Game Systems] Opt-in conversion wizard: free-form to system with LLM-assisted stat backfill #145
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Motivation/Context
Registry linkage (#133) and system-aware prompts (#137) work immediately for new
choices, but existing campaigns have years of free-form
game_systemtext and loosesidebar_fieldsrows describing NPC/creature/PC stats informally ("HP: 22","AC" / "15","Class" / "Rogue 3"). No automatic backfill was done in the storage migrations(#133, #139) deliberately — free text is too dirty to convert silently, and a wrong
silent link would change LLM prompts and future Foundry behaviour (per the report,
§4.1). This issue is the last mile: a GM-driven, fully reversible, per-campaign wizard
that performs the actual migration, with the LLM doing the tedious mapping work and the
GM approving every change through the existing review rail.
Approach
GM-triggered from campaign settings, two independently skippable steps:
Step 1 — Link the system. Pick from the registry (already possible via #135's
selector, or the alias-match nudge). Writes
game_system_id. Effect is immediate butshallow: display strings, prompt hints (#137), and (later) Foundry adapter selection
now come from the registry. Nothing touches lore entries yet.
Step 2 — Backfill structured stats (optional, LLM-assisted, GM-reviewed). For each
npc/creature/player_characterentry (opt-in per entry, or a "review all" queue):run_lore_entry_draft_generation,webapp/backend/app/tasks/reminder_tasks.py:~2079) sends the entrybody, existingloose
sidebar_fieldsrows, and the target schema (from #139) to the LLM viagenerate_structured_text(json_mode=True), instructed to map, not invent: emitvaluesonly for stats actually present in the source material, plus aconfidence/sourcenote per field.LoreEntryDraft,draft_service.py, review/iterate/approve endpointswebapp/backend/app/routers/campaigns.py:~2212-2389) as a new draft mode(
convert_stats) — same UX asrephrase/mergeand #130's modes: old vs. new,inline edit, iterate with feedback, approve or discard.
statsenvelope (per #139); never delete or rewritethe original
sidebar_fieldsrows or body prose — append-only, same philosophy as#130. A later manual cleanup of now-redundant sidebar rows is the GM's call; the
review UI offers per-row "hide this sidebar row (superseded by stats)" checkboxes.
LoreEntryVersion(existing behaviour) — per-entryrollback is the existing version-restore path, no new rollback mechanism needed.
Rollback/unlink guarantee (§4.4 of the report): setting
game_system_id = NULLrestores the exact pre-link display text (the free-text column was never touched).
Existing
statsenvelopes become dormant rather than breaking — they self-describe{system, schema_version}so they render read-only with a "not the campaign's currentsystem" badge (per #140's edge-state handling) rather than erroring; the GM may delete
them per-entry. Re-linking the same system reactivates them.
Frontend: the wizard UI (step 1/step 2 flow, per-entry or "review all" queue,
draft review reusing #140's/existing draft-review components) lives alongside the
campaign settings UI from #135.
Dependencies
statsstorage + validation service) — the wizardwrites into the
statsenvelope this issue defines and validates against it.rendering this issue's rollback guarantee relies on, and the review surface pattern
the
convert_statsdraft mode follows.Out of scope
step 1 of this wizard consumes them, doesn't reimplement them.
backfilling existing free-form data, not generating new content from scratch.
approval; nothing is ever auto-applied.
Acceptance criteria
affecting any lore entry.
stat backfill (step 2); each produces a
convert_statsdraft via the Celery taskpattern used by existing draft generation.
material (body + sidebar_fields), with per-field confidence/source metadata surfaced
in the review UI.
convert_statsuses the existing review rail (old vs. new, inlineedit, iterate, approve/discard) — no new review UI pattern invented.
statsenvelope and snapshots aLoreEntryVersion; the originalsidebar_fieldsrows andbodyprose are never deleted or rewritten by this flow.game_system_id = NULLon a converted campaign restores the originalfree-text display exactly, and existing
statsenvelopes become read-only/dormant(not deleted, not erroring) until re-linked.
pre-wizard state.
Picking this up as the final issue of the v3.9.0 pillar, on
feat/145-conversion-wizard(PR to target the integration branch). Doing it in two passes on one branch — backend now, then the frontend wizard/review UI — merged as a single PR.Backend approach: a new
convert_statsdraft mode that LLM-maps an entry's existing free-form data (body + loosesidebar_fields) into schema-validated structuredstats(map, not invent; per-field confidence/source surfaced in review), entering the existing draft-review rail. Much of the plumbing is already in place from #142 — thelore_entry_drafts.current_statscolumn and the approval path that writes a re-validated envelope toentry.stats. This flow stagescurrent_statswhile leavingcurrent_sidebar_fieldsnull andcurrent_bodyunchanged, so approval is append-only — the original sidebar rows and prose are never deleted or rewritten (per the issue's §4.1/rollback guarantees). Adds a "review all" queue endpoint for eligible npc/creature entries. Step 1 (linking a system) is already handled by #135; unlink/dormant-stats rollback by #133/#140. New enum value → migration via Alembic's autocommit block. No bot API change.Done and verified — merged into the integration branch via PR #211 (backend + frontend, two commits).
Verification (Docker):
test_convert_stats.py), no regressions. Migratione9f0a1b2c3d4up/down/up clean (enum value +current_stats_notescolumn).node:20-alpine).Delivered:
convert_statsdraft mode that LLM-maps existing body + loose sidebar_fields into schema-validatedstats(map-not-invent, per-field confidence/source, retry-once-then-fail), reusing #142's staging + append-only approval (original prose/sidebar rows never rewritten); a GM-only eligibility queue endpoint; the CampaignDetail wizard card + the WikiDraftReviewconvert_statsbranch (prose read-only, mapped stats with schema labels +confidence · source).Latent #139 fix included:
LoreEntry/LoreEntryVersion.statsnow usenone_as_null=Trueso a PythonNonestores as SQLNULL(not the JSON scalar'null') — surfaced by the queue'sIS NULLfilter; also fixes #140's "delete stat block" round-trip.Closing. This completes the v3.9.0 Game-Aware Systems pillar (7/7 issues). Next: the release (version bump + changelog) and the integration →
mainPR.statsenvelope through campaign export/import #214