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:
2026-03-26 10:05:01 +01:00
parent a3dea0ef19
commit 4514fc9033
5 changed files with 1114 additions and 2 deletions
+8 -2
View File
@@ -28,7 +28,13 @@ Decimal phases appear between their surrounding integers in numeric order.
2. The Backend Schema Registry defines at least Azure Blob, S3, and S3-compatible backends with typed field definitions 2. The Backend Schema Registry defines at least Azure Blob, S3, and S3-compatible backends with typed field definitions
3. Zod validation schemas can be derived from the registry and validate correct/incorrect input correctly 3. Zod validation schemas can be derived from the registry and validate correct/incorrect input correctly
4. The WizardState useReducer store initializes, accepts dispatch actions, and state is accessible via Context 4. The WizardState useReducer store initializes, accepts dispatch actions, and state is accessible via Context
**Plans**: TBD **Plans**: 4 plans
Plans:
- [ ] 01-01-PLAN.md — Scaffold Vite 6 + React 18 + TypeScript 5, install all Phase 1 deps, configure Tailwind v4, set up Vitest
- [ ] 01-02-PLAN.md — Create Backend Schema Registry (registry.ts) and Wave 0 test stubs for SC-2, SC-3, SC-4
- [ ] 01-03-PLAN.md — Implement Zod schemas derived from registry (index.ts), all schema tests green
- [ ] 01-04-PLAN.md — Implement WizardState types, pure reducer, Context provider, and wire into App
### Phase 2: Generators ### Phase 2: Generators
**Goal**: Given a completed wizard state, the app can produce correct, deployment-ready file content for all output types **Goal**: Given a completed wizard state, the app can produce correct, deployment-ready file content for all output types
@@ -73,7 +79,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4
| Phase | Plans Complete | Status | Completed | | Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------| |-------|----------------|--------|-----------|
| 1. Foundation | 0/? | Not started | - | | 1. Foundation | 0/4 | Not started | - |
| 2. Generators | 0/? | Not started | - | | 2. Generators | 0/? | Not started | - |
| 3. Wizard UI | 0/? | Not started | - | | 3. Wizard UI | 0/? | Not started | - |
| 4. Review, Download & Security | 0/? | Not started | - | | 4. Review, Download & Security | 0/? | Not started | - |
@@ -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>