feat(spells): required ?edition= on GET /api/spells + SpellClass className index - #1734
Conversation
…assName index (#1712) F3 of epic #1517, reversing #1377's "no ?edition=". GET /api/spells now 400s without ?edition=, applying withEditionOrShared + resolveEditionCatalog so a genuine 2014/2024 fork resolves to one row per name (same shape as featsRouter/referenceRouter). The creation picker (resolveCreationSpells) and level-up learn path (loadPickCatalogRows) gain a non-breaking cross-edition admission check (rejectCrossEditionSpellForks): a submitted spell id is rejected only when the requesting edition actually resolves a DIFFERENT row for that name — not on a bare tag mismatch, since today's catalog is uniformly EDITION_2024-tagged with no 2014 counterpart yet, and a strict tag check would have regressed #1729's shipped 2014 known-caster level-up. Every frontend/src/api caller of the endpoint now threads the viewing character's (or creation draft's) edition through. Folds in #1711's review follow-up: SpellClass gains @@index([className]) via a hand-written migration (Prisma 7.8 gates DDL), ahead of the 2014 content slices multiplying the join's row count. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018rre9Ho8Vx8zNtzzKkpvFn
|
Claude finished @Sandersland's task in 4m 27s —— View job Code Review: feat(spells): required ?edition= on GET /api/spells + SpellClass className indexTasks
Overall AssessmentThis is a clean, well-reasoned PR. The required-edition contract is consistent with Two findings worth calling out; neither is a blocker. Finding 1 — Latent stale-spells in
|
…atalog resolution graceful, not exclusive Two fixes surfaced by running the e2e suite against PR #1734: 1. frontend/e2e/global-setup.ts's resolveSpellIds and frontend/e2e/helpers/api.ts's learnSpells both called GET /api/spells with no ?edition=, so the now-required param 400s the whole suite at global setup. Both now pass EDITION_2024 explicitly, mirroring resolveSpeciesId/fetchManeuvers' existing precedent in the same files. 2. Fixing (1) surfaced a deeper regression: GET /api/spells previously used withEditionOrShared + resolveEditionCatalog (the feats.ts/reference.ts pattern), which excludes any row whose edition tag doesn't match the request and has no shared/null fallback. That pattern is correct for Feat/Subclass/Background, which have full coverage on both editions — but the spell catalog does not: today's ~109 rows are ALL tagged EDITION_2024 with no 2014 counterpart (2014 content slices #1713-#1721 haven't landed). Applied literally, it emptied the creation/level-up spell picker for every 2014 caster, timing out creation.spec.ts's "a 2014 warlock must choose its patron at creation" e2e test — which documents "level1SpellPicks is edition-invariant, so a 2014 Warlock still walks it exactly like the 2024 case" as existing, deliberate product behavior. Replaced with resolveSpellCatalogForEdition (spell-classes.ts): same exact-then-shared preference, but falls back to a name's only candidate instead of excluding it. A genuine 2014/2024 fork still resolves correctly (exact-match wins before the fallback runs, proven by spells.test.ts's fork-disjointness suite, updated to also cover a lone single-edition row leaking gracefully and a fork correctly cutting that leak off once a real sibling lands). This mirrors the non-breaking design already used for the write-path admission check (rejectCrossEditionSpellForks) in the same commit — the read side needed the identical "shared until a real fork exists" posture, not the stricter feats.ts pattern. Verified via the containerized Playwright e2e suite (3 full runs): every spell/creation test passes consistently across all runs; the only failures seen were on unrelated specs (shadow-step, visual screenshots, resistance), each different between runs and passing on isolated re-run — host/container networking flakiness, not a regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018rre9Ho8Vx8zNtzzKkpvFn
|
Claude finished @Sandersland's task in 4m 0s —— View job Code Review: feat(spells): required ?edition= on GET /api/spells + SpellClass className indexTasks
Overall AssessmentA clean, well-reasoned PR. The required-edition contract matches the Finding 1 —
|
Summary
?edition=" onGET /api/spells. The route now 400s without?edition=(absent or unrecognized), applyingwithEditionOrShared+resolveEditionCatalog(catalog-edition.ts) so a genuine 2014/2024 fork resolves to exactly one row per name — the same shapefeatsRouter/referenceRouteralready use.editionOf(char):resolveCreationSpells(character-create.ts) and the level-up learn path (loadPickCatalogRows/assertOnSpellList, level-up-transaction.ts). Both gain a newrejectCrossEditionSpellForksadmission check (lib/spellcasting/spell-classes.ts) — it rejects a submitted spell id only when the requesting edition actually resolves a different row for that name, not on a bare tag mismatch. This matters because today's real catalog (~109 rows) is uniformlyEDITION_2024-tagged with no 2014 counterpart yet; a strictcrossEditionRejection-style check would have rejected every 2014 spell pick and regressed feat(leveling): 2014 known-caster level-up spell picks, Ranger swap, known-vs-prepared copy #1729's just-shipped 2014 known-caster level-up. Verified empirically both ways (see Concerns below).frontend/src/apicaller chain of the spells endpoint to pass the viewing character's (or creation draft's) edition — no unfiltered browse remains.SpellClassgains@@index([className])via a hand-written migration (Prisma 7.8 gates DDL), ahead of the 2014 content slices (2014 spell catalog CS: transcribe PHB'14 shared / multi-list spells (3+ class lists), authored once #1713-2014 spell catalog: transcribe the PHB'14 Ranger spell list (levels 0-9) #1721) multiplying the join's row count.Required-param contract
GET /api/spells?edition=— same shape asfeatsRouter/referenceRouter(#1411/#1412):"Missing required query parameter: edition""Unknown edition: <value>"?class=/?maxLevel=remain optional, applied inside the edition-scoped query.Index migration
Hand-written (
20260804231500_spell_class_class_name_index/migration.sql) rather thanprisma migrate dev-generated, matching #1711's own join migration precedent — Prisma 7.8 gates DDL against this schema. Pure additiveCREATE INDEX, applied + verified viaprisma migrate status(no drift) in the worktree DB.Tests (RED → GREEN)
spells.test.ts: new describe blocks for the required-param 400s and a genuine fixture fork (2014+2024 rows sharing a name) proving disjoint resolution — confirmed RED (5/5 failing) by stashing the route change, then GREEN.character-create-spells.test.ts/level-up-transaction.test.ts: new fork-rejection tests (fixture rows only, since no real 2014 spell content exists yet) — confirmed RED by temporarily removing therejectCrossEditionSpellForkscall, then GREEN.Frontend callers updated
fetchSpells/useSpellCatalognow take a requirededition. Threaded through:CreationSpellsStep,SpeciesCantripSection(creation draft'srulesEdition),NewSpellsStep,ReviewStep's spell-name resolver (character.rulesEdition),SpellCatalogTab→AddSpellPanel→SpellsSection(character'srulesEdition), andCapabilityEditor(DM item authoring) → threaded up throughCampaignItemFields/CampaignItemFormfromCampaignItemsPanel's existing campaigneditionprop.Drifted refs
The issue's line numbers (
character-create.ts:1304-1333,level-up-transaction.ts:404-439) predate #1711 (SpellClass join) and #1729 (2014 known-caster level-up), both merged into this branch's base. Re-grepped by symbol per the build brief.Concerns
rejectCrossEditionSpellForks) is deliberately not a strictrow.edition === editiongate — see its docstring for the full reasoning. Worth a second look: this is a judgment call favoring today's shipped behavior over a stricter (but currently catalog-incompatible) interpretation of "a 2014 Bard must never see a 2024-only row." Once real 2014 spell content lands (2014 spell catalog CS: transcribe PHB'14 shared / multi-list spells (3+ class lists), authored once #1713-2014 spell catalog: transcribe the PHB'14 Ranger spell list (levels 0-9) #1721), this check starts doing real work; until then it's inert on the real catalog (proven only by the fixture-fork tests).resolveSpeciesCantripGrant(character-create.ts, High Elf's Cantrip) sharescreationPickErrorwithresolveCreationSpellsbut was left out of this fork check — same latent gap, out of the issue's explicit two-function scope.Closes #1712