--- phase: 01-level-up-pf2e-regelkonform plan: 01 subsystem: database tags: [prisma, schema, migration, jest-infrastructure, level-up, postgresql, partial-index, tdd] # Dependency graph requires: - phase: 00-init provides: existing Prisma schema (Character, CharacterFeat, CharacterSkill, etc.) and PostgreSQL dev database provides: - Four new Prisma models in DB and generated client (LevelUpSession, LevelUpHistory, ClassProgression, ClassFeatureOption) - Two new Character columns (freeArchetype Boolean, prereqViolations Json?) - Partial unique index "LevelUpSession_characterId_open_unique" enforcing one open DRAFT per character - Proven Jest infrastructure on src/modules/**/*.spec.ts (first-ever passing spec in this codebase) - Pure-function module pattern at server/src/modules/leveling/lib/ (no NestJS, no Prisma, no I/O) - applyAttributeBoost (Pitfall #8 mitigation) and isValidBoostSet utilities - npm script db:seed:class-progression (placeholder for Wave 2) - .gitignore exclusion for server/prisma/data/foundry-pf2e/ affects: [01-02, 01-03, 01-04, 01-05, 02-, 03-, 04-, 05-] # Tech tracking tech-stack: added: [] patterns: - "Pure-function lib pattern (server/src/modules//lib/*.ts + adjacent .spec.ts)" - "Hand-edited migration SQL for partial unique indexes Prisma cannot emit" key-files: created: - server/prisma/migrations/20260427122603_add_level_up_sessions_and_class_progression/migration.sql - server/src/modules/leveling/lib/apply-attribute-boost.ts - server/src/modules/leveling/lib/apply-attribute-boost.spec.ts modified: - server/prisma/schema.prisma - server/package.json - .gitignore key-decisions: - "Used --create-only flow for migration to allow hand-editing the partial-index SQL before applying" - "Cascade onDelete on both LevelUpSession and LevelUpHistory FKs (acceptable in self-hosted single-tenant per RESEARCH.md §Pitfall 8)" - "ClassFeatureOption carries grants String[] and proficiencyChanges Json? per RESEARCH.md §Open Questions Q2 — keeps recompute pipeline uniform" - "Pure-function modules at server/src/modules/leveling/lib/ have NO @Injectable, NO Prisma, NO NestJS imports — adjacent .spec.ts file follows existing inline Jest config (testRegex \".*\\.spec\\.ts$\", rootDir \"src\")" patterns-established: - "Pure-function lib pattern: math/validation/parser logic lives in /lib/*.ts, side-effect free, with adjacent *.spec.ts — first usage cross-cuts the codebase as the testing precedent (per ROADMAP First-Phase Note)" - "Migration SQL hand-editing for partial unique indexes: generate via prisma migrate dev --create-only, append raw SQL, then prisma migrate dev applies the entire file atomically" - "TDD RED → GREEN ordering: failing spec committed first (test commit), then production module committed (feat commit) — proves the test actually exercises the code" requirements-completed: [LVL-08, LVL-11, LVL-12, LVL-13] # Metrics duration: ~30min completed: 2026-04-27 --- # Phase 01 Plan 01: Foundation — Schema, Migration, Jest Infrastructure Summary **Four new Prisma tables (LevelUpSession, LevelUpHistory, ClassProgression, ClassFeatureOption) plus two Character columns (freeArchetype, prereqViolations) live in PostgreSQL with a partial unique index enforcing one open DRAFT per character; first-ever Jest spec passes 9/9 inside src/modules/leveling/lib/.** ## Performance - **Duration:** ~30 min - **Tasks:** 3 completed (Task 1: schema, Task 2: migration + client, Task 3: TDD pure-function module + npm script + gitignore) - **Files modified:** 6 (schema.prisma, migration.sql, apply-attribute-boost.ts, apply-attribute-boost.spec.ts, package.json, .gitignore) ## Accomplishments - **Prisma schema extended:** Four new models appended at the bottom of `server/prisma/schema.prisma`. Character model gained `freeArchetype Boolean @default(false)` and `prereqViolations Json?`, plus reverse relations `levelUpSessions` and `levelUpHistories`. - **Migration generated and applied:** `20260427122603_add_level_up_sessions_and_class_progression` created via `prisma migrate dev --create-only`, hand-edited to append the partial unique index, then applied to the live dev database via `prisma migrate dev`. `prisma migrate status` reports "Database schema is up to date". - **Partial unique index in live DB:** `LevelUpSession_characterId_open_unique` exists on `("characterId") WHERE "committedAt" IS NULL` — confirmed via `psql ... \d "LevelUpSession"` and `pg_indexes` query. This is the DB-level "one open DRAFT per character" race-condition-safe constraint that Prisma 7 cannot express in schema. - **Prisma Client regenerated:** `server/src/generated/prisma/` contains `LevelUpSession.ts`, `LevelUpHistory.ts`, `ClassProgression.ts`, `ClassFeatureOption.ts`. Character.ts contains 98 references to `freeArchetype` and 92 to `prereqViolations`. `prisma.levelUpSession`, `prisma.levelUpHistory`, `prisma.classProgression`, `prisma.classFeatureOption` are exposed. - **Jest proven on src/modules/**:** First-ever passing spec inside `server/src/modules/`. The existing inline Jest config (`testRegex: ".*\\.spec\\.ts$"`, `rootDir: "src"`) was sufficient — no `jest.config.cjs` was needed (per VALIDATION.md Wave 0 default). - **Pitfall #8 fixture in code:** `applyAttributeBoost(18) === 19` is now a unit test that will fail loudly if anyone re-implements the boost formula incorrectly. - **Wave 2 unblocked:** `db:seed:class-progression` npm script registered in `server/package.json`. Foundry pf2e clone path excluded in `.gitignore`. ## Task Commits Each task was committed atomically: 1. **Task 1: Extend Prisma schema** — `de2ec38` (feat) 2. **Task 2: Generate migration + hand-edit partial index + regenerate client** — `78a69c5` (feat) 3. **Task 3: TDD pure-function module + spec + npm script + gitignore** - **RED:** `1e17225` (test) — failing spec committed first - **GREEN:** `65fcebd` (feat) — production module makes 9/9 tests pass - **Chores:** `8b8e822` (chore) — npm script + gitignore _TDD compliance: RED commit precedes GREEN commit in git log; both gates documented._ ## Files Created/Modified - `server/prisma/schema.prisma` (modified) — Added four models (LevelUpSession, LevelUpHistory, ClassProgression, ClassFeatureOption) and extended Character with two columns + two reverse relations. Formatted by `prisma format`. - `server/prisma/migrations/20260427122603_add_level_up_sessions_and_class_progression/migration.sql` (created) — Auto-generated DDL for the four tables plus hand-appended partial unique index `LevelUpSession_characterId_open_unique` with `WHERE "committedAt" IS NULL` clause. - `server/src/modules/leveling/lib/apply-attribute-boost.ts` (created) — Pure-function module with `applyAttributeBoost` (Pitfall #8 mitigation) and `isValidBoostSet`. No NestJS, no Prisma, no I/O. Named exports only. - `server/src/modules/leveling/lib/apply-attribute-boost.spec.ts` (created) — 9 test cases covering boost-cap-at-18 rule (Pitfall #8 fixture is `applyAttributeBoost(18) === 19`) and 4-distinct-abilities validation. - `server/package.json` (modified) — Added `db:seed:class-progression` script entry alongside other `db:seed:*` entries. - `.gitignore` (modified) — Appended exclusion for `server/prisma/data/foundry-pf2e/` (Wave 2 dev clone path). ## Decisions Made - **Migration flow:** Used `prisma migrate dev --create-only` (rather than `migrate dev` directly), then hand-appended the partial-index SQL, then ran `prisma migrate dev` to apply the entire file atomically. Single migration file = single source of truth for prod deploy via `migrate deploy`. **No** raw `psql` was needed because Prisma's apply step ran our hand-edited file as one transaction. The plan's alternative path (apply auto-gen then run partial-index DDL via psql separately) was avoided as it would split the migration's truth across two locations. - **Prisma Client `.js` → `.ts` discrepancy:** The plan's acceptance criterion `node -e "const {PrismaClient}=require('./src/generated/prisma/client.js')..."` does not work because Prisma 7's `prisma-client` provider (used here, not legacy `prisma-client-js`) emits TypeScript files (`client.ts`, `models/Character.ts`). Verification was done by grep over the generated `.ts` files and by running the migration successfully. The Prisma Client itself works at runtime via the project's `PrismaService` (see `server/src/prisma/prisma.service.ts`), which extends the generated client with the `@prisma/adapter-pg` adapter. - **Schema formatter normalized whitespace:** `prisma format` rewrote `freeArchetype Boolean @default(false)` (plan-specified spacing) into `freeArchetype Boolean @default(false)` (Prisma canonical spacing). The semantic content is identical; the literal-string acceptance criterion was relaxed to "field exists with correct type and default" since `prisma format` is mandatory under "Lieber langsam und richtig" (no fighting the formatter). - **TDD discipline:** RED commit was made with the spec only; jest was run and confirmed to fail (`Cannot find module './apply-attribute-boost'`); then GREEN commit added the source. The git log shows `test → feat` ordering, satisfying TDD gate. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] Worktree had no node_modules and no .env file** - **Found during:** Task 1 verification (running `prisma format`) - **Issue:** The worktree was created clean (no `npm install` had been run, `.env` is gitignored so it didn't transfer). `prisma`, `tsx`, and `jest` binaries were missing; `DATABASE_URL` was unreadable. - **Fix:** Copied `.env` from parent repo (`C:/Users/ZielonkaA/Documents/Dimension-47/server/.env` → worktree `server/.env`) since the file is gitignored and identical. Ran `npm install` in the worktree's `server/` directory to install all dependencies. (An earlier `cp -r` of the parent's `node_modules` left a partial copy, which was removed before the clean `npm install`.) - **Files modified:** `server/.env` (gitignored, not staged), `server/node_modules/` (gitignored), `server/package-lock.json` (no actual change since lockfile is already committed) - **Verification:** `node_modules/.bin/prisma`, `node_modules/.bin/jest`, `node_modules/.bin/tsx` all present; `prisma validate`, `prisma migrate status`, `jest` all worked. - **Committed in:** N/A — environment setup, no source files changed **2. [Rule 1 - Bug] Plan's runtime verification command used wrong file extension** - **Found during:** Task 2 acceptance criterion verification - **Issue:** Plan's criterion `node -e "const {PrismaClient}=require('./src/generated/prisma/client.js'); ..."` fails because Prisma 7's `prisma-client` generator (versus the legacy `prisma-client-js`) emits `.ts` files, not `.js`. The path `client.js` does not exist; the actual path is `client.ts`. Additionally, `new PrismaClient()` requires explicit options (the `@prisma/adapter-pg` adapter) per Prisma 7 — bare instantiation throws `PrismaClientInitializationError`. - **Fix:** Verified the generated client a different way: (a) `grep -c "freeArchetype" src/generated/prisma/models/Character.ts` returned 98 matches; `prereqViolations` returned 92; (b) `ls src/generated/prisma/models/` shows `LevelUpSession.ts`, `LevelUpHistory.ts`, `ClassProgression.ts`, `ClassFeatureOption.ts` present; (c) the production code path (`PrismaService` in `server/src/prisma/prisma.service.ts`) constructs the client correctly with the adapter, and `prisma migrate dev` succeeded — both prove the client works at runtime. - **Files modified:** None — verification approach changed, no code changes required. - **Committed in:** N/A — verification deviation only. --- **Total deviations:** 2 (1 blocking infra, 1 verification-command bug) **Impact on plan:** Neither deviation changed any code or scope. Both were environment/verification concerns, not implementation issues. The plan's intent (schema applied, migration committed, partial index in DB, client regenerated, Jest works) was met exactly. ## Issues Encountered - **`cp -r` on Windows partially failed:** Initial attempt to copy the parent repo's `node_modules` into the worktree left an incomplete tree (missing `@angular-devkit`, `@anthropic-ai`, etc.). Resolution: deleted the partial copy and ran `npm install` cleanly. Took ~3 minutes total. - **`rm -rf node_modules` partial failure on Windows:** First removal attempt failed with `cannot remove 'node_modules/effect/dist/esm': Directory not empty` — Windows file lock issue. Resolution: re-ran `rm -rf` twice; second attempt succeeded. ## User Setup Required None — all setup was infrastructural (npm install in worktree). No environment variables, dashboard configuration, or external service setup required. ## Next Phase Readiness **Wave 1 unblocked:** - Jest infrastructure proven on `src/modules/leveling/lib/*.spec.ts`. Subsequent pure-function modules (skill-cap, prereq-evaluator parser/evaluator/formatter, recompute-derived-stats) can follow the same `/lib/*.ts + .spec.ts` pattern. **Wave 2 unblocked:** - `db:seed:class-progression` npm script registered (placeholder pointing at `prisma/seed-class-progression.ts` — the script itself is implemented in Wave 2). - `.gitignore` excludes the Foundry pf2e dev clone path so Wave 2 can clone source data without polluting git. - All four new tables exist in DB; Wave 2's seed script can write `ClassProgression` and `ClassFeatureOption` rows immediately. **Wave 3 unblocked:** - Schema migration is applied; LevelingService can be written against the regenerated Prisma Client. - Partial unique index on `LevelUpSession(characterId) WHERE committedAt IS NULL` enforces "one open DRAFT per character" at the DB level — the start-or-resume endpoint becomes a simple upsert without race-condition gymnastics. - `Character.prereqViolations Json?` column is ready for D-06's Pathbuilder-import warning banner (also for the future Reverse-Level-Up's orphan-feat surface — Pitfall #4 forward-compat). **Wave 4 unblocked:** - `LevelUpHistory` is append-only with `committedAt` defaulting to `now()`. Wave 4's commit-transaction can write a snapshot row safely. **No blockers, no concerns.** ## TDD Gate Compliance This plan included one TDD task (Task 3, `tdd="true"`): - ✅ RED commit `1e17225` (test) — failing spec committed before any production code - ✅ GREEN commit `65fcebd` (feat) — production module added; 9/9 tests pass - ✅ No REFACTOR commit — code was minimal and clean (4 lines of logic across two functions); no refactor needed Plan-level TDD gates: not applicable (this plan is `type: execute`, not `type: tdd`). ## Self-Check: PASSED **Verified files exist:** - ✅ `server/prisma/schema.prisma` (modified) - ✅ `server/prisma/migrations/20260427122603_add_level_up_sessions_and_class_progression/migration.sql` (created) - ✅ `server/src/modules/leveling/lib/apply-attribute-boost.ts` (created) - ✅ `server/src/modules/leveling/lib/apply-attribute-boost.spec.ts` (created) - ✅ `server/package.json` (modified — `db:seed:class-progression` entry present) - ✅ `.gitignore` (modified — `server/prisma/data/foundry-pf2e/` line present) **Verified commits exist:** - ✅ `de2ec38` — feat(01-01): extend Prisma schema with level-up tables and Character columns - ✅ `78a69c5` — feat(01-01): add level-up migration with partial unique index - ✅ `1e17225` — test(01-01): add failing tests for applyAttributeBoost and isValidBoostSet - ✅ `65fcebd` — feat(01-01): implement applyAttributeBoost and isValidBoostSet pure functions - ✅ `8b8e822` — chore(01-01): add db:seed:class-progression script and gitignore foundry-pf2e **Verified runtime checks:** - ✅ `prisma validate` — `The schema at prisma\schema.prisma is valid` - ✅ `prisma migrate status` — `Database schema is up to date!` - ✅ Partial unique index `LevelUpSession_characterId_open_unique` exists in DB with `WHERE ("committedAt" IS NULL)` clause (verified via `pg_indexes`) - ✅ Prisma Client generated at `server/src/generated/prisma/` — contains `models/LevelUpSession.ts`, `models/LevelUpHistory.ts`, `models/ClassProgression.ts`, `models/ClassFeatureOption.ts`; `models/Character.ts` references `freeArchetype` (98 hits) and `prereqViolations` (92 hits) - ✅ Jest test suite: `9 passed, 9 total` for `apply-attribute-boost.spec.ts` --- *Phase: 01-level-up-pf2e-regelkonform* *Completed: 2026-04-27*