Files
kawa 4514fc9033 docs(01-foundation): create phase plan
4 plans across 3 waves: scaffold, registry+test stubs, Zod schemas and WizardState store (parallel wave 3).
2026-03-26 10:05:01 +01:00

11 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01-foundation 04 execute 3
01-02
src/store/types.ts
src/store/reducer.ts
src/store/context.tsx
src/App.tsx
true
truths artifacts key_links
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
path provides exports
src/store/types.ts WizardState interface, WizardAction union type, INITIAL_STATE constant
WizardState
WizardAction
INITIAL_STATE
path provides exports
src/store/reducer.ts Pure wizardReducer function
wizardReducer
path provides exports
src/store/context.tsx WizardContext, WizardProvider component, useWizard hook
WizardProvider
useWizard
from to via pattern
src/store/context.tsx src/store/reducer.ts useReducer(wizardReducer, INITIAL_STATE) useReducer.*wizardReducer
from to via pattern
src/App.tsx src/store/context.tsx WizardProvider wraps entire app 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.

<execution_context> @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md </execution_context>

@.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<string, string>; // 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<string, string> }
  | { type: 'SET_DEPLOYMENT'; payload: Partial<WizardState['deployment']> }
  | { 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<WizardAction>;
}

const WizardContext = createContext<WizardContextValue | null>(null);

export function WizardProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(wizardReducer, INITIAL_STATE);
  return (
    <WizardContext.Provider value={{ state, dispatch }}>
      {children}
    </WizardContext.Provider>
  );
}

export function useWizard(): WizardContextValue {
  const ctx = useContext(WizardContext);
  if (!ctx) throw new Error('useWizard must be used inside <WizardProvider>');
  return ctx;
}
```

Update src/App.tsx to wrap with WizardProvider:
```tsx
// src/App.tsx
import { WizardProvider } from './store/context';

export default function App() {
  return (
    <WizardProvider>
      <div className="p-4">
        <h1 className="text-2xl font-bold">Ready2Blob</h1>
      </div>
    </WizardProvider>
  );
}
```

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

<success_criteria>

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