docs(intake): mark the drafting worker plan complete through task 7 (#223)
Only the manual verification against a real photograph is left, and it needs an API key that does not exist yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Intake Drafting Worker Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
|
||||
|
||||
**Goal:** A submitted item arrives with a drafted name, description, category, tags and suggested price, written from its photos and the sender's note.
|
||||
|
||||
@@ -62,7 +62,7 @@ A directory rather than six files loose in `src/`: they are one concern, they ch
|
||||
**Interfaces:**
|
||||
- Produces: `@anthropic-ai/sdk` and `zod` available; a boot warning when `ANTHROPIC_API_KEY` is absent.
|
||||
|
||||
- [ ] **Step 1: Install**
|
||||
- [x] **Step 1: Install**
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
@@ -80,7 +80,7 @@ Task 4 imports `zodOutputFormat` from `@anthropic-ai/sdk/helpers/zod`. If the in
|
||||
|
||||
Both are production dependencies — the worker runs in the container. Confirm they landed under `"dependencies"`, not `"devDependencies"`: the final Docker stage installs with `--omit=dev`, so the wrong section produces a container that fails on the first submission and nowhere else. That is exactly how sharp went wrong in #226.
|
||||
|
||||
- [ ] **Step 2: Write the failing test**
|
||||
- [x] **Step 2: Write the failing test**
|
||||
|
||||
Add to `backend/tests/unit/envValidation.test.ts`:
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('the intake drafting key', () => {
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run it to verify it fails**
|
||||
- [x] **Step 3: Run it to verify it fails**
|
||||
|
||||
```bash
|
||||
cd backend && npx jest -c jest.unit.config.js envValidation
|
||||
@@ -116,7 +116,7 @@ cd backend && npx jest -c jest.unit.config.js envValidation
|
||||
|
||||
Expected: FAIL on the warning assertions.
|
||||
|
||||
- [ ] **Step 4: Add the warning**
|
||||
- [x] **Step 4: Add the warning**
|
||||
|
||||
In `backend/src/envValidation.ts`, alongside the other warning rules:
|
||||
|
||||
@@ -136,7 +136,7 @@ function checkDraftingKey(env: NodeJS.ProcessEnv): string[] {
|
||||
|
||||
Add it to the warning composition the same way the existing rules are composed; do not nest it inside `validateEnv`, which is what keeps that function's cognitive complexity down.
|
||||
|
||||
- [ ] **Step 5: Verify**
|
||||
- [x] **Step 5: Verify**
|
||||
|
||||
```bash
|
||||
cd backend && npm run test:unit && npm run lint && npm run build
|
||||
@@ -144,7 +144,7 @@ cd backend && npm run test:unit && npm run lint && npm run build
|
||||
|
||||
Expected: PASS, lint no new warnings, build clean.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
- [x] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/package.json backend/package-lock.json backend/src/envValidation.ts backend/tests/unit/envValidation.test.ts
|
||||
@@ -161,7 +161,7 @@ git commit -m "build(intake): add the Anthropic SDK and warn when its key is abs
|
||||
**Interfaces:**
|
||||
- Produces: `DraftSchema` (Zod), `type DraftResult = z.infer<typeof DraftSchema>`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
- [x] **Step 1: Write the failing test**
|
||||
|
||||
Create `backend/tests/unit/draftSchema.test.ts`:
|
||||
|
||||
@@ -217,7 +217,7 @@ describe('DraftSchema', () => {
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run it to verify it fails**
|
||||
- [x] **Step 2: Run it to verify it fails**
|
||||
|
||||
```bash
|
||||
cd backend && npx jest -c jest.unit.config.js draftSchema
|
||||
@@ -225,7 +225,7 @@ cd backend && npx jest -c jest.unit.config.js draftSchema
|
||||
|
||||
Expected: FAIL — module not found.
|
||||
|
||||
- [ ] **Step 3: Write the schema**
|
||||
- [x] **Step 3: Write the schema**
|
||||
|
||||
Create `backend/src/intake/draftSchema.ts`:
|
||||
|
||||
@@ -273,7 +273,7 @@ export const DraftSchema = z.object({
|
||||
export type DraftResult = z.infer<typeof DraftSchema>;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run it to verify it passes**
|
||||
- [x] **Step 4: Run it to verify it passes**
|
||||
|
||||
```bash
|
||||
cd backend && npx jest -c jest.unit.config.js draftSchema
|
||||
@@ -281,7 +281,7 @@ cd backend && npx jest -c jest.unit.config.js draftSchema
|
||||
|
||||
Expected: PASS, 6 tests.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
- [x] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/intake/draftSchema.ts backend/tests/unit/draftSchema.test.ts
|
||||
@@ -303,7 +303,7 @@ This is the correctness surface. Read "The correctness surface" above before wri
|
||||
- `buildSystemPrompt(categories: string[], tags: string[]): string`
|
||||
- `buildUserContent(photos: {mediaType: string; base64: string}[], note: string | null): unknown[]`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
- [x] **Step 1: Write the failing test**
|
||||
|
||||
Create `backend/tests/unit/draftPrompt.test.ts`:
|
||||
|
||||
@@ -372,7 +372,7 @@ describe('buildUserContent', () => {
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run it to verify it fails**
|
||||
- [x] **Step 2: Run it to verify it fails**
|
||||
|
||||
```bash
|
||||
cd backend && npx jest -c jest.unit.config.js draftPrompt
|
||||
@@ -380,7 +380,7 @@ cd backend && npx jest -c jest.unit.config.js draftPrompt
|
||||
|
||||
Expected: FAIL — module not found.
|
||||
|
||||
- [ ] **Step 3: Write the prompt builder**
|
||||
- [x] **Step 3: Write the prompt builder**
|
||||
|
||||
Create `backend/src/intake/draftPrompt.ts`:
|
||||
|
||||
@@ -455,7 +455,7 @@ export function buildUserContent(photos: Photo[], note: string | null): unknown[
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run it to verify it passes**
|
||||
- [x] **Step 4: Run it to verify it passes**
|
||||
|
||||
```bash
|
||||
cd backend && npx jest -c jest.unit.config.js draftPrompt
|
||||
@@ -463,7 +463,7 @@ cd backend && npx jest -c jest.unit.config.js draftPrompt
|
||||
|
||||
Expected: PASS, 8 tests.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
- [x] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/intake/draftPrompt.ts backend/tests/unit/draftPrompt.test.ts
|
||||
@@ -487,12 +487,12 @@ Rates confirmed against the pricing page on 2026-08-31, not recalled: Sonnet 5 $
|
||||
**Interfaces:**
|
||||
- Produces: `DRAFTING_MODELS`, `DEFAULT_DRAFTING_MODEL`, `isDraftingModel()`, `costMicros()`, and a `draftingModel` admin setting.
|
||||
|
||||
- [ ] **Step 1: The catalogue, test first.** `costMicros(model, input, output)` in whole micros. An unrecognised model must price above zero — a budget that reads as unspent however much was spent is the one failure a spend guard cannot have.
|
||||
- [ ] **Step 2: Add a `choice` type to `adminSettings.ts`.** The module's own docstring says adding a setting means adding a row and nothing else; that holds for `hours` and `text`, and a third type is what makes it hold for a constrained one. Row: `{ key: 'drafting_model', name: 'draftingModel', type: 'choice', fallback: DEFAULT_DRAFTING_MODEL, options: [...] }`.
|
||||
- [ ] **Step 3: Validate membership in the PUT route**, as a third loop beside the hours and text loops. A value outside the set is a 400, not a stored string that breaks drafting later.
|
||||
- [ ] **Step 4: The dropdown in `Settings.tsx`**, showing each model's price so the person switching can see that Opus costs 2.5x Sonnet before they pick it.
|
||||
- [ ] **Step 5:** `npm run lint && npm run build && npm run test:unit`, and the frontend's checks.
|
||||
- [ ] **Step 6: Commit.**
|
||||
- [x] **Step 1: The catalogue, test first.** `costMicros(model, input, output)` in whole micros. An unrecognised model must price above zero — a budget that reads as unspent however much was spent is the one failure a spend guard cannot have.
|
||||
- [x] **Step 2: Add a `choice` type to `adminSettings.ts`.** The module's own docstring says adding a setting means adding a row and nothing else; that holds for `hours` and `text`, and a third type is what makes it hold for a constrained one. Row: `{ key: 'drafting_model', name: 'draftingModel', type: 'choice', fallback: DEFAULT_DRAFTING_MODEL, options: [...] }`.
|
||||
- [x] **Step 3: Validate membership in the PUT route**, as a third loop beside the hours and text loops. A value outside the set is a 400, not a stored string that breaks drafting later.
|
||||
- [x] **Step 4: The dropdown in `Settings.tsx`**, showing each model's price so the person switching can see that Opus costs 2.5x Sonnet before they pick it.
|
||||
- [x] **Step 5:** `npm run lint && npm run build && npm run test:unit`, and the frontend's checks.
|
||||
- [x] **Step 6: Commit.**
|
||||
|
||||
---
|
||||
|
||||
@@ -509,7 +509,7 @@ Rates confirmed against the pricing page on 2026-08-31, not recalled: Sonnet 5 $
|
||||
- `costMicros(model: string, inputTokens: number, outputTokens: number): number`
|
||||
- `draftListing(client, input): Promise<{ draft: DraftResult; usage: {...} }>`
|
||||
|
||||
- [ ] **Step 1: Write the failing cost test**
|
||||
- [x] **Step 1: Write the failing cost test**
|
||||
|
||||
Create `backend/tests/unit/draftCost.test.ts`:
|
||||
|
||||
@@ -542,7 +542,7 @@ describe('costMicros', () => {
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run it to verify it fails**
|
||||
- [x] **Step 2: Run it to verify it fails**
|
||||
|
||||
```bash
|
||||
cd backend && npx jest -c jest.unit.config.js draftCost
|
||||
@@ -550,7 +550,7 @@ cd backend && npx jest -c jest.unit.config.js draftCost
|
||||
|
||||
Expected: FAIL — module not found.
|
||||
|
||||
- [ ] **Step 3: Write the client factory**
|
||||
- [x] **Step 3: Write the client factory**
|
||||
|
||||
Create `backend/src/intake/anthropicClient.ts`:
|
||||
|
||||
@@ -587,7 +587,7 @@ export function resetAnthropicClient(): void {
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Write the drafting call**
|
||||
- [x] **Step 4: Write the drafting call**
|
||||
|
||||
Create `backend/src/intake/draftListing.ts`:
|
||||
|
||||
@@ -681,7 +681,7 @@ export async function draftListing(
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run the tests and build**
|
||||
- [x] **Step 5: Run the tests and build**
|
||||
|
||||
```bash
|
||||
cd backend && npx jest -c jest.unit.config.js draftCost && npm run build && npm run lint
|
||||
@@ -689,7 +689,7 @@ cd backend && npx jest -c jest.unit.config.js draftCost && npm run build && npm
|
||||
|
||||
Expected: PASS 5 tests, build clean, no new lint warnings.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
- [x] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/intake/anthropicClient.ts backend/src/intake/draftListing.ts backend/tests/unit/draftCost.test.ts
|
||||
@@ -708,7 +708,7 @@ git commit -m "feat(intake): draft a listing from photos and a note (#223)"
|
||||
- Consumes: `DraftResult`
|
||||
- Produces: `applyDraft(client: PoolClient, itemId: number, outcome: DraftOutcome): Promise<void>`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
- [x] **Step 1: Write the failing test**
|
||||
|
||||
Create `backend/tests/integration/drafting.integration.test.ts`:
|
||||
|
||||
@@ -839,7 +839,7 @@ describe('applying a draft', () => {
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run it to verify it fails**
|
||||
- [x] **Step 2: Run it to verify it fails**
|
||||
|
||||
Bring up a database first (see Global Constraints), then:
|
||||
|
||||
@@ -849,7 +849,7 @@ cd backend && npx jest -c jest.integration.config.js --runInBand drafting.integr
|
||||
|
||||
Expected: FAIL — module not found.
|
||||
|
||||
- [ ] **Step 3: Write it**
|
||||
- [x] **Step 3: Write it**
|
||||
|
||||
Create `backend/src/intake/applyDraft.ts`:
|
||||
|
||||
@@ -932,7 +932,7 @@ export async function applyDraft(
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run it to verify it passes**
|
||||
- [x] **Step 4: Run it to verify it passes**
|
||||
|
||||
```bash
|
||||
cd backend && npx jest -c jest.integration.config.js --runInBand drafting.integration
|
||||
@@ -940,7 +940,7 @@ cd backend && npx jest -c jest.integration.config.js --runInBand drafting.integr
|
||||
|
||||
Expected: PASS, 5 tests.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
- [x] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/intake/applyDraft.ts backend/tests/integration/drafting.integration.test.ts
|
||||
@@ -959,7 +959,7 @@ git commit -m "feat(intake): record a draft without publishing it (#223)"
|
||||
- Consumes: everything above
|
||||
- Produces: `draftQueued(limit?: number): Promise<{ drafted: number; failed: number; skipped: number }>`
|
||||
|
||||
- [ ] **Step 1: Write the failing tests**
|
||||
- [x] **Step 1: Write the failing tests**
|
||||
|
||||
Append to `backend/tests/integration/drafting.integration.test.ts`:
|
||||
|
||||
@@ -1006,7 +1006,7 @@ describe('the drafting worker', () => {
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run it to verify it fails**
|
||||
- [x] **Step 2: Run it to verify it fails**
|
||||
|
||||
```bash
|
||||
cd backend && npx jest -c jest.integration.config.js --runInBand drafting.integration
|
||||
@@ -1014,7 +1014,7 @@ cd backend && npx jest -c jest.integration.config.js --runInBand drafting.integr
|
||||
|
||||
Expected: FAIL — module not found.
|
||||
|
||||
- [ ] **Step 3: Write the worker**
|
||||
- [x] **Step 3: Write the worker**
|
||||
|
||||
Create `backend/src/intake/draftingWorker.ts`:
|
||||
|
||||
@@ -1156,7 +1156,7 @@ export async function draftQueued(
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run it to verify it passes**
|
||||
- [x] **Step 4: Run it to verify it passes**
|
||||
|
||||
```bash
|
||||
cd backend && npx jest -c jest.integration.config.js --runInBand drafting.integration
|
||||
@@ -1164,7 +1164,7 @@ cd backend && npx jest -c jest.integration.config.js --runInBand drafting.integr
|
||||
|
||||
Expected: PASS, 7 tests.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
- [x] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/intake/draftingWorker.ts backend/tests/integration/drafting.integration.test.ts
|
||||
@@ -1178,7 +1178,7 @@ git commit -m "feat(intake): draft queued submissions without ever losing one (#
|
||||
**Files:**
|
||||
- Modify: `backend/src/routes/intake.ts`, `backend/src/server.ts`
|
||||
|
||||
- [ ] **Step 1: Kick the worker after a successful submission**
|
||||
- [x] **Step 1: Kick the worker after a successful submission**
|
||||
|
||||
In `backend/src/routes/intake.ts`, after `await client.query('COMMIT');` and before the response:
|
||||
|
||||
@@ -1196,7 +1196,7 @@ Import it at the top:
|
||||
import { draftQueued } from '../intake/draftingWorker';
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add the sweeper**
|
||||
- [x] **Step 2: Add the sweeper**
|
||||
|
||||
In `backend/src/server.ts`, beside the existing schedules:
|
||||
|
||||
@@ -1214,7 +1214,7 @@ Import it beside the other background work:
|
||||
import { draftQueued } from './intake/draftingWorker';
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify nothing regressed**
|
||||
- [x] **Step 3: Verify nothing regressed**
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
@@ -1226,7 +1226,7 @@ npx jest -c jest.integration.config.js --runInBand
|
||||
|
||||
Expected: all pass. The intake suite matters most — submissions must still succeed with no key configured, which is the state the test database runs in.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
- [x] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/src/routes/intake.ts backend/src/server.ts
|
||||
@@ -1235,6 +1235,9 @@ git commit -m "feat(intake): run the drafting worker after a submission and on a
|
||||
|
||||
---
|
||||
|
||||
|
||||
> **Tasks 1-7 complete** as of 2026-08-31, on `feature/223-drafting-worker`. Task 8 is the only manual step and is blocked on an API key existing.
|
||||
|
||||
### Task 8: One real call
|
||||
|
||||
Everything above is stubbed. This is the only step that spends money, and it is the only one that proves the prompt produces something worth reading.
|
||||
|
||||
Reference in New Issue
Block a user