---
phase: 01-foundation
plan: 04
type: execute
wave: 3
depends_on:
- 01-02
files_modified:
- src/store/types.ts
- src/store/reducer.ts
- src/store/context.tsx
- src/App.tsx
autonomous: true
requirements: []
must_haves:
truths:
- "wizardReducer(INITIAL_STATE, {type:'SET_STEP', payload:2}) returns state with currentStep: 2"
- "wizardReducer(INITIAL_STATE, {type:'RESET'}) returns INITIAL_STATE"
- "wizardReducer is a pure function — does not mutate its input"
- "WizardProvider wraps App.tsx and exposes state + dispatch via useWizard hook"
- "All 8 tests in src/store/reducer.test.ts pass"
- "WizardState is never written to localStorage or sessionStorage"
artifacts:
- path: "src/store/types.ts"
provides: "WizardState interface, WizardAction union type, INITIAL_STATE constant"
exports: ["WizardState", "WizardAction", "INITIAL_STATE"]
- path: "src/store/reducer.ts"
provides: "Pure wizardReducer function"
exports: ["wizardReducer"]
- path: "src/store/context.tsx"
provides: "WizardContext, WizardProvider component, useWizard hook"
exports: ["WizardProvider", "useWizard"]
key_links:
- from: "src/store/context.tsx"
to: "src/store/reducer.ts"
via: "useReducer(wizardReducer, INITIAL_STATE)"
pattern: "useReducer.*wizardReducer"
- from: "src/App.tsx"
to: "src/store/context.tsx"
via: "WizardProvider wraps entire app"
pattern: "WizardProvider"
---
Implement the WizardState store: types, a pure reducer function handling all action types, a Context provider, and the useWizard hook. Wire WizardProvider into App.tsx so all future components can access state without prop drilling.
Purpose: Per-step local state causes data loss on back-navigation (a known anti-pattern). All form data must live in this centralized store. SECU-03 requires that state is never persisted to browser storage.
Output: Complete wizard state infrastructure with all 8 reducer tests passing and WizardProvider wired in App.
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/phases/01-foundation/01-02-SUMMARY.md
Task 1: Implement WizardState types and pure reducer
src/store/types.ts, src/store/reducer.ts
- INITIAL_STATE has currentStep:0, remote.name:'', remote.backendType:null, remote.params:{}, deployment.includeInstall:false, deployment.configPath:'machine-wide', deployment.scriptTargets:['intune','rmm']
- SET_STEP replaces currentStep with payload
- SET_BACKEND_TYPE replaces remote.backendType with payload
- SET_REMOTE_NAME replaces remote.name with payload
- SET_REMOTE_PARAMS replaces remote.params with payload (full replacement, not merge)
- SET_DEPLOYMENT merges payload into deployment (partial update via spread)
- RESET returns INITIAL_STATE
- Default case returns state unchanged (required for React strict mode double-invocation)
- Reducer is pure: returns new object, never mutates input
Run the failing test to confirm RED state:
```
npx vitest run src/store/reducer.test.ts
```
Expected: "Cannot find module './reducer'" error.
Create src/store/types.ts:
```typescript
// src/store/types.ts
// WizardState shape, action union type, and initial state.
// SECURITY: This state is exclusively in-memory.
// NEVER add localStorage.setItem, sessionStorage.setItem, or IndexedDB here.
// Closing the browser tab is the intended "clear credentials" operation (SECU-03).
import type { BackendType } from '../schemas/registry';
// Re-export BackendType so store consumers import from one place
export type { BackendType };
export interface WizardState {
currentStep: number;
remote: {
name: string;
backendType: BackendType | null;
params: Record; // backend-specific key/value pairs (rclone config keys)
};
deployment: {
includeInstall: boolean;
configPath: 'machine-wide' | 'user-profile'; // machine-wide = C:\ProgramData\rclone\
scriptTargets: ('intune' | 'rmm')[];
};
}
export type WizardAction =
| { type: 'SET_STEP'; payload: number }
| { type: 'SET_BACKEND_TYPE'; payload: BackendType }
| { type: 'SET_REMOTE_NAME'; payload: string }
| { type: 'SET_REMOTE_PARAMS'; payload: Record }
| { type: 'SET_DEPLOYMENT'; payload: Partial }
| { type: 'RESET' };
export const INITIAL_STATE: WizardState = {
currentStep: 0,
remote: {
name: '',
backendType: null,
params: {},
},
deployment: {
includeInstall: false,
configPath: 'machine-wide',
scriptTargets: ['intune', 'rmm'],
},
};
```
Create src/store/reducer.ts:
```typescript
// src/store/reducer.ts
// Pure reducer — no side effects, no localStorage, no async operations.
import { WizardState, WizardAction, INITIAL_STATE } from './types';
export function wizardReducer(
state: WizardState = INITIAL_STATE,
action: WizardAction
): WizardState {
switch (action.type) {
case 'SET_STEP':
return { ...state, currentStep: action.payload };
case 'SET_BACKEND_TYPE':
return {
...state,
remote: { ...state.remote, backendType: action.payload },
};
case 'SET_REMOTE_NAME':
return {
...state,
remote: { ...state.remote, name: action.payload },
};
case 'SET_REMOTE_PARAMS':
return {
...state,
remote: { ...state.remote, params: action.payload },
};
case 'SET_DEPLOYMENT':
return {
...state,
deployment: { ...state.deployment, ...action.payload },
};
case 'RESET':
return INITIAL_STATE;
default:
return state;
}
}
```
Run tests:
```
npx vitest run src/store/reducer.test.ts
```
All 8 tests must pass (GREEN). Fix any failures before proceeding.
npx vitest run src/store/reducer.test.ts 2>&1
All 8 tests in src/store/reducer.test.ts pass. wizardReducer is pure (immutable state transitions). INITIAL_STATE matches expected shape.
Task 2: Create WizardContext provider and wire into App
src/store/context.tsx, src/App.tsx
Create src/store/context.tsx:
```typescript
// src/store/context.tsx
// WizardContext, WizardProvider, and useWizard hook.
// useWizard throws if used outside WizardProvider — prevents silent "undefined state" bugs.
import React, { createContext, useContext, useReducer } from 'react';
import { WizardState, WizardAction, INITIAL_STATE } from './types';
import { wizardReducer } from './reducer';
interface WizardContextValue {
state: WizardState;
dispatch: React.Dispatch;
}
const WizardContext = createContext(null);
export function WizardProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(wizardReducer, INITIAL_STATE);
return (
{children}
);
}
export function useWizard(): WizardContextValue {
const ctx = useContext(WizardContext);
if (!ctx) throw new Error('useWizard must be used inside ');
return ctx;
}
```
Update src/App.tsx to wrap with WizardProvider:
```tsx
// src/App.tsx
import { WizardProvider } from './store/context';
export default function App() {
return (
Ready2Blob
);
}
```
Verify the build still passes (context.tsx is a React file — TypeScript must accept it):
```
npm run build
```
Run the full test suite:
```
npx vitest run
```
Expected: registry.test.ts (7), schemas/index.test.ts (wait — Plan 03 must complete first), reducer.test.ts (8) pass. If running before Plan 03, index.test.ts may still fail — that is acceptable; this plan's scope is the store only.
npm run build 2>&1 | tail -5 && npx vitest run src/store/reducer.test.ts 2>&1 | tail -10
src/store/context.tsx exports WizardProvider and useWizard. App.tsx wraps content with WizardProvider. npm run build exits 0. All 8 reducer tests pass.
1. `npx vitest run src/store/reducer.test.ts` — 8 tests pass
2. `npm run build` — exits 0, no TypeScript errors
3. Confirm no localStorage/sessionStorage references in any store file:
`grep -r "localStorage\|sessionStorage\|IndexedDB" src/store/` — must return empty
4. Manual: `npm run dev` → browser opens → no console errors
- src/store/types.ts exports WizardState, WizardAction, INITIAL_STATE, BackendType
- src/store/reducer.ts exports wizardReducer as a pure function (8 tests green)
- src/store/context.tsx exports WizardProvider and useWizard hook
- App.tsx wraps content in WizardProvider
- No localStorage, sessionStorage, or IndexedDB access in any store file
- npm run build exits 0