# Phase 13: AI Field Placement and Pre-fill - Research **Researched:** 2026-04-03 (re-research — complete rewrite) **Domain:** pdfjs-dist text-layer blank extraction + GPT-4.1 structured output field classification + coordinate system **Confidence:** HIGH ## Phase Requirements | ID | Description | Research Support | |----|-------------|-----------------| | AI-01 | Agent can click one button to have AI auto-place all field types (text, checkbox, initials, date, agent signature, client signature) on a PDF in correct positions | extractBlanks() extracts blanks with exact PDF user-space coords from pdfjs text layer; classifyFieldsWithAI() sends blank descriptions to GPT-4.1 for type classification; no coordinate conversion needed (pdfjs coords stored directly) | | AI-02 | AI pre-fills text fields with known values from the client profile (name, property address, date) | classifyFieldsWithAI() returns textFillData keyed by field UUID; merged into DocumentPageClient.textFillData state | --- ## Summary Phase 13 is implemented through Plans 01–03 and is awaiting final E2E verification in Plan 04. Three plans are complete. The architecture evolved significantly from the original research: the system now uses **direct PDF text-layer extraction** (pdfjs-dist `getTextContent()` with transform matrix coordinates) rather than GPT-4o vision. This eliminates the coordinate conversion bug entirely — pdfjs returns coordinates already in PDF user-space (bottom-left origin, points), which is exactly what FieldPlacer stores and renders. The original vision-based approach (render pages as JPEG → GPT-4o → xPct/yPct → aiCoordsToPagePdfSpace conversion) was attempted and abandoned due to a systematic 30-40% vertical offset that could not be resolved. The text-extraction approach (extract blank positions from pdfjs text layer → GPT-4.1 for type classification only → store raw coordinates) is now working code in the repository. **Plan 04 is the only remaining task**: run unit tests, fix the TypeScript build, and perform human E2E verification. The `ai-coords.test.ts` file was deleted when the vision approach was abandoned and must NOT be recreated for the new architecture (it tested a function that no longer exists). Plan 04 Task 1 must be updated to reflect the current test infrastructure. **Primary recommendation:** Plan 04 should confirm TypeScript compiles clean, run `prepare-document.test.ts` (10 tests passing), then proceed directly to human E2E verification. The coordinate bug is resolved by architecture — no code fix required for coordinates. --- ## Current Implementation State ### What Is Already Built (Plans 01–03 Complete) The following files are implemented and committed: | File | Status | Purpose | |------|--------|---------| | `src/lib/ai/extract-text.ts` | Complete (uncommitted changes) | pdfjs-dist blank extraction via text layer | | `src/lib/ai/field-placement.ts` | Complete (uncommitted changes) | GPT-4.1 field type classification | | `src/app/api/documents/[id]/ai-prepare/route.ts` | Complete (uncommitted changes) | POST route orchestrating the pipeline | | `src/lib/pdf/__tests__/ai-coords.test.ts` | **Deleted** | Was for vision approach; no longer needed | | `src/lib/pdf/__tests__/prepare-document.test.ts` | Complete | 10 tests passing — Y-flip formula | ### Architecture (Current — Text Extraction Based) ``` PDF file ↓ extractBlanks() [extract-text.ts] → pdfjs getTextContent() → transform[4]=x, transform[5]=y (PDF user-space, bottom-left origin) → 4 detection strategies (underscore runs, embedded underscores, bracket items) → groupIntoLines() with ±5pt y-tolerance → deduplication for Strategy 3+4 overlap → returns BlankField[] with {page, x, y, width, contextBefore, contextAfter, contextAbove, contextBelow, rowIndex, rowTotal} ↓ classifyFieldsWithAI() [field-placement.ts] → GPT-4.1 receives compact text descriptions (index, page, row=N/T, context strings) → returns {index, fieldType, prefillValue} per blank → deterministic post-processing (Rules A/B/C/D) overrides AI errors → FIELD_HEIGHTS map (type → height in pts) → SIZE_LIMITS map (type → {minW, maxW} in pts) → stores: { id: UUID, page, x: blank.x, y: blank.y-2, width: clamped, height: by-type } → returns { fields: SignatureFieldData[], textFillData: Record } ↓ POST /api/documents/[id]/ai-prepare [route.ts] → writes fields to DB (signatureFields column) → returns { fields, textFillData } ↓ DocumentPageClient (React) → setAiPlacementKey(k+1) → FieldPlacer re-fetches from DB → setTextFillData(prev => { ...prev, ...aiTextFill }) — merge, not replace ``` ### Coordinate System — Fully Resolved **The coordinate bug from the vision approach is NOT present in the current architecture.** The text-extraction approach works because: - pdfjs `item.transform[4]` = x position in PDF user-space (points, left from page left edge) - pdfjs `item.transform[5]` = y position in PDF user-space (points, up from page bottom) - These are stored directly as `field.x` and `field.y` in SignatureFieldData - FieldPlacer renders stored fields using `pdfToScreenCoords(field.x, field.y, renderedW, renderedH, pageInfo)` which uses `pageInfo.originalWidth/originalHeight` from `page.view[2]/page.view[3]` (react-pdf mediaBox dimensions) - Since both extraction (pdfjs transform matrix) and rendering (react-pdf mediaBox) read from the same PDF mediaBox, they are inherently consistent **No `aiCoordsToPagePdfSpace` conversion function is needed or present in the current code.** The `aiCoordsToPagePdfSpace` function and its test (`ai-coords.test.ts`) were created for the vision approach and then deleted when the approach changed. Do not recreate them. --- ## Standard Stack ### Core | Library | Version | Purpose | Why Standard | |---------|---------|---------|--------------| | pdfjs-dist | 5.4.296 (hoisted from react-pdf) | PDF text-layer extraction via `getTextContent()` | Already installed; legacy build works in Node.js with file:// workerSrc | | openai | ^6.32.0 (installed) | GPT-4.1 structured output for field type classification | Official SDK; installed; manual json_schema required (Zod v4 incompatibility) | ### Supporting | Library | Version | Purpose | When to Use | |---------|---------|---------|-------------| | @napi-rs/canvas | ^0.1.97 (installed, no longer used) | Was for server-side JPEG rendering | Kept in package.json but no longer imported; `serverExternalPackages` still lists it in next.config.ts — leave that entry to avoid breaking changes | | crypto.randomUUID() | Node built-in | UUID generation for field IDs | Used in classifyFieldsWithAI to assign IDs before textFillData keying | ### Verified Package Versions ```bash # pdfjs-dist version confirmed: cat node_modules/pdfjs-dist/package.json | grep '"version"' # → "version": "5.4.296" # openai version confirmed: cat node_modules/openai/package.json | grep '"version"' ``` **No installation needed** — all required packages are already in node_modules. --- ## Architecture Patterns ### Recommended Project Structure (Current) ``` src/ ├── lib/ │ ├── ai/ │ │ ├── extract-text.ts # pdfjs-dist blank extraction (4 strategies) │ │ └── field-placement.ts # GPT-4.1 type classification + post-processing │ └── pdf/ │ └── __tests__/ │ └── prepare-document.test.ts # 10 tests — Y-flip formula (passing) └── app/ └── api/ └── documents/ └── [id]/ └── ai-prepare/ └── route.ts # POST handler ``` ### Pattern 1: pdfjs-dist Blank Extraction (Text Layer) **What:** Use pdfjs `getTextContent()` to get all text items. Each item has a `transform` matrix where `transform[4]` = x and `transform[5]` = y in PDF user-space (bottom-left origin, points). Find underscore sequences (Strategy 1: pure runs, Strategy 2: embedded runs) and bracket patterns (Strategy 3: single-item `[ ]`, Strategy 4: multi-item `[ … ]`). Group items into lines with ±5pt y-tolerance. **Why text-layer over vision:** Coordinates are exact (sub-point accuracy). No DPI scaling math, no image rendering, no API image tokens, no coordinate conversion needed. **pdfjs-dist 5.x workerSrc (critical):** Must use `file://` URL pointing to the worker .mjs file — empty string is falsy and causes PDFWorker to throw before the fake-worker import runs. ```typescript // Source: extract-text.ts (confirmed working) import { join } from 'node:path'; GlobalWorkerOptions.workerSrc = `file://${join(process.cwd(), 'node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs')}`; ``` **Text item transform matrix:** `[scaleX, skewY, skewX, scaleY, translateX, translateY]` - `transform[4]` = x left edge of item (PDF points from page left) - `transform[5]` = y baseline of item (PDF points from page bottom) - `transform[0]` = font size (approximately) when no rotation ### Pattern 2: GPT-4.1 Type Classification (Text-Only, No Vision) **What:** Send a compact text description of each detected blank to GPT-4.1. The description includes blank index, page number, row position metadata (`row=N/T`), and context strings (contextBefore, contextAfter, contextAbove, contextBelow). AI returns only the field type and prefill value — coordinates come from pdfjs directly. **Schema:** Manual `json_schema` with `strict: true`, all properties in `required`, `additionalProperties: false` at every nesting level. Do NOT use `zodResponseFormat` (broken with Zod v4). ```typescript // Source: field-placement.ts (confirmed working) const CLASSIFICATION_SCHEMA = { type: 'object', properties: { fields: { type: 'array', items: { type: 'object', properties: { index: { type: 'integer' }, fieldType: { type: 'string', enum: ['text', 'initials', 'date', 'client-signature', 'agent-signature', 'agent-initials', 'checkbox'] }, prefillValue: { type: 'string' }, }, required: ['index', 'fieldType', 'prefillValue'], additionalProperties: false, }, }, }, required: ['fields'], additionalProperties: false, } as const; ``` **Note:** The `checkbox` type is in the enum so the AI can classify inline checkboxes — but fields with `fieldType === 'checkbox'` are filtered out (not added to the fields array). They represent selection options, not placed fields. ### Pattern 3: Coordinate Handling — No Conversion Needed **What:** Blank coordinates from pdfjs text layer are already in PDF user-space. Store them directly. ```typescript // Source: field-placement.ts (confirmed working) const y = Math.max(0, blank.y - 2); // -2pt: anchor just below baseline (underscores descend slightly) fields.push({ id, page: blank.page, x: blank.x, y, width, height, type: fieldType }); ``` **The -2pt y offset:** Underscore characters descend slightly below the text baseline. Moving the field bottom edge 2pt below the baseline positions the field box to sit ON the underline visually. **Why this works without conversion:** pdfjs `transform[5]` is the text baseline Y in the same coordinate space (PDF user-space, bottom-left origin) that FieldPlacer's `pdfToScreenCoords` expects for rendering. `pageInfo.originalHeight` in FieldPlacer comes from `page.view[3]` (react-pdf mediaBox), which is the same value as pdfjs `getViewport({scale:1}).height` for standard PDF pages. ### Pattern 4: Deterministic Post-Processing Rules **What:** Four rules that override AI classifications for structurally unambiguous cases. | Rule | Condition | Override | |------|-----------|---------| | A | `contextBefore` last word is "date" | → `date` | | B | `contextBefore` last word is "initials" | → `initials` | | C | `rowTotal > 1` AND `rowIndex > 1` AND AI classified as signature | → `date` (if last+contextBelow has "(Date)") or `text` | | D | `rowTotal=2`, `rowIndex=1`, AI classified as signature, contextBelow has "(Address/Phone)"+"(Date)" but NOT "(Seller"/"(Buyer" | → `text` | **Why needed:** The footer pattern `Seller's Initials [ ] Date ___` and signature block rows `[sig] [address/phone] [date]` are structurally deterministic but the AI sometimes misclassifies them. ### Pattern 5: FieldPlacer Coordinate Rendering Formula **What:** How stored PDF coordinates are converted back to screen pixels for rendering. ```typescript // Source: FieldPlacer.tsx pdfToScreenCoords (lines 44-54) function pdfToScreenCoords(pdfX, pdfY, renderedW, renderedH, pageInfo) { const left = (pdfX / pageInfo.originalWidth) * renderedW; // top is distance from DOM top to BOTTOM EDGE of field const top = renderedH - (pdfY / pageInfo.originalHeight) * renderedH; return { left, top }; } // Rendering (FieldPlacer.tsx line 630): // top: top - heightPx + canvasOffset.y ← shift up by height to get visual top of field ``` `pageInfo.originalWidth/originalHeight` are from react-pdf's `page.view[2]/page.view[3]` (mediaBox dimensions in PDF points at scale 1.0). ### Pattern 6: AI Auto-place Route and Client State Update **What:** POST to `/api/documents/[id]/ai-prepare` → receive `{ fields, textFillData }` → update FieldPlacer state via `aiPlacementKey` increment. ```typescript // Source: DocumentPageClient.tsx (confirmed working in Plan 03) async function handleAiAutoPlace() { const res = await fetch(`/api/documents/${docId}/ai-prepare`, { method: 'POST' }); if (res.ok) { const { textFillData: aiTextFill } = await res.json(); setTextFillData(prev => ({ ...prev, ...aiTextFill })); // MERGE — preserves manual values setAiPlacementKey(k => k + 1); // triggers FieldPlacer re-fetch from DB setPreviewToken(null); // invalidate stale preview } } ``` ### Anti-Patterns to Avoid - **DO NOT** recreate `aiCoordsToPagePdfSpace` — that function was for the abandoned vision approach and does not belong in the current architecture - **DO NOT** recreate `ai-coords.test.ts` — it tested a function that no longer exists; Plan 04 Task 1 must reference `prepare-document.test.ts` instead - **DO NOT** import from `'pdfjs-dist/legacy/build/pdf.mjs'` with `GlobalWorkerOptions.workerSrc = ''` (empty string) — this is the pdfjs v3+ breaking change; always use the `file://` URL path - **DO NOT** use `zodResponseFormat` — broken with Zod v4.3.6 (confirmed GitHub issues #1540, #1602, #1709) - **DO NOT** add `@napi-rs/canvas` imports back to extract-text.ts — the vision approach was abandoned; the `serverExternalPackages` entry in next.config.ts can stay but the import is gone - **DO NOT** use vision/image-based approach — the text-extraction approach is simpler, cheaper, and coordinate-accurate - **DO NOT** lock fields after AI placement — agent must be able to edit, move, resize, delete --- ## Don't Hand-Roll | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | PDF text extraction | Custom PDF parser | pdfjs-dist `getTextContent()` | Already installed; handles encoding, multi-page, returns transform matrix with exact coordinates | | Field type classification | Rule-based regex | GPT-4.1 with manual json_schema | Context analysis (above/below/before/after) is complex; AI handles natural language labels | | UUID generation for field IDs | Custom ID generator | `crypto.randomUUID()` | Node built-in; same pattern used everywhere in the project | | Structured AI output | Parse JSON manually | OpenAI `json_schema` response_format with `strict: true` | 100% schema compliance guaranteed; no parse failures | **Key insight:** The current architecture is correct and complete. Plan 04 is verification-only — no new code is needed. --- ## Common Pitfalls ### Pitfall 1: Plan 04 Task 1 References Deleted Test File **What goes wrong:** Plan 04 Task 1 runs `npx jest src/lib/pdf/__tests__/ai-coords.test.ts`. That file was deleted when the vision approach was abandoned. Running this command fails immediately. **Why it happens:** The plan was written for the vision approach. The architecture pivoted after the plan was written. **How to avoid:** Plan 04 Task 1 must run `npx jest src/lib/pdf/__tests__/prepare-document.test.ts` instead. The `prepare-document.test.ts` file has 10 passing tests covering the Y-flip coordinate formula — these remain valid and relevant. **Warning signs:** `No tests found for path pattern 'src/lib/pdf/__tests__/ai-coords.test.ts'` ### Pitfall 2: pdfjs-dist 5.x Fake-Worker Requires `file://` URL **What goes wrong:** Using `GlobalWorkerOptions.workerSrc = ''` (empty string) throws `PDFWorker: workerSrc not set` in Node.js route handlers with pdfjs-dist 5.x. **Why it happens:** pdfjs-dist 5.x changed fake-worker mode. Empty string is falsy; the PDFWorker getter throws before attempting the dynamic import. The workerSrc must be a valid URL the Node.js dynamic importer can resolve. **How to avoid:** Use `file://${join(process.cwd(), 'node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs')}`. This is already correct in the current `extract-text.ts`. **Warning signs:** `Error: Setting up fake worker failed` or `workerSrc not set` in server logs. ### Pitfall 3: OpenAI Strict Mode Schema Cascades to Nested Objects **What goes wrong:** A JSON schema with `strict: true` that omits `required` or `additionalProperties: false` on a nested object causes a 400 API error. **Why it happens:** OpenAI strict mode requires `required` listing ALL properties AND `additionalProperties: false` at EVERY object level — including `items` inside arrays. **How to avoid:** The current `CLASSIFICATION_SCHEMA` in `field-placement.ts` is correct. Do not modify the schema structure. **Warning signs:** `400 BadRequestError: Invalid schema for response_format` ### Pitfall 4: Checkbox Fields Must Be Filtered Out Before Storing **What goes wrong:** GPT-4.1 returns `fieldType: "checkbox"` for inline selection checkboxes (e.g., `[ ] ARE [ ] ARE NOT`). If these are added to `SignatureFieldData[]`, they appear as placed fields that cannot be properly handled by the signing flow. **Why it happens:** The AI correctly identifies these as checkboxes (not fill-in blanks). They should be classified but not placed. **How to avoid:** The current code already filters: `if (result.fieldType === 'checkbox') continue;`. This is correct. **Warning signs:** Dozens of tiny checkbox-type fields placed throughout the document body. ### Pitfall 5: textFillData Keys Must Be Field UUIDs **What goes wrong:** textFillData keyed by label string ("clientName") does not match DocumentPageClient's lookup of `textFillData[field.id]`. **Why it happens:** Phase 12.1 wired textFillData to use field UUID as key (STATE.md confirmed locked decision). Label-keyed maps are silently ignored. **How to avoid:** The route handler creates UUIDs (`crypto.randomUUID()`) BEFORE building textFillData, then uses those same UUIDs as keys. This is already correct in the current code. **Warning signs:** Text pre-fill values don't appear in the preview even though AI returned them. ### Pitfall 6: Y Coordinate Is Baseline, Not Field Bottom **What goes wrong:** A field placed exactly at `blank.y` (text baseline) appears above the underline, not on it. Underscores descend slightly below the baseline. **Why it happens:** PDF text baseline is where capital letters sit. Descenders (underscores, lowercase g/y/p) extend below the baseline. **How to avoid:** The current code uses `const y = Math.max(0, blank.y - 2)`. The -2pt offset anchors the field bottom edge slightly below the baseline, sitting on the underline visually. **Warning signs:** Fields appear to float just above the underline instead of sitting on it. ### Pitfall 7: Debug Console.log Statements Are Still in Production Code **What goes wrong:** Two `console.log` statements remain in `classifyFieldsWithAI`: one printing the blank count, one printing ALL blank descriptions (can be very verbose for large forms), and one printing all AI classifications. **Why it happens:** Debugging statements added during development were not removed. **How to avoid:** Plan 04 should include removing these console.log statements before final sign-off. They do not affect correctness but should not ship in production code. **Warning signs:** ``` [ai-prepare] calling gpt-4.1 with 47 blanks [ai-prepare] blank descriptions: [0] page1 before="..." ... ``` --- ## Code Examples ### Verified: How pdfjs Text Coordinates Map to Field Positions ```typescript // pdfjs transform matrix: [scaleX, skewY, skewX, scaleY, translateX, translateY] // For a text item on a US Letter page (612 × 792 pts): // transform[4] = x (distance from left edge of page, in points) // transform[5] = y (distance from BOTTOM of page, in points — PDF user-space) // // Example: text near top of page // transform[5] ≈ 720 (720pt from bottom = ~1" from top on a 792pt page) // // FieldPlacer renders this as: // top = renderedH - (720 / 792) * renderedH = renderedH * 0.091 ≈ 9% from top ✓ // // Example: footer initials near bottom of page // transform[5] ≈ 36 (36pt from bottom = 0.5" from bottom) // // FieldPlacer renders this as: // top = renderedH - (36 / 792) * renderedH = renderedH * 0.955 ≈ 95.5% from top ✓ ``` ### Verified: groupIntoLines Threshold ```typescript // Source: extract-text.ts groupIntoLines (line 58) // ±5pt tolerance groups items on the same visual line. // This handles multi-run items (same underline split across font boundaries) // AND minor baseline variations in real PDFs. // // A 3-blank signature row (sig | addr/phone | date) will group correctly IF // all three blanks have y-values within ±5pt of each other. // If NOT grouped (y-drift > 5pt), Rule D in post-processing handles the misclassification. ``` ### Verified: Route Handler Security and Error Pattern ```typescript // Source: ai-prepare/route.ts (confirmed complete) // Guards in order: // 1. auth() — session check // 2. OPENAI_API_KEY existence — 503 if missing // 3. document lookup — 404 if not found // 4. filePath check — 422 if no PDF // 5. status === 'Draft' — 403 if locked // 6. path traversal check — 403 if escapes UPLOADS_DIR // 7. try/catch wrapping extractBlanks + classifyFieldsWithAI — 500 with message // 8. DB write — direct update, no status change // 9. return { fields, textFillData } ``` --- ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | GPT-4o vision (render pages as JPEG) | pdfjs text-layer extraction | After Plan 01, during debugging | Eliminates coordinate conversion math and the 30-40% vertical offset bug | | `aiCoordsToPagePdfSpace()` + `ai-coords.test.ts` | Direct pdfjs coordinates — no conversion | After Plan 01 pivot | Deleted function and test; `prepare-document.test.ts` is the only relevant test | | GPT-4o (vision model) | GPT-4.1 (text model) | After vision approach abandoned | Cheaper, faster, sufficient for text classification task | | `GlobalWorkerOptions.workerSrc = ''` | `file://` URL path | pdfjs-dist 5.x requirement | Required for fake-worker mode; empty string is falsy in 5.x | | `zodResponseFormat` helper | Manual `json_schema` | Zod v4 incompatibility (issues #1540, #1602, #1709) | Permanent — project uses Zod v4.3.6 | | `@napi-rs/canvas` for image rendering | Not used (deleted from imports) | After vision approach abandoned | Package still installed but not imported; `serverExternalPackages` entry can remain | **Deprecated/outdated items that should NOT appear in new code:** - `import { createCanvas } from '@napi-rs/canvas'` in extract-text.ts — deleted, do not restore - `aiCoordsToPagePdfSpace()` function — deleted, do not recreate - `ai-coords.test.ts` — deleted, do not recreate - `zodResponseFormat` — broken with Zod v4, do not use - `GlobalWorkerOptions.workerSrc = ''` (empty string) — wrong for pdfjs 5.x, do not use --- ## Open Questions 1. **Debug console.log statements in classifyFieldsWithAI** - What we know: Three `console.log` calls remain in `field-placement.ts` (blank count, all descriptions, all classifications). These print verbose output to server logs on every AI auto-place request. - What's unclear: Whether to remove before Plan 04 verification or after. - Recommendation: Remove before E2E verification so server logs are clean for manual testing. Add to Plan 04 Task 1. 2. **gpt-4.1 model availability** - What we know: `field-placement.ts` uses `model: 'gpt-4.1'`. This is the current model. The project's OPENAI_API_KEY must have access to this model. - What's unclear: Whether the developer's API key has gpt-4.1 access (it's not universally available to all OpenAI accounts as of 2026). - Recommendation: If the API call returns a 404 model error, fall back to `gpt-4o` (which was used in an earlier iteration and is broadly available). The prompt and schema work identically for both models. 3. **Real Utah REPC 20-page accuracy** - What we know: The text-extraction approach extracts blanks accurately based on the 4 detection strategies. Accuracy depends on the quality of the PDF text layer. - What's unclear: How many blanks are missed or double-detected on the full 20-page Utah REPC. The system prompt is tuned for Utah real estate signature patterns. - Recommendation: Plan 04 human verification on a real Utah REPC is non-negotiable. Expect 80-95% accuracy — imperfect placement is acceptable as the agent reviews before sending. 4. **Plan 04 Task 1 command mismatch** - What we know: Plan 04 Task 1 runs `npx jest src/lib/pdf/__tests__/ai-coords.test.ts` — that file is deleted. - Recommendation: Plan 04 must be updated to run `npx jest src/lib/pdf/__tests__/prepare-document.test.ts --no-coverage --verbose` and `npx tsc --noEmit` instead. --- ## Environment Availability | Dependency | Required By | Available | Version | Fallback | |------------|------------|-----------|---------|----------| | pdfjs-dist | extractBlanks() | ✓ | 5.4.296 | — (required, already installed) | | pdfjs worker file | pdfjs fake-worker | ✓ | `node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs` | — | | openai SDK | classifyFieldsWithAI() | ✓ | ^6.32.0 | — (required, already installed) | | OPENAI_API_KEY env var | classifyFieldsWithAI() | Unknown | — | Route returns 503 with message if missing | | Node.js | All server-side code | ✓ | v23.6.0 | — | | @napi-rs/canvas | NOT required anymore | ✓ (installed) | ^0.1.97 | N/A — no longer imported | **Missing dependencies with no fallback:** - OPENAI_API_KEY in `.env.local` — must be set before Plan 04 verification. Route returns 503 if missing (actionable error, not a crash). --- ## Validation Architecture ### Test Framework | Property | Value | |----------|-------| | Framework | Jest 29.7.0 + ts-jest | | Config file | package.json `"jest": { "preset": "ts-jest", "testEnvironment": "node" }` | | Quick run command | `npx jest src/lib/pdf/__tests__/prepare-document.test.ts --no-coverage --verbose` | | Full suite command | `npx jest --no-coverage --verbose` | ### Phase Requirements → Test Map | Req ID | Behavior | Test Type | Automated Command | File Exists? | |--------|----------|-----------|-------------------|-------------| | AI-01 | Y-flip coordinate formula correct (FieldPlacer screen↔PDF) | unit | `npx jest src/lib/pdf/__tests__/prepare-document.test.ts --no-coverage --verbose` | ✅ | | AI-01 | AI auto-place button appears, fields land on canvas, correct vertical positions | manual E2E | human verification (Plan 04 Task 2) | N/A | | AI-02 | Text pre-fill values from client profile appear in field values | manual E2E | human verification (Plan 04 Task 2) | N/A | ### Sampling Rate - **Per task commit:** `npx jest --no-coverage` - **Per wave merge:** `npx jest --no-coverage && npx tsc --noEmit` - **Phase gate:** Full suite green + TypeScript clean + human E2E approval before phase complete ### Wave 0 Gaps None — existing test infrastructure covers coordinate math. No new test files required for Plan 04. The deleted `ai-coords.test.ts` is intentionally not recreated (it tested a deleted function). --- ## Sources ### Primary (HIGH confidence) - `/Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/ai/extract-text.ts` — current implementation, text-layer extraction approach - `/Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/ai/field-placement.ts` — current implementation, GPT-4.1 classification - `/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/documents/[id]/ai-prepare/route.ts` — current route implementation - `/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/FieldPlacer.tsx` — `pdfToScreenCoords` function (lines 44-54) and drag-end coordinate formula (lines 291-292) - `/Users/ccopeland/temp/red/teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx` — `pageInfo.originalWidth/originalHeight` from `page.view[2]/page.view[3]` (react-pdf) - `.planning/phases/13-ai-field-placement-and-pre-fill/.continue-here.md` — complete record of what was attempted, what failed, and current state - `git log --oneline` — confirmed commit history showing vision→text pivot - STATE.md — locked decisions including manual json_schema, pdfjs-dist legacy build, Zod v4 incompatibility - `/Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/pdf/__tests__/prepare-document.test.ts` — 10 tests, all passing ### Secondary (MEDIUM confidence) - pdfjs-dist transform matrix format — verified by reading pdfjs docs + confirmed by the implementation's usage of `transform[4]`/`transform[5]` for x/y - react-pdf `page.view` array format — confirmed from PdfViewer.tsx `page.view[0], page.view[2]` / `page.view[1], page.view[3]` usage with comment `// Math.max handles non-standard mediaBox ordering` ### Tertiary (LOW confidence) - GPT-4.1 model availability for all API keys — not verified; gpt-4.1 may not be available in all tiers - Real Utah REPC 20-page blank extraction accuracy — not measured; text-layer quality varies by PDF --- ## Metadata **Confidence breakdown:** - Standard stack: HIGH — all packages confirmed installed and version-verified - Architecture: HIGH — current code read directly, coordinate system verified analytically and confirmed by test results - Pitfalls: HIGH — all pitfalls documented from actual bugs encountered during development (per .continue-here.md) or confirmed from code inspection - Plan 04 gap (Task 1 test command): HIGH — confirmed by checking git status (ai-coords.test.ts deleted), running jest (prepare-document.test.ts passes) **Research date:** 2026-04-03 **Valid until:** 2026-05-03 (30 days — stack stable; coordinate math is deterministic; no external dependencies changing)