13 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-planning-threads | 01 | tdd | 1 |
|
true |
|
|
Purpose: Establish the data model and API that the frontend (Plan 02) will consume. Thread resolution -- the atomic operation that creates a collection item from a candidate and archives the thread -- is the core business logic of this phase.
Output: Working API endpoints for thread CRUD, candidate CRUD, and thread resolution, with comprehensive tests.
<execution_context> @/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md @/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-planning-threads/02-RESEARCH.mdFrom src/db/schema.ts (existing tables to extend):
export const categories = sqliteTable("categories", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull().unique(),
emoji: text("emoji").notNull().default("\u{1F4E6}"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => new Date()),
});
export const items = sqliteTable("items", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
weightGrams: real("weight_grams"),
priceCents: integer("price_cents"),
categoryId: integer("category_id").notNull().references(() => categories.id),
notes: text("notes"),
productUrl: text("product_url"),
imageFilename: text("image_filename"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => new Date()),
});
From src/shared/schemas.ts (existing pattern to follow):
export const createItemSchema = z.object({
name: z.string().min(1, "Name is required"),
weightGrams: z.number().nonnegative().optional(),
priceCents: z.number().int().nonnegative().optional(),
categoryId: z.number().int().positive(),
notes: z.string().optional(),
productUrl: z.string().url().optional().or(z.literal("")),
});
From src/server/services/item.service.ts (DI pattern):
type Db = typeof prodDb;
export function createItem(db: Db = prodDb, data: ...) { ... }
From src/server/index.ts (route mounting):
app.route("/api/items", itemRoutes);
From tests/helpers/db.ts (test DB pattern):
export function createTestDb() {
const sqlite = new Database(":memory:");
sqlite.run("PRAGMA foreign_keys = ON");
// CREATE TABLE statements...
const db = drizzle(sqlite, { schema });
db.insert(schema.categories).values({ name: "Uncategorized", emoji: "\u{1F4E6}" }).run();
return db;
}
1. Add `threads` and `threadCandidates` tables to `src/db/schema.ts` following the existing pattern. Schema per RESEARCH.md Pattern 1: threads has id, name, status (default "active"), resolvedCandidateId, createdAt, updatedAt. threadCandidates has id, threadId (FK to threads with cascade delete), and the same fields as items (name, weightGrams, priceCents, categoryId FK to categories, notes, productUrl, imageFilename, createdAt, updatedAt).
2. Add Zod schemas to `src/shared/schemas.ts`: createThreadSchema (name required), updateThreadSchema (name optional), createCandidateSchema (same shape as createItemSchema), updateCandidateSchema (partial of create), resolveThreadSchema (candidateId required).
3. Add types to `src/shared/types.ts`: Thread (inferred from Drizzle threads table), ThreadCandidate (inferred from Drizzle threadCandidates table), CreateThread, UpdateThread, CreateCandidate, UpdateCandidate, ResolveThread (from Zod schemas).
4. Update `tests/helpers/db.ts`: Add CREATE TABLE statements for `threads` and `thread_candidates` matching the Drizzle schema (use same pattern as existing items/categories tables).
5. Write `tests/services/thread.service.test.ts` with failing tests covering all behaviors listed above. Follow the pattern from `tests/services/item.service.test.ts`. Each test uses `createTestDb()` for isolation.
**GREEN phase:**
6. Implement `src/server/services/thread.service.ts` following the DI pattern from item.service.ts (db as first param with prodDb default). Functions: getAllThreads (with subquery aggregates for candidateCount and price range), getThreadWithCandidates (with candidate+category join), createThread, updateThread, deleteThread (with image cleanup collection), createCandidate, updateCandidate, deleteCandidate, resolveThread (transactional: validate thread is active + candidate belongs to thread, insert into items from candidate data, update thread status to "resolved" and set resolvedCandidateId). On resolution, if candidate's categoryId no longer exists, fall back to categoryId=1 (Uncategorized). On resolution, if candidate has imageFilename, copy the file to a new filename so the item has an independent image copy.
All tests must pass after implementation.
cd /home/jean-luc-makiola/Development/projects/GearBox && bun test tests/services/thread.service.test.ts --bail
All thread service unit tests pass. Schema, shared schemas, types, and test helper updated. Service layer implements full thread + candidate CRUD and transactional resolution.
Task 2: Thread API routes with integration tests
src/server/routes/threads.ts, src/server/index.ts, tests/routes/threads.test.ts
- POST /api/threads with valid body returns 201 + thread object
- POST /api/threads with empty name returns 400
- GET /api/threads returns array of active threads with metadata
- GET /api/threads?includeResolved=true includes archived threads
- GET /api/threads/:id returns thread with candidates
- GET /api/threads/:id for non-existent returns 404
- PUT /api/threads/:id updates thread name
- DELETE /api/threads/:id removes thread
- POST /api/threads/:id/candidates adds candidate, returns 201
- PUT /api/threads/:threadId/candidates/:candidateId updates candidate
- DELETE /api/threads/:threadId/candidates/:candidateId removes candidate
- POST /api/threads/:id/resolve with valid candidateId returns 200 + created item
- POST /api/threads/:id/resolve on already-resolved thread returns 400
- POST /api/threads/:id/resolve with wrong candidateId returns 400
**RED phase first:**
1. Write `tests/routes/threads.test.ts` following the pattern from `tests/routes/items.test.ts`. Use `createTestDb()`, inject test DB via Hono context middleware (`c.set("db", testDb)`), and use `app.request()` for integration tests. Cover all behaviors above.
**GREEN phase:**
2. Create `src/server/routes/threads.ts` as a Hono app. Follow the exact pattern from `src/server/routes/items.ts`:
- Use `zValidator("json", schema)` for request body validation
- Get DB from `c.get("db") ?? prodDb` for testability
- Thread CRUD: GET / (with optional ?includeResolved query param), POST /, GET /:id, PUT /:id, DELETE /:id
- Candidate CRUD nested under thread: POST /:id/candidates (with image upload support via formData, same pattern as items), PUT /:threadId/candidates/:candidateId, DELETE /:threadId/candidates/:candidateId (with image file cleanup)
- Resolution: POST /:id/resolve with resolveThreadSchema validation
- Return appropriate status codes (201 for creation, 200 for success, 400 for validation/business errors, 404 for not found)
3. Mount routes in `src/server/index.ts`: `app.route("/api/threads", threadRoutes)` alongside existing routes.
For candidate image upload: follow the same pattern as the items image upload route. Candidates need a POST endpoint that accepts multipart form data with an optional image file. Use the same file validation (type/size) and storage pattern.
All integration tests must pass.
cd /home/jean-luc-makiola/Development/projects/GearBox && bun test tests/routes/threads.test.ts --bail
All thread API integration tests pass. Routes mounted in server index. Full thread and candidate CRUD available via REST API. Resolution endpoint creates collection item and archives thread.
```bash
# All tests pass (Phase 1 + Phase 2)
cd /home/jean-luc-makiola/Development/projects/GearBox && bun test --bail
Thread API responds
curl -s http://localhost:3000/api/threads | head -1
</verification>
<success_criteria>
- All thread service unit tests pass
- All thread API integration tests pass
- All existing Phase 1 tests still pass (no regressions)
- POST /api/threads creates a thread
- POST /api/threads/:id/candidates adds a candidate
- POST /api/threads/:id/resolve creates a collection item and archives the thread
- Thread resolution is transactional (atomic create item + archive thread)
</success_criteria>
<output>
After completion, create `.planning/phases/02-planning-threads/02-01-SUMMARY.md`
</output>