---
phase: 01-level-up-pf2e-regelkonform
plan: 01
type: execute
wave: 0
depends_on: []
files_modified:
  - server/prisma/schema.prisma
  - server/prisma/migrations/YYYYMMDDHHMMSS_add_level_up_sessions_and_class_progression/migration.sql
  - server/package.json
  - .gitignore
  - server/src/modules/leveling/lib/apply-attribute-boost.ts
  - server/src/modules/leveling/lib/apply-attribute-boost.spec.ts
autonomous: true
requirements: [LVL-08, LVL-11, LVL-12, LVL-13]
tags: [prisma, schema, migration, jest-infrastructure, level-up]
must_haves:
  truths:
    - "Three new Prisma tables exist in the live PostgreSQL database (LevelUpSession, LevelUpHistory, ClassProgression, ClassFeatureOption)."
    - "Character row has two new columns: freeArchetype (Boolean, default false) and prereqViolations (Json, nullable)."
    - "A partial unique index enforces at most one open DRAFT (committedAt IS NULL) per character at the database level."
    - "Running 'cd server && npx prisma migrate status' reports 'Database schema is up to date'."
    - "Jest discovers and runs *.spec.ts files in server/src/ — proven by a passing apply-attribute-boost.spec.ts."
    - "npm script db:seed:class-progression exists in server/package.json and points at tsx prisma/seed-class-progression.ts."
    - ".gitignore excludes server/prisma/data/foundry-pf2e/ from version control."
  artifacts:
    - path: "server/prisma/schema.prisma"
      provides: "Schema models LevelUpSession, LevelUpHistory, ClassProgression, ClassFeatureOption + extended Character"
      contains: "model LevelUpSession"
    - path: "server/prisma/migrations/*_add_level_up_sessions_and_class_progression/migration.sql"
      provides: "Migration SQL with hand-added partial unique index"
      contains: "CREATE UNIQUE INDEX \"LevelUpSession_characterId_open_unique\""
    - path: "server/src/modules/leveling/lib/apply-attribute-boost.ts"
      provides: "First pure-function module — applyAttributeBoost + isValidBoostSet"
      exports: ["applyAttributeBoost", "isValidBoostSet"]
    - path: "server/src/modules/leveling/lib/apply-attribute-boost.spec.ts"
      provides: "Proves Jest infrastructure works on *.spec.ts inside src/modules/leveling/lib/"
      contains: "describe('applyAttributeBoost'"
    - path: "server/package.json"
      provides: "db:seed:class-progression npm script"
      contains: "db:seed:class-progression"
    - path: ".gitignore"
      provides: "Excludes Foundry pf2e dev clone"
      contains: "server/prisma/data/foundry-pf2e/"
  key_links:
    - from: "server/prisma/schema.prisma"
      to: "PostgreSQL database"
      via: "prisma migrate dev"
      pattern: "migrate dev .*add_level_up_sessions_and_class_progression"
    - from: "Character model"
      to: "LevelUpSession[]"
      via: "Prisma relation field 'levelUpSessions'"
      pattern: "levelUpSessions\\s+LevelUpSession\\[\\]"
    - from: "Character model"
      to: "LevelUpHistory[]"
      via: "Prisma relation field 'levelUpHistories'"
      pattern: "levelUpHistories\\s+LevelUpHistory\\[\\]"
---

<objective>
Lay the foundation for Phase 1: extend the Prisma schema with the four new tables (LevelUpSession, LevelUpHistory, ClassProgression, ClassFeatureOption) and two new Character columns (freeArchetype, prereqViolations); generate the migration; hand-edit the migration SQL to add the partial unique index that Prisma 7 cannot emit from the schema; regenerate the Prisma Client; prove Jest infrastructure works on a real spec file; and add the npm script + .gitignore entry needed by the Wave 2 seed pipeline.

Purpose: Without this plan, no later wave can run — Wave 1 needs Jest proven, Wave 2 needs the seed npm script wired, Wave 3 needs the schema/tables to exist in the database before the LevelingService can be written.

