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

12 KiB
Raw Permalink Blame History

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
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
true
AI-01
AI-02
truths artifacts key_links
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
path provides exports
teressa-copeland-homes/src/lib/ai/extract-text.ts extractPdfText(filePath) returning PageText[] with page, text, width, height
extractPdfText
PageText
path provides exports
teressa-copeland-homes/src/lib/ai/field-placement.ts aiCoordsToPagePdfSpace() + classifyFieldsWithAI() using GPT-4o-mini structured output
aiCoordsToPagePdfSpace
classifyFieldsWithAI
AiFieldCoords
path provides contains
teressa-copeland-homes/src/lib/pdf/__tests__/ai-coords.test.ts Unit tests for aiCoordsToPagePdfSpace with US Letter dimensions aiCoordsToPagePdfSpace
from to via pattern
teressa-copeland-homes/src/lib/ai/field-placement.ts teressa-copeland-homes/src/lib/ai/extract-text.ts import { PageText } from './extract-text' from.*extract-text
from to via pattern
teressa-copeland-homes/src/lib/ai/field-placement.ts openai SDK import OpenAI from 'openai' import OpenAI
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)

<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.md

Read 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)
Task 1: Write failing unit test for aiCoordsToPagePdfSpace teressa-copeland-homes/src/lib/pdf/__tests__/ai-coords.test.ts 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=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

Task 2: Implement extract-text.ts and field-placement.ts (GREEN) teressa-copeland-homes/src/lib/ai/extract-text.ts teressa-copeland-homes/src/lib/ai/field-placement.ts **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:

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:

  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)

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.

- `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`)

<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>
After completion, create `.planning/phases/13-ai-field-placement-and-pre-fill/13-01-SUMMARY.md`