docs(01-foundation): create phase plan
4 plans across 3 waves: scaffold, registry+test stubs, Zod schemas and WizardState store (parallel wave 3).
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
---
|
||||
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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<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>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/phases/01-foundation/01-02-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- From src/schemas/registry.ts — BackendType already defined there -->
|
||||
<!-- src/store/types.ts will re-export or re-declare BackendType for the store layer -->
|
||||
<!-- The store's BackendType must stay in sync with the registry's BackendType -->
|
||||
|
||||
<!-- Test expectations from src/store/reducer.test.ts (created in Plan 02) -->
|
||||
<!-- wizardReducer(undefined, @@INIT) → initializes with currentStep:0, backendType:null, etc. -->
|
||||
<!-- wizardReducer(state, SET_STEP:2) → currentStep === 2 -->
|
||||
<!-- wizardReducer(state, SET_BACKEND_TYPE:'azureblob') → remote.backendType === 'azureblob' -->
|
||||
<!-- wizardReducer(state, SET_REMOTE_NAME:'my-blob') → remote.name === 'my-blob' -->
|
||||
<!-- wizardReducer(state, SET_REMOTE_PARAMS:{account:'x'}) → remote.params === {account:'x'} -->
|
||||
<!-- wizardReducer(state, SET_DEPLOYMENT:{includeInstall:true}) → deployment.includeInstall === true, configPath unchanged -->
|
||||
<!-- wizardReducer(modified, RESET) → returns INITIAL_STATE -->
|
||||
<!-- Pure function: does not mutate input state object -->
|
||||
|
||||
<!-- SECURITY: No localStorage.setItem, no sessionStorage.setItem, no IndexedDB in any store file -->
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Implement WizardState types and pure reducer</name>
|
||||
<files>src/store/types.ts, src/store/reducer.ts</files>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run src/store/reducer.test.ts 2>&1</automated>
|
||||
</verify>
|
||||
<done>All 8 tests in src/store/reducer.test.ts pass. wizardReducer is pure (immutable state transitions). INITIAL_STATE matches expected shape.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create WizardContext provider and wire into App</name>
|
||||
<files>src/store/context.tsx, src/App.tsx</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm run build 2>&1 | tail -5 && npx vitest run src/store/reducer.test.ts 2>&1 | tail -10</automated>
|
||||
</verify>
|
||||
<done>src/store/context.tsx exports WizardProvider and useWizard. App.tsx wraps content with WizardProvider. npm run build exits 0. All 8 reducer tests pass.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-04-SUMMARY.md` using the summary template.
|
||||
</output>
|
||||
Reference in New Issue
Block a user