docs(08): create phase 8 plan files

This commit is contained in:
Chandler Copeland
2026-03-21 11:43:42 -06:00
parent 4a9a6a18f5
commit dbd217b2e2
3 changed files with 563 additions and 1 deletions

View File

@@ -176,7 +176,7 @@ Plans:
2. `/api/sign/[token]` route filters `signatureFields` to client-visible types only — `agent-signature` fields are never returned to the signing page 2. `/api/sign/[token]` route filters `signatureFields` to client-visible types only — `agent-signature` fields are never returned to the signing page
3. `SigningPageClient.tsx` branches on field type and does not attempt to open the signature modal for non-client-signature types 3. `SigningPageClient.tsx` branches on field type and does not attempt to open the signature modal for non-client-signature types
4. Drizzle migration runs cleanly against the existing database with no data loss on existing documents 4. Drizzle migration runs cleanly against the existing database with no data loss on existing documents
**Plans**: TBD **Plans**: 2 plans
Plans: Plans:
- [ ] 08-01-PLAN.md — Discriminated union type extension in schema.ts, Drizzle migration, backward-compat fallback in all field-reading paths - [ ] 08-01-PLAN.md — Discriminated union type extension in schema.ts, Drizzle migration, backward-compat fallback in all field-reading paths

View File

