docs(01): final plan cleanup — narrative consistency for chain-revalidation deferral and Plan 03b sequential framing
This commit is contained in:
@@ -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 && npx tsc --noEmit -p tsconfig.app.json 2>&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
|
||||
|
||||
Reference in New Issue
Block a user