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,196 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- package.json
|
||||
- vite.config.ts
|
||||
- tsconfig.json
|
||||
- tsconfig.app.json
|
||||
- tsconfig.node.json
|
||||
- src/index.css
|
||||
- src/main.tsx
|
||||
- src/App.tsx
|
||||
- vitest.config.ts
|
||||
- index.html
|
||||
autonomous: true
|
||||
requirements: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "npm run dev starts the Vite dev server with no console errors"
|
||||
- "npm run build succeeds with no TypeScript errors"
|
||||
- "npx vitest run exits 0 (even with zero test files)"
|
||||
- "Tailwind utility classes applied in JSX produce visible styles"
|
||||
artifacts:
|
||||
- path: "vite.config.ts"
|
||||
provides: "Vite build config with React plugin and Tailwind v4 plugin"
|
||||
contains: "@tailwindcss/vite"
|
||||
- path: "src/index.css"
|
||||
provides: "Tailwind v4 CSS entry point"
|
||||
contains: "@import \"tailwindcss\""
|
||||
- path: "vitest.config.ts"
|
||||
provides: "Vitest configuration for pure-function unit tests"
|
||||
contains: "environment: 'node'"
|
||||
- path: "package.json"
|
||||
provides: "All Phase 1 dependencies declared"
|
||||
contains: "zod"
|
||||
key_links:
|
||||
- from: "vite.config.ts"
|
||||
to: "src/index.css"
|
||||
via: "tailwindcss() plugin processes @import directive"
|
||||
pattern: "tailwindcss\\(\\)"
|
||||
- from: "src/main.tsx"
|
||||
to: "src/index.css"
|
||||
via: "import './index.css'"
|
||||
pattern: "import.*index\\.css"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Bootstrap the project: scaffold a Vite 6 + React 18 + TypeScript 5 SPA, install all Phase 1 dependencies, configure Tailwind v4 via the Vite plugin, and install Vitest so automated tests can run.
|
||||
|
||||
Purpose: Every downstream plan needs a working project with correct tooling. Getting the scaffold wrong (e.g., Tailwind v3 path, old shadcn CLI) causes painful migration later.
|
||||
Output: A runnable dev server, a passing build, and Vitest ready to execute unit tests.
|
||||
</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/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
|
||||
<!-- Key pitfalls from research (mandatory reading before executing): -->
|
||||
<!-- 1. Tailwind v4 uses @tailwindcss/vite plugin — NOT postcss + tailwind.config.js -->
|
||||
<!-- 2. shadcn CLI is `npx shadcn@latest` — NOT `npx shadcn-ui@latest` -->
|
||||
<!-- 3. Vite 6 requires Node.js 20.19+ or 22.12+ — verify with `node --version` first -->
|
||||
<!-- 4. Zod v4 requires @hookform/resolvers v5 — do NOT install resolvers v3/v4 -->
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Scaffold and install all Phase 1 dependencies</name>
|
||||
<files>package.json, vite.config.ts, src/index.css, src/main.tsx, src/App.tsx, index.html, tsconfig.json, tsconfig.app.json, tsconfig.node.json</files>
|
||||
<action>
|
||||
First verify Node.js version meets Vite 6 requirements:
|
||||
```
|
||||
node --version
|
||||
```
|
||||
Must be 20.19+ or 22.12+. If below, stop and report — do not proceed.
|
||||
|
||||
Scaffold the project in the current directory (not a subdirectory — the repo root IS the project):
|
||||
```
|
||||
npm create vite@latest . -- --template react-ts
|
||||
```
|
||||
Accept overwrite prompts if any files exist.
|
||||
|
||||
Install all Phase 1 production and dev dependencies in one command:
|
||||
```
|
||||
npm install
|
||||
npm install zod react-hook-form @hookform/resolvers
|
||||
npm install -D tailwindcss @tailwindcss/vite
|
||||
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom
|
||||
```
|
||||
|
||||
Update vite.config.ts to add the Tailwind v4 plugin (use @tailwindcss/vite, NOT postcss):
|
||||
```typescript
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
});
|
||||
```
|
||||
|
||||
Replace the contents of src/index.css with the single Tailwind v4 import directive:
|
||||
```css
|
||||
@import "tailwindcss";
|
||||
```
|
||||
Do NOT add tailwind.config.js, do NOT add postcss.config.js, do NOT add content globs.
|
||||
|
||||
Clean up the Vite default boilerplate in src/App.tsx — replace with a minimal placeholder that uses one Tailwind class to confirm styles work:
|
||||
```tsx
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h1 className="text-2xl font-bold">Ready2Blob</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Verify the dev server starts without console errors:
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
Then Ctrl+C. Verify the build succeeds:
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npm run build 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>npm run build exits 0 with no TypeScript errors. npm run dev starts without console errors (manual check: open browser, check devtools console).</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Configure Vitest for pure-function unit tests</name>
|
||||
<files>vitest.config.ts</files>
|
||||
<action>
|
||||
Create vitest.config.ts at the project root with the node environment (pure functions do not need a DOM):
|
||||
```typescript
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
globals: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Add a test script to package.json if not already present:
|
||||
```json
|
||||
"test": "vitest run"
|
||||
```
|
||||
|
||||
Run the test suite to confirm Vitest executes (zero tests is acceptable at this stage):
|
||||
```
|
||||
npx vitest run
|
||||
```
|
||||
Expected output: "No test files found" or similar — this is correct. Exit code must be 0.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run 2>&1; echo "exit: $?"</automated>
|
||||
</verify>
|
||||
<done>npx vitest run exits 0. vitest.config.ts exists with environment: 'node' and globals: true.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
Run in sequence after both tasks complete:
|
||||
1. `npm run build` — exits 0, no TypeScript errors
|
||||
2. `npx vitest run` — exits 0
|
||||
3. Manual: `npm run dev`, open browser, confirm no console errors and "Ready2Blob" heading renders with bold styling
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Vite 6 + React 18 + TypeScript 5 project builds cleanly
|
||||
- Tailwind v4 configured via @tailwindcss/vite plugin (no postcss.config.js, no tailwind.config.js)
|
||||
- All Phase 1 dependencies installed: zod, react-hook-form, @hookform/resolvers, vitest, @testing-library/react
|
||||
- Vitest configured and executable (exits 0 with no test files)
|
||||
- src/index.css contains only `@import "tailwindcss"` for Tailwind v4
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md` using the summary template.
|
||||
</output>
|
||||
@@ -0,0 +1,439 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 01-01
|
||||
files_modified:
|
||||
- src/schemas/registry.ts
|
||||
- src/schemas/registry.test.ts
|
||||
- src/schemas/index.test.ts
|
||||
- src/store/reducer.test.ts
|
||||
autonomous: true
|
||||
requirements: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "BACKEND_REGISTRY exports entries for 'azureblob', 's3', and 's3-compatible'"
|
||||
- "Each registry entry is an array of FieldDef objects with the correct rclone config keys"
|
||||
- "Test stubs exist for all three success criteria (SC-2, SC-3, SC-4) and fail with informative messages"
|
||||
- "npx vitest run fails on registry.test.ts (RED — implementation not yet written for index.ts and reducer)"
|
||||
artifacts:
|
||||
- path: "src/schemas/registry.ts"
|
||||
provides: "BackendType union, FieldDef interface, BACKEND_REGISTRY constant"
|
||||
exports: ["BackendType", "FieldDef", "BACKEND_REGISTRY"]
|
||||
- path: "src/schemas/registry.test.ts"
|
||||
provides: "Test stubs for SC-2 (registry structure verification)"
|
||||
contains: "BACKEND_REGISTRY"
|
||||
- path: "src/schemas/index.test.ts"
|
||||
provides: "Test stubs for SC-3 (Zod schema safeParse verification)"
|
||||
contains: "BACKEND_SCHEMAS"
|
||||
- path: "src/store/reducer.test.ts"
|
||||
provides: "Test stubs for SC-4 (WizardState reducer action verification)"
|
||||
contains: "wizardReducer"
|
||||
key_links:
|
||||
- from: "src/schemas/registry.ts"
|
||||
to: "rclone config keys"
|
||||
via: "FieldDef.key values must match rclone's INI key names exactly"
|
||||
pattern: "key:.*'account'|key:.*'access_key_id'|key:.*'secret_access_key'"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the Backend Schema Registry — the single source of truth for all backend field definitions — and write the Wave 0 test stubs that will drive implementation in Plans 03 and 04.
|
||||
|
||||
Purpose: The registry is architecturally critical: Phase 2 generators use its keys to build rclone.conf, Phase 3 uses it to render dynamic forms. Wrong keys here cause silently broken configs. Writing test stubs first establishes the acceptance criteria before any implementation.
|
||||
Output: src/schemas/registry.ts with typed field definitions for three backends, plus three test stub files (failing, per TDD RED phase).
|
||||
</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/ROADMAP.md
|
||||
@.planning/phases/01-foundation/01-01-SUMMARY.md
|
||||
|
||||
<!-- CRITICAL: rclone config key names (from research — MEDIUM confidence, verify vs rclone.org before Phase 2) -->
|
||||
<!-- Azure Blob: type=azureblob, account (storage account name), key (access key), sas_url (SAS URL) -->
|
||||
<!-- S3: type=s3, provider=AWS, access_key_id, secret_access_key, region -->
|
||||
<!-- S3-compatible: type=s3, provider=Other, access_key_id, secret_access_key, endpoint (required), region (optional) -->
|
||||
<!-- Note: s3 and s3-compatible both use type=s3 in rclone.conf; the provider field differentiates them -->
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Create the Backend Schema Registry</name>
|
||||
<files>src/schemas/registry.ts</files>
|
||||
<behavior>
|
||||
- BackendType is a union: 'azureblob' | 's3' | 's3-compatible'
|
||||
- FieldDef has: key (string), label (string), inputType ('text'|'password'|'select'|'toggle'), required (boolean), and optional placeholder, helpText, options
|
||||
- BACKEND_REGISTRY['azureblob'] has at least 3 fields: account (required), key (optional), sas_url (optional)
|
||||
- BACKEND_REGISTRY['s3'] has at least 4 fields: provider (required, select, value='AWS'), access_key_id (required), secret_access_key (required, password), region (required)
|
||||
- BACKEND_REGISTRY['s3-compatible'] has at least 5 fields: provider (required, select, value='Other'), access_key_id (required), secret_access_key (required, password), endpoint (required), region (optional)
|
||||
- All FieldDef.key values use snake_case matching rclone's actual config key names (not camelCase)
|
||||
</behavior>
|
||||
<action>
|
||||
Create src/schemas/registry.ts:
|
||||
|
||||
```typescript
|
||||
// src/schemas/registry.ts
|
||||
// Backend Schema Registry — single source of truth for all rclone backend field definitions.
|
||||
// IMPORTANT: FieldDef.key values MUST match rclone config key names exactly.
|
||||
// These keys are used by Phase 2 generators to build rclone.conf INI content.
|
||||
// Verify against https://rclone.org/azureblob/ and https://rclone.org/s3/ before Phase 2.
|
||||
|
||||
export type BackendType = 'azureblob' | 's3' | 's3-compatible';
|
||||
|
||||
export interface FieldDef {
|
||||
key: string; // MUST match rclone config key exactly (snake_case)
|
||||
label: string;
|
||||
inputType: 'text' | 'password' | 'select' | 'toggle';
|
||||
required: boolean;
|
||||
placeholder?: string;
|
||||
helpText?: string;
|
||||
options?: { value: string; label: string }[]; // for inputType: 'select'
|
||||
}
|
||||
|
||||
export const BACKEND_REGISTRY: Record<BackendType, FieldDef[]> = {
|
||||
azureblob: [
|
||||
{
|
||||
key: 'account',
|
||||
label: 'Storage Account Name',
|
||||
inputType: 'text',
|
||||
required: true,
|
||||
placeholder: 'mystorageaccount',
|
||||
helpText: 'The storage account name (not the full URL)',
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
label: 'Access Key',
|
||||
inputType: 'password',
|
||||
required: false,
|
||||
helpText: 'Base64-encoded storage account key. Provide either this or a SAS URL, not both.',
|
||||
},
|
||||
{
|
||||
key: 'sas_url',
|
||||
label: 'SAS URL',
|
||||
inputType: 'password',
|
||||
required: false,
|
||||
placeholder: 'https://mystorageaccount.blob.core.windows.net/?sv=...',
|
||||
helpText: 'Full SAS URL including account and container. Provide either this or an access key, not both.',
|
||||
},
|
||||
],
|
||||
s3: [
|
||||
{
|
||||
key: 'provider',
|
||||
label: 'Provider',
|
||||
inputType: 'select',
|
||||
required: true,
|
||||
options: [{ value: 'AWS', label: 'Amazon S3' }],
|
||||
},
|
||||
{
|
||||
key: 'access_key_id',
|
||||
label: 'Access Key ID',
|
||||
inputType: 'text',
|
||||
required: true,
|
||||
placeholder: 'AKIAIOSFODNN7EXAMPLE',
|
||||
},
|
||||
{
|
||||
key: 'secret_access_key',
|
||||
label: 'Secret Access Key',
|
||||
inputType: 'password',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'region',
|
||||
label: 'Region',
|
||||
inputType: 'text',
|
||||
required: true,
|
||||
placeholder: 'us-east-1',
|
||||
},
|
||||
],
|
||||
's3-compatible': [
|
||||
{
|
||||
key: 'provider',
|
||||
label: 'Provider',
|
||||
inputType: 'select',
|
||||
required: true,
|
||||
options: [{ value: 'Other', label: 'S3-Compatible' }],
|
||||
helpText: 'Covers Wasabi, MinIO, Cloudflare R2, and any S3-compatible storage',
|
||||
},
|
||||
{
|
||||
key: 'access_key_id',
|
||||
label: 'Access Key ID',
|
||||
inputType: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'secret_access_key',
|
||||
label: 'Secret Access Key',
|
||||
inputType: 'password',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'endpoint',
|
||||
label: 'Endpoint URL',
|
||||
inputType: 'text',
|
||||
required: true,
|
||||
placeholder: 'https://s3.wasabisys.com',
|
||||
helpText: 'The S3-compatible endpoint URL for your storage provider',
|
||||
},
|
||||
{
|
||||
key: 'region',
|
||||
label: 'Region',
|
||||
inputType: 'text',
|
||||
required: false,
|
||||
placeholder: 'us-east-1',
|
||||
helpText: 'Optional for most S3-compatible providers',
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run src/schemas/registry.test.ts 2>&1; echo "exit: $?"</automated>
|
||||
</verify>
|
||||
<done>registry.ts exports BackendType, FieldDef, and BACKEND_REGISTRY. All three backend entries have the correct rclone-compatible key names. registry.test.ts passes.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Write Wave 0 test stubs for SC-2, SC-3, SC-4</name>
|
||||
<files>src/schemas/registry.test.ts, src/schemas/index.test.ts, src/store/reducer.test.ts</files>
|
||||
<action>
|
||||
Create three test stub files. These are the Wave 0 test scaffolds — they define acceptance criteria now so Plans 03 and 04 implement against them (TDD RED phase). Tests for registry.test.ts should PASS; tests for index.test.ts and reducer.test.ts will FAIL (the implementation files don't exist yet).
|
||||
|
||||
**src/schemas/registry.test.ts** (SC-2 — verifies registry structure; should PASS after Task 1):
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { BACKEND_REGISTRY, BackendType } from './registry';
|
||||
|
||||
const EXPECTED_BACKENDS: BackendType[] = ['azureblob', 's3', 's3-compatible'];
|
||||
|
||||
describe('Backend Schema Registry', () => {
|
||||
it('exports all three required backend types', () => {
|
||||
for (const backend of EXPECTED_BACKENDS) {
|
||||
expect(BACKEND_REGISTRY[backend]).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('each backend has at least one field definition', () => {
|
||||
for (const backend of EXPECTED_BACKENDS) {
|
||||
expect(BACKEND_REGISTRY[backend].length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('each FieldDef has a non-empty key (snake_case, no camelCase)', () => {
|
||||
for (const backend of EXPECTED_BACKENDS) {
|
||||
for (const field of BACKEND_REGISTRY[backend]) {
|
||||
expect(field.key).toBeTruthy();
|
||||
// Reject camelCase — rclone keys are snake_case or lowercase
|
||||
expect(field.key).not.toMatch(/[A-Z]/);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('each FieldDef has a non-empty label', () => {
|
||||
for (const backend of EXPECTED_BACKENDS) {
|
||||
for (const field of BACKEND_REGISTRY[backend]) {
|
||||
expect(field.label).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('Azure Blob has account field (required)', () => {
|
||||
const accountField = BACKEND_REGISTRY.azureblob.find(f => f.key === 'account');
|
||||
expect(accountField).toBeDefined();
|
||||
expect(accountField!.required).toBe(true);
|
||||
});
|
||||
|
||||
it('S3 has access_key_id, secret_access_key, and region fields', () => {
|
||||
const keys = BACKEND_REGISTRY.s3.map(f => f.key);
|
||||
expect(keys).toContain('access_key_id');
|
||||
expect(keys).toContain('secret_access_key');
|
||||
expect(keys).toContain('region');
|
||||
});
|
||||
|
||||
it('S3-compatible has endpoint field (required)', () => {
|
||||
const endpointField = BACKEND_REGISTRY['s3-compatible'].find(f => f.key === 'endpoint');
|
||||
expect(endpointField).toBeDefined();
|
||||
expect(endpointField!.required).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**src/schemas/index.test.ts** (SC-3 — will FAIL until Plan 03 creates src/schemas/index.ts):
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { BACKEND_SCHEMAS } from './index';
|
||||
|
||||
describe('Azure Blob Zod schema', () => {
|
||||
it('accepts valid account + access key', () => {
|
||||
const result = BACKEND_SCHEMAS.azureblob.safeParse({
|
||||
account: 'mystorageaccount',
|
||||
key: 'dGVzdGtleQ==',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts valid account + sas_url', () => {
|
||||
const result = BACKEND_SCHEMAS.azureblob.safeParse({
|
||||
account: 'mystorageaccount',
|
||||
sas_url: 'https://mystorageaccount.blob.core.windows.net/?sv=2021-01-01',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects empty account (required field)', () => {
|
||||
const result = BACKEND_SCHEMAS.azureblob.safeParse({ account: '' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects missing account (required field)', () => {
|
||||
const result = BACKEND_SCHEMAS.azureblob.safeParse({ key: 'dGVzdGtleQ==' });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('S3 Zod schema', () => {
|
||||
it('accepts valid S3 credentials', () => {
|
||||
const result = BACKEND_SCHEMAS.s3.safeParse({
|
||||
provider: 'AWS',
|
||||
access_key_id: 'AKIAIOSFODNN7EXAMPLE',
|
||||
secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
||||
region: 'us-east-1',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects missing access_key_id (required field)', () => {
|
||||
const result = BACKEND_SCHEMAS.s3.safeParse({
|
||||
provider: 'AWS',
|
||||
secret_access_key: 'secret',
|
||||
region: 'us-east-1',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('S3-compatible Zod schema', () => {
|
||||
it('accepts valid S3-compatible credentials with endpoint', () => {
|
||||
const result = BACKEND_SCHEMAS['s3-compatible'].safeParse({
|
||||
provider: 'Other',
|
||||
access_key_id: 'mykey',
|
||||
secret_access_key: 'mysecret',
|
||||
endpoint: 'https://s3.wasabisys.com',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects missing endpoint (required for s3-compatible)', () => {
|
||||
const result = BACKEND_SCHEMAS['s3-compatible'].safeParse({
|
||||
provider: 'Other',
|
||||
access_key_id: 'mykey',
|
||||
secret_access_key: 'mysecret',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**src/store/reducer.test.ts** (SC-4 — will FAIL until Plan 04 creates reducer.ts and types.ts):
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { wizardReducer } from './reducer';
|
||||
import { INITIAL_STATE, WizardState } from './types';
|
||||
|
||||
describe('wizardReducer', () => {
|
||||
it('returns INITIAL_STATE on first call', () => {
|
||||
// @ts-expect-error intentional undefined action for initialization test
|
||||
const state = wizardReducer(undefined, { type: '@@INIT' });
|
||||
expect(state.currentStep).toBe(0);
|
||||
expect(state.remote.backendType).toBeNull();
|
||||
expect(state.remote.name).toBe('');
|
||||
expect(state.remote.params).toEqual({});
|
||||
expect(state.deployment.includeInstall).toBe(false);
|
||||
expect(state.deployment.configPath).toBe('machine-wide');
|
||||
});
|
||||
|
||||
it('SET_STEP updates currentStep', () => {
|
||||
const state = wizardReducer(INITIAL_STATE, { type: 'SET_STEP', payload: 2 });
|
||||
expect(state.currentStep).toBe(2);
|
||||
});
|
||||
|
||||
it('SET_BACKEND_TYPE updates remote.backendType', () => {
|
||||
const state = wizardReducer(INITIAL_STATE, { type: 'SET_BACKEND_TYPE', payload: 'azureblob' });
|
||||
expect(state.remote.backendType).toBe('azureblob');
|
||||
});
|
||||
|
||||
it('SET_REMOTE_NAME updates remote.name', () => {
|
||||
const state = wizardReducer(INITIAL_STATE, { type: 'SET_REMOTE_NAME', payload: 'my-blob' });
|
||||
expect(state.remote.name).toBe('my-blob');
|
||||
});
|
||||
|
||||
it('SET_REMOTE_PARAMS updates remote.params', () => {
|
||||
const state = wizardReducer(INITIAL_STATE, {
|
||||
type: 'SET_REMOTE_PARAMS',
|
||||
payload: { account: 'myaccount', key: 'mykey' },
|
||||
});
|
||||
expect(state.remote.params).toEqual({ account: 'myaccount', key: 'mykey' });
|
||||
});
|
||||
|
||||
it('SET_DEPLOYMENT partially updates deployment', () => {
|
||||
const state = wizardReducer(INITIAL_STATE, {
|
||||
type: 'SET_DEPLOYMENT',
|
||||
payload: { includeInstall: true },
|
||||
});
|
||||
expect(state.deployment.includeInstall).toBe(true);
|
||||
expect(state.deployment.configPath).toBe('machine-wide'); // unchanged
|
||||
});
|
||||
|
||||
it('RESET returns to INITIAL_STATE', () => {
|
||||
const modified: WizardState = {
|
||||
...INITIAL_STATE,
|
||||
currentStep: 3,
|
||||
remote: { name: 'test', backendType: 's3', params: { region: 'us-east-1' } },
|
||||
};
|
||||
const state = wizardReducer(modified, { type: 'RESET' });
|
||||
expect(state).toEqual(INITIAL_STATE);
|
||||
});
|
||||
|
||||
it('is a pure function — does not mutate input state', () => {
|
||||
const before = { ...INITIAL_STATE };
|
||||
wizardReducer(INITIAL_STATE, { type: 'SET_STEP', payload: 5 });
|
||||
expect(INITIAL_STATE.currentStep).toBe(before.currentStep);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
After creating all three files, run the test suite. registry.test.ts should pass; index.test.ts and reducer.test.ts will fail with "Cannot find module" errors — this is correct (RED phase).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run src/schemas/registry.test.ts 2>&1; echo "registry exit: $?"</automated>
|
||||
</verify>
|
||||
<done>All three test stub files exist. registry.test.ts passes (GREEN). index.test.ts and reducer.test.ts fail with module-not-found errors (RED — expected). Test stubs provide complete acceptance criteria for Plans 03 and 04.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `npx vitest run src/schemas/registry.test.ts` — all tests pass
|
||||
2. `npx vitest run src/schemas/index.test.ts` — fails with "Cannot find module './index'" (expected at this stage)
|
||||
3. `npx vitest run src/store/reducer.test.ts` — fails with "Cannot find module './reducer'" (expected at this stage)
|
||||
4. `cat src/schemas/registry.ts` — confirm no camelCase keys (grep for uppercase in key values)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- src/schemas/registry.ts exports BackendType, FieldDef, BACKEND_REGISTRY
|
||||
- All FieldDef.key values are snake_case matching rclone config key names
|
||||
- BACKEND_REGISTRY contains entries for all three backends with correct field definitions
|
||||
- registry.test.ts passes (7 tests green)
|
||||
- index.test.ts and reducer.test.ts exist and fail with module-not-found (RED phase — correct)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md` using the summary template.
|
||||
</output>
|
||||
@@ -0,0 +1,167 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on:
|
||||
- 01-02
|
||||
files_modified:
|
||||
- src/schemas/index.ts
|
||||
autonomous: true
|
||||
requirements: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "BACKEND_SCHEMAS['azureblob'].safeParse({account: 'x'}) returns success: true"
|
||||
- "BACKEND_SCHEMAS['azureblob'].safeParse({account: ''}) returns success: false"
|
||||
- "BACKEND_SCHEMAS['s3-compatible'].safeParse({...without endpoint}) returns success: false"
|
||||
- "Zod schemas are derived programmatically from BACKEND_REGISTRY — no hand-written z.object() calls"
|
||||
- "All 12 tests in src/schemas/index.test.ts pass"
|
||||
artifacts:
|
||||
- path: "src/schemas/index.ts"
|
||||
provides: "BACKEND_SCHEMAS constant and BackendFormValues utility type"
|
||||
exports: ["BACKEND_SCHEMAS", "BackendFormValues"]
|
||||
key_links:
|
||||
- from: "src/schemas/index.ts"
|
||||
to: "src/schemas/registry.ts"
|
||||
via: "imports BACKEND_REGISTRY and BackendType to build schemas programmatically"
|
||||
pattern: "import.*BACKEND_REGISTRY.*registry"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement the Zod schema builder that derives runtime validation schemas programmatically from the Backend Schema Registry.
|
||||
|
||||
Purpose: Schema and registry must never drift — if someone adds a field to the registry, validation automatically covers it. Writing Zod schemas by hand separate from the registry breaks this invariant.
|
||||
Output: src/schemas/index.ts with BACKEND_SCHEMAS and BackendFormValues type — all 12 tests in src/schemas/index.test.ts pass.
|
||||
</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 (created in Plan 02) -->
|
||||
```typescript
|
||||
export type BackendType = 'azureblob' | 's3' | 's3-compatible';
|
||||
|
||||
export interface FieldDef {
|
||||
key: string;
|
||||
label: string;
|
||||
inputType: 'text' | 'password' | 'select' | 'toggle';
|
||||
required: boolean;
|
||||
placeholder?: string;
|
||||
helpText?: string;
|
||||
options?: { value: string; label: string }[];
|
||||
}
|
||||
|
||||
export const BACKEND_REGISTRY: Record<BackendType, FieldDef[]>;
|
||||
// azureblob fields: account (required), key (optional), sas_url (optional)
|
||||
// s3 fields: provider (required), access_key_id (required), secret_access_key (required), region (required)
|
||||
// s3-compatible fields: provider (required), access_key_id (required), secret_access_key (required), endpoint (required), region (optional)
|
||||
```
|
||||
|
||||
<!-- Test expectations from src/schemas/index.test.ts (created in Plan 02) -->
|
||||
<!-- BACKEND_SCHEMAS.azureblob.safeParse({account: 'x'}) → success: true -->
|
||||
<!-- BACKEND_SCHEMAS.azureblob.safeParse({account: ''}) → success: false -->
|
||||
<!-- BACKEND_SCHEMAS.azureblob.safeParse({key: 'x'}) (no account) → success: false -->
|
||||
<!-- BACKEND_SCHEMAS.s3.safeParse({provider,access_key_id,secret_access_key,region}) → success: true -->
|
||||
<!-- BACKEND_SCHEMAS.s3.safeParse({...missing access_key_id}) → success: false -->
|
||||
<!-- BACKEND_SCHEMAS['s3-compatible'].safeParse({...with endpoint}) → success: true -->
|
||||
<!-- BACKEND_SCHEMAS['s3-compatible'].safeParse({...without endpoint}) → success: false -->
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Implement schema builder and BACKEND_SCHEMAS</name>
|
||||
<files>src/schemas/index.ts</files>
|
||||
<behavior>
|
||||
- buildZodSchema(backendType) constructs a z.object() from BACKEND_REGISTRY[backendType]
|
||||
- required fields → z.string().min(1, '{label} is required')
|
||||
- optional fields → z.string().optional()
|
||||
- BACKEND_SCHEMAS is a const object keyed by BackendType
|
||||
- BackendFormValues<T> infers the TypeScript type from the schema using z.infer
|
||||
- No z.object() calls hardcoded with field names — all field shapes derived from the registry loop
|
||||
</behavior>
|
||||
<action>
|
||||
Run the failing test first to confirm RED state:
|
||||
```
|
||||
npx vitest run src/schemas/index.test.ts
|
||||
```
|
||||
Expected: "Cannot find module './index'" error.
|
||||
|
||||
Create src/schemas/index.ts:
|
||||
|
||||
```typescript
|
||||
// src/schemas/index.ts
|
||||
// Zod schemas derived programmatically from the Backend Schema Registry.
|
||||
// DO NOT hand-write z.object() calls with hardcoded field names — all shapes come from the registry.
|
||||
// Adding a field to BACKEND_REGISTRY automatically adds it to validation.
|
||||
|
||||
import { z } from 'zod';
|
||||
import { BACKEND_REGISTRY, BackendType } from './registry';
|
||||
|
||||
function buildZodSchema(backendType: BackendType): z.ZodObject<Record<string, z.ZodTypeAny>> {
|
||||
const fields = BACKEND_REGISTRY[backendType];
|
||||
const shape: Record<string, z.ZodTypeAny> = {};
|
||||
for (const field of fields) {
|
||||
shape[field.key] = field.required
|
||||
? z.string().min(1, `${field.label} is required`)
|
||||
: z.string().optional();
|
||||
}
|
||||
return z.object(shape);
|
||||
}
|
||||
|
||||
export const BACKEND_SCHEMAS = {
|
||||
azureblob: buildZodSchema('azureblob'),
|
||||
s3: buildZodSchema('s3'),
|
||||
's3-compatible': buildZodSchema('s3-compatible'),
|
||||
} as const;
|
||||
|
||||
// Utility type: infer TypeScript type from a backend's Zod schema
|
||||
export type BackendFormValues<T extends BackendType> =
|
||||
z.infer<typeof BACKEND_SCHEMAS[T]>;
|
||||
```
|
||||
|
||||
Run the tests:
|
||||
```
|
||||
npx vitest run src/schemas/index.test.ts
|
||||
```
|
||||
All tests must pass (GREEN). If any fail, diagnose and fix — do not move on with failing tests.
|
||||
|
||||
Run the full test suite to confirm no regressions:
|
||||
```
|
||||
npx vitest run
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
<automated>npx vitest run src/schemas/index.test.ts 2>&1</automated>
|
||||
</verify>
|
||||
<done>All tests in src/schemas/index.test.ts pass. BACKEND_SCHEMAS exported with entries for all three backends. BackendFormValues type exported. npx vitest run (full suite) exits 0 with registry and schema tests green.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `npx vitest run src/schemas/index.test.ts` — all tests pass (12 tests green)
|
||||
2. `npx vitest run src/schemas/registry.test.ts` — still passing (no regression)
|
||||
3. `npx vitest run` — full suite green for all schema tests
|
||||
4. Confirm src/schemas/index.ts has no hardcoded field names in z.object() — only the loop over BACKEND_REGISTRY
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- src/schemas/index.ts creates Zod schemas by looping over BACKEND_REGISTRY field definitions
|
||||
- All 12 tests in src/schemas/index.test.ts pass
|
||||
- BACKEND_SCHEMAS and BackendFormValues are exported
|
||||
- Full test suite (registry + index) exits 0
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md` using the summary template.
|
||||
</output>
|
||||
@@ -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