docs(01): final plan cleanup — narrative consistency for chain-revalidation deferral and Plan 03b sequential framing

This commit is contained in:
2026-04-27 13:56:19 +02:00
parent 5a62ad8cae
commit 096edbf950
2 changed files with 255 additions and 304 deletions

View File

@@ -2,8 +2,8 @@
phase: 01-level-up-pf2e-regelkonform
plan: 05
type: execute
wave: 4
depends_on: ["01-01", "01-02", "01-03", "01-04"]
wave: 5
depends_on: ["01-01", "01-02", "01-03", "01-03b", "01-04"]
files_modified:
- client/src/features/characters/components/level-up/wizard-state-reducer.ts
- client/src/features/characters/components/level-up/use-level-up-session.ts
@@ -36,7 +36,7 @@ must_haves:
- "Wizard chrome (header, stepper with progress label, body, footer) matches `01-UI-SPEC.md` exactly — no redesign."
- "Choice-cards show source-color badges (Klassen/Abstammung/etc. — reuse existing featSourceColors) and a yellow AlertTriangle for non-evaluable prereqs (D-03)."
- "Boost step uses +/- counters at h-11 w-11 (44×44 touch targets), shows 'wird {newScore}' live with cap-bei-18 chip when applicable."
- "Review step shows Vorher/Nachher cards (HP-Max, RK, Klassen-DC, Wahrnehmung, Saves) using server-computed preview, with Ändern links + chain re-validation contract."
- "Review step shows Vorher/Nachher cards (HP-Max, RK, Klassen-DC, Wahrnehmung, Saves) using server-computed preview, with Ändern links to revise upstream choices."
- "Bestätigen runs the commit; wizard closes; toast shows; WebSocket level_up_committed event arrives at all other open clients of the character within ~1s."
- "DRAFT-Resume banner appears at the top of the character-sheet when an open DRAFT exists, with Fortsetzen + Verwerfen actions."
- "Pathbuilder-import-violations banner appears below the avatar header when Character.prereqViolations is non-null."
@@ -63,11 +63,13 @@ must_haves:
- path: "client/src/features/characters/components/character-sheet-page.tsx (extended)"
provides: "Header button + 2 banner mounts + wizard mount"
- path: "client/src/shared/lib/api.ts (extended)"
provides: "5 new methods: startLevelUp, patchLevelUp, getLevelUpPreview, commitLevelUp, discardLevelUp"
provides: "8 new methods: startLevelUp, patchLevelUp, getLevelUpPreview, commitLevelUp, discardLevelUp, getOpenLevelUpDraft, getLevelUpFeats, getLevelUpClassFeatureOptions"
- path: "client/src/shared/types/index.ts (extended)"
provides: "LevelUpSession, LevelUpPreview, WizardChoices types + extended Character with freeArchetype, prereqViolations"
- path: "client/src/shared/hooks/use-character-socket.ts (extended)"
provides: "Adds 'level_up_committed' to CharacterUpdateType union + onLevelUpCommitted callback"
gotchas:
- "Ändern (revision) does NOT auto-clear downstream choices that depended on the revised upstream choice. Per D-12 (review-only recompute, no live per-step recompute) the wizard intentionally keeps stale downstream picks; commit-time validation in Plan 04 (LevelingService.commit + isValidBoostSet/prereq guards) is the source of truth and rejects invalid combinations with a German BadRequestException that the wizard surfaces inline. Per-dependency clearing is a v2 enhancement."
key_links:
- from: "level-up-wizard.tsx"
to: "LevelingService REST API"
@@ -333,6 +335,43 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
`/characters/${characterId}/level-up/${sessionId}`,
);
}
/** GET open DRAFT for resume-banner detection. Returns null on 404. */
async getOpenLevelUpDraft(characterId: string): Promise<LevelUpSession | null> {
try {
const response = await this.client.get(`/characters/${characterId}/level-up`);
return response.data;
} catch (err) {
if (axios.isAxiosError(err) && err.response?.status === 404) return null;
throw err;
}
}
/** GET filtered feat list for a wizard slot. Driven by Plan 04 FeatFilterService. */
async getLevelUpFeats(
characterId: string,
sessionId: string,
slot: 'class' | 'skill' | 'general' | 'ancestry' | 'archetype',
includeUnavailable: boolean = false,
): Promise<unknown[]> {
const response = await this.client.get(
`/characters/${characterId}/level-up/${sessionId}/feats`,
{ params: { slot, includeUnavailable: includeUnavailable ? 'true' : 'false' } },
);
return response.data;
}
/** GET ClassFeatureOption rows for a class-feature choice step. */
async getLevelUpClassFeatureOptions(
characterId: string,
sessionId: string,
optionsRef: string,
): Promise<unknown[]> {
const response = await this.client.get(
`/characters/${characterId}/level-up/${sessionId}/class-feature-options/${encodeURIComponent(optionsRef)}`,
);
return response.data;
}
```
Add the imports at the top of api.ts:
@@ -386,7 +425,7 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
<acceptance_criteria>
- `client/src/shared/types/index.ts` exports `LevelUpSession`, `LevelUpPreview`, `DerivedStats`, `WizardChoices`, `StepKind`, `Proficiency`, `AbilityAbbreviation`, `PrereqViolation`
- `client/src/shared/types/index.ts` Character interface has `freeArchetype?: boolean` and `prereqViolations?` fields
- `client/src/shared/lib/api.ts` contains 5 new method names: `startLevelUp`, `patchLevelUp`, `getLevelUpPreview`, `commitLevelUp`, `discardLevelUp`
- `client/src/shared/lib/api.ts` contains 8 new method names: `startLevelUp`, `patchLevelUp`, `getLevelUpPreview`, `commitLevelUp`, `discardLevelUp`, `getOpenLevelUpDraft`, `getLevelUpFeats`, `getLevelUpClassFeatureOptions`
- `client/src/shared/hooks/use-character-socket.ts` CharacterUpdateType union contains `'level_up_committed'`
- `client/src/shared/hooks/use-character-socket.ts` UseCharacterSocketOptions interface contains `onLevelUpCommitted?:`
- `cd client && npx tsc --noEmit -p tsconfig.app.json` exits 0
@@ -435,7 +474,8 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
| { type: 'GO_PREV' }
| { type: 'GO_TO_STEP'; idx: number }
| { type: 'GO_TO_STEP_FROM_REVIEW'; idx: number } // sets revisionMode
| { type: 'RETURN_TO_REVIEW' } // clears revisionMode + chain re-validation
// RETURN_TO_REVIEW: clears revisionMode flag and routes back to review step. NOTE: downstream picks are NOT auto-cleared (v1) — commit-time validation in Plan 04 surfaces invalid combos. See must_haves.gotchas.
| { type: 'RETURN_TO_REVIEW' }
| { type: 'SET_BOOST_TARGETS'; targets: WizardChoices['boostTargets'] }
| { type: 'SET_SKILL_INCREASE'; pick: WizardChoices['skillIncrease'] }
| { type: 'SET_FEAT'; slot: 'class' | 'skill' | 'general' | 'ancestry' | 'archetype'; featId: string }
@@ -462,11 +502,12 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
}
case 'RETURN_TO_REVIEW': {
const reviewIdx = state.steps.findIndex(s => s === 'review');
// CHAIN RE-VALIDATION: clear any later-step choice that depends on revised state.
// Phase 1 implementation: clear all feat slots + spellcaster picks if a boost was changed
// after the user revised (conservative). Production refinement
// can be more granular per UI-SPEC §Ändern revision contract.
// For now: clear nothing; let Plan 04's commit-time validation surface invalid combos.
// GOTCHA (intentional v1 behaviour): RETURN_TO_REVIEW does NOT auto-clear downstream
// choices that depended on an upstream revision. Per Plan-05 must_haves.gotchas and
// D-12 (no live recompute per step — review-only), commit-time validation in Plan 04
// is the source of truth: invalid combinations surface as a German BadRequestException
// when the user clicks Bestätigen, and the wizard surfaces the message inline.
// Refinement to per-dependency clearing is a v2 enhancement.
return { ...state, currentIdx: reviewIdx, revisionMode: null };
}
case 'SET_BOOST_TARGETS':
@@ -907,7 +948,7 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
**A. `level-up-step-class-features.tsx`** — Auto-summary, no input. Renders the `ClassProgression.grants` list for the new level as a read-only list. Header + sub-line per UI-SPEC. Server endpoint to fetch the list: GET ClassProgression for `(className, targetLevel)` — Plan 04 may need a small read endpoint OR the wizard can fetch from a public ClassProgression endpoint. If neither exists, the wizard receives the data via the LevelUpSession's preview endpoint.
**B. `level-up-step-class-feature-choice.tsx`** D-19 sub-step (Cleric Doctrine, Wizard School, etc.). Fetches options from `ClassFeatureOption` table where `optionsRef = ClassProgression.choiceOptionsRef`. Renders one ChoiceCard per option (single-select / radio-group semantics). Dispatches `SET_CLASS_FEATURE_CHOICE`. **Server endpoint required for fetching options** — if not in Plan 04, either add a small public GET endpoint OR include the option list in the LevelUpSession.state at start time. Planner: prefer adding `GET /class-feature-options/:optionsRef` to the leveling controller as a small Plan 04 patch, OR include in the start-session response. Document the chosen path in plan SUMMARY.
**B. `level-up-step-class-feature-choice.tsx`** -- D-19 sub-step (Cleric Doctrine, Wizard School, etc.). Calls `api.getLevelUpClassFeatureOptions(characterId, sessionId, optionsRef)` (added in Task 1 alongside the other api.* methods) which hits `GET /characters/:characterId/level-up/:sessionId/class-feature-options/:optionsRef` -- this endpoint is provided by Plan 04's LevelingController (see 01-04-PLAN.md Task 5). Renders one ChoiceCard per option (single-select / radio-group semantics). Dispatches `SET_CLASS_FEATURE_CHOICE`. No server-side edits in this plan -- the endpoint is already present.
**C. `level-up-step-boost.tsx`** — UI-SPEC lines 362-423 give the FULL JSX. Use it verbatim with these wires:
- State source: `state.choices.boostTargets` (current selection — array of 0..4 strings)
@@ -919,11 +960,11 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
**D. `level-up-step-skill-increase.tsx`** — UI-SPEC lines 426-450. Renders all skills as compact rows. For each row: current rank → next rank (or "Cap erreicht" chip if `canIncreaseSkill` would return false; mirror Plan 02 logic client-side). Dispatch `SET_SKILL_INCREASE` on click.
**E. `level-up-step-feat-class.tsx`** through **`level-up-step-feat-archetype.tsx`** (5 files) All five feat steps share the same shape:
- Fetch filtered feat list from a server endpoint. **Endpoint needed:** `GET /characters/:characterId/level-up/:sessionId/feats?slot={class|skill|general|ancestry|archetype}` returning `FeatWithEval[]` (Plan 04's FeatFilterService output). If Plan 04 didn't expose this endpoint via the LevelingController, ADD it now as a Plan 04 patch — see Task 4 below for the patch action.
**E. `level-up-step-feat-class.tsx`** through **`level-up-step-feat-archetype.tsx`** (5 files) -- All five feat steps share the same shape:
- Fetch filtered feat list via `api.getLevelUpFeats(characterId, sessionId, slot, includeUnavailable)` (added in Task 1 alongside the other api.* methods). It hits `GET /characters/:characterId/level-up/:sessionId/feats?slot=class|skill|general|ancestry|archetype&includeUnavailable=true|false` returning `FeatWithEval[]` -- this endpoint is provided by Plan 04's LevelingController (see 01-04-PLAN.md Task 5). No server-side edits in this plan.
- Render each feat as a ChoiceCard.
- For non-evaluable prereqs: show yellow AlertTriangle on the card; clicking the card opens the PrereqConfirmDialog (Task 3) on confirm, dispatch `ACKNOWLEDGE_PREREQ_WARNING` + `SET_FEAT`.
- For failed prereqs (`{ok:false}`): hidden by default; toggle "Auch nicht erfüllbare anzeigen" reveals them grayed.
- For non-evaluable prereqs: show yellow AlertTriangle on the card; clicking the card opens the PrereqConfirmDialog (Task 3) -> on confirm, dispatch `ACKNOWLEDGE_PREREQ_WARNING` + `SET_FEAT`.
- For failed prereqs (`{ok:false}`): hidden by default; toggle "Auch nicht erfuellbare anzeigen" passes `includeUnavailable=true` to the api call to reveal them grayed.
- Slot-specific: feat-archetype only renders if `state.steps.includes('feat-archetype')` (FA enabled); shows the "vor/nach Dedication" filter per UI-SPEC line 462.
**F. `level-up-step-spellcaster.tsx`** — UI-SPEC lines 466-477. Two sub-shapes:
@@ -943,21 +984,12 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
- All touch targets ≥ 44px.
- No `: any`.
**Plan 04 patch (do this BEFORE the feat steps fetch from it):** Add `GET /characters/:characterId/level-up/:sessionId/feats?slot=...` to LevelingController. Implementation:
```typescript
@Get(':sessionId/feats')
async getFeats(
@Param('characterId') characterId: string,
@Param('sessionId') sessionId: string,
@Query('slot') slot: SlotKind,
@CurrentUser('id') userId: string,
) {
return this.levelingService.getFeatsForSlot(characterId, sessionId, slot, userId);
}
```
The corresponding service method calls `featFilterService.getFilteredFeats({ slot, character: ctx, ... })`. Add it to leveling.service.ts (Plan 04 file already exists; this is a small extension).
Similarly add `GET /characters/:characterId/level-up/:sessionId/class-feature-options/:optionsRef` for the class-feature-choice step.
**Server endpoints used by these steps (already provided by Plan 04 -- no patches in this plan):**
- `GET /characters/:characterId/level-up/:sessionId/feats?slot=<kind>&includeUnavailable=<bool>` -> `FeatWithEval[]`
- `GET /characters/:characterId/level-up/:sessionId/class-feature-options/:optionsRef` -> `ClassFeatureOption[]`
- `GET /characters/:characterId/level-up` -> open DRAFT or 404 (used by character-sheet-page banner detection)
All three are declared in 01-04-PLAN.md Task 5 (LevelingController) and 01-04-PLAN.md Task 4 (LevelingService).
LevelUpSessionDto already includes `steps: StepKind[]` (Plan 04 Task 1) -- the wizard reads it directly from the start/resume response.
</action>
<verify>
<automated>cd client &amp;&amp; npx tsc --noEmit -p tsconfig.app.json 2&gt;&amp;1 | grep -E "level-up-step" || echo "tsc clean"</automated>
@@ -976,10 +1008,10 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
- All step files contain NO `: any` outside comments
- All step files contain NO emoji literals
- `cd client && npx tsc --noEmit -p tsconfig.app.json` exits 0
- `cd server && npm run build` exits 0 (the Plan 04 patch for new GET endpoints compiles)
- `cd server && npm run build` exits 0 (no server changes in this plan; the build remains green from Plan 04)
</acceptance_criteria>
<done>
All 11 step components implemented per UI-SPEC; all use German copy; all use ChoiceCard for talent picks; Review step wires up the preview query; the supporting Plan 04 patches for feats and class-feature-options GET endpoints land cleanly.
All 11 step components implemented per UI-SPEC; all use German copy; all use ChoiceCard for talent picks; Review step wires up the preview query. Server endpoints consumed are owned by Plan 04 -- no server-side edits in this plan.
</done>
</task>
@@ -1002,7 +1034,7 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
Create `level-up-wizard.tsx` — the outer container that:
1. Renders the modal chrome per UI-SPEC §Component Contract — Wizard Chrome.
2. Loads the session via `useStartLevelUpMutation` on mount (start-or-resume).
3. Computes the step list the wizard receives the StepKind list from the session (server adds it to the start-session response, OR the wizard fetches GET ClassProgression to know `choiceType` and computes locally; planner: prefer server-provided list to keep one source of truth — Plan 04 patch: `LevelUpSessionDto` includes a `steps: StepKind[]` field).
3. Reads the step list from the session response: `LevelUpSessionDto.steps: StepKind[]` is populated server-side by Plan 04 (LevelingService.startOrResume runs `computeApplicableSteps` on the character + targetLevel and stores the result in the response payload). The wizard simply uses `session.steps` directly -- no client-side computation, no extra round-trip.
4. Initializes the reducer via `initWizardState(session, steps)`.
5. Renders the active step component based on `state.steps[state.currentIdx]`.
6. PATCHes the DRAFT after each meaningful state change (debounced — every 500ms after a SET_* event).
@@ -1296,7 +1328,7 @@ export type StepKind = 'class-features' | 'class-feature-choice' | 'boost' | 'sk
}, [character]);
```
Note for executor: To detect a DRAFT for the **banner**, add `GET /characters/:characterId/level-up` to the controller (Plan 04 patch) returning the open DRAFT or null. Wire that to `useQuery` here. Update plan SUMMARY with the chosen approach.
Note for executor: DRAFT detection uses the `GET /characters/:characterId/level-up` endpoint already provided by Plan 04 (LevelingController.getOpenDraft -- 404 when none, 200 with the DRAFT row when present). Wire it via `api.getOpenLevelUpDraft(characterId)` (added in Task 1) and use `useQuery({ queryKey: ['levelUpDraft', characterId], queryFn: () => api.getOpenLevelUpDraft(characterId), retry: false })`. The banner reads `data` and `setHasOpenDraft(!!data)`. No server-side changes in this plan.
**Add 3 — Header button** (in the header cluster around lines 1607-1626 — INSERT AS FIRST in the cluster, left of Download):
@@ -1515,7 +1547,7 @@ cd client && npx tsc --noEmit -p tsconfig.app.json
# Client production build clean
cd client && npm run build
# Server still builds (in case Plan 04 patches in Task 4 were needed)
# Server still builds (no server changes in this plan -- sanity check only)
cd server && npm run build
# Full server test suite still green
@@ -1534,22 +1566,21 @@ All four commands must exit 0 before the human checkpoint.
- Choice-Card primitive matches UI-SPEC §Component Contract — Choice-Card (states, source badges, prereq warning)
- Boost step uses h-11 w-11 +/- buttons + live "wird {newScore}" + cap-bei-18 chip
- Review step uses font-mono for stat numbers, two-column Vorher/Nachher cards
- Ändern revision contract implemented (revision-mode flag + Zurück-zur-Übersicht button + chain re-validation)
- Ändern revision contract implemented (revision-mode flag + Zurück-zur-Übersicht button chain re-validation deferred to v2 per must_haves.gotchas)
- DRAFT-Resume banner implemented per UI-SPEC §Component Contract — DRAFT-Resume Banner
- Pathbuilder-Import-Violations banner implemented per UI-SPEC §Component Contract — Pathbuilder-Import-Violations Banner
- WebSocket level_up_committed callback wired to invalidate character query
- Motion respects prefers-reduced-motion
- TypeScript strict — no `: any`
- Client production build clean (`cd client && npm run build` exits 0)
- Server still builds and tests green (no regressions from Plan 04 patches)
- Server still builds and tests green (this plan adds no server changes)
- Human verification checkpoint approved
</success_criteria>
<output>
After completion, create `.planning/phases/01-level-up-pf2e-regelkonform/01-05-SUMMARY.md` documenting:
- Final file list (all 18 new client files + 4 extended)
- Whether the GET DRAFT-detect endpoint was added in Plan 04 patches or alternative chosen (e.g. lazy detect on first wizard open)
- Whether the GET feats / GET class-feature-options endpoints were added as Plan 04 patches
- Confirmation that the wizard consumes the Plan 04-owned endpoints (GET open-draft, GET feats, GET class-feature-options) without any server-side edits in this plan
- Toast library used (if any introduced) or pattern chosen (browser alert is NOT acceptable; planner discretion: react-hot-toast, sonner, or hand-rolled context — record the decision)
- Any deviations from UI-SPEC noted with rationale
- Result of the human-verification checkpoint