---
phase: 01-level-up-pf2e-regelkonform
plan: 02
subsystem: testing
tags: [pure-functions, jest, tdd, level-up, prereq-evaluator, recompute, pf2e-rules]

# Dependency graph
requires:
  - phase: 01-level-up-pf2e-regelkonform
    provides: applyAttributeBoost + AbilityAbbreviation (Plan 01-01) — boost-cap-at-18 helper
provides:
  - Five pure-function modules (types.ts + 4 logic modules) covering all PF2e level-up math
  - skill-increase-cap (LVL-06) — TRAINED→EXPERT@L3, EXPERT→MASTER@L7, MASTER→LEGENDARY@L15
  - prereq-evaluator (D-01..D-04) — DSL parser + evaluator + German formatter, UNKNOWN-aggressive
  - recompute-derived-stats (Pitfall #8/#9 safe) — pure pipeline, never outputs current/temp HP fields
  - compute-applicable-steps (D-10 ordering) — wizard step list per (level, FA, caster, choiceType)
  - 46 passing Jest tests across 4 specs (≥17 prereq, 13 steps, 9 skill, 5 recompute)
  - Test discipline pattern: strict RED→GREEN per module, separate commits
affects: [Plan 01-04 LevelingService integration, Plan 01-05 wizard UI preview, Plan 01-03 progression seed]

# Tech tracking
tech-stack:
  added: []  # No new dependencies — ts-jest + jest already present
  patterns:
    - "Pure-function lib (no NestJS, no Prisma, no I/O) under server/src/modules/<feature>/lib/"
    - "Discriminated-union return values with explicit type-guard helpers for TS-strict narrowing"
    - "Sibling .spec.ts test files using Jest's testRegex auto-discovery"
    - "TDD: spec written first (RED commit) → minimal implementation (GREEN commit)"

key-files:
  created:
    - server/src/modules/leveling/lib/types.ts
    - server/src/modules/leveling/lib/skill-increase-cap.ts
    - server/src/modules/leveling/lib/skill-increase-cap.spec.ts
    - server/src/modules/leveling/lib/prereq-evaluator.ts
    - server/src/modules/leveling/lib/prereq-evaluator.spec.ts
    - server/src/modules/leveling/lib/recompute-derived-stats.ts
    - server/src/modules/leveling/lib/recompute-derived-stats.spec.ts
    - server/src/modules/leveling/lib/compute-applicable-steps.ts
    - server/src/modules/leveling/lib/compute-applicable-steps.spec.ts
    - server/src/modules/leveling/lib/apply-attribute-boost.ts  # Rule 3 dependency from Plan 01-01
  modified: []

key-decisions:
  - "Type-guard helpers (isUnknown, isOk, isFail) preferred over discriminant property access for TS-strict narrowing"
  - "OR-list parser uses Oxford-comma normalization (', or ' → ' or ') before tokenizing to avoid leftover 'or' prefix"
  - "Feat-name heuristic restricts to ≤4 words and rejects free-text function-words (you/a/the/of/and) to prevent sentence-as-feat misclassification"
  - "L5 step list deviates from plan: PF2e CRB has class/skill feats only at EVEN levels; plan's L5 list was rules-incorrect"
  - "Boost-cap-at-18 enforced via applyAttributeBoost() import — math is not duplicated in recompute"
  - "DerivedStats output object NEVER includes hpCurrent/hpTemp fields — Pitfall #9 enforced by spec assertion AND by absence of those literal strings in production code"

patterns-established:
  - "RED commit (failing spec) → GREEN commit (minimal impl) gate sequence per module"
  - "UNKNOWN-aggressive prereq evaluation: any non-classifiable atom poisons the whole clause (per D-03)"
  - "ClassProgression-driven proficiency overrides with character pre-existing rank as fallback"
  - "German user-facing reason strings: 'Du benötigst...', 'Dir fehlt das Talent...', 'Voraussetzung nicht erfüllt: ...'"

requirements-completed: [LVL-02, LVL-06, LVL-09, LVL-10, LVL-01, LVL-13, LVL-14]

# Metrics
duration: ~30min
completed: 2026-04-27
---

# Phase 1 Plan 02: Level-Up Pure-Function Library Summary

**Five pure-function modules (skill-cap, prereq-DSL, recompute, step-ordering, shared types) implemented strict-TDD with 46 passing Jest tests, establishing the test discipline for the Level-Up subsystem and isolating bug-prone PF2e math (Pitfall #8/#9) behind a fully-tested boundary.**

## Tasks

| # | Task | Status | Commits |
|---|------|--------|---------|
| 0 | Add apply-attribute-boost dependency (Rule 3 — Plan 01-01 work not merged into worktree base) | Done | 7e40449 |
| 1 | types.ts — shared type vocabulary | Done | 4d2cb5e |
| 2 | skill-increase-cap (RED→GREEN) | Done | 3a4267d, f189750 |
| 3 | prereq-evaluator (RED→GREEN) | Done | 66d9d5c, da82d9b |
| 4 | recompute-derived-stats (RED→GREEN) | Done | 8dd55b6, 6011024 |
| 5 | compute-applicable-steps (RED→GREEN) | Done | 70ec7bb, de07fc8, d9ed18c |
| 6 | Full leveling test suite gate | Done (46 passing) | (verification only) |
| — | TS-strict narrowing fix for discriminated unions | Done (Rule 1) | be6eaee |

## Test Counts

| Spec File | Tests |
|-----------|-------|
| skill-increase-cap.spec.ts | 9 |
| prereq-evaluator.spec.ts | 19 |
| recompute-derived-stats.spec.ts | 5 |
| compute-applicable-steps.spec.ts | 13 |
| **Total** | **46** |

The plan target of "≥50 tests" assumed Plan 01-01's 9-test apply-attribute-boost.spec.ts would be present (5 specs, ≥50 tests). In this worktree, Plan 01-01 work is not yet merged into the base — only its production module was created here as a Rule 3 dependency. After orchestrator merge, the combined count will reach 55. This plan delivers all of its own 46 tests, which fully cover VALIDATION.md rows 1-W1-06 through 1-W1-29.

Run command:
```
cd server && npm test -- --testPathPatterns=leveling
```

Output:
```
PASS src/modules/leveling/lib/recompute-derived-stats.spec.ts
PASS src/modules/leveling/lib/skill-increase-cap.spec.ts
PASS src/modules/leveling/lib/compute-applicable-steps.spec.ts
PASS src/modules/leveling/lib/prereq-evaluator.spec.ts

Test Suites: 4 passed, 4 total
Tests:       46 passed, 46 total
```

`cd server && npx tsc --noEmit -p tsconfig.json` — exits 0 for the leveling lib (errors elsewhere are pre-existing Prisma-client codegen issues out of scope per Scope Boundary rule).

## Verification Checklist

- [x] All 5 production modules + types.ts created in `server/src/modules/leveling/lib/`
- [x] All 4 spec files created (5th — apply-attribute-boost.spec.ts — owned by Plan 01-01)
- [x] TDD discipline: each module's spec was written first and observed failing (RED commit) before implementation (GREEN commit)
- [x] Pitfall #8 (boost-cap-at-18) enforced — recompute test for CON 18 → 19 (not 20)
- [x] Pitfall #9 (no hpCurrent in output) enforced — `expect(result).not.toHaveProperty('hpCurrent')` + literal string absent from production code
- [x] D-01 evaluable patterns covered: skill rank, OR-list, AND-list, feat name, heritage, class
- [x] D-02 non-evaluable patterns return `{unknown:true}`: spellcasting, deity, age, vision, free-text
- [x] Step ordering matches D-10 (with PF2e correction at L5 — see Deviations)
- [x] Zero `: any` types in production code (only English-prose use of word "any" in a JSDoc comment)
- [x] Zero `@nestjs/` imports in any lib file
- [x] Zero Prisma client imports in any lib file
- [x] German failure reasons in prereq-evaluator (D-15)

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 3 — Blocking] Created apply-attribute-boost.ts in this worktree**
- **Found during:** Task 1 (types.ts imports `AbilityAbbreviation` from `./apply-attribute-boost`)
- **Issue:** Plan 01-02 depends on Plan 01-01's `apply-attribute-boost.ts` module, but Plan 01-01's work is on a separate parallel-execution worktree branch and not yet merged into this worktree's base (`096edbf`). Without it, types.ts and recompute-derived-stats.ts cannot compile.
- **Fix:** Created `server/src/modules/leveling/lib/apply-attribute-boost.ts` with content that exactly matches the Plan 01-01 specification (verbatim from 01-01-PLAN.md lines 351-380). When orchestrator merges Plan 01-01's worktree branch, content will be byte-identical and merge will be conflict-free.
- **Files modified:** `server/src/modules/leveling/lib/apply-attribute-boost.ts`
- **Commit:** 7e40449

**2. [Rule 1 — Bug] Fixed L5 expected step list (PF2e rules correction)**
- **Found during:** Task 5 GREEN phase (one of 13 tests failed against the implementation)
- **Issue:** The plan's expected step list for `computeApplicableSteps(5, 'Fighter', ...)` included `feat-class` and `feat-skill`. PF2e CRB places class feats and skill feats at EVEN levels only (2, 4, 6, ..., 20). L5 is odd → no class/skill feat slot. The plan's other tests at L4 (even, has feat-class+feat-skill) and L3 (odd, NOT has feat-class) are internally consistent with the even-level rule; only L5 was misstated.
- **Fix:** Changed expected list at L5 to `['class-features', 'boost', 'skill-increase', 'feat-ancestry', 'review']` and added a comment block explaining the deviation. The implementation rule "feat-class/feat-skill on even levels" is unchanged because the project's "regelkonform" goal (CLAUDE.md / PROJECT.md) requires PF2e correctness above plan literalism.
- **Files modified:** `server/src/modules/leveling/lib/compute-applicable-steps.spec.ts`
- **Commit:** de07fc8

**3. [Rule 1 — Bug] Type-guard helpers for TS-strict EvalResult narrowing**
- **Found during:** Task 6 (`tsc --noEmit` after all RED/GREEN commits)
- **Issue:** TypeScript strict mode could not narrow `EvalResult = {ok:true} | {ok:false; reason} | {unknown:true; raw}` via `r.ok` access — the unknown variant has no `ok` property. Tests passed at runtime but `tsc` reported `error TS2339: Property 'ok' does not exist on type 'EvalResult'` in 8 lines of the spec and 4 lines of the production walker.
- **Fix:** Added explicit type-guard helpers (`isUnknown`, `isOk`, `isFail` in production; `isFail` in spec) that use `'ok' in r` membership checks. Replaced direct `r.ok` accesses with guard calls. Runtime behavior unchanged.
- **Files modified:** `server/src/modules/leveling/lib/prereq-evaluator.ts`, `server/src/modules/leveling/lib/prereq-evaluator.spec.ts`
- **Commit:** be6eaee

### Authentication Gates

None — this plan introduces no I/O.

### Architectural Changes

None — pure-function modules only.

## Notes on Prereq-Evaluator Edge Cases (for future grammar tuning)

While implementing the parser, two interesting edge cases came up that future PF2e prereq corpora may stress:

1. **Oxford-comma "X, Y, or Z" splitting.** A naive `,\s*|\s+or\s+` regex split leaves a stray leading "or " on the last segment because the `,\s*` alternative consumes the comma but stops before the "or" keyword. Solution: pre-normalize `,\s*or\s+` → ` or ` before splitting.

2. **Free-text-as-feat misclassification.** Without guards, a sentence like "You worship a god of fire and destruction" matches the feat fallback regex `^[A-Z][A-Za-z' \-]*$` (it's all letters/spaces and starts with uppercase). Added two heuristics: ≤4 words AND no presence of common function-words (you, a, the, of, and, to, with, from, by). Future PF2e feats with names like "Heir to the Vows of Asmodeus" would fail this guard — but no such canonical feat exists today, and the planner's UNKNOWN-aggressive intent (D-03) prefers false-unknown over false-evaluable.

## TDD Gate Compliance

For each of Tasks 2–5, a `test(...)` commit (RED gate) precedes the corresponding `feat(...)` commit (GREEN gate). Verified in git log:

| Module | RED commit | GREEN commit |
|--------|-----------|---------------|
| skill-increase-cap | 3a4267d | f189750 |
| prereq-evaluator | 66d9d5c | da82d9b |
| recompute-derived-stats | 8dd55b6 | 6011024 |
| compute-applicable-steps | 70ec7bb | d9ed18c |

The compute-applicable-steps module additionally has commit de07fc8 (test fix between RED and GREEN, separated as a deviation rather than amended into RED to preserve the audit trail of the plan-vs-PF2e-rules conflict).

## Confirmation: types.ts Imported by All Downstream Modules

```bash
grep -l "from './types'" server/src/modules/leveling/lib/*.ts
```
Result:
- `compute-applicable-steps.ts` (StepKind)
- `prereq-evaluator.ts` (CharacterContext, EvalResult, Proficiency)
- `recompute-derived-stats.ts` (CharacterContext, ClassProgressionRow, DerivedStats, Proficiency, WizardChoices, PROFICIENCY_BASE_BONUS, AbilityAbbreviation)
- `skill-increase-cap.ts` (Proficiency)

All four logic modules import from `./types` — no duplicate type definitions.

## Self-Check: PASSED

- `server/src/modules/leveling/lib/types.ts` — FOUND
- `server/src/modules/leveling/lib/skill-increase-cap.ts` — FOUND
- `server/src/modules/leveling/lib/skill-increase-cap.spec.ts` — FOUND
- `server/src/modules/leveling/lib/prereq-evaluator.ts` — FOUND
- `server/src/modules/leveling/lib/prereq-evaluator.spec.ts` — FOUND
- `server/src/modules/leveling/lib/recompute-derived-stats.ts` — FOUND
- `server/src/modules/leveling/lib/recompute-derived-stats.spec.ts` — FOUND
- `server/src/modules/leveling/lib/compute-applicable-steps.ts` — FOUND
- `server/src/modules/leveling/lib/compute-applicable-steps.spec.ts` — FOUND
- `server/src/modules/leveling/lib/apply-attribute-boost.ts` — FOUND (Rule 3 dependency)
- Commit 7e40449 — FOUND
- Commit 4d2cb5e — FOUND
- Commit 3a4267d — FOUND
- Commit f189750 — FOUND
- Commit 66d9d5c — FOUND
- Commit da82d9b — FOUND
- Commit 8dd55b6 — FOUND
- Commit 6011024 — FOUND
- Commit 70ec7bb — FOUND
- Commit de07fc8 — FOUND
- Commit d9ed18c — FOUND
- Commit be6eaee — FOUND
