Files
Chandler Copeland df4676c23c docs(13): create phase 13 AI field placement plan
4 plans in 4 sequential waves covering:
- Plan 01 (TDD): openai install, extract-text.ts, field-placement.ts, aiCoordsToPagePdfSpace unit test
- Plan 02: POST /api/documents/[id]/ai-prepare route with all guards
- Plan 03: UI wiring — aiPlacementKey in FieldPlacer, AI Auto-place button in PreparePanel
- Plan 04: Unit test gate + human E2E verification checkpoint

Satisfies AI-01, AI-02. Completes v1.1 Smart Document Preparation milestone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 16:54:43 -06:00

256 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 13-ai-field-placement-and-pre-fill
plan: 01
type: tdd
wave: 1
depends_on: []
files_modified:
- teressa-copeland-homes/src/lib/ai/extract-text.ts
- teressa-copeland-homes/src/lib/ai/field-placement.ts
- teressa-copeland-homes/src/lib/pdf/__tests__/ai-coords.test.ts
autonomous: true
requirements:
- AI-01
- AI-02
must_haves:
truths:
- "pdfjs-dist extracts per-page text and page dimensions from a server-side file path"
- "aiCoordsToPagePdfSpace converts AI top-left percentage coordinates to PDF bottom-left point coordinates with correct Y-axis flip"
- "GPT-4o-mini structured output call returns a typed AiPlacedField[] array via manual json_schema response_format"
artifacts:
- path: "teressa-copeland-homes/src/lib/ai/extract-text.ts"
provides: "extractPdfText(filePath) returning PageText[] with page, text, width, height"
exports: ["extractPdfText", "PageText"]
- path: "teressa-copeland-homes/src/lib/ai/field-placement.ts"
provides: "aiCoordsToPagePdfSpace() + classifyFieldsWithAI() using GPT-4o-mini structured output"
exports: ["aiCoordsToPagePdfSpace", "classifyFieldsWithAI", "AiFieldCoords"]
- path: "teressa-copeland-homes/src/lib/pdf/__tests__/ai-coords.test.ts"
provides: "Unit tests for aiCoordsToPagePdfSpace with US Letter dimensions"
contains: "aiCoordsToPagePdfSpace"
key_links:
- from: "teressa-copeland-homes/src/lib/ai/field-placement.ts"
to: "teressa-copeland-homes/src/lib/ai/extract-text.ts"
via: "import { PageText } from './extract-text'"
pattern: "from.*extract-text"
- from: "teressa-copeland-homes/src/lib/ai/field-placement.ts"
to: "openai SDK"
via: "import OpenAI from 'openai'"
pattern: "import OpenAI"
---
<objective>
Install the openai SDK and create the two server-only AI utility modules plus a unit test for the coordinate conversion function.
Purpose: These are the foundation layer for the AI auto-place feature. The route (Plan 02) and the UI button (Plan 03) both depend on these utilities existing and being correct. The coordinate conversion is the most failure-prone part of the feature — the unit test locks in correctness before it is wired into a live route.
Output:
- `src/lib/ai/extract-text.ts` — pdfjs-dist server-side text extraction
- `src/lib/ai/field-placement.ts` — GPT-4o-mini structured output + aiCoordsToPagePdfSpace utility
- `src/lib/pdf/__tests__/ai-coords.test.ts` — unit test for aiCoordsToPagePdfSpace (TDD plan: write test first, then implement)
</objective>
<execution_context>
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/13-ai-field-placement-and-pre-fill/13-RESEARCH.md
Read next.config.ts and check for Next.js guide in node_modules/next/dist/docs/ before writing route code.
</context>
<interfaces>
<!-- Key types the executor needs. Extracted from codebase. -->
From teressa-copeland-homes/src/lib/db/schema.ts:
```typescript
export type SignatureFieldType =
| 'client-signature'
| 'initials'
| 'text'
| 'checkbox'
| 'date'
| 'agent-signature'
| 'agent-initials';
export interface SignatureFieldData {
id: string;
page: number; // 1-indexed
x: number; // PDF user space, bottom-left origin, points
y: number; // PDF user space, bottom-left origin, points
width: number; // PDF points (default: 144 — 2 inches)
height: number; // PDF points (default: 36 — 0.5 inches)
type?: SignatureFieldType;
}
```
From teressa-copeland-homes/src/lib/pdf/__tests__/prepare-document.test.ts (unit test pattern):
```typescript
// Jest + ts-jest (see package.json jest config)
// Test file pattern: src/lib/pdf/__tests__/*.test.ts
// Run: cd teressa-copeland-homes && npx jest src/lib/pdf/__tests__/ai-coords.test.ts
```
Existing coordinate formula from FieldPlacer.tsx (lines 289-290) — aiCoordsToPagePdfSpace MUST replicate this:
```typescript
const pdfX = (clampedX / renderedW) * pageInfo.originalWidth;
const pdfY = ((renderedH - (clampedY + fieldHpx)) / renderedH) * pageInfo.originalHeight;
// Where clampedX/clampedY are screen top-left origin, fieldHpx is field height in pixels
// Translated to percentage inputs:
// pdfX = (xPct / 100) * pageWidth
// screenY = (yPct / 100) * pageHeight (top-left origin from AI)
// fieldH = (heightPct / 100) * pageHeight
// pdfY = pageHeight - screenY - fieldH (bottom edge in PDF space)
```
</interfaces>
<tasks>
<task type="tdd">
<name>Task 1: Write failing unit test for aiCoordsToPagePdfSpace</name>
<files>teressa-copeland-homes/src/lib/pdf/__tests__/ai-coords.test.ts</files>
<action>
Create the test file. Import `aiCoordsToPagePdfSpace` from `'../../../lib/ai/field-placement'` (it does not exist yet — the import will fail, making the test RED). Write these test cases covering US Letter (612×792 pts):
Test group: "aiCoordsToPagePdfSpace — AI top-left percentage to PDF bottom-left points"
Case 1 — text field near top of page:
- Input: { page:1, fieldType:'text', xPct:10, yPct:5, widthPct:30, heightPct:5, prefillValue:'' }, pageWidth:612, pageHeight:792
- Expected: x ≈ 61.2, y ≈ 712.8, width ≈ 183.6, height ≈ 39.6
- Formula: x=612*0.1=61.2; fieldH=792*0.05=39.6; screenY=792*0.05=39.6; y=792-39.6-39.6=712.8
Case 2 — checkbox near bottom-right:
- Input: { page:1, fieldType:'checkbox', xPct:90, yPct:95, widthPct:3, heightPct:3, prefillValue:'' }, pageWidth:612, pageHeight:792
- Expected: x ≈ 550.8, y ≈ 15.84, width ≈ 18.36, height ≈ 23.76
- Formula: x=612*0.9=550.8; fieldH=792*0.03=23.76; screenY=792*0.95=752.4; y=792-752.4-23.76=15.84
Case 3 — field at exact center:
- Input: { page:1, fieldType:'client-signature', xPct:50, yPct:50, widthPct:20, heightPct:5, prefillValue:'' }, pageWidth:612, pageHeight:792
- Expected: x ≈ 306, y ≈ 357.84, width ≈ 122.4, height ≈ 39.6
- Formula: x=306; fieldH=39.6; screenY=396; y=792-396-39.6=356.4 (use toBeCloseTo(1))
Run: `cd teressa-copeland-homes && npx jest src/lib/pdf/__tests__/ai-coords.test.ts --no-coverage`
Tests must FAIL (RED) because the module does not exist yet. Commit: `test(13-01): add failing aiCoordsToPagePdfSpace unit tests`
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx jest src/lib/pdf/__tests__/ai-coords.test.ts --no-coverage 2>&1 | tail -20</automated>
</verify>
<done>Test file exists, tests run and fail with "Cannot find module" or similar import error — RED phase confirmed</done>
</task>
<task type="tdd">
<name>Task 2: Implement extract-text.ts and field-placement.ts (GREEN)</name>
<files>
teressa-copeland-homes/src/lib/ai/extract-text.ts
teressa-copeland-homes/src/lib/ai/field-placement.ts
</files>
<action>
**Step 0: Install openai SDK**
```bash
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm install openai
```
**Step 1: Create `src/lib/ai/extract-text.ts`**
Use the exact pattern from RESEARCH.md Pattern 1. Key points:
- Import from `'pdfjs-dist/legacy/build/pdf.mjs'`
- Set `GlobalWorkerOptions.workerSrc = ''` at module level (Node.js fake-worker mode — NOT browser pattern)
- Export `PageText` interface: `{ page: number; text: string; width: number; height: number }`
- Export `extractPdfText(filePath: string): Promise<PageText[]>`
- Uses `readFile` from `node:fs/promises`, loads PDF bytes as `Uint8Array`
- Iterates all pages, calls `page.getViewport({ scale: 1.0 })` for dimensions, `getTextContent()` for text
- Joins text items by filtering `'str' in item` then mapping `.str`, joined with `' '`
- Cap text per page at 2000 chars with `text.slice(0, 2000)` to stay within GPT-4o-mini context
- Add comment: `// server-only — never import from client components`
**Step 2: Create `src/lib/ai/field-placement.ts`**
Exports:
```typescript
export interface AiFieldCoords {
page: number;
fieldType: SignatureFieldType;
xPct: number; // % from left, top-left origin (AI output)
yPct: number; // % from top, top-left origin (AI output)
widthPct: number;
heightPct: number;
prefillValue: string;
}
export function aiCoordsToPagePdfSpace(
coords: AiFieldCoords,
pageWidth: number,
pageHeight: number,
): { x: number; y: number; width: number; height: number }
```
Implement `aiCoordsToPagePdfSpace` using the formula:
```typescript
const fieldWidth = (coords.widthPct / 100) * pageWidth;
const fieldHeight = (coords.heightPct / 100) * pageHeight;
const screenX = (coords.xPct / 100) * pageWidth;
const screenY = (coords.yPct / 100) * pageHeight; // screen Y from top
const x = screenX;
const y = pageHeight - screenY - fieldHeight; // Y-axis flip: bottom-left origin
return { x, y, width: fieldWidth, height: fieldHeight };
```
Also export `classifyFieldsWithAI(pageTexts: PageText[], client: { name: string | null; propertyAddress: string | null } | null): Promise<{ fields: SignatureFieldData[], textFillData: Record<string, string> }>`
Implementation of `classifyFieldsWithAI`:
1. Build the FIELD_PLACEMENT_SCHEMA (exact schema from RESEARCH.md Pattern 2 — all fields required, `additionalProperties: false` at every nesting level, `strict: true`)
2. Build `pagesSummary` string: for each page, format as `Page {n} ({w}x{h}pt):\n{text}\n` — truncate text to 2000 chars per page
3. Create `const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })`
4. Check `if (!process.env.OPENAI_API_KEY) throw new Error('OPENAI_API_KEY not configured')`
5. Call `openai.chat.completions.create` with model `'gpt-4o-mini'`, manual json_schema response_format (NOT zodResponseFormat), system prompt from RESEARCH.md Pattern 2, user message includes client name, property address, pagesSummary
6. Parse response: `JSON.parse(response.choices[0].message.content!)` typed as `{ fields: AiFieldCoords[] }`
7. Map each AI field to `SignatureFieldData` using `aiCoordsToPagePdfSpace` with the page's dimensions from `pageTexts.find(p => p.page === field.page)`
8. Assign `id: crypto.randomUUID()` to each field, set `type: field.fieldType`
9. Default field sizes: checkbox=24×24pt, all others=144×36pt (ignore AI width/height for consistent sizing — override with standard sizes)
10. Build `textFillData: Record<string, string>` — for each text field where `prefillValue` is non-empty string, add `{ [field.id]: prefillValue }`
11. Return `{ fields: SignatureFieldData[], textFillData }`
Add comment: `// server-only — never import from client components`
**Step 3: Run tests (GREEN)**
```bash
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx jest src/lib/pdf/__tests__/ai-coords.test.ts --no-coverage
```
Tests must PASS. Commit: `feat(13-01): implement aiCoordsToPagePdfSpace and AI field utilities`
Note on TypeScript import: if `'pdfjs-dist/legacy/build/pdf.mjs'` triggers type errors, add `// @ts-ignore` above the import or use the main types: `import type { PDFDocumentProxy, TextItem } from 'pdfjs-dist'`.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx jest src/lib/pdf/__tests__/ai-coords.test.ts --no-coverage</automated>
</verify>
<done>All 3+ aiCoordsToPagePdfSpace test cases pass (GREEN). extract-text.ts and field-placement.ts exist with correct exports. openai package is in package.json dependencies.</done>
</task>
</tasks>
<verification>
- `npx jest src/lib/pdf/__tests__/ai-coords.test.ts --no-coverage` passes all cases
- `src/lib/ai/extract-text.ts` exports `extractPdfText` and `PageText`
- `src/lib/ai/field-placement.ts` exports `aiCoordsToPagePdfSpace`, `classifyFieldsWithAI`, `AiFieldCoords`
- `openai` is listed in `package.json` dependencies (not devDependencies)
- Neither file is imported from any client component (server-only comment guard present)
- `GlobalWorkerOptions.workerSrc = ''` set in extract-text.ts (NOT the browser `new URL(...)` pattern)
- Manual `json_schema` response_format used (NOT `zodResponseFormat`)
</verification>
<success_criteria>
- Unit test passes: aiCoordsToPagePdfSpace correctly flips Y-axis for all 3 test cases
- openai SDK installed and importable
- Both AI utility modules exist, export correct types, have server-only guard comments
- TypeScript compiles without errors: `npx tsc --noEmit`
</success_criteria>
<output>
After completion, create `.planning/phases/13-ai-field-placement-and-pre-fill/13-01-SUMMARY.md`
</output>