Output: Schema file, migration SQL with partial unique index, regenerated Prisma client, npm script entry, .gitignore entry, first proven Jest spec.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/REQUIREMENTS.md
@.planning/phases/01-level-up-pf2e-regelkonform/01-CONTEXT.md
@.planning/phases/01-level-up-pf2e-regelkonform/01-RESEARCH.md
@.planning/phases/01-level-up-pf2e-regelkonform/01-PATTERNS.md
@.planning/phases/01-level-up-pf2e-regelkonform/01-VALIDATION.md
@server/prisma/schema.prisma
@server/prisma/migrations/20260120080237_add_alchemy_and_rest_system/migration.sql
@server/package.json

<interfaces>
<!-- Existing Character model relevant fields (from server/prisma/schema.prisma:171-220) -->
<!-- All new columns are non-breaking additions with defaults; existing data is unaffected. -->

```prisma
model Character {
  id          String   @id @default(uuid())
  // … existing fields (level, hpMax, ancestryId, classId, …) unchanged …
  // NEW (this plan adds these two columns + two reverse relations):
  freeArchetype     Boolean   @default(false)
  prereqViolations  Json?
  levelUpSessions   LevelUpSession[]
  levelUpHistories  LevelUpHistory[]
}
```

<!-- Analog migration shape (server/prisma/migrations/20260120080237_add_alchemy_and_rest_system/migration.sql) -->
<!-- Use exactly this style: CREATE TYPE, CREATE TABLE, CREATE INDEX, ALTER TABLE ADD CONSTRAINT FK -->

