docs(intake): mark the drafting worker plan complete through task 7 (#223)
Linting / lint (pull_request) Successful in 2m13s
SonarQube Analysis / sonarqube (pull_request) Successful in 20m55s

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:
2026-08-31 19:49:42 -05:00
co-authored by Claude Opus 5
parent 6a1466680d
commit 58d1bd15e8
@@ -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.