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>
12 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 13-ai-field-placement-and-pre-fill | 01 | tdd | 1 |
|
true |
|
|
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 extractionsrc/lib/ai/field-placement.ts— GPT-4o-mini structured output + aiCoordsToPagePdfSpace utilitysrc/lib/pdf/__tests__/ai-coords.test.ts— unit test for aiCoordsToPagePdfSpace (TDD plan: write test first, then implement)
<execution_context> @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/13-ai-field-placement-and-pre-fill/13-RESEARCH.mdRead next.config.ts and check for Next.js guide in node_modules/next/dist/docs/ before writing route code.
From teressa-copeland-homes/src/lib/db/schema.ts:
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):
// 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:
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)
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=6120.1=61.2; fieldH=7920.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=6120.9=550.8; fieldH=7920.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
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx jest src/lib/pdf/tests/ai-coords.test.ts --no-coverage 2>&1 | tail -20
Test file exists, tests run and fail with "Cannot find module" or similar import error — RED phase confirmed
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
PageTextinterface:{ page: number; text: string; width: number; height: number } - Export
extractPdfText(filePath: string): Promise<PageText[]> - Uses
readFilefromnode:fs/promises, loads PDF bytes asUint8Array - Iterates all pages, calls
page.getViewport({ scale: 1.0 })for dimensions,getTextContent()for text - Joins text items by filtering
'str' in itemthen 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:
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:
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:
- Build the FIELD_PLACEMENT_SCHEMA (exact schema from RESEARCH.md Pattern 2 — all fields required,
additionalProperties: falseat every nesting level,strict: true) - Build
pagesSummarystring: for each page, format asPage {n} ({w}x{h}pt):\n{text}\n— truncate text to 2000 chars per page - Create
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }) - Check
if (!process.env.OPENAI_API_KEY) throw new Error('OPENAI_API_KEY not configured') - Call
openai.chat.completions.createwith 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 - Parse response:
JSON.parse(response.choices[0].message.content!)typed as{ fields: AiFieldCoords[] } - Map each AI field to
SignatureFieldDatausingaiCoordsToPagePdfSpacewith the page's dimensions frompageTexts.find(p => p.page === field.page) - Assign
id: crypto.randomUUID()to each field, settype: field.fieldType - Default field sizes: checkbox=24×24pt, all others=144×36pt (ignore AI width/height for consistent sizing — override with standard sizes)
- Build
textFillData: Record<string, string>— for each text field whereprefillValueis non-empty string, add{ [field.id]: prefillValue } - Return
{ fields: SignatureFieldData[], textFillData }
Add comment: // server-only — never import from client components
Step 3: Run tests (GREEN)
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'.
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx jest src/lib/pdf/tests/ai-coords.test.ts --no-coverage
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.
<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>