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

17 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01-foundation 02 execute 2
01-01
src/schemas/registry.ts
src/schemas/registry.test.ts
src/schemas/index.test.ts
src/store/reducer.test.ts
true
truths artifacts key_links
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)
path provides exports
src/schemas/registry.ts BackendType union, FieldDef interface, BACKEND_REGISTRY constant
BackendType
FieldDef
BACKEND_REGISTRY
path provides contains
src/schemas/registry.test.ts Test stubs for SC-2 (registry structure verification) BACKEND_REGISTRY
path provides contains
src/schemas/index.test.ts Test stubs for SC-3 (Zod schema safeParse verification) BACKEND_SCHEMAS
path provides contains
src/store/reducer.test.ts Test stubs for SC-4 (WizardState reducer action verification) wizardReducer
from to via pattern
src/schemas/registry.ts rclone config keys FieldDef.key values must match rclone's INI key names exactly key:.*'account'|key:.*'access_key_id'|key:.*'secret_access_key'
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).

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

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-foundation/01-01-SUMMARY.md Task 1: Create the Backend Schema Registry src/schemas/registry.ts - 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) 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',
    },
  ],
};
```
npx vitest run src/schemas/registry.test.ts 2>&1; echo "exit: $?" registry.ts exports BackendType, FieldDef, and BACKEND_REGISTRY. All three backend entries have the correct rclone-compatible key names. registry.test.ts passes. Task 2: Write Wave 0 test stubs for SC-2, SC-3, SC-4 src/schemas/registry.test.ts, src/schemas/index.test.ts, src/store/reducer.test.ts 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).
npx vitest run src/schemas/registry.test.ts 2>&1; echo "registry exit: $?" 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. 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)

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