# 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>
## 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 |
</phase_requirements>

---

## 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<UUID, string> }
  ↓
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)
