[Game Systems] GameSystem registry + campaign linkage (backend) #133
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
Today "game system" is a single nullable free-text string on
Campaign(
webapp/backend/app/models/campaign.py:35) whose only job is to be interpolated intoLLM prompts and Discord embed footers. There is no registry of systems, no structured
stat model, and no rules awareness anywhere in the codebase. Real-world values are
unnormalised free text —
"D&D 5e","Pathfinder","PF2e","5e","Call of Cthulhu"all appear interchangeably in fixtures (
webapp/backend/tests/test_campaigns.py:47,122,webapp/frontend/src/pages/Dashboard.test.jsx:37-38,scripts/seed_dev.py:74).This issue is the foundational step of the Game-Aware Systems pillar (see the
2026-07-15 investigation report,
docs/.internal/game-aware-systems-investigation-2026-07-15.md):a first-class
GameSystemregistry that later prompt integration, stat schemas, thestat-block editor, the conversion wizard, and the Foundry adapter registry (#24) all key
off. Everything else in the pillar depends on this landing first.
Approach
New table
game_systems:Keep the builtin set at exactly
dnd5e,pf2e,genericfor phase 1;genericcarriesno stat schema and exists so a campaign can be "system-linked" for Foundry passthrough
(#10) without claiming a schema. Custom systems (
is_builtin=false) are explicitly outof scope for phase 1 but shape the table design.
Campaign reference (coexistence contract):
game_system_id IS NULLbehaves exactly as today.effective_system_name = registry.short_name if game_system_id else campaign.game_system. The raw text column is never overwrittenwhen a system is linked (preserved for rollback/flavour, e.g. "D&D 5e (homebrewed)").
CampaignSummary/CampaignResponse(schemas/campaign.py:139,151) keepgame_system: str | nullpopulated via the resolution helper, and addgame_system_ref: {id, key, name, short_name} | null.routers/bot.py:226/271,:411/446) keep sending the resolved plainstring — zero bot-side changes required;
bot/questboard_bot/api_client.py:59,72stays untouched.
CampaignCreate/CampaignUpdate(schemas/campaign.py:45-118) gaingame_system_id: UUID | None; validation rejects unknown/inactive ids.Migration (one Alembic revision, following house rules):
CREATE TABLE game_systems …— plain columns, nosa.Enuminop.create_table(per the project's enum house rule, illustrated by
webapp/backend/alembic/versions/p6q7r8s9t0u1_expand_lore_types.py:38-75).INSERTseed rows fordnd5e,pf2e,genericwith fixed UUIDs sodev/prod/test agree.
ALTER TABLE campaigns ADD COLUMN game_system_id UUID NULL REFERENCES game_systems(id) ON DELETE SET NULL.No backfill in this migration — matching is a UI nudge, not automatic (see issue 2).
Downgrade drops only the additive column/table; no existing data is touched or lost.
Dependencies
None — this is the foundational issue for the pillar.
Out of scope
(issue 5), the conversion wizard (issue 7), and Foundry alignment (issue 8) — all
build on this but are separate issues.
is_builtin=falseauthoring flow).free text is too dirty to convert silently.
Acceptance criteria
game_systemstable exists withdnd5e,pf2e,genericseeded via raw-SQL insertin the migration, fixed UUIDs.
campaigns.game_system_idis a nullable FK togame_systems(id)withON DELETE SET NULL; existingcampaigns.game_systemcolumn and its behaviour are unchanged.schemas, exports, and (later) prompts — no duplicate resolution logic.
CampaignSummary/CampaignResponseexpose bothgame_system(resolved string,backward compatible) and the new
game_system_ref.CampaignCreate/CampaignUpdateacceptgame_system_id, validated against activeregistry rows.
routers/bot.py:226,411) andbot/questboard_bot/api_client.py:59,72require no changes — verified by existing bot contract tests passing unmodified.
game_system_id = NULL) show zero behaviour change in tests.statsstorage + validation service #139Picking this up as the foundational issue of the v3.9.0 Game-Aware Systems pillar.
Working on branch
feat/133-gamesystem-registry-backend, PR to target the integration branchfeat/v3.9-game-aware-systems(per-issue PRs onto the integration branch, then one merge tomain).Recon notes grounding the implementation against current code (issue line numbers were stale after #130/#117):
a3b4c5d6e7f8; the new migration chains off it.CampaignPK is UUID;game_systemis free-textText(schema-capped at 100). The free-text column stays untouched when a system is linked (rollback/flavour), per the coexistence contract.game_systemreaches the bot in exactly tworouters/bot.pyresponse models (SessionTimeslotsResponse,NextSessionResponse), both sending the plain string — they'll send the resolved effective name via the new helper, no shape change, noBOT_CONTRACT_VERSIONbump (stays 1).services/game_system_service.py(which #137/#139 also build on), plus aGET /api/game-systemslist endpoint for #135's selector.Scope held exactly to this issue: registry table + seed (dnd5e/pf2e/generic, fixed UUIDs),
campaigns.game_system_idFK (ON DELETE SET NULL), resolution helper,game_system_refon campaign responses,game_system_idon create/update with validation. No UI, no prompt integration, no stat schemas (those are #135/#137/#139).Done and verified — merged into the integration branch
feat/v3.9-game-aware-systemsvia PR #205.Verification (Docker, pinned Python 3.12):
CampaignResponsebuilder refactor touches every campaign endpoint — all green.game_system_ref, invalid/inactive id → 400, free-text campaign unchanged, unlink restores free text, both bot endpoints resolving correctly.b5c6d7e8f9a0against a real Postgres 16:upgrade head→downgrade -1→upgrade headall clean; dnd5e/pf2e/generic seeded;campaigns.game_system_idconfirmed asuuidFKON DELETE SET NULL.Notes for the dependent issues (#135/#137/#139):
services/game_system_service.py:resolve_effective_system_name,build_game_system_ref,validate_game_system_id,get_active_system,list_active_systems,ensure_builtin_systems.GET /api/game-systems(authenticated).game_system_service.BUILTIN_SYSTEMS;ensure_builtin_systemsself-heals for thecreate_alltest harness (tests don't run migrations — relevant for #139, which also seeds).game_systemreaches the bot resolved;BOT_CONTRACT_VERSIONstays 1.Closing; ships to
mainwith the v3.9.0 release (integration branch → main).