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>
|
||||
|
||||
Reference in New Issue
Block a user