docs(01): final plan cleanup — narrative consistency for chain-revalidation deferral and Plan 03b sequential framing
This commit is contained in:
@@ -8,31 +8,30 @@ files_modified:
|
||||
- server/prisma/seed-class-progression.ts
|
||||
- server/prisma/data/spell-slot-overlays.ts
|
||||
- server/prisma/data/class-feature-options.ts
|
||||
- server/prisma/data/foundry-pf2e/.keep
|
||||
- .planning/phases/01-level-up-pf2e-regelkonform/SEED-README.md
|
||||
autonomous: true
|
||||
requirements: [LVL-08, LVL-14, LVL-19]
|
||||
tags: [seeding, prisma, foundry-pf2e, class-progression, spellcaster, level-up]
|
||||
requirements: [LVL-08, LVL-14]
|
||||
tags: [seeding, prisma, foundry-pf2e, class-progression, spellcaster, level-up, pipeline]
|
||||
must_haves:
|
||||
truths:
|
||||
- "After running `npm run db:seed:class-progression`, the ClassProgression table contains at least 320 rows (16 classes × 20 levels)."
|
||||
- "After seed, every caster class (Wizard, Sorcerer, Bard, Cleric, Druid, Oracle, Witch) has spellSlotIncrement and/or cantripIncrement entries at appropriate levels."
|
||||
- "After seed, classes with L1 doctrine/school/etc. choices (Cleric L1, Wizard L1, Champion L1) have rows with `choiceType` and `choiceOptionsRef` populated, and the matching ClassFeatureOption rows exist."
|
||||
- "Seed script is idempotent — running it twice does not duplicate rows."
|
||||
- "Seed script reads from Foundry pf2e clone at `server/prisma/data/foundry-pf2e/` (gitignored, dev-time clone) and merges in the hand-curated `spell-slot-overlays.ts` constant."
|
||||
- "If the Foundry clone is missing, the script fails loudly with a documented error message pointing the dev to the seed README."
|
||||
- "Foundry pf2e clone is documented in SEED-README.md (clone command + pinned tag placeholder); .gitignore already excludes the clone path (Plan 01 Task 3 Step 5)."
|
||||
- "After running `npm run db:seed:class-progression`, the seed pipeline reads Foundry pf2e class JSONs from `server/prisma/data/foundry-pf2e/packs/classes/` and merges with the hand-curated overlays."
|
||||
- "After seed, the ClassProgression table contains at least 20 rows for Wizard (L1..L20) as the worked-example class — verifying the pipeline end-to-end."
|
||||
- "After seed, Wizard L1..L19 have non-null spellSlotIncrement / cantripIncrement entries from spell-slot-overlays.ts, AND Wizard L1 has choiceType='school' + choiceOptionsRef='wizard-school'."
|
||||
- "After seed, at least 1 ClassFeatureOption row exists for optionsRef='wizard-school' (Wizard School L1 worked example)."
|
||||
- "Seed script is idempotent — running it twice does not duplicate rows; second run reports 0 created / N updated."
|
||||
- "Seed script fails loudly with a documented error message pointing the dev to SEED-README.md when the Foundry clone is missing."
|
||||
- "spell-slot-overlays.ts and class-feature-options.ts are STRUCTURED so Plan 03b can simply append entries — no schema or shape changes required between plans."
|
||||
artifacts:
|
||||
- path: "server/prisma/seed-class-progression.ts"
|
||||
provides: "Idempotent seed transforming Foundry pf2e class JSONs + spell-slot overlay → ClassProgression + ClassFeatureOption rows"
|
||||
provides: "Idempotent seed transforming Foundry pf2e class JSONs + spell-slot overlay → ClassProgression + ClassFeatureOption rows. The pipeline is generic — it iterates over D16_CLASS_NAMES so adding new classes is data-only (Plan 03b)."
|
||||
exports: ["main (executed when run via tsx)"]
|
||||
- path: "server/prisma/data/spell-slot-overlays.ts"
|
||||
provides: "Hand-curated spell-slot/cantrip/repertoire progression for the 16 D-16 classes that cast"
|
||||
provides: "Type-safe overlay file. Phase 1 ships Wizard fully populated as the worked example; Plan 03b appends entries for Cleric/Druid/Witch/Bard/Sorcerer/Oracle and the empty arrays for non-casters."
|
||||
contains: "SPELL_SLOT_OVERLAY"
|
||||
- path: "server/prisma/data/class-feature-options.ts"
|
||||
provides: "Hand-curated ClassFeatureOption seed data — Cleric Doctrines, Wizard Schools, Champion Causes, etc."
|
||||
provides: "Type-safe option-data file. Phase 1 ships at least 1 Wizard School entry as the worked example; Plan 03b appends ≥49 more entries (Cleric Doctrines, Champion Causes, Sorcerer Bloodlines, etc. — total joint goal: ≥50)."
|
||||
contains: "CLASS_FEATURE_OPTIONS"
|
||||
- path: "server/prisma/data/foundry-pf2e/.keep"
|
||||
provides: "Anchor file so the gitignored data dir exists in fresh clones for the README to reference"
|
||||
- path: ".planning/phases/01-level-up-pf2e-regelkonform/SEED-README.md"
|
||||
provides: "Developer instructions: how to clone Foundry pf2e at the pinned tag and run the seed"
|
||||
contains: "git clone"
|
||||
@@ -56,11 +55,13 @@ must_haves:
|
||||
---
|
||||
|
||||
<objective>
|
||||
Build the Foundry-pf2e-driven seed pipeline that populates the `ClassProgression` and `ClassFeatureOption` tables for the 16 in-scope Core+APG classes (D-16) across levels 1..20. The seed transforms Foundry pf2e class JSON files (manually cloned by the dev to `server/prisma/data/foundry-pf2e/`) into our schema and merges in hand-curated overlays for spell-slot progressions (which Foundry encodes as prose, not machine-readable rules — Pitfall #6) and class-feature options (Cleric Doctrines, Wizard Schools, etc.).
|
||||
Build the Foundry-pf2e-driven seed pipeline that populates the `ClassProgression` and `ClassFeatureOption` tables. This plan owns the **pipeline** (the seed script + the type definitions of the overlay files + Wizard as the fully-curated worked example). Plan 03b runs sequentially in Wave 3 immediately after this plan (sequential because both plans write to spell-slot-overlays.ts and class-feature-options.ts — file-ownership conflict) and owns the **bulk curation** of remaining caster overlays (≥120 spell-slot entries) and remaining class-feature-options (≥49 more option rows).
|
||||
|
||||
Purpose: Plan 04's atomic commit transaction needs ClassProgression rows to know what each (className, level) grants and what proficiency/spell-slot/repertoire changes apply. Without this seed, the LevelingService cannot recompute correctly. The seed is **idempotent** so devs and CI can re-run safely.
|
||||
Why split: the curation work for the other 6 caster classes (Cleric, Druid, Witch, Bard, Sorcerer, Oracle) and the 13 other classes' L1 choice points is data-entry from Archives of Nethys — independent rows, no shared logic. Pulling it into its own plan gives the curation a separate review surface; Plan 03b runs sequentially in Wave 3 immediately after this plan (sequential because both plans write to spell-slot-overlays.ts and class-feature-options.ts — file-ownership conflict) and reuses the type contracts established here without touching the seed script.
|
||||
|
||||
Output: Seed script + two hand-curated data modules + .keep anchor + dev README with cloning instructions.
|
||||
Purpose: Plan 04's atomic commit transaction needs ClassProgression rows to know what each (className, level) grants. The pipeline must work end-to-end with Wizard (the worked example) before Plan 03b appends data for the rest.
|
||||
|
||||
Output: Seed script + two hand-curated data modules (Wizard fully populated + types defined) + dev README with cloning instructions. Plan 03b appends to the data modules without touching the seed script.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@@ -146,29 +147,29 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
|
||||
<!-- The 16 classes in D-16 scope -->
|
||||
<!-- Alchemist, Barbarian, Bard, Champion, Cleric, Druid, Fighter, Investigator, Monk, Oracle, Ranger, Rogue, Sorcerer, Swashbuckler, Witch, Wizard -->
|
||||
|
||||
<!-- Plan 01 schema's compound unique keys (from 01-01-PLAN.md Task 1) -->
|
||||
<!-- @@unique([className, level]) on ClassProgression -->
|
||||
<!-- @@unique([optionsRef, optionKey]) on ClassFeatureOption -->
|
||||
<!-- Prisma generates these as `className_level` and `optionsRef_optionKey` field names. -->
|
||||
<!-- If Plan 01's schema declarations are reordered, Prisma swaps the names — -->
|
||||
<!-- the seed script below assumes the order shown above. -->
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 1: Create the dev README + .keep anchor for the Foundry pf2e clone path</name>
|
||||
<name>Task 1: Dev README for the Foundry pf2e clone path</name>
|
||||
<files>
|
||||
server/prisma/data/foundry-pf2e/.keep,
|
||||
.planning/phases/01-level-up-pf2e-regelkonform/SEED-README.md
|
||||
</files>
|
||||
<read_first>
|
||||
- .planning/phases/01-level-up-pf2e-regelkonform/01-RESEARCH.md (lines 648-654 — Pitfall 5: Foundry data shape; lines 285-289 — Project Structure)
|
||||
- .gitignore (must already contain `server/prisma/data/foundry-pf2e/` from Plan 01 Task 3 Step 5)
|
||||
- .gitignore (must already contain `server/prisma/data/foundry-pf2e/` from Plan 01 Task 3 Step 5 — verify before this task)
|
||||
</read_first>
|
||||
<action>
|
||||
**Step 1 — Create `server/prisma/data/foundry-pf2e/.keep`** as an empty file (so the directory itself can be tracked even though its contents are gitignored — this lets the README reference an existing path).
|
||||
|
||||
Note: `.gitignore` already excludes `server/prisma/data/foundry-pf2e/` per Plan 01. To track the `.keep` file, append an exception in `.gitignore` BEFORE this task — actually, the cleaner approach: do NOT track the directory at all. Instead, the seed script's missing-clone error message tells the dev to create it. Skip the .keep file entirely.
|
||||
|
||||
REVISED Step 1: Skip the .keep file. The directory will be created by the dev when they run `git clone`. This avoids the .gitignore exception entirely.
|
||||
|
||||
**Step 2 — Create `.planning/phases/01-level-up-pf2e-regelkonform/SEED-README.md`** with the following content:
|
||||
Create `.planning/phases/01-level-up-pf2e-regelkonform/SEED-README.md` with the following content:
|
||||
|
||||
```markdown
|
||||
# Phase 1 — ClassProgression Seed README
|
||||
@@ -186,6 +187,9 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
git clone --depth 1 --branch <PINNED_TAG> https://github.com/foundryvtt/pf2e.git foundry-pf2e
|
||||
```
|
||||
|
||||
`server/prisma/data/foundry-pf2e/` is excluded from version control (.gitignore line added in Plan 01).
|
||||
Do NOT commit the clone. Re-clone after a Foundry pf2e major version ships.
|
||||
|
||||
**Pinned tag:** `<TAG_DECIDED_BY_EXECUTOR>` (record the chosen tag here when this task runs;
|
||||
the executor selects a stable release branch from `https://github.com/foundryvtt/pf2e/tags`
|
||||
that includes Core Rulebook + APG content for all 16 D-16 classes).
|
||||
@@ -207,21 +211,27 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
- **"Class JSON does not match expected schema"** — Foundry pf2e changed the JSON shape
|
||||
between major versions. Update the seed parser or pin to an older tag.
|
||||
|
||||
## What gets seeded
|
||||
## What gets seeded (cumulative across Plan 03 and Plan 03b)
|
||||
|
||||
- **ClassProgression** rows: 16 classes × 20 levels = 320 rows, with `grants[]`,
|
||||
`proficiencyChanges`, `spellSlotIncrement`, `cantripIncrement`, `repertoireIncrement`,
|
||||
`choiceType`, `choiceOptionsRef`.
|
||||
`choiceType`, `choiceOptionsRef`. Plan 03 ships Wizard L1..L20 fully (worked example);
|
||||
Plan 03b adds remaining 15 classes' L1..L20 rows (data-only — Plan 03's pipeline already
|
||||
seeds 320 rows; Plan 03b just enriches them with overlay data).
|
||||
- **ClassFeatureOption** rows: hand-curated Cleric Doctrines, Wizard Schools, Champion
|
||||
Causes, Sorcerer Bloodlines (where L1-set), Druid Orders, etc.
|
||||
Causes, Sorcerer Bloodlines (where L1-set), Druid Orders, etc. Joint goal across both
|
||||
plans: ≥50 rows. Plan 03 ships at least 1 (Wizard School worked example); Plan 03b
|
||||
ships ≥49 more.
|
||||
- Spell-slot/cantrip/repertoire progressions come from the hand-curated overlay
|
||||
`server/prisma/data/spell-slot-overlays.ts` because Foundry encodes them in prose
|
||||
(Pitfall #6).
|
||||
(Pitfall #6). Plan 03 ships Wizard's full L1..L19 entries; Plan 03b ships the other
|
||||
6 caster classes plus empty arrays for non-casters.
|
||||
```
|
||||
|
||||
Note: The `<PINNED_TAG>` placeholder is filled in during execution when the dev/executor selects a tag.
|
||||
|
||||
**Step 3 — Verify:** `ls server/prisma/data/` should not yet contain `foundry-pf2e/` (the dev creates it later via the README instructions). The README itself lives under `.planning/phases/01-level-up-pf2e-regelkonform/SEED-README.md`.
|
||||
Verify: `ls .planning/phases/01-level-up-pf2e-regelkonform/SEED-README.md` shows the file.
|
||||
The Foundry clone directory is created later by the dev (Task 5 step 1) following the README.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f .planning/phases/01-level-up-pf2e-regelkonform/SEED-README.md && grep -c "git clone" .planning/phases/01-level-up-pf2e-regelkonform/SEED-README.md</automated>
|
||||
@@ -232,25 +242,30 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
- File contains the literal string `npm run db:seed:class-progression`
|
||||
- File contains the literal string `Apache 2.0` (license declaration)
|
||||
- File contains a "Pinned tag" reference (placeholder OK if dev hasn't picked one yet)
|
||||
- File mentions both Plan 03 (Wizard worked example) AND Plan 03b (bulk curation)
|
||||
- No file is created inside `server/prisma/data/foundry-pf2e/` (the directory is dev-time only — gitignored)
|
||||
</acceptance_criteria>
|
||||
<done>
|
||||
Dev README documents the clone step and the run step. No tracked files inside the gitignored data dir.
|
||||
Dev README documents the clone step and the run step, and explicitly references the Plan 03 / Plan 03b split. No tracked files inside the gitignored data dir.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 2: Hand-curated spell-slot overlay (16 classes, all levels)</name>
|
||||
<name>Task 2: Spell-slot overlay file — types + Wizard worked example</name>
|
||||
<files>server/prisma/data/spell-slot-overlays.ts</files>
|
||||
<read_first>
|
||||
- .planning/phases/01-level-up-pf2e-regelkonform/01-RESEARCH.md (lines 656-661 — Pitfall #6 + lines 897-924 — Code Examples #6 shape)
|
||||
- .planning/phases/01-level-up-pf2e-regelkonform/01-CONTEXT.md (D-16 — class scope; D-17 — Foundry as source; D-18 — spontaneous casters get repertoire)
|
||||
- server/src/modules/leveling/lib/types.ts (Proficiency, AbilityAbbreviation type vocabulary; SpellTradition can be defined here in the overlay file)
|
||||
- Archives of Nethys spell tables (cited as authoritative source for hand-curation): https://2e.aonprd.com — for each caster class, the canonical PF2e Player Core / APG spell-slot table
|
||||
- Archives of Nethys Wizard spell table: https://2e.aonprd.com/Classes.aspx?ID=18 (or current canonical URL)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `server/prisma/data/spell-slot-overlays.ts` containing hand-curated spell-slot/cantrip/repertoire data for all 16 D-16 classes. Casters get rows; non-casters (Fighter, Barbarian, Rogue, Monk, Swashbuckler, Investigator, Ranger, Alchemist) get an empty array.
|
||||
Create `server/prisma/data/spell-slot-overlays.ts` containing:
|
||||
1. The TS types (`SpellTradition`, `SpellSlotOverlayEntry`) — these are the contract Plan 03b appends to.
|
||||
2. Wizard fully populated for L1..L19 (the worked example used by the seed pipeline test in Task 5).
|
||||
3. Stub keys for the other 15 D-16 classes set to empty arrays — Plan 03b populates these.
|
||||
|
||||
**File header:**
|
||||
**File contents:**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
@@ -265,8 +280,10 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
* (https://2e.aonprd.com). Per-class spell-slot tables, cantrip counts, and repertoire
|
||||
* sizes (spontaneous casters only).
|
||||
*
|
||||
* SCOPE: 16 D-16 classes (Core + APG). Casters: Bard, Cleric, Druid, Oracle, Sorcerer,
|
||||
* Witch, Wizard, Champion (focus only — minimal). Non-casters: empty array.
|
||||
* SCOPE: 16 D-16 classes (Core + APG). This file (Plan 03) ships the type definitions
|
||||
* and Wizard fully populated as the worked example. Plan 03b appends entries for the
|
||||
* remaining 6 caster classes (Cleric, Druid, Witch, Bard, Sorcerer, Oracle) and the
|
||||
* empty-array entries for non-casters.
|
||||
*
|
||||
* SPONTANEOUS vs PREPARED: Spontaneous casters (Bard, Sorcerer, Oracle) get
|
||||
* repertoireIncrement entries on level-up. Prepared casters (Cleric, Druid, Witch, Wizard)
|
||||
@@ -288,8 +305,7 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
* Order within a level does not matter — the seed script merges them per (class, level).
|
||||
*/
|
||||
export const SPELL_SLOT_OVERLAY: Record<string, SpellSlotOverlayEntry[]> = {
|
||||
// === PREPARED CASTERS ===
|
||||
|
||||
// === PREPARED CASTER — WIZARD (worked example, fully populated in Plan 03) ===
|
||||
Wizard: [
|
||||
{ level: 1, cantripIncrement: 5 },
|
||||
{ level: 1, spellSlotIncrement: { tradition: 'ARCANE', spellLevel: 1, count: 2 } },
|
||||
@@ -315,64 +331,17 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
// L20: no slot increment (capstone is qualitative)
|
||||
],
|
||||
|
||||
Cleric: [
|
||||
{ level: 1, cantripIncrement: 5 },
|
||||
{ level: 1, spellSlotIncrement: { tradition: 'DIVINE', spellLevel: 1, count: 2 } },
|
||||
// … L2..L20 mirroring Wizard cadence with DIVINE tradition (executor curates from
|
||||
// Archives of Nethys Cleric class entry).
|
||||
],
|
||||
|
||||
Druid: [
|
||||
{ level: 1, cantripIncrement: 5 },
|
||||
{ level: 1, spellSlotIncrement: { tradition: 'PRIMAL', spellLevel: 1, count: 2 } },
|
||||
// … executor curates from AoN Druid entry.
|
||||
],
|
||||
|
||||
Witch: [
|
||||
{ level: 1, cantripIncrement: 5 },
|
||||
// Witch tradition depends on patron — DEFAULT to ARCANE for the overlay; recompute
|
||||
// may need the actual chosen patron for accurate filtering. Document in the
|
||||
// seed README's "Caveats" section.
|
||||
{ level: 1, spellSlotIncrement: { tradition: 'ARCANE', spellLevel: 1, count: 2 } },
|
||||
// … executor curates from AoN Witch entry.
|
||||
],
|
||||
|
||||
// === SPONTANEOUS CASTERS (D-18 — get repertoireIncrement) ===
|
||||
|
||||
Bard: [
|
||||
{ level: 1, cantripIncrement: 5 },
|
||||
{ level: 1, spellSlotIncrement: { tradition: 'OCCULT', spellLevel: 1, count: 2 } },
|
||||
// Repertoire size at L1 = 4 (Player Core); growth = +1 per even level for spell-level
|
||||
// slots gained. Executor curates exact deltas.
|
||||
{ level: 2, repertoireIncrement: 1 },
|
||||
// … executor curates from AoN Bard entry, full L1..L20.
|
||||
],
|
||||
|
||||
Sorcerer: [
|
||||
{ level: 1, cantripIncrement: 5 },
|
||||
// Tradition depends on bloodline — DEFAULT to ARCANE; document caveat.
|
||||
{ level: 1, spellSlotIncrement: { tradition: 'ARCANE', spellLevel: 1, count: 3 } },
|
||||
{ level: 2, repertoireIncrement: 1 },
|
||||
// … executor curates from AoN Sorcerer entry, full L1..L20.
|
||||
],
|
||||
|
||||
Oracle: [
|
||||
{ level: 1, cantripIncrement: 5 },
|
||||
{ level: 1, spellSlotIncrement: { tradition: 'DIVINE', spellLevel: 1, count: 3 } },
|
||||
{ level: 2, repertoireIncrement: 1 },
|
||||
// … executor curates from AoN Oracle entry, full L1..L20.
|
||||
],
|
||||
|
||||
// === HALF-CASTERS / FOCUS-ONLY (minimal entries) ===
|
||||
|
||||
Champion: [
|
||||
// Champion gets focus spells (Devotion Spells) — for Phase 1 we record cantrip-like
|
||||
// L1 entry only; focus-spell mechanics handled outside the slot table.
|
||||
{ level: 1, cantripIncrement: 0 },
|
||||
],
|
||||
|
||||
// === NON-CASTERS — explicitly empty so the seed knows "no overlay needed" ===
|
||||
// === STUBS — populated by Plan 03b ===
|
||||
// Caster stubs (Plan 03b will replace [] with full L1..L20 entries):
|
||||
Cleric: [],
|
||||
Druid: [],
|
||||
Witch: [],
|
||||
Bard: [],
|
||||
Sorcerer: [],
|
||||
Oracle: [],
|
||||
Champion: [], // focus-only; minimal entries
|
||||
|
||||
// Non-caster stubs (Plan 03b confirms these stay empty):
|
||||
Alchemist: [],
|
||||
Barbarian: [],
|
||||
Fighter: [],
|
||||
@@ -384,17 +353,9 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
};
|
||||
```
|
||||
|
||||
**Executor's curation responsibility:**
|
||||
|
||||
For Wizard, the table above is fully populated as a worked example. For the other 6 casters (Cleric, Druid, Witch, Bard, Sorcerer, Oracle), the executor must curate the full L1..L20 entries from Archives of Nethys before this task is `done`. Use the Wizard cadence as the template; tradition and counts vary per class.
|
||||
|
||||
For non-casters (Alchemist, Barbarian, Fighter, Investigator, Monk, Ranger, Rogue, Swashbuckler) the empty array is correct.
|
||||
|
||||
Champion has minimal entries (focus-spell mechanics are outside the slot table).
|
||||
|
||||
**Constraint:** No `: any` types. Every entry strictly typed against `SpellSlotOverlayEntry`.
|
||||
|
||||
**Constraint:** This file is hand-edited human knowledge — no programmatic generation. It is small (≤500 lines) and stable across PF2e printings.
|
||||
**Constraint:** Every D-16 class name must appear as a key (even with `[]`) so Plan 03b can simply replace `[]` with curated entries — preventing accidental missing-key bugs in the seed.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd server && npx tsc --noEmit -p tsconfig.json 2>&1 | grep -E "spell-slot-overlays" || echo "tsc clean"</automated>
|
||||
@@ -404,47 +365,32 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
- File exports `SPELL_SLOT_OVERLAY`
|
||||
- File exports `SpellTradition` type and `SpellSlotOverlayEntry` interface
|
||||
- All 16 D-16 class names appear as keys in the SPELL_SLOT_OVERLAY object (Alchemist, Barbarian, Bard, Champion, Cleric, Druid, Fighter, Investigator, Monk, Oracle, Ranger, Rogue, Sorcerer, Swashbuckler, Witch, Wizard)
|
||||
- Wizard array has at least 19 entries spanning L1..L19 (verifiable: count entries with `level:` keys)
|
||||
- Bard, Sorcerer, Oracle arrays each contain at least one `repertoireIncrement` entry (D-18)
|
||||
- Wizard array has at least 19 entries spanning L1..L19 (verifiable: count `level:` keys in Wizard array)
|
||||
- Cleric/Druid/Witch/Bard/Sorcerer/Oracle keys exist (their values may be `[]` — those are populated by Plan 03b)
|
||||
- File contains NO `: any` outside comments
|
||||
- `cd server && npx tsc --noEmit` exits 0 (no type errors involving this file)
|
||||
- `cd server && npx tsc --noEmit` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>
|
||||
Spell-slot overlay populated for all 7 caster classes (Wizard fully, others curated by executor) and explicitly empty for non-casters. Type-safe. Imported by Task 4.
|
||||
Spell-slot overlay file exists with types + Wizard fully populated + stub keys for the other 15 classes. Plan 03b can append without touching shape.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 3: Hand-curated class-feature-options data (D-19 choices)</name>
|
||||
<name>Task 3: Class-feature-options file — types + Wizard School worked example</name>
|
||||
<files>server/prisma/data/class-feature-options.ts</files>
|
||||
<read_first>
|
||||
- .planning/phases/01-level-up-pf2e-regelkonform/01-CONTEXT.md (D-19 — Wahl-Klassenmerkmale; D-15 — choiceOptionsRef; D-16 — class scope)
|
||||
- .planning/phases/01-level-up-pf2e-regelkonform/01-RESEARCH.md (lines 802-813 — ClassFeatureOption schema with grants + proficiencyChanges; lines 1064-1067 — Open Question Q2 recommendation)
|
||||
- .planning/phases/01-level-up-pf2e-regelkonform/01-RESEARCH.md (lines 802-813 — ClassFeatureOption schema with grants + proficiencyChanges; lines 1064-1067 — Open Question Q2 RESOLVED recommendation)
|
||||
- server/prisma/schema.prisma (post-Plan-01 — ClassFeatureOption model)
|
||||
- Archives of Nethys class entries for: Cleric (Doctrines), Wizard (Schools), Champion (Causes), Druid (Orders), Sorcerer (Bloodlines, abbreviated), Bard (Muses), Barbarian (Instincts), Monk (no L1 choice but L8 Mountain Stance etc. — defer to v2)
|
||||
- Archives of Nethys Wizard schools entry
|
||||
</read_first>
|
||||
<action>
|
||||
Create `server/prisma/data/class-feature-options.ts` containing hand-curated ClassFeatureOption seed data for the L1 (and any other) choice points across the 16 D-16 classes.
|
||||
Create `server/prisma/data/class-feature-options.ts` containing:
|
||||
1. The TS interface `ClassFeatureOptionEntry` — the contract Plan 03b appends to.
|
||||
2. At least 1 Wizard School entry (Battle Magic) as the worked example.
|
||||
3. Section-headed comment blocks for all the other classes with `// (populated by Plan 03b)` placeholders so Plan 03b knows exactly where to append.
|
||||
|
||||
**Scope for Phase 1 (executor curates from AoN):**
|
||||
- **Cleric Doctrines** (L1) — Cloistered Cleric, Warpriest (and any others available in Player Core)
|
||||
- **Wizard Schools** (L1) — Arcane School: Battle Magic, Civic Wizardry, Mentalism, Protean Form, Unified Magical Theory, Universalist (Player Core arcane schools)
|
||||
- **Champion Causes** (L1) — Liberator (Good), Paladin (Good), Redeemer (Good), Antipaladin (Evil), Tyrant (Evil), Desecrator (Evil) — and any neutral or remaster-equivalent
|
||||
- **Druid Orders** (L1) — Animal, Leaf, Storm, Wild, plus any in APG
|
||||
- **Sorcerer Bloodlines** (L1) — Aberrant, Angelic, Demonic, Diabolic, Draconic, Elemental, Fey, Genie, Hag, Imperial, Nymph, Phoenix, Psychopomp, Shadow, Undead, plus APG additions
|
||||
- **Bard Muses** (L1) — Enigma, Maestro, Polymath, plus APG (Warrior etc.)
|
||||
- **Barbarian Instincts** (L1) — Animal, Dragon, Fury, Giant, Spirit, plus APG (Superstition etc.)
|
||||
- **Witch Patrons** (L1) — Faith, Fervor, Mosquito Witch, Resentment, Silence, Spinner of Threads, Starless Shadow, Wilding, plus Curse-of-the-Hag-Eye etc.
|
||||
- **Oracle Mysteries** (L1) — Battle, Bones, Cosmos, Flames, Life, Lore, Tempest, plus APG
|
||||
- **Investigator Methodologies** (APG) — Empiricism, Forensic Medicine, Interrogation, Sensate, Stratagem (or whatever exists)
|
||||
- **Monk Stances** etc. — DEFER to v2 (Monk's L1 choice is minimal; complex stances are L2+ feats handled via the Klassentalent step)
|
||||
- **Ranger Edges** (APG) — Flurry, Outwit, Precision
|
||||
- **Rogue Rackets** — Eldritch Trickster, Mastermind, Ruffian, Scoundrel, Thief
|
||||
- **Swashbuckler Styles** — Battledancer, Braggart, Fencer, Gymnast, Wit
|
||||
- **Alchemist Research Fields** — Bomber, Chirurgeon, Mutagenist, Toxicologist
|
||||
- **Fighter** — no L1 choice (handled at L5 Weapon Mastery via the L5 ClassProgression `choiceType`)
|
||||
|
||||
**File structure:**
|
||||
**File contents:**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
@@ -454,11 +400,13 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
* can resolve choice → option list at runtime.
|
||||
*
|
||||
* `grants` and `proficiencyChanges` are option-level mechanical effects applied at commit
|
||||
* (per RESEARCH.md §Open Question Q2 — symmetric with ClassProgression so the recompute
|
||||
* pipeline is uniform).
|
||||
* (per RESEARCH.md §Open Question Q2 RESOLVED — symmetric with ClassProgression so the
|
||||
* recompute pipeline is uniform).
|
||||
*
|
||||
* SOURCE: Archives of Nethys — PF2e Player Core + APG class entries.
|
||||
* SCOPE: 16 D-16 classes' L1 (and other) choice points.
|
||||
* SPLIT: Plan 03 ships Wizard School (worked example, ≥1 entry). Plan 03b ships the
|
||||
* remaining classes — joint goal across both plans is ≥50 entries.
|
||||
*/
|
||||
|
||||
import type { Proficiency } from '../../src/modules/leveling/lib/types';
|
||||
@@ -467,91 +415,54 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
optionsRef: string; // matches ClassProgression.choiceOptionsRef
|
||||
optionKey: string; // unique within optionsRef
|
||||
name: string; // English (German via TranslationsService at runtime)
|
||||
nameGerman?: string; // optional pre-translated German name (Phase 1 may leave this undefined and let TranslationsService handle on-demand)
|
||||
nameGerman?: string; // optional pre-translated German name
|
||||
description: string; // English
|
||||
grants: string[]; // class-feature names this option awards (e.g. ["Cloistered Cleric", "Domain Spell"])
|
||||
proficiencyChanges?: Partial<Record<'fortitude' | 'reflex' | 'will' | 'perception' | 'classDc' | 'ac', Proficiency>>;
|
||||
}
|
||||
|
||||
export const CLASS_FEATURE_OPTIONS: ClassFeatureOptionEntry[] = [
|
||||
// === CLERIC DOCTRINE (optionsRef: 'cleric-doctrine') ===
|
||||
{
|
||||
optionsRef: 'cleric-doctrine',
|
||||
optionKey: 'cloistered-cleric',
|
||||
name: 'Cloistered Cleric',
|
||||
description: 'You have devoted your life to the study of religion and the casting of divine spells…',
|
||||
grants: ['Cloistered Cleric Doctrine'],
|
||||
// Doctrine bumps spell DC trained → expert at varying levels; encoded via ClassProgression at L1
|
||||
},
|
||||
{
|
||||
optionsRef: 'cleric-doctrine',
|
||||
optionKey: 'warpriest',
|
||||
name: 'Warpriest',
|
||||
description: 'You are a martial defender of your faith…',
|
||||
grants: ['Warpriest Doctrine'],
|
||||
// Warpriest at L1 grants martial weapon proficiency = trained
|
||||
},
|
||||
|
||||
// === WIZARD SCHOOL (optionsRef: 'wizard-school') ===
|
||||
// ============================================================
|
||||
// === WIZARD SCHOOL — worked example (Plan 03 ships ≥1 entry)
|
||||
// === optionsRef: 'wizard-school'
|
||||
// ============================================================
|
||||
{
|
||||
optionsRef: 'wizard-school',
|
||||
optionKey: 'battle-magic',
|
||||
name: 'School of Battle Magic',
|
||||
description: 'You focus on offensive evocations and battlefield control…',
|
||||
grants: ['Battle Magic Curriculum'],
|
||||
description: 'You focus on offensive evocations and battlefield control, learning to bring overwhelming magical force to bear against your foes.',
|
||||
grants: ['Battle Magic Curriculum', 'School Spell: Force Bolt'],
|
||||
},
|
||||
// … executor curates remaining 5 schools
|
||||
// (Plan 03b appends remaining Wizard schools: civic-wizardry, mentalism, protean-form, unified-magical-theory, universalist)
|
||||
|
||||
// === CHAMPION CAUSE ===
|
||||
// … executor curates 6 causes
|
||||
|
||||
// === DRUID ORDER ===
|
||||
// … executor curates ~4 orders
|
||||
|
||||
// === SORCERER BLOODLINE ===
|
||||
// … executor curates ~15 bloodlines
|
||||
|
||||
// === BARD MUSE ===
|
||||
// … executor curates 3-4 muses
|
||||
|
||||
// === BARBARIAN INSTINCT ===
|
||||
// … executor curates 5+ instincts
|
||||
|
||||
// === WITCH PATRON ===
|
||||
// … executor curates patrons
|
||||
|
||||
// === ORACLE MYSTERY ===
|
||||
// … executor curates mysteries
|
||||
|
||||
// === INVESTIGATOR METHODOLOGY ===
|
||||
// … executor curates methodologies
|
||||
|
||||
// === RANGER EDGE ===
|
||||
// … executor curates edges
|
||||
|
||||
// === ROGUE RACKET ===
|
||||
// … executor curates 5 rackets
|
||||
|
||||
// === SWASHBUCKLER STYLE ===
|
||||
// … executor curates 5 styles
|
||||
|
||||
// === ALCHEMIST RESEARCH FIELD ===
|
||||
// … executor curates 4 research fields (already in DB as ResearchField enum — cross-reference)
|
||||
// ============================================================
|
||||
// === Plan 03b appends entries below — section anchors:
|
||||
// ============================================================
|
||||
// === CLERIC DOCTRINE (optionsRef: 'cleric-doctrine') — Plan 03b
|
||||
// === CHAMPION CAUSE (optionsRef: 'champion-cause') — Plan 03b
|
||||
// === DRUID ORDER (optionsRef: 'druid-order') — Plan 03b
|
||||
// === SORCERER BLOOD. (optionsRef: 'sorcerer-bloodline') — Plan 03b
|
||||
// === BARD MUSE (optionsRef: 'bard-muse') — Plan 03b
|
||||
// === BARBARIAN INST. (optionsRef: 'barbarian-instinct') — Plan 03b
|
||||
// === WITCH PATRON (optionsRef: 'witch-patron') — Plan 03b
|
||||
// === ORACLE MYSTERY (optionsRef: 'oracle-mystery') — Plan 03b
|
||||
// === INVESTIGATOR (optionsRef: 'investigator-methodology') — Plan 03b
|
||||
// === RANGER EDGE (optionsRef: 'ranger-edge') — Plan 03b
|
||||
// === ROGUE RACKET (optionsRef: 'rogue-racket') — Plan 03b
|
||||
// === SWASHBUCKLER (optionsRef: 'swashbuckler-style') — Plan 03b
|
||||
// === ALCHEMIST RES. (optionsRef: 'alchemist-research-field') — Plan 03b
|
||||
// (Fighter / Monk: no L1 choice — Fighter L5 weapon-mastery and Monk L1 stance via class feats)
|
||||
];
|
||||
```
|
||||
|
||||
**Executor's curation responsibility:**
|
||||
|
||||
The Cleric Doctrine and Wizard School blocks are seeded above as worked examples. The executor must populate the remaining classes by reading each class's L1 entry on Archives of Nethys and producing a ClassFeatureOptionEntry object per option.
|
||||
|
||||
**Cross-references the executor must maintain:**
|
||||
- `optionsRef` strings must match the `choiceOptionsRef` values used in `seed-class-progression.ts` (Task 4) when it sets `choiceType` and `choiceOptionsRef` on the L1 ClassProgression rows.
|
||||
- `optionKey` values are stable identifiers — once chosen, do not rename (would break commits).
|
||||
**Cross-references the executor (and Plan 03b) must maintain:**
|
||||
- `optionsRef` strings must match the `choiceOptionsRef` values used in `seed-class-progression.ts` (Task 4 below) when it sets `choiceType` and `choiceOptionsRef` on the L1 ClassProgression rows.
|
||||
- `optionKey` values are stable identifiers — once chosen, do not rename (would break existing commits).
|
||||
- `nameGerman` may be left undefined in this seed — the TranslationsService picks up English `name` and translates on demand (D-15).
|
||||
|
||||
**Constraint:** No `any` types. Type-checked.
|
||||
|
||||
**Constraint:** Alchemist research fields cross-reference the existing `ResearchField` enum in `server/prisma/schema.prisma` (BOMBER, CHIRURGEON, etc.) — the `optionKey` values for Alchemist must match the enum values lowercase-kebab so the existing `CharacterAlchemyState.researchField` column can be set from the option pick.
|
||||
**Constraint:** Alchemist research fields (Plan 03b will add) cross-reference the existing `ResearchField` enum in `server/prisma/schema.prisma` (BOMBER, CHIRURGEON, etc.) — `optionKey` values for Alchemist must match the enum values lowercase-kebab so the existing `CharacterAlchemyState.researchField` column can be set from the option pick.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd server && npx tsc --noEmit -p tsconfig.json 2>&1 | grep -E "class-feature-options" || echo "tsc clean"</automated>
|
||||
@@ -560,14 +471,14 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
- File `server/prisma/data/class-feature-options.ts` exists
|
||||
- File exports `CLASS_FEATURE_OPTIONS` array
|
||||
- File exports `ClassFeatureOptionEntry` interface
|
||||
- Array contains at least 50 entries (cumulative across all classes — Cleric 2-4, Wizard 6, Champion 6, Druid 4, Sorcerer ≥10, Bard 3, Barbarian 5, Witch ≥5, Oracle ≥7, Investigator 3-5, Ranger 3, Rogue 5, Swashbuckler 5, Alchemist 4)
|
||||
- Array contains at least 1 entry with `optionsRef: 'wizard-school'` (Plan 03 worked example)
|
||||
- Every entry has a non-empty `optionsRef`, `optionKey`, `name`, `description`, `grants` field
|
||||
- At least 5 distinct `optionsRef` values appear (verifying all class choice points are represented)
|
||||
- File contains anchor comments naming all the other class optionsRef strings (so Plan 03b knows exactly where to insert)
|
||||
- File contains NO `: any` outside comments
|
||||
- `cd server && npx tsc --noEmit` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>
|
||||
Class-feature options seeded for all D-16 classes that have L1 choice points. optionsRef strings established as the contract between this file and Task 4's seed script.
|
||||
Class-feature options file exists with types + Wizard School worked example + anchor comments for Plan 03b. The Wizard L1 choice is functional end-to-end after Task 5.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
@@ -581,7 +492,7 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
- .planning/phases/01-level-up-pf2e-regelkonform/01-RESEARCH.md (lines 686-725 — Foundry shape + mapping; lines 538-575 — seed pattern from PATTERNS.md analog)
|
||||
- .planning/phases/01-level-up-pf2e-regelkonform/01-PATTERNS.md (lines 537-587 — exact seed pattern)
|
||||
- server/package.json (db:seed:class-progression script wired in Plan 01 Task 3)
|
||||
- server/prisma/schema.prisma (ClassProgression and ClassFeatureOption models)
|
||||
- server/prisma/schema.prisma (ClassProgression and ClassFeatureOption models — verify the `@@unique` order before assuming compound-key field names)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `server/prisma/seed-class-progression.ts` with the following structure (executor implements bodies):
|
||||
@@ -597,6 +508,13 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
* - CLASS_FEATURE_OPTIONS from `data/class-feature-options.ts` (D-19 choices).
|
||||
*
|
||||
* Idempotent — safe to re-run.
|
||||
*
|
||||
* Compound unique keys (generated by Prisma from Plan 01 schema):
|
||||
* - ClassProgression has `@@unique([className, level])` -> field name: `className_level`
|
||||
* - ClassFeatureOption has `@@unique([optionsRef, optionKey])` -> field name: `optionsRef_optionKey`
|
||||
* If Plan 01's `@@unique([...])` declarations are reordered, Prisma swaps the field names.
|
||||
* Verify Plan 01's schema field order before assuming. If the order changes, update the
|
||||
* findUnique calls below accordingly.
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
@@ -620,7 +538,6 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
] as const;
|
||||
|
||||
// Map of (className → choiceType, choiceOptionsRef) for the L1 doctrine/school/etc. step.
|
||||
// Executor populates from the optionsRef values defined in class-feature-options.ts.
|
||||
const L1_CHOICE_MAP: Record<string, { choiceType: string; choiceOptionsRef: string } | null> = {
|
||||
Cleric: { choiceType: 'doctrine', choiceOptionsRef: 'cleric-doctrine' },
|
||||
Wizard: { choiceType: 'school', choiceOptionsRef: 'wizard-school' },
|
||||
@@ -699,12 +616,6 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
.map(item => item.name);
|
||||
|
||||
// 2. Proficiency changes — for L1 only, take the class's base proficiency from the JSON.
|
||||
// For L2+, executor adds hand-curated proficiency bumps as needed (e.g. Fighter L5
|
||||
// martial → master). The Foundry data does NOT carry per-level proficiency bump
|
||||
// info on the class JSON; that lives in classfeatures compendium files (Pitfall #6).
|
||||
// For Phase 1 minimum: seed only L1 base proficiencies; populate higher-level bumps
|
||||
// via a hand-curated overlay if time permits, otherwise leave proficiencyChanges
|
||||
// empty and rely on the recompute pipeline's class-feature-driven lookups in v2.
|
||||
let proficiencyChanges: Record<string, string> = {};
|
||||
if (level === 1) {
|
||||
proficiencyChanges = {
|
||||
@@ -750,7 +661,7 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
|
||||
async function seedClassProgression(): Promise<{ created: number; updated: number; errors: number }> {
|
||||
let created = 0; let updated = 0; let errors = 0;
|
||||
console.log('📚 Seeding ClassProgression for 16 classes × 20 levels…');
|
||||
console.log('Seeding ClassProgression for 16 classes x 20 levels...');
|
||||
|
||||
for (const className of D16_CLASS_NAMES) {
|
||||
let foundry: FoundryClassJson;
|
||||
@@ -764,6 +675,10 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
for (let level = 1; level <= 20; level++) {
|
||||
const row = buildProgressionRow(className, level, foundry);
|
||||
try {
|
||||
// Compound unique key field name `className_level` is generated from
|
||||
// `@@unique([className, level])` declared in Plan 01's schema. If that
|
||||
// declaration is reordered, Prisma will name the key `level_className`
|
||||
// instead — verify schema before changing.
|
||||
const existing = await prisma.classProgression.findUnique({
|
||||
where: { className_level: { className, level } },
|
||||
});
|
||||
@@ -783,16 +698,19 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(` ✓ ClassProgression: ${created} created, ${updated} updated, ${errors} errors`);
|
||||
console.log(` ClassProgression: ${created} created, ${updated} updated, ${errors} errors`);
|
||||
return { created, updated, errors };
|
||||
}
|
||||
|
||||
async function seedClassFeatureOptions(): Promise<{ created: number; updated: number; errors: number }> {
|
||||
let created = 0; let updated = 0; let errors = 0;
|
||||
console.log(`📚 Seeding ${CLASS_FEATURE_OPTIONS.length} ClassFeatureOption rows…`);
|
||||
console.log(`Seeding ${CLASS_FEATURE_OPTIONS.length} ClassFeatureOption rows...`);
|
||||
|
||||
for (const opt of CLASS_FEATURE_OPTIONS) {
|
||||
try {
|
||||
// Compound unique key field name `optionsRef_optionKey` is generated from
|
||||
// `@@unique([optionsRef, optionKey])` in Plan 01's schema. Same caveat as above
|
||||
// applies — verify the field order before touching this.
|
||||
const existing = await prisma.classFeatureOption.findUnique({
|
||||
where: { optionsRef_optionKey: { optionsRef: opt.optionsRef, optionKey: opt.optionKey } },
|
||||
});
|
||||
@@ -827,7 +745,7 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
console.error(`Failed to seed option ${opt.optionsRef}/${opt.optionKey}:`, (e as Error).message);
|
||||
}
|
||||
}
|
||||
console.log(` ✓ ClassFeatureOption: ${created} created, ${updated} updated, ${errors} errors`);
|
||||
console.log(` ClassFeatureOption: ${created} created, ${updated} updated, ${errors} errors`);
|
||||
return { created, updated, errors };
|
||||
}
|
||||
|
||||
@@ -839,7 +757,7 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
console.error(`\nSeed completed with ${totalErrors} errors.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('\n✅ ClassProgression + ClassFeatureOption seed complete.');
|
||||
console.log('\nClassProgression + ClassFeatureOption seed complete.');
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -851,8 +769,7 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
- `: any` is forbidden. Use the `FoundryClassJson` interface (or expand it).
|
||||
- The script must fail loudly with the README pointer when the Foundry clone is missing.
|
||||
- Idempotency: every row uses `findUnique → update OR create` per RESEARCH.md §Pitfall 5 + analog `seed-equipment.ts` lines 105-130.
|
||||
- `prisma.classProgression.findUnique` uses the compound unique key `className_level` (Prisma generates this from `@@unique([className, level])`).
|
||||
- `prisma.classFeatureOption.findUnique` uses `optionsRef_optionKey` (from `@@unique([optionsRef, optionKey])`).
|
||||
- The script is **generic** — it iterates over D16_CLASS_NAMES and SPELL_SLOT_OVERLAY/CLASS_FEATURE_OPTIONS, so when Plan 03b appends entries to those modules nothing in this script needs to change.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd server && npx tsc --noEmit -p tsconfig.json 2>&1 | grep -E "seed-class-progression" || echo "tsc clean"</automated>
|
||||
@@ -864,16 +781,17 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
- File contains the literal string `prisma.classProgression.findUnique` (proves idempotent pattern)
|
||||
- File contains the literal string `prisma.classFeatureOption.findUnique` (proves idempotent pattern)
|
||||
- File contains the literal string `Foundry pf2e clone not found at` (proves loud-fail pattern)
|
||||
- File contains the compound-key explanatory comment about `@@unique` ordering
|
||||
- File contains NO `: any` outside comments
|
||||
- `cd server && npx tsc --noEmit -p tsconfig.json` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>
|
||||
Seed script type-checks cleanly. Idempotent CRUD against ClassProgression + ClassFeatureOption. Imports both hand-curated data modules.
|
||||
Seed script type-checks cleanly. Idempotent CRUD against ClassProgression + ClassFeatureOption. Imports both hand-curated data modules. Generic over D16_CLASS_NAMES so Plan 03b is data-only.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="false">
|
||||
<name>Task 5: Run the seed (manual verification — requires Foundry clone)</name>
|
||||
<name>Task 5: Run the seed (worked-example verification — Wizard end-to-end)</name>
|
||||
<files>(no file writes — execution + verification only)</files>
|
||||
<read_first>
|
||||
- .planning/phases/01-level-up-pf2e-regelkonform/SEED-README.md
|
||||
@@ -904,63 +822,63 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
cd server && npm run db:seed:class-progression
|
||||
```
|
||||
|
||||
Expected output (approximate):
|
||||
Plan 03 ships only Wizard fully populated in the overlays, so the expected output for THIS plan (before Plan 03b runs) is approximately:
|
||||
|
||||
```
|
||||
📚 Seeding ClassProgression for 16 classes × 20 levels…
|
||||
✓ ClassProgression: 320 created, 0 updated, 0 errors
|
||||
📚 Seeding 60 ClassFeatureOption rows…
|
||||
✓ ClassFeatureOption: 60 created, 0 updated, 0 errors
|
||||
✅ ClassProgression + ClassFeatureOption seed complete.
|
||||
Seeding ClassProgression for 16 classes x 20 levels...
|
||||
ClassProgression: 320 created, 0 updated, 0 errors
|
||||
Seeding 1 ClassFeatureOption rows...
|
||||
ClassFeatureOption: 1 created, 0 updated, 0 errors
|
||||
ClassProgression + ClassFeatureOption seed complete.
|
||||
```
|
||||
|
||||
(The 320 ClassProgression rows are populated regardless — the overlays just leave non-Wizard rows with null spellSlotIncrement / null choiceOptionsRef. Plan 03b updates those rows in place.)
|
||||
|
||||
**Step 4 — Run the seed AGAIN to prove idempotency:**
|
||||
|
||||
```bash
|
||||
cd server && npm run db:seed:class-progression
|
||||
```
|
||||
|
||||
Expected output: `0 created, 320 updated, 0 errors` for ClassProgression and `0 created, N updated, 0 errors` for ClassFeatureOption.
|
||||
Expected output: `0 created, 320 updated, 0 errors` for ClassProgression and `0 created, 1 updated, 0 errors` for ClassFeatureOption.
|
||||
|
||||
**Step 5 — Verify row counts:**
|
||||
**Step 5 — Verify Wizard worked example end-to-end:**
|
||||
|
||||
```bash
|
||||
psql $DATABASE_URL -c 'SELECT "className", COUNT(*) FROM "ClassProgression" GROUP BY "className" ORDER BY "className"'
|
||||
psql $DATABASE_URL -c 'SELECT "level", "spellSlotIncrement", "cantripIncrement" FROM "ClassProgression" WHERE "className" = '"'"'Wizard'"'"' ORDER BY "level" LIMIT 5'
|
||||
```
|
||||
|
||||
Expected: 16 rows, each `count = 20`.
|
||||
Expected: rows for Wizard L1..L5 with non-null `spellSlotIncrement` JSON containing `tradition: ARCANE`. L1 also has cantripIncrement=5.
|
||||
|
||||
```bash
|
||||
psql $DATABASE_URL -c 'SELECT "optionsRef", COUNT(*) FROM "ClassFeatureOption" GROUP BY "optionsRef" ORDER BY "optionsRef"'
|
||||
psql $DATABASE_URL -c 'SELECT "className", "choiceType", "choiceOptionsRef" FROM "ClassProgression" WHERE "className" = '"'"'Wizard'"'"' AND "level" = 1'
|
||||
```
|
||||
|
||||
Expected: at least 5 distinct optionsRef rows, each with ≥2 options.
|
||||
|
||||
**Step 6 — Verify spellcaster overlays applied:**
|
||||
Expected: 1 row with `choiceType=school`, `choiceOptionsRef=wizard-school`.
|
||||
|
||||
```bash
|
||||
psql $DATABASE_URL -c 'SELECT "className", "level", "spellSlotIncrement" FROM "ClassProgression" WHERE "className" = '"'"'Wizard'"'"' AND "level" <= 5 ORDER BY "level"'
|
||||
psql $DATABASE_URL -c 'SELECT "optionKey", "name" FROM "ClassFeatureOption" WHERE "optionsRef" = '"'"'wizard-school'"'"''
|
||||
```
|
||||
|
||||
Expected: rows for Wizard L1..L5 with non-null `spellSlotIncrement` JSON containing `tradition: ARCANE`.
|
||||
Expected: at least 1 row (Wizard School worked example: `battle-magic`).
|
||||
|
||||
If any verification step fails, the plan is NOT done — fix the seed script or overlay data.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd server && psql $DATABASE_URL -t -c 'SELECT COUNT(*) FROM "ClassProgression"' | tr -d ' \n'</automated>
|
||||
<automated>cd server && psql $DATABASE_URL -t -c 'SELECT COUNT(*) FROM "ClassProgression" WHERE "className" = '"'"'Wizard'"'"'' | tr -d ' \n'</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `server/prisma/data/foundry-pf2e/packs/classes/` directory exists with at least 16 .json files (Foundry clone present)
|
||||
- `cd server && npm run db:seed:class-progression` exits 0
|
||||
- First run reports `≥320 created` for ClassProgression and `≥40 created` for ClassFeatureOption
|
||||
- First run reports `≥320 created` for ClassProgression and `≥1 created` for ClassFeatureOption
|
||||
- Second run reports `0 created, ≥320 updated` for ClassProgression (proves idempotency)
|
||||
- `psql $DATABASE_URL -t -c 'SELECT COUNT(*) FROM "ClassProgression"'` outputs at least 320
|
||||
- `psql $DATABASE_URL -t -c 'SELECT COUNT(*) FROM "ClassFeatureOption"'` outputs at least 40
|
||||
- `psql $DATABASE_URL -c 'SELECT * FROM "ClassProgression" WHERE "className" = '"'"'Wizard'"'"' AND "level" = 1'` returns a row with non-null `spellSlotIncrement` and non-null `cantripIncrement`
|
||||
- `psql $DATABASE_URL -c 'SELECT * FROM "ClassProgression" WHERE "className" = '"'"'Cleric'"'"' AND "level" = 1'` returns a row with `choiceType = doctrine` and `choiceOptionsRef = cleric-doctrine`
|
||||
- `psql $DATABASE_URL -t -c 'SELECT COUNT(*) FROM "ClassProgression" WHERE "className" = '"'"'Wizard'"'"''` outputs at least 20 (Wizard L1..L20)
|
||||
- `psql $DATABASE_URL -t -c 'SELECT COUNT(*) FROM "ClassFeatureOption" WHERE "optionsRef" = '"'"'wizard-school'"'"''` outputs at least 1
|
||||
- `psql $DATABASE_URL -c 'SELECT * FROM "ClassProgression" WHERE "className" = '"'"'Wizard'"'"' AND "level" = 1'` returns a row with non-null `spellSlotIncrement`, non-null `cantripIncrement`, `choiceType=school`, `choiceOptionsRef=wizard-school`
|
||||
- SEED-README.md has the `<PINNED_TAG>` placeholder replaced with the actual tag chosen
|
||||
</acceptance_criteria>
|
||||
<done>
|
||||
Seed script runs cleanly twice, all 320+ ClassProgression rows present, all ClassFeatureOption rows present, spell-slot overlay applied to caster classes, choiceType/choiceOptionsRef set on L1 rows for classes with L1 choices.
|
||||
Seed script runs cleanly twice; Wizard worked example fully populated end-to-end (20 ClassProgression rows + 1+ ClassFeatureOption row). Plan 03b can append further data and re-run to bulk-update the remaining classes without changes to the script.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
@@ -979,10 +897,10 @@ export const SPELL_SLOT_OVERLAY: Record<string, Array<{
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-1-W2-01 | Tampering | Foundry pf2e tag is moved underneath us between seed runs | mitigate | SEED-README.md instructs the dev to clone a specific pinned tag; the seed script's loud-fail on schema mismatch (Pitfall #5) catches drift. |
|
||||
| T-1-W2-02 | Tampering | Hand-curated overlay contains a bug (e.g. wrong slot count for class X) | mitigate | Plan 04's integration tests assert ClassProgression-driven recompute results for a representative caster (Wizard L1 cantrips, Sorcerer L5 repertoire). Bugs surface there. |
|
||||
| T-1-W2-02 | Tampering | Hand-curated overlay contains a bug (e.g. wrong slot count for class X) | mitigate | Plan 04's integration tests assert ClassProgression-driven recompute results for a representative caster (Wizard L1 cantrips). Bugs surface there. |
|
||||
| T-1-W2-03 | Information Disclosure | DATABASE_URL leaked in seed output / error logs | accept | Self-hosted single-tenant; developer controls .env; no production deploy yet exposes seed script. |
|
||||
| T-1-W2-04 | Repudiation | Seed errors silently corrupt rows | mitigate | Idempotent pattern with try/catch per row; error count reported in console; non-zero error count exits 1 (CI catches this). |
|
||||
| T-1-W2-05 | DoS | Cascading seed errors flood DB connection pool | mitigate | Sequential per-row CRUD (no Promise.all); seed completes in <30s for 320+60 rows. |
|
||||
| T-1-W2-05 | DoS | Cascading seed errors flood DB connection pool | mitigate | Sequential per-row CRUD (no Promise.all); seed completes in <30s for 320+50 rows. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
@@ -998,35 +916,37 @@ cd server && npx tsc --noEmit -p tsconfig.json
|
||||
# Seed runs idempotently (run twice, second run = all updates)
|
||||
cd server && npm run db:seed:class-progression && npm run db:seed:class-progression
|
||||
|
||||
# Row counts
|
||||
psql $DATABASE_URL -c 'SELECT COUNT(*) FROM "ClassProgression"' # >= 320
|
||||
psql $DATABASE_URL -c 'SELECT COUNT(*) FROM "ClassFeatureOption"' # >= 40
|
||||
# Wizard worked-example row counts
|
||||
psql $DATABASE_URL -c 'SELECT COUNT(*) FROM "ClassProgression" WHERE "className" = '"'"'Wizard'"'"'' # >= 20
|
||||
psql $DATABASE_URL -c 'SELECT COUNT(*) FROM "ClassFeatureOption" WHERE "optionsRef" = '"'"'wizard-school'"'"'' # >= 1
|
||||
|
||||
# Spellcaster verification
|
||||
psql $DATABASE_URL -c 'SELECT className, level, spellSlotIncrement->>'"'"'tradition'"'"' FROM "ClassProgression" WHERE className IN ('"'"'Wizard'"'"','"'"'Sorcerer'"'"','"'"'Bard'"'"','"'"'Cleric'"'"','"'"'Druid'"'"','"'"'Witch'"'"','"'"'Oracle'"'"') AND spellSlotIncrement IS NOT NULL ORDER BY className, level LIMIT 30'
|
||||
# Wizard spellcaster verification
|
||||
psql $DATABASE_URL -c 'SELECT level, "spellSlotIncrement", "cantripIncrement" FROM "ClassProgression" WHERE "className" = '"'"'Wizard'"'"' AND "spellSlotIncrement" IS NOT NULL ORDER BY level LIMIT 10'
|
||||
|
||||
# Choice-type verification (Cleric L1 should have doctrine choice)
|
||||
psql $DATABASE_URL -c 'SELECT className, choiceType, choiceOptionsRef FROM "ClassProgression" WHERE level = 1 AND choiceType IS NOT NULL ORDER BY className'
|
||||
# Wizard L1 choice verification
|
||||
psql $DATABASE_URL -c 'SELECT "choiceType", "choiceOptionsRef" FROM "ClassProgression" WHERE "className" = '"'"'Wizard'"'"' AND level = 1'
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- SEED-README.md exists with cloning instructions and a recorded pinned Foundry tag
|
||||
- spell-slot-overlays.ts populated for all 7 caster classes + empty for non-casters; type-clean
|
||||
- class-feature-options.ts populated with ≥40 entries across ≥10 distinct optionsRef values; type-clean
|
||||
- seed-class-progression.ts type-checks cleanly, imports both overlays, fails loudly on missing Foundry clone
|
||||
- Running `npm run db:seed:class-progression` populates ≥320 ClassProgression rows + ≥40 ClassFeatureOption rows
|
||||
- spell-slot-overlays.ts exports types + Wizard fully populated for L1..L19; type-clean
|
||||
- class-feature-options.ts exports types + at least 1 Wizard School entry; type-clean
|
||||
- seed-class-progression.ts type-checks cleanly, imports both overlays, fails loudly on missing Foundry clone, generic over D16_CLASS_NAMES
|
||||
- Running `npm run db:seed:class-progression` populates ≥320 ClassProgression rows
|
||||
- Running it a second time updates 0 created / N updated (idempotent)
|
||||
- Wizard, Sorcerer, Bard, Cleric, Druid, Witch, Oracle have non-null spellSlotIncrement at appropriate levels
|
||||
- Cleric L1, Wizard L1, Champion L1 (and other classes with L1 choices) have choiceType + choiceOptionsRef set
|
||||
- Plan 04 (LevelingService) can `prisma.classProgression.findUnique` for any (className, level) and get rows
|
||||
- Wizard L1..L19 have non-null spellSlotIncrement at appropriate levels
|
||||
- Wizard L1 has choiceType=school + choiceOptionsRef=wizard-school
|
||||
- ClassFeatureOption has ≥1 row for optionsRef=wizard-school (worked example)
|
||||
- Plan 04 (LevelingService) can `prisma.classProgression.findUnique` for `(Wizard, 1)..(Wizard, 20)` and get rows with overlay data
|
||||
- Plan 03b (runs sequentially in Wave 3 immediately after this plan — sequential because both plans write to spell-slot-overlays.ts and class-feature-options.ts — file-ownership conflict) has a clear contract: append entries to spell-slot-overlays.ts + class-feature-options.ts, then re-run seed to bulk-populate the other classes
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-level-up-pf2e-regelkonform/01-03-SUMMARY.md` documenting:
|
||||
- The pinned Foundry pf2e tag chosen
|
||||
- Final row counts (ClassProgression, ClassFeatureOption — by class breakdown)
|
||||
- Any deviations from the planned overlay contents (e.g. Witch tradition default note)
|
||||
- Any classes/levels where Foundry data was incomplete and a hand-curated fallback was applied
|
||||
- Final row counts after Plan 03 alone (ClassProgression: ~320; ClassFeatureOption: ≥1)
|
||||
- Confirmation that Wizard L1..L19 spell-slot entries are correct against AoN
|
||||
- Confirmation idempotency observed on second run
|
||||
- Note that Plan 03b will append data and re-run the seed without code changes
|
||||
</output>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
phase: 01-level-up-pf2e-regelkonform
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: ["01-01", "01-02", "01-03", "01-04"]
|
||||
wave: 5
|
||||
depends_on: ["01-01", "01-02", "01-03", "01-03b", "01-04"]
|
||||
files_modified:
|
||||
- client/src/features/characters/components/level-up/wizard-state-reducer.ts
|
||||
- client/src/features/characters/components/level-up/use-level-up-session.ts
|
||||
@@ -36,7 +36,7 @@ must_haves:
|
||||
- "Wizard chrome (header, stepper with progress label, body, footer) matches `01-UI-SPEC.md` exactly — no redesign."
|
||||
- "Choice-cards show source-color badges (Klassen/Abstammung/etc. — reuse existing featSourceColors) and a yellow AlertTriangle for non-evaluable prereqs (D-03)."
|
||||
- "Boost step uses +/- counters at h-11 w-11 (44×44 touch targets), shows 'wird {newScore}' live with cap-bei-18 chip when applicable."
|
||||
- "Review step shows Vorher/Nachher cards (HP-Max, RK, Klassen-DC, Wahrnehmung, Saves) using server-computed preview, with Ändern links + chain re-validation contract."
|
||||
- "Review step shows Vorher/Nachher cards (HP-Max, RK, Klassen-DC, Wahrnehmung, Saves) using server-computed preview, with Ändern links to revise upstream choices."
|
||||
- "Bestätigen runs the commit; wizard closes; toast shows; WebSocket level_up_committed event arrives at all other open clients of the character within ~1s."
|
||||
- "DRAFT-Resume banner appears at the top of the character-sheet when an open DRAFT exists, with Fortsetzen + Verwerfen actions."
|
||||
- "Pathbuilder-import-violations banner appears below the avatar header when Character.prereqViolations is non-null."
|
||||
@@ -63,11 +63,13 @@ must_haves:
|
||||
- path: "client/src/features/characters/components/character-sheet-page.tsx (extended)"
|
||||
provides: "Header button + 2 banner mounts + wizard mount"
|
||||
- path: "client/src/shared/lib/api.ts (extended)"
|
||||
provides: "5 new methods: startLevelUp, patchLevelUp, getLevelUpPreview, commitLevelUp, discardLevelUp"
|
||||
provides: "8 new methods: startLevelUp, patchLevelUp, getLevelUpPreview, commitLevelUp, discardLevelUp, getOpenLevelUpDraft, getLevelUpFeats, getLevelUpClassFeatureOptions"
|
||||
- path: "client/src/shared/types/index.ts (extended)"
|
||||
provides: "LevelUpSession, LevelUpPreview, WizardChoices types + extended Character with freeArchetype, prereqViolations"
|
||||
- path: "client/src/shared/hooks/use-character-socket.ts (extended)"
|
||||
provides: "Adds 'level_up_committed' to CharacterUpdateType union + onLevelUpCommitted callback"
|
||||
gotchas:
|
||||
- "Ändern (revision) does NOT auto-clear downstream choices that depended on the revised upstream choice. Per D-12 (review-only recompute, no live per-step recompute) the wizard intentionally keeps stale downstream picks; commit-time validation in Plan 04 (LevelingService.commit + isValidBoostSet/prereq guards) is the source of truth and rejects invalid combinations with a German BadRequestException that the wizard surfaces inline. Per-dependency clearing is a v2 enhancement."
|
||||
key_links:
|
||||
- from: "level-up-wizard.tsx"
|
||||
to: "LevelingService REST API"
|
||||
@@ -333,6 +335,43 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
|
||||
`/characters/${characterId}/level-up/${sessionId}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** GET open DRAFT for resume-banner detection. Returns null on 404. */
|
||||
async getOpenLevelUpDraft(characterId: string): Promise<LevelUpSession | null> {
|
||||
try {
|
||||
const response = await this.client.get(`/characters/${characterId}/level-up`);
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err) && err.response?.status === 404) return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/** GET filtered feat list for a wizard slot. Driven by Plan 04 FeatFilterService. */
|
||||
async getLevelUpFeats(
|
||||
characterId: string,
|
||||
sessionId: string,
|
||||
slot: 'class' | 'skill' | 'general' | 'ancestry' | 'archetype',
|
||||
includeUnavailable: boolean = false,
|
||||
): Promise<unknown[]> {
|
||||
const response = await this.client.get(
|
||||
`/characters/${characterId}/level-up/${sessionId}/feats`,
|
||||
{ params: { slot, includeUnavailable: includeUnavailable ? 'true' : 'false' } },
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/** GET ClassFeatureOption rows for a class-feature choice step. */
|
||||
async getLevelUpClassFeatureOptions(
|
||||
characterId: string,
|
||||
sessionId: string,
|
||||
optionsRef: string,
|
||||
): Promise<unknown[]> {
|
||||
const response = await this.client.get(
|
||||
`/characters/${characterId}/level-up/${sessionId}/class-feature-options/${encodeURIComponent(optionsRef)}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
```
|
||||
|
||||
Add the imports at the top of api.ts:
|
||||
@@ -386,7 +425,7 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
|
||||
<acceptance_criteria>
|
||||
- `client/src/shared/types/index.ts` exports `LevelUpSession`, `LevelUpPreview`, `DerivedStats`, `WizardChoices`, `StepKind`, `Proficiency`, `AbilityAbbreviation`, `PrereqViolation`
|
||||
- `client/src/shared/types/index.ts` Character interface has `freeArchetype?: boolean` and `prereqViolations?` fields
|
||||
- `client/src/shared/lib/api.ts` contains 5 new method names: `startLevelUp`, `patchLevelUp`, `getLevelUpPreview`, `commitLevelUp`, `discardLevelUp`
|
||||
- `client/src/shared/lib/api.ts` contains 8 new method names: `startLevelUp`, `patchLevelUp`, `getLevelUpPreview`, `commitLevelUp`, `discardLevelUp`, `getOpenLevelUpDraft`, `getLevelUpFeats`, `getLevelUpClassFeatureOptions`
|
||||
- `client/src/shared/hooks/use-character-socket.ts` CharacterUpdateType union contains `'level_up_committed'`
|
||||
- `client/src/shared/hooks/use-character-socket.ts` UseCharacterSocketOptions interface contains `onLevelUpCommitted?:`
|
||||
- `cd client && npx tsc --noEmit -p tsconfig.app.json` exits 0
|
||||
@@ -435,7 +474,8 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
|
||||
| { type: 'GO_PREV' }
|
||||
| { type: 'GO_TO_STEP'; idx: number }
|
||||
| { type: 'GO_TO_STEP_FROM_REVIEW'; idx: number } // sets revisionMode
|
||||
| { type: 'RETURN_TO_REVIEW' } // clears revisionMode + chain re-validation
|
||||
// RETURN_TO_REVIEW: clears revisionMode flag and routes back to review step. NOTE: downstream picks are NOT auto-cleared (v1) — commit-time validation in Plan 04 surfaces invalid combos. See must_haves.gotchas.
|
||||
| { type: 'RETURN_TO_REVIEW' }
|
||||
| { type: 'SET_BOOST_TARGETS'; targets: WizardChoices['boostTargets'] }
|
||||
| { type: 'SET_SKILL_INCREASE'; pick: WizardChoices['skillIncrease'] }
|
||||
| { type: 'SET_FEAT'; slot: 'class' | 'skill' | 'general' | 'ancestry' | 'archetype'; featId: string }
|
||||
@@ -462,11 +502,12 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
|
||||
}
|
||||
case 'RETURN_TO_REVIEW': {
|
||||
const reviewIdx = state.steps.findIndex(s => s === 'review');
|
||||
// CHAIN RE-VALIDATION: clear any later-step choice that depends on revised state.
|
||||
// Phase 1 implementation: clear all feat slots + spellcaster picks if a boost was changed
|
||||
// after the user revised (conservative). Production refinement
|
||||
// can be more granular per UI-SPEC §Ändern revision contract.
|
||||
// For now: clear nothing; let Plan 04's commit-time validation surface invalid combos.
|
||||
// GOTCHA (intentional v1 behaviour): RETURN_TO_REVIEW does NOT auto-clear downstream
|
||||
// choices that depended on an upstream revision. Per Plan-05 must_haves.gotchas and
|
||||
// D-12 (no live recompute per step — review-only), commit-time validation in Plan 04
|
||||
// is the source of truth: invalid combinations surface as a German BadRequestException
|
||||
// when the user clicks Bestätigen, and the wizard surfaces the message inline.
|
||||
// Refinement to per-dependency clearing is a v2 enhancement.
|
||||
return { ...state, currentIdx: reviewIdx, revisionMode: null };
|
||||
}
|
||||
case 'SET_BOOST_TARGETS':
|
||||
@@ -907,7 +948,7 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
|
||||
|
||||
**A. `level-up-step-class-features.tsx`** — Auto-summary, no input. Renders the `ClassProgression.grants` list for the new level as a read-only list. Header + sub-line per UI-SPEC. Server endpoint to fetch the list: GET ClassProgression for `(className, targetLevel)` — Plan 04 may need a small read endpoint OR the wizard can fetch from a public ClassProgression endpoint. If neither exists, the wizard receives the data via the LevelUpSession's preview endpoint.
|
||||
|
||||
**B. `level-up-step-class-feature-choice.tsx`** — D-19 sub-step (Cleric Doctrine, Wizard School, etc.). Fetches options from `ClassFeatureOption` table where `optionsRef = ClassProgression.choiceOptionsRef`. Renders one ChoiceCard per option (single-select / radio-group semantics). Dispatches `SET_CLASS_FEATURE_CHOICE`. **Server endpoint required for fetching options** — if not in Plan 04, either add a small public GET endpoint OR include the option list in the LevelUpSession.state at start time. Planner: prefer adding `GET /class-feature-options/:optionsRef` to the leveling controller as a small Plan 04 patch, OR include in the start-session response. Document the chosen path in plan SUMMARY.
|
||||
**B. `level-up-step-class-feature-choice.tsx`** -- D-19 sub-step (Cleric Doctrine, Wizard School, etc.). Calls `api.getLevelUpClassFeatureOptions(characterId, sessionId, optionsRef)` (added in Task 1 alongside the other api.* methods) which hits `GET /characters/:characterId/level-up/:sessionId/class-feature-options/:optionsRef` -- this endpoint is provided by Plan 04's LevelingController (see 01-04-PLAN.md Task 5). Renders one ChoiceCard per option (single-select / radio-group semantics). Dispatches `SET_CLASS_FEATURE_CHOICE`. No server-side edits in this plan -- the endpoint is already present.
|
||||
|
||||
**C. `level-up-step-boost.tsx`** — UI-SPEC lines 362-423 give the FULL JSX. Use it verbatim with these wires:
|
||||
- State source: `state.choices.boostTargets` (current selection — array of 0..4 strings)
|
||||
@@ -919,11 +960,11 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
|
||||
|
||||
**D. `level-up-step-skill-increase.tsx`** — UI-SPEC lines 426-450. Renders all skills as compact rows. For each row: current rank → next rank (or "Cap erreicht" chip if `canIncreaseSkill` would return false; mirror Plan 02 logic client-side). Dispatch `SET_SKILL_INCREASE` on click.
|
||||
|
||||
**E. `level-up-step-feat-class.tsx`** through **`level-up-step-feat-archetype.tsx`** (5 files) — All five feat steps share the same shape:
|
||||
- Fetch filtered feat list from a server endpoint. **Endpoint needed:** `GET /characters/:characterId/level-up/:sessionId/feats?slot={class|skill|general|ancestry|archetype}` returning `FeatWithEval[]` (Plan 04's FeatFilterService output). If Plan 04 didn't expose this endpoint via the LevelingController, ADD it now as a Plan 04 patch — see Task 4 below for the patch action.
|
||||
**E. `level-up-step-feat-class.tsx`** through **`level-up-step-feat-archetype.tsx`** (5 files) -- All five feat steps share the same shape:
|
||||
- Fetch filtered feat list via `api.getLevelUpFeats(characterId, sessionId, slot, includeUnavailable)` (added in Task 1 alongside the other api.* methods). It hits `GET /characters/:characterId/level-up/:sessionId/feats?slot=class|skill|general|ancestry|archetype&includeUnavailable=true|false` returning `FeatWithEval[]` -- this endpoint is provided by Plan 04's LevelingController (see 01-04-PLAN.md Task 5). No server-side edits in this plan.
|
||||
- Render each feat as a ChoiceCard.
|
||||
- For non-evaluable prereqs: show yellow AlertTriangle on the card; clicking the card opens the PrereqConfirmDialog (Task 3) → on confirm, dispatch `ACKNOWLEDGE_PREREQ_WARNING` + `SET_FEAT`.
|
||||
- For failed prereqs (`{ok:false}`): hidden by default; toggle "Auch nicht erfüllbare anzeigen" reveals them grayed.
|
||||
- For non-evaluable prereqs: show yellow AlertTriangle on the card; clicking the card opens the PrereqConfirmDialog (Task 3) -> on confirm, dispatch `ACKNOWLEDGE_PREREQ_WARNING` + `SET_FEAT`.
|
||||
- For failed prereqs (`{ok:false}`): hidden by default; toggle "Auch nicht erfuellbare anzeigen" passes `includeUnavailable=true` to the api call to reveal them grayed.
|
||||
- Slot-specific: feat-archetype only renders if `state.steps.includes('feat-archetype')` (FA enabled); shows the "vor/nach Dedication" filter per UI-SPEC line 462.
|
||||
|
||||
**F. `level-up-step-spellcaster.tsx`** — UI-SPEC lines 466-477. Two sub-shapes:
|
||||
@@ -943,21 +984,12 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
|
||||
- All touch targets ≥ 44px.
|
||||
- No `: any`.
|
||||
|
||||
**Plan 04 patch (do this BEFORE the feat steps fetch from it):** Add `GET /characters/:characterId/level-up/:sessionId/feats?slot=...` to LevelingController. Implementation:
|
||||
```typescript
|
||||
@Get(':sessionId/feats')
|
||||
async getFeats(
|
||||
@Param('characterId') characterId: string,
|
||||
@Param('sessionId') sessionId: string,
|
||||
@Query('slot') slot: SlotKind,
|
||||
@CurrentUser('id') userId: string,
|
||||
) {
|
||||
return this.levelingService.getFeatsForSlot(characterId, sessionId, slot, userId);
|
||||
}
|
||||
```
|
||||
The corresponding service method calls `featFilterService.getFilteredFeats({ slot, character: ctx, ... })`. Add it to leveling.service.ts (Plan 04 file already exists; this is a small extension).
|
||||
|
||||
Similarly add `GET /characters/:characterId/level-up/:sessionId/class-feature-options/:optionsRef` for the class-feature-choice step.
|
||||
**Server endpoints used by these steps (already provided by Plan 04 -- no patches in this plan):**
|
||||
- `GET /characters/:characterId/level-up/:sessionId/feats?slot=<kind>&includeUnavailable=<bool>` -> `FeatWithEval[]`
|
||||
- `GET /characters/:characterId/level-up/:sessionId/class-feature-options/:optionsRef` -> `ClassFeatureOption[]`
|
||||
- `GET /characters/:characterId/level-up` -> open DRAFT or 404 (used by character-sheet-page banner detection)
|
||||
All three are declared in 01-04-PLAN.md Task 5 (LevelingController) and 01-04-PLAN.md Task 4 (LevelingService).
|
||||
LevelUpSessionDto already includes `steps: StepKind[]` (Plan 04 Task 1) -- the wizard reads it directly from the start/resume response.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd client && npx tsc --noEmit -p tsconfig.app.json 2>&1 | grep -E "level-up-step" || echo "tsc clean"</automated>
|
||||
@@ -976,10 +1008,10 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
|
||||
- All step files contain NO `: any` outside comments
|
||||
- All step files contain NO emoji literals
|
||||
- `cd client && npx tsc --noEmit -p tsconfig.app.json` exits 0
|
||||
- `cd server && npm run build` exits 0 (the Plan 04 patch for new GET endpoints compiles)
|
||||
- `cd server && npm run build` exits 0 (no server changes in this plan; the build remains green from Plan 04)
|
||||
</acceptance_criteria>
|
||||
<done>
|
||||
All 11 step components implemented per UI-SPEC; all use German copy; all use ChoiceCard for talent picks; Review step wires up the preview query; the supporting Plan 04 patches for feats and class-feature-options GET endpoints land cleanly.
|
||||
All 11 step components implemented per UI-SPEC; all use German copy; all use ChoiceCard for talent picks; Review step wires up the preview query. Server endpoints consumed are owned by Plan 04 -- no server-side edits in this plan.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
@@ -1002,7 +1034,7 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
|
||||
Create `level-up-wizard.tsx` — the outer container that:
|
||||
1. Renders the modal chrome per UI-SPEC §Component Contract — Wizard Chrome.
|
||||
2. Loads the session via `useStartLevelUpMutation` on mount (start-or-resume).
|
||||
3. Computes the step list — the wizard receives the StepKind list from the session (server adds it to the start-session response, OR the wizard fetches GET ClassProgression to know `choiceType` and computes locally; planner: prefer server-provided list to keep one source of truth — Plan 04 patch: `LevelUpSessionDto` includes a `steps: StepKind[]` field).
|
||||
3. Reads the step list from the session response: `LevelUpSessionDto.steps: StepKind[]` is populated server-side by Plan 04 (LevelingService.startOrResume runs `computeApplicableSteps` on the character + targetLevel and stores the result in the response payload). The wizard simply uses `session.steps` directly -- no client-side computation, no extra round-trip.
|
||||
4. Initializes the reducer via `initWizardState(session, steps)`.
|
||||
5. Renders the active step component based on `state.steps[state.currentIdx]`.
|
||||
6. PATCHes the DRAFT after each meaningful state change (debounced — every 500ms after a SET_* event).
|
||||
@@ -1296,7 +1328,7 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
|
||||
}, [character]);
|
||||
```
|
||||
|
||||
Note for executor: To detect a DRAFT for the **banner**, add `GET /characters/:characterId/level-up` to the controller (Plan 04 patch) returning the open DRAFT or null. Wire that to `useQuery` here. Update plan SUMMARY with the chosen approach.
|
||||
Note for executor: DRAFT detection uses the `GET /characters/:characterId/level-up` endpoint already provided by Plan 04 (LevelingController.getOpenDraft -- 404 when none, 200 with the DRAFT row when present). Wire it via `api.getOpenLevelUpDraft(characterId)` (added in Task 1) and use `useQuery({ queryKey: ['levelUpDraft', characterId], queryFn: () => api.getOpenLevelUpDraft(characterId), retry: false })`. The banner reads `data` and `setHasOpenDraft(!!data)`. No server-side changes in this plan.
|
||||
|
||||
**Add 3 — Header button** (in the header cluster around lines 1607-1626 — INSERT AS FIRST in the cluster, left of Download):
|
||||
|
||||
@@ -1515,7 +1547,7 @@ cd client && npx tsc --noEmit -p tsconfig.app.json
|
||||
# Client production build clean
|
||||
cd client && npm run build
|
||||
|
||||
# Server still builds (in case Plan 04 patches in Task 4 were needed)
|
||||
# Server still builds (no server changes in this plan -- sanity check only)
|
||||
cd server && npm run build
|
||||
|
||||
# Full server test suite still green
|
||||
@@ -1534,22 +1566,21 @@ All four commands must exit 0 before the human checkpoint.
|
||||
- Choice-Card primitive matches UI-SPEC §Component Contract — Choice-Card (states, source badges, prereq warning)
|
||||
- Boost step uses h-11 w-11 +/- buttons + live "wird {newScore}" + cap-bei-18 chip
|
||||
- Review step uses font-mono for stat numbers, two-column Vorher/Nachher cards
|
||||
- Ändern revision contract implemented (revision-mode flag + Zurück-zur-Übersicht button + chain re-validation)
|
||||
- Ändern revision contract implemented (revision-mode flag + Zurück-zur-Übersicht button — chain re-validation deferred to v2 per must_haves.gotchas)
|
||||
- DRAFT-Resume banner implemented per UI-SPEC §Component Contract — DRAFT-Resume Banner
|
||||
- Pathbuilder-Import-Violations banner implemented per UI-SPEC §Component Contract — Pathbuilder-Import-Violations Banner
|
||||
- WebSocket level_up_committed callback wired to invalidate character query
|
||||
- Motion respects prefers-reduced-motion
|
||||
- TypeScript strict — no `: any`
|
||||
- Client production build clean (`cd client && npm run build` exits 0)
|
||||
- Server still builds and tests green (no regressions from Plan 04 patches)
|
||||
- Server still builds and tests green (this plan adds no server changes)
|
||||
- Human verification checkpoint approved
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-level-up-pf2e-regelkonform/01-05-SUMMARY.md` documenting:
|
||||
- Final file list (all 18 new client files + 4 extended)
|
||||
- Whether the GET DRAFT-detect endpoint was added in Plan 04 patches or alternative chosen (e.g. lazy detect on first wizard open)
|
||||
- Whether the GET feats / GET class-feature-options endpoints were added as Plan 04 patches
|
||||
- Confirmation that the wizard consumes the Plan 04-owned endpoints (GET open-draft, GET feats, GET class-feature-options) without any server-side edits in this plan
|
||||
- Toast library used (if any introduced) or pattern chosen (browser alert is NOT acceptable; planner discretion: react-hot-toast, sonner, or hand-rolled context — record the decision)
|
||||
- Any deviations from UI-SPEC noted with rationale
|
||||
- Result of the human-verification checkpoint
|
||||
|
||||
Reference in New Issue
Block a user