@@ -0,0 +1,218 @@
---
phase: 08-schema-foundation-and-signing-page-safety
plan: "01"
type: execute
wave: 1
depends_on: []
files_modified:
- teressa-copeland-homes/src/lib/db/schema.ts
- teressa-copeland-homes/drizzle/0006_type_discriminant.sql
autonomous: true
requirements:
- FIELD-01
must_haves:
truths:
- "SignatureFieldData interface has an optional 'type' field accepting all six SignatureFieldType literals"
- "getFieldType() helper exported from schema.ts returns field.type ?? 'client-signature' — never returns undefined"
- "isClientVisibleField() pure function exported from schema.ts returns false for agent-signature, true for all others"
- "Drizzle meta snapshot is in sync with schema.ts (npm run db:generate runs clean)"
- "Existing documents with no 'type' on their JSONB fields continue to work (backward-compat)"
artifacts:
- path: "teressa-copeland-homes/src/lib/db/schema.ts"
provides: "SignatureFieldType union, extended SignatureFieldData interface, getFieldType helper, isClientVisibleField predicate"
contains: "SignatureFieldType"
- path: "teressa-copeland-homes/drizzle/0006_type_discriminant.sql"
provides: "Drizzle migration snapshot sync (may be empty SQL — that is correct)"
key_links:
- from: "teressa-copeland-homes/src/lib/db/schema.ts"
to: "teressa-copeland-homes/src/app/api/sign/[token]/route.ts"
via: "isClientVisibleField import (added in plan 08-02)"
pattern: "isClientVisibleField"
- from: "teressa-copeland-homes/src/lib/db/schema.ts"
to: "teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx"
via: "getFieldType import (added in plan 08-02)"
pattern: "getFieldType"
---
<objective>
Extend the SignatureFieldData TypeScript interface with a type discriminant and export two pure helper functions that all field-reading code will use. Run the Drizzle migration to keep the meta snapshot current.
Purpose: Every phase 9-13 feature that places a new field type on a document depends on this discriminant. Without it, the signing page treats every field as a required client-signature, and an agent-signature field would surface as an unsigned required field to the client. This plan establishes the single source of truth for field type resolution.
Output: Extended schema.ts with SignatureFieldType, updated SignatureFieldData interface, getFieldType() and isClientVisibleField() helpers, and migration file 0006.
</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/08-schema-foundation-and-signing-page-safety/08-RESEARCH.md
IMPORTANT: Read AGENTS.md in the Next.js app root before writing any code. The app uses a version of Next.js that may differ from training data.
</context>
<interfaces>
<!-- Current SignatureFieldData (to be extended) — from src/lib/db/schema.ts -->
```typescript
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)
}
// Current JSONB column annotation (documents table):
signatureFields: jsonb("signature_fields").$type<SignatureFieldData[]>(),
```
</interfaces>
<tasks>
<task type="auto">
<name>Task 1: Extend SignatureFieldData in schema.ts with type discriminant and helper exports</name>
<files>teressa-copeland-homes/src/lib/db/schema.ts</files>
<action>
Add the following to schema.ts BEFORE the `SignatureFieldData` interface declaration (i.e., above line 4):
```typescript
export type SignatureFieldType =
| 'client-signature'
| 'initials'
| 'text'
| 'checkbox'
| 'date'
| 'agent-signature';
```
Then update the existing `SignatureFieldData` interface by adding one optional field after `height`:
```typescript
type?: SignatureFieldType; // Optional — v1.0 documents have no type; fallback = 'client-signature'
```
Then add two exported helper functions immediately after the `SignatureFieldData` interface (before the `users` table declaration):
```typescript
/**
* Safe field type reader — always returns a SignatureFieldType, never undefined.
* v1.0 documents have no `type` on their JSONB fields; this coalesces to 'client-signature'.
* ALWAYS use this instead of reading field.type directly.
*/
export function getFieldType(field: SignatureFieldData): SignatureFieldType {
return field.type ?? 'client-signature';
}
/**
* Returns true for field types that should be visible in the client signing session.
* agent-signature fields are embedded during document preparation and must never
* surface to the client as required unsigned fields.
*/
export function isClientVisibleField(field: SignatureFieldData): boolean {
return getFieldType(field) !== 'agent-signature';
}
```
Do NOT change the `.$type<SignatureFieldData[]>()` annotation on the `signatureFields` column — it already annotates the JSONB column with the correct type and will automatically reflect the updated interface.
Do NOT touch any other tables, enums, or imports. The rest of schema.ts is unchanged.
Why type is optional: all v1.0 documents in the database have JSONB field objects with no `type` property. Making it required would break TypeScript on every FieldPlacer call site that constructs a SignatureFieldData without a type. The `getFieldType()` fallback handles the coercion centrally.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30</automated>
</verify>
<done>
- schema.ts exports SignatureFieldType, getFieldType, and isClientVisibleField
- TypeScript compilation passes with no errors related to schema.ts
- getFieldType({ id: 'x', page: 1, x: 0, y: 0, width: 144, height: 36 }) returns 'client-signature' (no type property = backward compat)
- getFieldType({ ..., type: 'agent-signature' }) returns 'agent-signature'
- isClientVisibleField({ ..., type: 'agent-signature' }) returns false
- isClientVisibleField({ ..., type: 'client-signature' }) returns true
- isClientVisibleField({ id: 'x', page: 1, x: 0, y: 0, width: 144, height: 36 }) returns true (no type = backward compat)
</done>
</task>
<task type="auto">
<name>Task 2: Run Drizzle migration to sync meta snapshot</name>
<files>teressa-copeland-homes/drizzle/0006_type_discriminant.sql</files>
<action>
Run the migration generator from the app directory:
```bash
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run db:generate
```
This produces `drizzle/0006_*.sql`. The SQL content will be empty or contain only a comment — that is CORRECT and expected. The `.$type<T>()` annotation on the JSONB column is a TypeScript-only change and generates no DDL.
After generation, rename the file to have a predictable name if the generated name is not `0006_type_discriminant.sql`:
```bash
cd /Users/ccopeland/temp/red/teressa-copeland-homes && ls drizzle/0006_*.sql
```
If the file was generated with a random name (e.g., `0006_milky_black_cat.sql`), rename it:
```bash
mv drizzle/0006_*.sql drizzle/0006_type_discriminant.sql
```
Then apply the migration to the local database:
```bash
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run db:migrate
```
If `db:migrate` fails because the database is not running, note the failure in the summary but DO NOT block plan completion — the TypeScript changes are the deliverable. The migration file itself is what must exist and be committed.
Why run even an empty migration: Drizzle stores a JSON snapshot of the TypeScript schema in `drizzle/meta/`. Skipping `db:generate` after a schema.ts change causes the next `db:generate` run (Phase 9 or later) to produce an incorrect diff that re-adds or re-creates things that already exist. The snapshot must stay in sync.
</action>
<verify>
<automated>ls /Users/ccopeland/temp/red/teressa-copeland-homes/drizzle/0006_*.sql 2>/dev/null && echo "Migration file exists" || echo "MISSING"</automated>
</verify>
<done>
- drizzle/0006_type_discriminant.sql (or 0006_*.sql) exists
- File content is empty SQL or a comment — no DDL statements (correct)
- drizzle/meta/ directory has been updated by db:generate
- npm run db:migrate ran without error (or was noted as pending if DB unavailable)
</done>
</task>
</tasks>
<verification>
Run TypeScript compilation to confirm no type errors introduced:
```bash
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit
```
Confirm all three exports are present:
```bash
grep -n "SignatureFieldType\|getFieldType\|isClientVisibleField" /Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/db/schema.ts
```
Confirm migration file exists:
```bash
ls /Users/ccopeland/temp/red/teressa-copeland-homes/drizzle/0006_*.sql
```
</verification>
<success_criteria>
- schema.ts exports: SignatureFieldType (union type), updated SignatureFieldData (with optional type field), getFieldType() (backward-compat fallback), isClientVisibleField() (pure predicate)
- TypeScript compilation passes: npx tsc --noEmit exits 0
- drizzle/0006_*.sql exists and was generated by db:generate (not hand-written)
- All existing v1.0 SignatureFieldData objects (no type property) continue to resolve as 'client-signature' via getFieldType()
</success_criteria>
<output>
After completion, create `.planning/phases/08-schema-foundation-and-signing-page-safety/08-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,344 @@
---
phase: 08-schema-foundation-and-signing-page-safety
plan: "02"
type: execute
wave: 2
depends_on:
- "08-01"
files_modified:
- teressa-copeland-homes/src/app/api/sign/[token]/route.ts
- teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx
autonomous: false
requirements:
- FIELD-01
must_haves:
truths:
- "GET /api/sign/[token] never returns agent-signature fields to the client — they are filtered server-side before the JSON response"
- "SigningPageClient.handleFieldClick does not open the signature modal for non-client-signature field types"
- "SigningPageClient.handleSubmit counts only client-signature fields in its completeness check — submit is not blocked by non-signable fields"
- "SigningProgressBar receives the correct total (client-signature fields only, not all fields)"
- "Existing v1.0 signing sessions are unaffected — all existing fields have no type property and read as client-signature"
- "The POST /api/sign/[token] route is NOT modified — signature embedding pipeline is unchanged"
artifacts:
- path: "teressa-copeland-homes/src/app/api/sign/[token]/route.ts"
provides: "Server-side agent-signature filter in GET response"
contains: "isClientVisibleField"
- path: "teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx"
provides: "Type-guarded handleFieldClick, type-filtered handleSubmit, correct SigningProgressBar total"
contains: "getFieldType"
key_links:
- from: "teressa-copeland-homes/src/app/api/sign/[token]/route.ts (GET)"
to: "client signing page JSON response"
via: ".filter(isClientVisibleField) on doc.signatureFields"
pattern: "isClientVisibleField"
- from: "teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx"
to: "signature modal"
via: "handleFieldClick type guard — getFieldType(field) !== 'client-signature' returns early"
pattern: "getFieldType"
---
<objective>
Apply the server-side field filter in the signing GET route and add type-branching guards to SigningPageClient. This is the security-critical half of Phase 8 — the schema change in 08-01 defines the types, this plan enforces them at the boundaries.
Purpose: Without the server-side filter, agent-signature field coordinates are leaked to the client network response. Without the client-side guard, clicking a non-client-signature field overlay would open the signature modal (a broken UX that will only get worse as Phase 10 adds more field types). Both guards must ship together.
Output: Modified route.ts (GET filter), modified SigningPageClient.tsx (type guards), human verification checkpoint.
</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/08-schema-foundation-and-signing-page-safety/08-RESEARCH.md
@.planning/phases/08-schema-foundation-and-signing-page-safety/08-01-SUMMARY.md
IMPORTANT: Read AGENTS.md in the Next.js app root before writing any code.
</context>
<interfaces>
<!-- From 08-01: helpers exported from schema.ts -->
```typescript
export function getFieldType(field: SignatureFieldData): SignatureFieldType {
return field.type ?? 'client-signature';
}
export function isClientVisibleField(field: SignatureFieldData): boolean {
return getFieldType(field) !== 'agent-signature';
}
```
<!-- Current unguarded line in GET /api/sign/[token] route.ts (line 88) -->
```typescript
// CURRENT — returns ALL fields including agent-signature
signatureFields: doc.signatureFields ?? [],
// TARGET — filters before response
signatureFields: (doc.signatureFields ?? []).filter(isClientVisibleField),
```
<!-- Current handleFieldClick in SigningPageClient.tsx (lines 85-92) — opens modal for ANY field -->
```typescript
const handleFieldClick = useCallback(
(fieldId: string) => {
if (signedFields.has(fieldId)) return;
setActiveFieldId(fieldId);
setModalOpen(true);
},
[signedFields]
);
```
<!-- Current handleSubmit count in SigningPageClient.tsx (line 129) — counts ALL fields -->
```typescript
if (signedFields.size < signatureFields.length || submitting) return;
```
<!-- Current SigningProgressBar total (line 328) — counts ALL fields -->
```typescript
<SigningProgressBar
total={signatureFields.length}
...
```
</interfaces>
<tasks>
<task type="auto">
<name>Task 1: Add isClientVisibleField filter to GET /api/sign/[token] route</name>
<files>teressa-copeland-homes/src/app/api/sign/[token]/route.ts</files>
<action>
Make two changes to the GET handler in `src/app/api/sign/[token]/route.ts`:
1. Add `isClientVisibleField` to the import from `@/lib/db/schema` (line 6 currently imports `signingTokens, documents, clients`):
```typescript
// Current line 6:
import { signingTokens, documents, clients } from '@/lib/db/schema';
// Updated (add isClientVisibleField):
import { signingTokens, documents, clients, isClientVisibleField } from '@/lib/db/schema';
```
2. On line 88 in the GET response (inside the `return NextResponse.json({...})` block), replace the unguarded field assignment:
```typescript
// REMOVE:
signatureFields: doc.signatureFields ?? [],
// ADD:
signatureFields: (doc.signatureFields ?? []).filter(isClientVisibleField),
```
That is the ONLY change to route.ts. Do NOT modify:
- The POST handler at all — the signing submission pipeline is correct and reads signatureFields from the DB directly (not from client), so it must remain untouched
- The token verification logic
- The audit logging logic
- Any other field in the GET response
Why server-side: If the filter were only in SigningPageClient, a caller could hit GET /api/sign/[token] directly (via curl, browser DevTools, etc.) and see agent-signature field coordinates and IDs. The server-side filter is the primary protection.
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 && grep -n "isClientVisibleField" src/app/api/sign/\[token\]/route.ts</automated>
</verify>
<done>
- isClientVisibleField is imported from @/lib/db/schema in route.ts
- Line 88 uses .filter(isClientVisibleField) on signatureFields
- TypeScript compilation passes (no new errors)
- POST handler is untouched (verify by diffing — only the import line and line 88 changed)
</done>
</task>
<task type="auto">
<name>Task 2: Add type-branching guards to SigningPageClient.tsx</name>
<files>teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx</files>
<action>
Make three targeted changes to `SigningPageClient.tsx`. All changes require importing `getFieldType` first.
**Change 1 — Add import.**
The current import on line 10 is:
```typescript
import type { SignatureFieldData } from '@/lib/db/schema';
```
Replace with:
```typescript
import type { SignatureFieldData } from '@/lib/db/schema';
import { getFieldType } from '@/lib/db/schema';
```
(Keep the `import type` separate — it is a type-only import. The `getFieldType` function import is a value import and must be on its own non-type import line.)
**Change 2 — Guard handleFieldClick.**
The current handleFieldClick (lines 85-92):
```typescript
const handleFieldClick = useCallback(
(fieldId: string) => {
if (signedFields.has(fieldId)) return;
setActiveFieldId(fieldId);
setModalOpen(true);
},
[signedFields]
);
```
Replace with:
```typescript
const handleFieldClick = useCallback(
(fieldId: string) => {
const field = signatureFields.find((f) => f.id === fieldId);
if (!field) return;
// Defense-in-depth: primary protection is the server filter in GET /api/sign/[token]
// Only client-signature fields open the modal; Phase 10 will expand this for initials
if (getFieldType(field) !== 'client-signature') return;
if (signedFields.has(fieldId)) return;
setActiveFieldId(fieldId);
setModalOpen(true);
},
[signatureFields, signedFields]
);
```
Note: `signatureFields` is now in the dependency array. This is correct — the callback uses `signatureFields.find`.
**Change 3 — Fix handleSubmit count and SigningProgressBar total.**
In `handleSubmit`, find the guard (currently line 129):
```typescript
if (signedFields.size < signatureFields.length || submitting) return;
```
Replace with:
```typescript
const clientSigFields = signatureFields.filter(
(f) => getFieldType(f) === 'client-signature'
);
if (signedFields.size < clientSigFields.length || submitting) return;
```
In the JSX near line 328, find the SigningProgressBar:
```typescript
<SigningProgressBar
total={signatureFields.length}
signed={signedFields.size}
onJumpToNext={handleJumpToNext}
onSubmit={handleSubmit}
submitting={submitting}
/>
```
Replace `total={signatureFields.length}` with:
```typescript
total={signatureFields.filter((f) => getFieldType(f) === 'client-signature').length}
```
Why these guards are needed even though the server already filters: The GET route filter is the primary protection. These client-side guards are defense-in-depth. They also prepare the component for Phase 10, which will add `initials` handling — at that point `handleFieldClick` will be expanded with an `else if (getFieldType(field) === 'initials')` branch. Establishing the type-switch pattern now makes Phase 10 a clean extension rather than a surgical patch.
Do NOT change:
- The `handleJumpToNext` callback (it finds the next unsigned field from signatureFields — this is fine because agent-signature fields are filtered by the GET route before the component receives them)
- The `fieldsByPage` grouping
- The PDF render logic
- The signature modal integration
- Any styling or overlay rendering
</action>
<verify>
<automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 && grep -n "getFieldType" src/app/sign/\[token\]/_components/SigningPageClient.tsx</automated>
</verify>
<done>
- getFieldType imported from @/lib/db/schema in SigningPageClient.tsx
- handleFieldClick returns early for any field where getFieldType(field) !== 'client-signature'
- signatureFields is in the handleFieldClick dependency array
- handleSubmit counts only client-signature fields for completeness check
- SigningProgressBar receives total = client-signature count (not all fields)
- TypeScript compilation passes
</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Human verification of Phase 8 safety gate</name>
<what-built>
Complete Phase 8 safety gate:
- 08-01: SignatureFieldData type discriminant, getFieldType(), isClientVisibleField() in schema.ts + Drizzle migration 0006
- 08-02: Server-side agent-signature filter in GET /api/sign/[token], type-branching guards in SigningPageClient.tsx
</what-built>
<how-to-verify>
1. Start the dev server if not running: cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run dev
2. TypeScript check — must pass clean:
npx tsc --noEmit
3. Verify the schema exports:
grep -n "SignatureFieldType\|getFieldType\|isClientVisibleField" /Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/db/schema.ts
Expected: all three present with correct signatures
4. Verify the GET route filter:
grep -n "isClientVisibleField" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/sign/\[token\]/route.ts
Expected: import line + filter applied to signatureFields in GET response
5. Verify SigningPageClient guards:
grep -n "getFieldType" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/sign/\[token\]/_components/SigningPageClient.tsx
Expected: 3+ lines (import, handleFieldClick guard, handleSubmit filter, progressbar total)
6. Verify POST handler is untouched (no isClientVisibleField/getFieldType references in POST section):
The POST handler begins at line 98 of route.ts. Confirm no changes below that line.
7. Verify the migration file exists:
ls /Users/ccopeland/temp/red/teressa-copeland-homes/drizzle/0006_*.sql
8. Manual backward-compat smoke test (if DB is running):
- Log in to the portal at http://localhost:3000/portal
- Open an existing signed or prepared document
- If a signing token exists, visit the /sign/[token] URL — the page should load and display fields normally
- No visual change expected (all existing fields have no type property and default to client-signature)
</how-to-verify>
<resume-signal>Type "approved" if all checks pass, or describe any issues found</resume-signal>
<action>Human reviews and approves all automated checks completed in Tasks 1 and 2. No code changes required — this is a verification-only step.</action>
<verify><automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -5</automated></verify>
<done>Human has approved Phase 8 safety gate: TypeScript compiles clean, server filter confirmed, client guards confirmed, backward-compat verified</done>
</task>
</tasks>
<verification>
TypeScript compilation passes:
```bash
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit
```
All three schema exports present:
```bash
grep -c "getFieldType\|isClientVisibleField\|SignatureFieldType" /Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/db/schema.ts
```
Expected: 3 (at minimum)
Server filter applied:
```bash
grep "isClientVisibleField" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/sign/\[token\]/route.ts
```
Client guard applied:
```bash
grep "getFieldType" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/sign/\[token\]/_components/SigningPageClient.tsx
```
</verification>
<success_criteria>
- GET /api/sign/[token] response only contains client-visible field types (agent-signature excluded)
- SigningPageClient does not open signature modal for non-client-signature field types
- handleSubmit completeness check counts only client-signature fields
- SigningProgressBar total reflects client-signature field count only
- TypeScript compilation passes across the entire project
- All existing v1.0 signing behavior is unchanged (no type = client-signature fallback)
- Phase 8 ships as an atomic unit: schema foundation + signing page safety active simultaneously
</success_criteria>
<output>
After completion, create `.planning/phases/08-schema-foundation-and-signing-page-safety/08-02-SUMMARY.md`
</output>