From 167b24e3ebd66c101a3e7d2ccf7ebed294d3f935 Mon Sep 17 00:00:00 2001 From: Roberto Musso Date: Thu, 19 Feb 2026 22:33:09 +0100 Subject: [PATCH] feat: add project requirements and user stories for Adiuva MVP - Created prd.json with detailed user stories, acceptance criteria, and implementation notes for the Adiuva project. - Updated progress.txt to reflect recent implementations of routers for checkpoints and notes, along with learnings for future iterations. - Modified ralph.sh to change the default tool from "amp" to "claude" for improved compatibility. --- .claude/.mcp.json | 12 ++ progress.txt | 127 ---------------- scripts/ralph/CLAUDE.md | 21 +-- .../ralph/prd-main.md | 0 prd.json => scripts/ralph/prd.json | 0 scripts/ralph/progress.txt | 136 ++++++++++++++++-- scripts/ralph/ralph.sh | 2 +- 7 files changed, 148 insertions(+), 150 deletions(-) create mode 100644 .claude/.mcp.json delete mode 100644 progress.txt rename tasks/prd-adiuva.md => scripts/ralph/prd-main.md (100%) rename prd.json => scripts/ralph/prd.json (100%) diff --git a/.claude/.mcp.json b/.claude/.mcp.json new file mode 100644 index 0000000..37cce59 --- /dev/null +++ b/.claude/.mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "playwright": { + "type": "stdio", + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ], + "env": {} + } + } +} \ No newline at end of file diff --git a/progress.txt b/progress.txt deleted file mode 100644 index dd56737..0000000 --- a/progress.txt +++ /dev/null @@ -1,127 +0,0 @@ -## Codebase Patterns -- `alias(table, 'alias_name')` from `drizzle-orm/sqlite-core` enables self-joins (e.g., clients → parentClients for hierarchy) -- `sql\`CASE WHEN ... THEN ... ELSE ... END\`` for conditional SELECT fields (e.g., clientName vs subClientName based on parentId) -- `or(like(col1, pattern), like(col2, pattern))` for multi-column search; SQLite LIKE on NULL columns safely returns NULL (falsy) so OR is safe -- Vite configs use `.mts` extension (not `.ts`) to avoid ESM/CJS conflict with electron-forge's externalize-deps plugin -- electron-trpc uses `exposeElectronTRPC()` in preload and `createIPCHandler({ router, windows })` in main; renderer uses `ipcLink()` from `electron-trpc/renderer` -- appRouter lives at `src/main/router/index.ts`; renderer client at `src/renderer/lib/trpc.ts` -- `@/*` path alias maps to `src/renderer/*` (configured in tsconfig.json paths) -- Drizzle ORM with better-sqlite3 (sync driver): SELECT queries MUST end with `.all()` to execute; INSERT/UPDATE/DELETE MUST end with `.run()` -- `inArray(column, values)` works with nullable columns when values is `string[]` (TypeScript covariance allows string[] → (string | null)[]) -- All DB tables use `CREATE TABLE IF NOT EXISTS` for non-destructive migrations -- All IDs are UUIDs generated via `crypto.randomUUID()` -- TypeScript strict mode + noUncheckedIndexedAccess enabled; always account for possible undefined on array access -- electron-store@8 (CJS) used for app settings; use lazy init pattern `getStore()` like `getDb()` to avoid calling before app ready -- ESLint uses `eslint-import-resolver-typescript` to resolve `@/*` aliases; configured in `.eslintrc.json` under `settings.import/resolver` -- App settings (sidebar state, etc.) exposed via `settings` tRPC sub-router for type-safe renderer access - ---- - -## 2026-02-19 - US-008 -- What was implemented: - - Full `checkpointsRouter` replacing stubs in `src/main/router/index.ts` - - Full `notesRouter` replacing stubs in `src/main/router/index.ts` - - Added `checkpoints` and `notes` to the schema import - - `checkpoints.list`: optional `projectId` filter, ordered by `asc(checkpoints.date)` - - `checkpoints.create`: inserts with UUID, createdAt=Date.now(), defaults isAiSuggested/isApproved to 0 - - `checkpoints.update`: partial set for title/date/isApproved - - `checkpoints.delete`: deletes by id, returns `{ success: true }` - - `notes.list`: returns `{ id, projectId, title, createdAt, updatedAt }` only — no content (performance) - - `notes.get`: returns full record or null via `.all()[0] ?? null` pattern - - `notes.create`: inserts with UUID, createdAt=updatedAt=Date.now() - - `notes.update`: partial set, always sets updatedAt=Date.now() regardless of which fields changed - - `notes.delete`: deletes by id, returns `{ success: true }` -- Files changed: `src/main/router/index.ts`, `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - `notes.update` must always set `updatedAt` — build the set object with updatedAt outside the conditional block - - `notes.list` intentionally excludes `content` column for performance; use `notes.get` for full record - - `checkpoints.projectId` is `.notNull()` in schema (unlike tasks.projectId which is nullable) — no null coalescing needed ---- - -## 2026-02-19 - US-003 -- What was implemented: - - Installed: electron-trpc, @trpc/server, @trpc/client, @trpc/react-query, @tanstack/react-query, zod - - Created `src/main/router/index.ts` with full appRouter: stub routers for health, clients, projects, tasks, checkpoints, notes, ai - - Updated `src/preload/index.ts` to call `exposeElectronTRPC()` - - Updated `src/main/index.ts` to call `createIPCHandler({ router: appRouter, windows: [win] })`; `createWindow()` now returns `BrowserWindow` - - Created `src/renderer/lib/trpc.ts` with `createTRPCReact()` - - Updated `src/renderer/index.tsx` to wrap app in `TRPCProvider` + `QueryClientProvider` - - Updated `src/renderer/routes/index.tsx` to call `trpc.health.ping.useQuery()` and display 'tRPC IPC bridge: pong' -- Files changed: package.json, package-lock.json, prd.json, src/main/index.ts, src/main/router/index.ts (new), src/preload/index.ts, src/renderer/index.tsx, src/renderer/lib/trpc.ts (new), src/renderer/routes/index.tsx -- **Learnings for future iterations:** - - electron-trpc `exposeElectronTRPC` is imported from `electron-trpc/main` (not a separate package) - - `ipcLink` is imported from `electron-trpc/renderer` in the renderer process - - `createTRPCReact()` requires importing the AppRouter type from the main process router — this is a type-only import so it doesn't bundle main process code into renderer - - The TRPCProvider must wrap QueryClientProvider (or be a sibling); both need the same queryClient instance - - Stub routers return empty arrays or null — they will be replaced in US-005 through US-008 ---- - -## 2026-02-19 - US-004 -- What was implemented: - - Installed: electron-store@8 (CJS-compatible, for persistent app settings), @fontsource/geist (self-hosted Geist font), eslint-import-resolver-typescript (ESLint path alias fix) - - Created `src/main/store.ts` with lazy `getStore()` pattern using electron-store - - Added `settings` tRPC sub-router with `getSidebarCollapsed` query and `setSidebarCollapsed` mutation - - Updated `src/renderer/components/layout/AppShell.tsx` to: persist sidebar collapse via tRPC, add right-edge 'keep scrolling for AI' vertical label with ChevronDown icon - - Updated `src/renderer/globals.css`: replaced Google Fonts CDN with @fontsource/geist imports (weights 400/500/600) - - Updated `index.html`: removed Google Fonts CDN links - - Updated `.eslintrc.json`: added eslint-import-resolver-typescript to fix @/* alias resolution (fixed all 7 pre-existing lint errors) -- Files changed: .eslintrc.json, index.html, package.json, package-lock.json, src/main/router/index.ts, src/main/store.ts (new), src/renderer/components/layout/AppShell.tsx, src/renderer/globals.css -- **Learnings for future iterations:** - - Use electron-store@8 (not v9+) — v9+ is ESM-only and breaks with CommonJS main process - - electron-store must NOT be initialized at module import time (before app.ready); use lazy `getStore()` like `getDb()` pattern - - For sidebar/UI state loaded from IPC: use `localState ?? queryData ?? default` pattern to avoid flash while query resolves - - @fontsource packages are the npm equivalent of Google Fonts — import weight-specific CSS files (e.g., `@fontsource/geist/400.css`) - - ESLint `import/no-unresolved` requires `eslint-import-resolver-typescript` with `alwaysTryTypes: true` to resolve TypeScript path aliases - - The `writingMode: 'vertical-rl'` + `transform: 'rotate(180deg)'` CSS pattern creates bottom-to-top text for vertical affordance labels ---- - -## 2026-02-19 - US-006 -- What was implemented: - - Full `projectsRouter` replacing stubs in `src/main/router/index.ts` - - Added `and` to drizzle-orm imports - - `projects.list`: uses `and()` with optional conditions for `clientId` filter and archived filter (defaults to active only) - - `projects.listAll`: returns only `{ id, name }` columns for dropdown use - - `projects.get`: `.all()` then `result[0] ?? null` pattern for nullable single-record lookup - - `projects.create`: inserts with UUID, status='active', createdAt=Date.now() - - `projects.update`: partial set object — only sets defined fields - - `projects.delete`: nulls `tasks.projectId` for all tasks in the project, then deletes the project -- Files changed: `src/main/router/index.ts`, `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - `and(...conditions)` from drizzle-orm accepts `(SQL | undefined)[]` — pass `undefined` for optional conditions and drizzle filters them out automatically - - For nullable single-record queries: use `.all()` and `result[0] ?? null` (strict mode forbids `.get()` direct null return without this pattern) - - `and()` returns `SQL | undefined` which `.where()` accepts directly (no extra wrapping needed) - ---- - -## 2026-02-19 - US-005 -- What was implemented: - - Full clients tRPC router replacing stubs in `src/main/router/index.ts` - - Added imports: `eq`, `asc`, `inArray` from `drizzle-orm`; `getDb` from `../db`; `clients`, `projects`, `tasks` from `../db/schema` - - `clients.list`: `db.select().from(clients).orderBy(asc(clients.name)).all()` - - `clients.create`: inserts with `crypto.randomUUID()` + `Date.now()` via `.run()` - - `clients.update`: partial update — only sets fields that are defined in input, skips if no-op - - `clients.delete`: checks for child clients and child projects; returns `{ error: string }` payload if any exist; otherwise deletes and returns `{ success: true }` - - `clients.deleteWithCascade`: BFS loop collects all descendant client IDs, finds their projects, nulls `projectId` on orphaned tasks, deletes projects, then deletes all clients -- Files changed: `src/main/router/index.ts`, `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - Drizzle ORM with better-sqlite3 sync driver: SELECT must call `.all()` to get an array; INSERT/UPDATE/DELETE must call `.run()` to execute — NOT calling these causes TypeScript errors (query builder ≠ result) - - `inArray(nullableColumn, string[])` is TypeScript-safe because `string[]` is assignable to `(string | null)[]` via covariance - - Guard against empty arrays before using `inArray` — while `allClientIds` is never empty (starts with input.id), `projectIds` could be empty; guarded with `if (projectIds.length > 0)` block - - `@typescript-eslint/no-non-null-assertion` is configured as a warning (not error) in this project — `queue.shift()!` is fine after a `length > 0` check ---- - -## 2026-02-19 - US-007 -- What was implemented: - - Full `tasksRouter` replacing stubs in `src/main/router/index.ts` - - Added imports: `or`, `like`, `sql` from `drizzle-orm`; `alias` from `drizzle-orm/sqlite-core` - - `tasks.list`: LEFT JOINs projects → clients → parentClients (alias for self-join); CASE WHEN for clientName/subClientName breadcrumb fields; `and()` with optional conditions for projectId/status/search; `like()` OR search on title+description; CASE expression for priority ordering - - `tasks.create`: inserts with UUID, defaults (status='todo', priority='medium'), createdAt=Date.now() - - `tasks.update`: partial set object — only sets defined fields - - `tasks.delete`: deletes by id, returns `{ success: true }` -- Files changed: `src/main/router/index.ts`, `prd.json`, `progress.txt` -- **Learnings for future iterations:** - - `alias(table, 'alias_name')` is from `drizzle-orm/sqlite-core` (NOT `drizzle-orm`) for SQLite self-joins - - `sql\`CASE WHEN ${col} IS NOT NULL THEN ${alias.col} ELSE ${col} END\`` for conditional field selection using drizzle template literals - - `or(like(col1, pattern), like(col2, pattern))` composes safely — null columns evaluate to NULL (falsy) in WHERE - - For priority ordering: `asc(sql\`CASE ${tasks.priority} WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END\`)` puts high priority first ---- diff --git a/scripts/ralph/CLAUDE.md b/scripts/ralph/CLAUDE.md index f95bb92..323ad25 100644 --- a/scripts/ralph/CLAUDE.md +++ b/scripts/ralph/CLAUDE.md @@ -4,16 +4,17 @@ You are an autonomous coding agent working on a software project. ## Your Task -1. Read the PRD at `prd.json` (in the same directory as this file) -2. Read the progress log at `progress.txt` (check Codebase Patterns section first) -3. Check you're on the correct branch from PRD `branchName`. If not, check it out or create from main. -4. Pick the **highest priority** user story where `passes: false` -5. Implement that single user story -6. Run quality checks (e.g., typecheck, lint, test - use whatever your project requires) -7. Update CLAUDE.md files if you discover reusable patterns (see below) -8. If checks pass, commit ALL changes with message: `feat: [Story ID] - [Story Title]` -9. Update the PRD to set `passes: true` for the completed story -10. Append your progress to `progress.txt` +1. Read the full app PRD at `prd-main.md` (in the same directory as this file) +2. Read the PRD at `prd.json` (in the same directory as this file) +3. Read the progress log at `progress.txt` (check Codebase Patterns section first) +4. Check you're on the correct branch from PRD `branchName`. If not, check it out or create from main. +5. Pick the **highest priority** user story where `passes: false` +6. Implement that single user story +7. Run quality checks (e.g., typecheck, lint, test - use whatever your project requires) +8. Update CLAUDE.md files if you discover reusable patterns (see below) +9. If checks pass, commit ALL changes with message: `feat: [Story ID] - [Story Title]` +10. Update the PRD to set `passes: true` for the completed story +11. Append your progress to `progress.txt` ## Progress Report Format diff --git a/tasks/prd-adiuva.md b/scripts/ralph/prd-main.md similarity index 100% rename from tasks/prd-adiuva.md rename to scripts/ralph/prd-main.md diff --git a/prd.json b/scripts/ralph/prd.json similarity index 100% rename from prd.json rename to scripts/ralph/prd.json diff --git a/scripts/ralph/progress.txt b/scripts/ralph/progress.txt index a155e1d..d4a5707 100644 --- a/scripts/ralph/progress.txt +++ b/scripts/ralph/progress.txt @@ -1,16 +1,19 @@ -# Ralph Progress Log -Started: Thu Feb 19 16:33:18 CET 2026 ---- - ## Codebase Patterns -- Vite configs use `.mts` extension to avoid ESM/CJS issues with electron-forge -- Native Node modules (e.g. better-sqlite3) must be externalized in `vite.main.config.mts` rollupOptions.external -- `AutoUnpackNativesPlugin` from `@electron-forge/plugin-auto-unpack-natives` must be first in plugins array in forge.config.ts for native module asar compatibility -- DB is initialized in main process `app.on('ready')` handler; use `getDb()` singleton everywhere else in main process -- All table IDs are TEXT (UUID); timestamps are INTEGER (unix ms); status/priority columns use TEXT with enum values -- TypeScript types use `InferSelectModel` / `InferInsertModel` from drizzle-orm — no manual type duplication -- `drizzle.config.ts` at project root points drizzle-kit CLI at `src/main/db/schema.ts` - +- `alias(table, 'alias_name')` from `drizzle-orm/sqlite-core` enables self-joins (e.g., clients → parentClients for hierarchy) +- `sql\`CASE WHEN ... THEN ... ELSE ... END\`` for conditional SELECT fields (e.g., clientName vs subClientName based on parentId) +- `or(like(col1, pattern), like(col2, pattern))` for multi-column search; SQLite LIKE on NULL columns safely returns NULL (falsy) so OR is safe +- Vite configs use `.mts` extension (not `.ts`) to avoid ESM/CJS conflict with electron-forge's externalize-deps plugin +- electron-trpc uses `exposeElectronTRPC()` in preload and `createIPCHandler({ router, windows })` in main; renderer uses `ipcLink()` from `electron-trpc/renderer` +- appRouter lives at `src/main/router/index.ts`; renderer client at `src/renderer/lib/trpc.ts` +- `@/*` path alias maps to `src/renderer/*` (configured in tsconfig.json paths) +- Drizzle ORM with better-sqlite3 (sync driver): SELECT queries MUST end with `.all()` to execute; INSERT/UPDATE/DELETE MUST end with `.run()` +- `inArray(column, values)` works with nullable columns when values is `string[]` (TypeScript covariance allows string[] → (string | null)[]) +- All DB tables use `CREATE TABLE IF NOT EXISTS` for non-destructive migrations +- All IDs are UUIDs generated via `crypto.randomUUID()` +- TypeScript strict mode + noUncheckedIndexedAccess enabled; always account for possible undefined on array access +- electron-store@8 (CJS) used for app settings; use lazy init pattern `getStore()` like `getDb()` to avoid calling before app ready +- ESLint uses `eslint-import-resolver-typescript` to resolve `@/*` aliases; configured in `.eslintrc.json` under `settings.import/resolver` +- App settings (sidebar state, etc.) exposed via `settings` tRPC sub-router for type-safe renderer access --- ## 2026-02-19 - US-002 @@ -28,3 +31,112 @@ Started: Thu Feb 19 16:33:18 CET 2026 - electron-forge rebuilds native modules automatically on `electron-forge start`; no manual rebuild step needed - `app.getPath('userData')` is only available after `app.on('ready')` fires — do not call earlier --- + +## 2026-02-19 - US-008 +- What was implemented: + - Full `checkpointsRouter` replacing stubs in `src/main/router/index.ts` + - Full `notesRouter` replacing stubs in `src/main/router/index.ts` + - Added `checkpoints` and `notes` to the schema import + - `checkpoints.list`: optional `projectId` filter, ordered by `asc(checkpoints.date)` + - `checkpoints.create`: inserts with UUID, createdAt=Date.now(), defaults isAiSuggested/isApproved to 0 + - `checkpoints.update`: partial set for title/date/isApproved + - `checkpoints.delete`: deletes by id, returns `{ success: true }` + - `notes.list`: returns `{ id, projectId, title, createdAt, updatedAt }` only — no content (performance) + - `notes.get`: returns full record or null via `.all()[0] ?? null` pattern + - `notes.create`: inserts with UUID, createdAt=updatedAt=Date.now() + - `notes.update`: partial set, always sets updatedAt=Date.now() regardless of which fields changed + - `notes.delete`: deletes by id, returns `{ success: true }` +- Files changed: `src/main/router/index.ts`, `prd.json`, `progress.txt` +- **Learnings for future iterations:** + - `notes.update` must always set `updatedAt` — build the set object with updatedAt outside the conditional block + - `notes.list` intentionally excludes `content` column for performance; use `notes.get` for full record + - `checkpoints.projectId` is `.notNull()` in schema (unlike tasks.projectId which is nullable) — no null coalescing needed +--- + +## 2026-02-19 - US-003 +- What was implemented: + - Installed: electron-trpc, @trpc/server, @trpc/client, @trpc/react-query, @tanstack/react-query, zod + - Created `src/main/router/index.ts` with full appRouter: stub routers for health, clients, projects, tasks, checkpoints, notes, ai + - Updated `src/preload/index.ts` to call `exposeElectronTRPC()` + - Updated `src/main/index.ts` to call `createIPCHandler({ router: appRouter, windows: [win] })`; `createWindow()` now returns `BrowserWindow` + - Created `src/renderer/lib/trpc.ts` with `createTRPCReact()` + - Updated `src/renderer/index.tsx` to wrap app in `TRPCProvider` + `QueryClientProvider` + - Updated `src/renderer/routes/index.tsx` to call `trpc.health.ping.useQuery()` and display 'tRPC IPC bridge: pong' +- Files changed: package.json, package-lock.json, prd.json, src/main/index.ts, src/main/router/index.ts (new), src/preload/index.ts, src/renderer/index.tsx, src/renderer/lib/trpc.ts (new), src/renderer/routes/index.tsx +- **Learnings for future iterations:** + - electron-trpc `exposeElectronTRPC` is imported from `electron-trpc/main` (not a separate package) + - `ipcLink` is imported from `electron-trpc/renderer` in the renderer process + - `createTRPCReact()` requires importing the AppRouter type from the main process router — this is a type-only import so it doesn't bundle main process code into renderer + - The TRPCProvider must wrap QueryClientProvider (or be a sibling); both need the same queryClient instance + - Stub routers return empty arrays or null — they will be replaced in US-005 through US-008 +--- + +## 2026-02-19 - US-004 +- What was implemented: + - Installed: electron-store@8 (CJS-compatible, for persistent app settings), @fontsource/geist (self-hosted Geist font), eslint-import-resolver-typescript (ESLint path alias fix) + - Created `src/main/store.ts` with lazy `getStore()` pattern using electron-store + - Added `settings` tRPC sub-router with `getSidebarCollapsed` query and `setSidebarCollapsed` mutation + - Updated `src/renderer/components/layout/AppShell.tsx` to: persist sidebar collapse via tRPC, add right-edge 'keep scrolling for AI' vertical label with ChevronDown icon + - Updated `src/renderer/globals.css`: replaced Google Fonts CDN with @fontsource/geist imports (weights 400/500/600) + - Updated `index.html`: removed Google Fonts CDN links + - Updated `.eslintrc.json`: added eslint-import-resolver-typescript to fix @/* alias resolution (fixed all 7 pre-existing lint errors) +- Files changed: .eslintrc.json, index.html, package.json, package-lock.json, src/main/router/index.ts, src/main/store.ts (new), src/renderer/components/layout/AppShell.tsx, src/renderer/globals.css +- **Learnings for future iterations:** + - Use electron-store@8 (not v9+) — v9+ is ESM-only and breaks with CommonJS main process + - electron-store must NOT be initialized at module import time (before app.ready); use lazy `getStore()` like `getDb()` pattern + - For sidebar/UI state loaded from IPC: use `localState ?? queryData ?? default` pattern to avoid flash while query resolves + - @fontsource packages are the npm equivalent of Google Fonts — import weight-specific CSS files (e.g., `@fontsource/geist/400.css`) + - ESLint `import/no-unresolved` requires `eslint-import-resolver-typescript` with `alwaysTryTypes: true` to resolve TypeScript path aliases + - The `writingMode: 'vertical-rl'` + `transform: 'rotate(180deg)'` CSS pattern creates bottom-to-top text for vertical affordance labels +--- + +## 2026-02-19 - US-006 +- What was implemented: + - Full `projectsRouter` replacing stubs in `src/main/router/index.ts` + - Added `and` to drizzle-orm imports + - `projects.list`: uses `and()` with optional conditions for `clientId` filter and archived filter (defaults to active only) + - `projects.listAll`: returns only `{ id, name }` columns for dropdown use + - `projects.get`: `.all()` then `result[0] ?? null` pattern for nullable single-record lookup + - `projects.create`: inserts with UUID, status='active', createdAt=Date.now() + - `projects.update`: partial set object — only sets defined fields + - `projects.delete`: nulls `tasks.projectId` for all tasks in the project, then deletes the project +- Files changed: `src/main/router/index.ts`, `prd.json`, `progress.txt` +- **Learnings for future iterations:** + - `and(...conditions)` from drizzle-orm accepts `(SQL | undefined)[]` — pass `undefined` for optional conditions and drizzle filters them out automatically + - For nullable single-record queries: use `.all()` and `result[0] ?? null` (strict mode forbids `.get()` direct null return without this pattern) + - `and()` returns `SQL | undefined` which `.where()` accepts directly (no extra wrapping needed) + +--- + +## 2026-02-19 - US-005 +- What was implemented: + - Full clients tRPC router replacing stubs in `src/main/router/index.ts` + - Added imports: `eq`, `asc`, `inArray` from `drizzle-orm`; `getDb` from `../db`; `clients`, `projects`, `tasks` from `../db/schema` + - `clients.list`: `db.select().from(clients).orderBy(asc(clients.name)).all()` + - `clients.create`: inserts with `crypto.randomUUID()` + `Date.now()` via `.run()` + - `clients.update`: partial update — only sets fields that are defined in input, skips if no-op + - `clients.delete`: checks for child clients and child projects; returns `{ error: string }` payload if any exist; otherwise deletes and returns `{ success: true }` + - `clients.deleteWithCascade`: BFS loop collects all descendant client IDs, finds their projects, nulls `projectId` on orphaned tasks, deletes projects, then deletes all clients +- Files changed: `src/main/router/index.ts`, `prd.json`, `progress.txt` +- **Learnings for future iterations:** + - Drizzle ORM with better-sqlite3 sync driver: SELECT must call `.all()` to get an array; INSERT/UPDATE/DELETE must call `.run()` to execute — NOT calling these causes TypeScript errors (query builder ≠ result) + - `inArray(nullableColumn, string[])` is TypeScript-safe because `string[]` is assignable to `(string | null)[]` via covariance + - Guard against empty arrays before using `inArray` — while `allClientIds` is never empty (starts with input.id), `projectIds` could be empty; guarded with `if (projectIds.length > 0)` block + - `@typescript-eslint/no-non-null-assertion` is configured as a warning (not error) in this project — `queue.shift()!` is fine after a `length > 0` check +--- + +## 2026-02-19 - US-007 +- What was implemented: + - Full `tasksRouter` replacing stubs in `src/main/router/index.ts` + - Added imports: `or`, `like`, `sql` from `drizzle-orm`; `alias` from `drizzle-orm/sqlite-core` + - `tasks.list`: LEFT JOINs projects → clients → parentClients (alias for self-join); CASE WHEN for clientName/subClientName breadcrumb fields; `and()` with optional conditions for projectId/status/search; `like()` OR search on title+description; CASE expression for priority ordering + - `tasks.create`: inserts with UUID, defaults (status='todo', priority='medium'), createdAt=Date.now() + - `tasks.update`: partial set object — only sets defined fields + - `tasks.delete`: deletes by id, returns `{ success: true }` +- Files changed: `src/main/router/index.ts`, `prd.json`, `progress.txt` +- **Learnings for future iterations:** + - `alias(table, 'alias_name')` is from `drizzle-orm/sqlite-core` (NOT `drizzle-orm`) for SQLite self-joins + - `sql\`CASE WHEN ${col} IS NOT NULL THEN ${alias.col} ELSE ${col} END\`` for conditional field selection using drizzle template literals + - `or(like(col1, pattern), like(col2, pattern))` composes safely — null columns evaluate to NULL (falsy) in WHERE + - For priority ordering: `asc(sql\`CASE ${tasks.priority} WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END\`)` puts high priority first +--- diff --git a/scripts/ralph/ralph.sh b/scripts/ralph/ralph.sh index baff052..5ceba03 100755 --- a/scripts/ralph/ralph.sh +++ b/scripts/ralph/ralph.sh @@ -5,7 +5,7 @@ set -e # Parse arguments -TOOL="amp" # Default to amp for backwards compatibility +TOOL="claude" # Default to claude for backwards compatibility MAX_ITERATIONS=10 while [[ $# -gt 0 ]]; do