<!-- Existing Jest config (server/package.json:88-104) — DO NOT REPLACE -->
```json
"jest": {
  "moduleFileExtensions": ["js","json","ts"],
  "rootDir": "src",
  "testRegex": ".*\\.spec\\.ts$",
  "transform": { "^.+\\.(t|j)s$": "ts-jest" },
  "collectCoverageFrom": ["**/*.(t|j)s"],
  "coverageDirectory": "../coverage",
  "testEnvironment": "node"
}
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="false">
  <name>Task 1: Extend Prisma schema with four new models + Character columns</name>
  <files>server/prisma/schema.prisma</files>
  <read_first>
    - server/prisma/schema.prisma (entire file — must understand existing Character model at lines 171-220 and existing CharacterAlchemyState at line 167+ as the analog pattern)
    - .planning/phases/01-level-up-pf2e-regelkonform/01-PATTERNS.md (lines 436-496 — exact schema additions specified)
    - .planning/phases/01-level-up-pf2e-regelkonform/01-RESEARCH.md (lines 750-824 — Code Examples §3 — full schema + ClassFeatureOption model)
    - server/prisma/migrations/20260120080237_add_alchemy_and_rest_system/migration.sql (analog migration to mirror style)
  </read_first>
  <action>
    Edit `server/prisma/schema.prisma` to APPEND the four new models AFTER the last existing model and EXTEND the existing Character model.

    **Step 1 — Append four new models at the bottom of the file (verbatim):**

    ```prisma
    model LevelUpSession {
      id            String    @id @default(uuid())
      characterId   String
      targetLevel   Int
      state         Json      @default("{}")
      committedAt   DateTime?
      createdAt     DateTime  @default(now())
      updatedAt     DateTime  @updatedAt

      character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)

      @@index([characterId])
      // Partial unique index "one open DRAFT per character" added in raw migration SQL —
      // Prisma 7 cannot emit partial indexes from schema (per RESEARCH.md §Don't Hand-Roll).
    }

    model LevelUpHistory {
      id              String   @id @default(uuid())
      characterId     String
      levelFrom       Int
      levelTo         Int
      snapshotBefore  Json
      choices         Json
      committedAt     DateTime @default(now())

      character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)

      @@index([characterId, committedAt(sort: Desc)])
    }

    model ClassProgression {
      id                   String   @id @default(uuid())
      className            String
      level                Int
      grants               String[]
      proficiencyChanges   Json
      spellSlotIncrement   Json?
      cantripIncrement     Int?
      repertoireIncrement  Int?
      choiceType           String?
      choiceOptionsRef     String?

      @@unique([className, level])
      @@index([className])
    }

    model ClassFeatureOption {
      id            String   @id @default(uuid())
      optionsRef    String
      optionKey     String
      name          String
      nameGerman    String?
      description   String
      grants        String[]
      proficiencyChanges Json?

      @@unique([optionsRef, optionKey])
      @@index([optionsRef])
    }
    ```

    Note: `ClassFeatureOption` carries `grants: String[]` and `proficiencyChanges: Json?` per RESEARCH.md §Open Questions Q2 recommendation — keeps the recompute pipeline uniform.

    **Step 2 — Extend the existing `model Character` block (find it around line 171). Add EXACTLY these two columns and two reverse relations inside the existing model block:**

    ```prisma
      // … existing fields stay unchanged …
      // === Phase 1 additions ===
      freeArchetype     Boolean  @default(false)        // D-08
      prereqViolations  Json?                           // D-06
      levelUpSessions   LevelUpSession[]
      levelUpHistories  LevelUpHistory[]
    ```

    Place these inside the Character block AFTER the last existing scalar field but BEFORE the existing relation block (or at the end inside the model body — Prisma is order-tolerant). Do NOT touch any existing column.

    **Constraints (per CLAUDE.md and RESEARCH.md):**
    - No `any` types (this is a schema; rule applies to following TS work).
    - Do NOT run `prisma db push` — Task 2 runs `prisma migrate dev`.
    - Cascade choice for both LevelUpSession and LevelUpHistory is `onDelete: Cascade` per RESEARCH.md §Pitfall 8 (acceptable in self-hosted single-tenant; documented in this plan).
  </action>
  <verify>
    <automated>cd server &amp;&amp; npx prisma format &amp;&amp; npx prisma validate</automated>
  </verify>
  <acceptance_criteria>
    - `server/prisma/schema.prisma` contains the literal string `model LevelUpSession {`
    - `server/prisma/schema.prisma` contains the literal string `model LevelUpHistory {`
    - `server/prisma/schema.prisma` contains the literal string `model ClassProgression {`
    - `server/prisma/schema.prisma` contains the literal string `model ClassFeatureOption {`
    - `server/prisma/schema.prisma` contains the literal string `freeArchetype     Boolean  @default(false)` (within the Character model block)
    - `server/prisma/schema.prisma` contains the literal string `prereqViolations  Json?`
    - `server/prisma/schema.prisma` contains the literal string `levelUpSessions   LevelUpSession[]`
    - `server/prisma/schema.prisma` contains the literal string `levelUpHistories  LevelUpHistory[]`
    - `server/prisma/schema.prisma` contains `@@unique([className, level])`
    - `server/prisma/schema.prisma` contains `@@unique([optionsRef, optionKey])`
    - `cd server && npx prisma format` exits 0 and rewrites the file in canonical formatting
    - `cd server && npx prisma validate` exits 0 with output containing `The schema is valid`
  </acceptance_criteria>
  <done>
    Schema file contains exactly four new models, two new Character columns, two reverse relations on Character. `prisma validate` passes. No existing column on Character was renamed or removed.
  </done>
</task>

<task type="auto" tdd="false">
  <name>Task 2 [BLOCKING]: Run Prisma migration, hand-edit migration SQL for partial unique index, regenerate Prisma Client</name>
  <files>
    server/prisma/migrations/YYYYMMDDHHMMSS_add_level_up_sessions_and_class_progression/migration.sql,
    server/src/generated/prisma/* (regenerated)
  </files>
  <read_first>
    - server/prisma/schema.prisma (post-Task-1 — must reflect the four new models)
    - server/prisma/migrations/20260120080237_add_alchemy_and_rest_system/migration.sql (analog SQL style — CREATE TABLE, CREATE INDEX, ALTER TABLE FK)
    - .planning/phases/01-level-up-pf2e-regelkonform/01-RESEARCH.md (lines 525-533 — exact partial-index SQL)
    - .planning/phases/01-level-up-pf2e-regelkonform/01-PATTERNS.md (lines 500-534 — migration pattern + partial index)
    - CLAUDE.md (Prisma `migrate dev`, NEVER `db push`)
  </read_first>
  <action>
    **Step 1 — Generate the migration (creates the timestamped folder + migration.sql):**

    Run from the project root:
    ```bash
    cd server && npm run db:migrate:dev -- --name add_level_up_sessions_and_class_progression
    ```

    This generates `server/prisma/migrations/{timestamp}_add_level_up_sessions_and_class_progression/migration.sql` AND applies it to the local PostgreSQL database AND regenerates the Prisma Client into `server/src/generated/prisma/`.

    If the database is not reachable: STOP, report the error, do NOT proceed. The migration must apply cleanly.

    **Step 2 — Hand-edit the freshly generated `migration.sql`** to append the partial unique index that Prisma 7 cannot emit from the schema. Open the file in the new migration folder and APPEND these lines at the very end (after the auto-generated `ALTER TABLE … ADD CONSTRAINT` lines):

    ```sql
    -- Partial unique index: enforce at most one open DRAFT per character.
    -- Prisma 7 cannot express partial indexes via @@unique; this raw SQL is required.
    -- See: .planning/phases/01-level-up-pf2e-regelkonform/01-RESEARCH.md §Don't Hand-Roll.
    CREATE UNIQUE INDEX "LevelUpSession_characterId_open_unique"
      ON "LevelUpSession"("characterId")
      WHERE "committedAt" IS NULL;
    ```

    **Step 3 — Apply the appended SQL** by re-running migrate-dev (it detects the migration is already applied but the SQL was edited; if it complains, use the resolve flow):

    ```bash
    cd server && npx prisma migrate resolve --applied "{full_migration_name}"
    ```

    OR — preferred — connect to the database and run the partial index DDL directly via psql once, then mark the migration as applied. Use whichever path keeps the schema state consistent with the migration history. Document the chosen path in the plan SUMMARY.

    Concrete preferred sequence (least error-prone):
    1. Run `npm run db:migrate:dev -- --name add_level_up_sessions_and_class_progression` (auto-generates and applies).
    2. Append the partial-index SQL to the generated migration.sql file.
    3. Connect to the dev database (`psql $DATABASE_URL`) and execute the partial-index `CREATE UNIQUE INDEX ... WHERE ...` statement once manually.
    4. Verify with `npm run db:migrate:status` — should report `Database schema is up to date`.
    5. Verify the partial index exists: `psql $DATABASE_URL -c "\d \"LevelUpSession\""` should list `LevelUpSession_characterId_open_unique`.

    **Step 4 — Regenerate the Prisma Client** (this normally runs automatically as part of `migrate dev`, but run explicitly to confirm):

    ```bash
    cd server && npm run db:generate
    ```

    This regenerates `server/src/generated/prisma/` so TS types for `LevelUpSession`, `LevelUpHistory`, `ClassProgression`, `ClassFeatureOption`, and the new Character columns are available to subsequent waves.

    **Constraints (per CLAUDE.md):**
    - NEVER use `db push`. The migration file must exist on disk for production deploys.
    - The hand-edited SQL must be committed to git (it is the source of truth for prod deployment).
  </action>
  <verify>
    <automated>cd server &amp;&amp; npx prisma migrate status</automated>
  </verify>
  <acceptance_criteria>
    - A new directory matching glob `server/prisma/migrations/*_add_level_up_sessions_and_class_progression/` exists
    - The directory contains `migration.sql`
    - The migration.sql file contains the literal string `CREATE TABLE "LevelUpSession"`
    - The migration.sql file contains the literal string `CREATE TABLE "LevelUpHistory"`
    - The migration.sql file contains the literal string `CREATE TABLE "ClassProgression"`
    - The migration.sql file contains the literal string `CREATE TABLE "ClassFeatureOption"`
    - The migration.sql file contains the literal string `CREATE UNIQUE INDEX "LevelUpSession_characterId_open_unique"` (the partial unique index)
    - The migration.sql file contains the literal string `WHERE "committedAt" IS NULL`
    - `cd server && npx prisma migrate status` exits 0 with output containing `Database schema is up to date`
    - `psql $DATABASE_URL -c "\\d \"LevelUpSession\""` lists the partial index `LevelUpSession_characterId_open_unique`
    - `cd server && node -e "const {PrismaClient}=require('./src/generated/prisma/client.js'); const p=new PrismaClient(); console.log(typeof p.levelUpSession.findMany);"` outputs `function`
    - The Character model in the regenerated client exposes `freeArchetype` and `prereqViolations` fields (verify with: `cd server && grep -E "freeArchetype|prereqViolations" src/generated/prisma/index.d.ts | head -5`)
  </acceptance_criteria>
  <done>
    Migration file exists with auto-generated SQL + hand-appended partial unique index. Migration is applied to the live dev database. Prisma Client regenerated. `migrate status` reports up-to-date.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 3: Prove Jest infrastructure with first real spec — apply-attribute-boost + isValidBoostSet</name>
  <files>
    server/src/modules/leveling/lib/apply-attribute-boost.ts,
    server/src/modules/leveling/lib/apply-attribute-boost.spec.ts,
    server/package.json,
    .gitignore
  </files>
  <read_first>
    - server/package.json (lines 88-104 — inline Jest config; testRegex is `.*\.spec\.ts$`, rootDir is `src`)
    - .planning/phases/01-level-up-pf2e-regelkonform/01-PATTERNS.md (lines 315-374 — pure-function lib pattern + first spec example + naming conventions)
    - .planning/phases/01-level-up-pf2e-regelkonform/01-RESEARCH.md (lines 478-512 — Pattern 3 example + canonical shape; lines 596-601 — anti-patterns)
    - .planning/phases/01-level-up-pf2e-regelkonform/01-VALIDATION.md (lines 43-48 — exact behaviors to assert)
    - .gitignore (current contents — must understand existing exclusions before appending)
  </read_first>
  <behavior>
    - `applyAttributeBoost(10)` returns 12 (PF2e: +2 below 18)
    - `applyAttributeBoost(17)` returns 19 (still +2 — 17 is below 18)
    - `applyAttributeBoost(18)` returns 19 (PF2e cap rule: +1 at or above 18) — **the load-bearing test, Pitfall #8 fixture**
    - `applyAttributeBoost(20)` returns 21 (+1 above cap)
    - `isValidBoostSet(['STR','DEX','CON','INT'])` returns true (4 distinct)
    - `isValidBoostSet(['STR','STR','CON','INT'])` returns false (duplicate)
    - `isValidBoostSet(['STR','DEX','CON'])` returns false (only 3)
    - `isValidBoostSet(['STR','DEX','CON','INT','WIS'])` returns false (5 — too many)
  </behavior>
  <action>
    **Step 1 — Create the production module `server/src/modules/leveling/lib/apply-attribute-boost.ts`** with this exact content:

    ```typescript
    /**
     * Pure function module — no NestJS, no Prisma, no I/O.
     * PF2e Boost Cap rule: +2 if score < 18, +1 if score >= 18 (Pitfall #8).
     * See: .planning/phases/01-level-up-pf2e-regelkonform/01-RESEARCH.md §Pattern 3
     */

    export type AbilityScore = number;

    export type AbilityAbbreviation = 'STR' | 'DEX' | 'CON' | 'INT' | 'WIS' | 'CHA';

    /**
     * Applies a single PF2e attribute boost to a score.
     * - Score below 18 → +2
     * - Score at or above 18 → +1 (cap rule, prevents Pitfall #8)
     */
    export function applyAttributeBoost(currentScore: AbilityScore): AbilityScore {
      return currentScore >= 18 ? currentScore + 1 : currentScore + 2;
    }

    /**
     * Validates a level-up boost set: must be exactly 4 distinct abilities.
     * PF2e: at boost levels (5/10/15/20), the player picks 4 different attributes.
     */
    export function isValidBoostSet(targets: readonly string[]): boolean {
      return targets.length === 4 && new Set(targets).size === 4;
    }
    ```

    **Step 2 — Create the spec file `server/src/modules/leveling/lib/apply-attribute-boost.spec.ts`** with this exact content (Pitfall #8 fixture is in here):

    ```typescript
    import { applyAttributeBoost, isValidBoostSet } from './apply-attribute-boost';

    describe('applyAttributeBoost', () => {
      it('adds +2 when score is 10 (below 18)', () => {
        expect(applyAttributeBoost(10)).toBe(12);
      });

      it('adds +2 when score is 17 (still below 18)', () => {
        expect(applyAttributeBoost(17)).toBe(19);
      });

      it('adds +1 when score is exactly 18 (PF2e cap rule — Pitfall #8)', () => {
        expect(applyAttributeBoost(18)).toBe(19);
      });

      it('adds +1 when score is 20 (above cap)', () => {
        expect(applyAttributeBoost(20)).toBe(21);
      });

      it('handles edge case: very high score (24)', () => {
        expect(applyAttributeBoost(24)).toBe(25);
      });
    });

    describe('isValidBoostSet', () => {
      it('accepts exactly 4 distinct abilities', () => {
        expect(isValidBoostSet(['STR', 'DEX', 'CON', 'INT'])).toBe(true);
      });

      it('rejects duplicates (3 distinct + 1 repeat)', () => {
        expect(isValidBoostSet(['STR', 'STR', 'CON', 'INT'])).toBe(false);
      });

      it('rejects too few (3 abilities)', () => {
        expect(isValidBoostSet(['STR', 'DEX', 'CON'])).toBe(false);
      });

      it('rejects too many (5 abilities)', () => {
        expect(isValidBoostSet(['STR', 'DEX', 'CON', 'INT', 'WIS'])).toBe(false);
      });
    });
    ```

    **Step 3 — Run Jest to confirm both files are picked up by the existing inline config:**

    ```bash
    cd server && npm test -- apply-attribute-boost.spec.ts
    ```

    Expected: 9 tests pass, 0 fail. If Jest does NOT discover the file, the testRegex or rootDir is misaligned — fix the inline config in `server/package.json`. Do NOT add `server/jest.config.cjs` unless the inline config genuinely fails (per VALIDATION.md Wave 0 default: trust the inline config).

    **Step 4 — Add the `db:seed:class-progression` npm script** to `server/package.json` in the `scripts` block. Insert this entry alongside the existing `db:seed:equipment` script:

    ```json
    "db:seed:class-progression": "tsx prisma/seed-class-progression.ts",
    ```

    Place it in alphabetical order with the other `db:seed:*` scripts.

    **Step 5 — Append `.gitignore` exclusion for the Foundry pf2e dev clone:**

    Append (or add if not present) the following lines at the end of `.gitignore`:

    ```
    # Phase 1 Wave 2 — Foundry pf2e dev clone (cloned manually by dev for seed script)
    server/prisma/data/foundry-pf2e/
    ```

    **Constraints:**
    - Pure-function module: NO `@Injectable()`, NO Prisma imports, NO NestJS imports. Strict TS — no `any`. Side-effect free.
    - Test file MUST sit alongside the source file (per `testRegex: ".*\.spec\.ts$"` and `rootDir: "src"`).
    - Use named exports only (no `export default`).
  </action>
  <verify>
    <automated>cd server &amp;&amp; npm test -- apply-attribute-boost.spec.ts</automated>
  </verify>
  <acceptance_criteria>
    - File `server/src/modules/leveling/lib/apply-attribute-boost.ts` exists
    - File contains the literal string `export function applyAttributeBoost`
    - File contains the literal string `export function isValidBoostSet`
    - File contains NO occurrence of the string `@Injectable` (verified with grep)
    - File contains NO occurrence of the string `from '@nestjs/` (verified with grep)
    - File contains NO occurrence of the string `: any` outside comments
    - File `server/src/modules/leveling/lib/apply-attribute-boost.spec.ts` exists
    - Spec file contains 9 `it(` invocations
    - `cd server && npm test -- apply-attribute-boost.spec.ts` exits 0 with output containing `9 passed` (or `Tests: 9 passed`)
    - `server/package.json` `scripts` section contains the literal key `"db:seed:class-progression": "tsx prisma/seed-class-progression.ts"`
    - `.gitignore` contains the literal line `server/prisma/data/foundry-pf2e/`
  </acceptance_criteria>
  <done>
    Pure-function module exists with both exports. Spec file exists with 9 passing tests. Jest infrastructure proven on a real spec inside `server/src/modules/`. npm script registered. .gitignore extended.
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| dev-shell → migration | Developer's shell executes `prisma migrate dev` against the dev database. Trust assumed within self-hosted single-tenant model. |
| schema → live database | DDL crosses from schema source-of-truth to the live PostgreSQL instance. |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-1-W0-01 | Tampering | Migration drift between schema.prisma and live DB | mitigate | The hand-edited partial unique index is committed to git inside the migration.sql file; `prisma migrate status` is the verification gate; production deploy uses `migrate deploy` which applies the same SQL atomically. |
| T-1-W0-02 | Repudiation | Lost migration history if dev runs `db push` instead | mitigate | CLAUDE.md hard-rule documented in plan action; `npx prisma migrate dev` only — no `db push` allowed. |
| T-1-W0-03 | DoS | Cascade delete of Character wipes LevelUpHistory permanently | accept | Per RESEARCH.md §Pitfall 8: self-hosted single-tenant, no soft-delete pattern for Character today; cascade is acceptable and documented. v2 may revisit. |
| T-1-W0-04 | Information Disclosure | Foundry pf2e clone committed to repo by accident | mitigate | `.gitignore` excludes `server/prisma/data/foundry-pf2e/` (Task 3 Step 5). |
</threat_model>

<verification>
Run end-to-end checks before declaring this plan done:

```bash
# Schema valid
cd server && npx prisma validate

# Migration applied
cd server && npx prisma migrate status   # must say "Database schema is up to date"

# Partial index exists
psql $DATABASE_URL -c "\d \"LevelUpSession\"" | grep "LevelUpSession_characterId_open_unique"

# Generated client knows the new models
cd server && grep -c "freeArchetype" src/generated/prisma/index.d.ts   # must be ≥ 1
cd server && grep -c "prereqViolations" src/generated/prisma/index.d.ts # must be ≥ 1
cd server && grep -c "model LevelUpSession" src/generated/prisma/index.d.ts || true  # types should exist

# Jest works on the new spec
cd server && npm test -- apply-attribute-boost.spec.ts   # 9 tests pass

# Npm script wired
cat server/package.json | grep "db:seed:class-progression"

# .gitignore extended
cat .gitignore | grep "server/prisma/data/foundry-pf2e/"
```

If any check fails, the plan is NOT done. Re-run the failing task.
</verification>

<success_criteria>
- Schema file extended with 4 new models + 2 new Character columns + 2 reverse relations
- Migration file generated, partial unique index hand-appended, applied to dev DB
- Prisma Client regenerated and includes types for the new models
- `prisma validate` and `prisma migrate status` both green
- First-ever Jest spec passes (apply-attribute-boost: 9 tests)
- npm script `db:seed:class-progression` registered in `server/package.json`
- `.gitignore` excludes `server/prisma/data/foundry-pf2e/`
- Wave 1, 2, 3, 4 are unblocked: schema is applied, Jest works, seed script entry exists, foundry source is gitignored
</success_criteria>

<output>
After completion, create `.planning/phases/01-level-up-pf2e-regelkonform/01-01-SUMMARY.md` documenting:
- Migration timestamp + folder name (so downstream plans know the exact filename)
- Whether the partial-index SQL was applied via raw psql or via re-running migrate-dev
- Confirmation that the Prisma Client exposes `prisma.levelUpSession`, `prisma.levelUpHistory`, `prisma.classProgression`, `prisma.classFeatureOption` and that Character has `freeArchetype` + `prereqViolations`
- Test result: 9/9 passing for apply-attribute-boost.spec.ts
- Any deviations from the plan
</output>
