Files
Dimension-47/.planning/phases/01-level-up-pf2e-regelkonform/01-02-SUMMARY.md
Alexander Zielonka df29617c3e docs(01-02): complete pure-function library plan
Five pure-function modules (types + 4 logic) implemented strict-TDD.
46 passing Jest tests across 4 spec files. Pitfall #8 (boost-cap-at-18)
and Pitfall #9 (no hpCurrent in output) both enforced by spec.

3 documented deviations:
- Rule 3: created apply-attribute-boost.ts dependency (Plan 01-01 work
  not yet merged into worktree base; content matches 01-01 spec exactly)
- Rule 1: corrected L5 expected step list (PF2e CRB has class/skill
  feats only at even levels; plan said feat-class+feat-skill at L5)
- Rule 1: TS-strict type-guard helpers for EvalResult narrowing

Requirements completed: LVL-02, LVL-06, LVL-09, LVL-10, LVL-01, LVL-13, LVL-14
2026-04-27 14:21:00 +02:00

218 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 25, 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