--- 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 After completion, create `.planning/phases/01-foundation/01-04-SUMMARY.md` using the summary template.