##  TypeScript Zod schemas

Here are the TypeScript Zod schemas matching your V2 PostgreSQL database definition.

These schemas cover:
1. Database Entities: Pure runtime validators representing rows fetched directly from PostgreSQL.
2. AI Payload Extensions: The structured document output stored inside job.ai_metadata.
3. Insert/Create Schemas: Utility types derived with .omit() for creating new records where auto-generated columns (id, created_at, updated_at, etc.) are handled by PostgreSQL defaults.

---

```
import { z } from "zod";

// ==========================================
// 1. ATOMIC & REUSABLE SCHEMAS
// ==========================================

export const UUIDSchema = z.string().uuid();
export const ISODateTimeSchema = z.coerce.date();

export const BoundingBoxSchema = z.object({
  ymin: z.number().min(0).max(1000),
  xmin: z.number().min(0).max(1000),
  ymax: z.number().min(0).max(1000),
  xmax: z.number().min(0).max(1000),
});

export const BlockTypeSchema = z.enum([
  "heading",
  "paragraph",
  "table",
  "margin_note",
  "signature",
  "footnote",
  "header",
]);

// ==========================================
// 2. PAGE-LEVEL AI METADATA SCHEMA (job_source.ai_metadata)
// ==========================================

export const TranscribedBlockSchema = z.object({
  text: z.string(),
  confidence: z.number().min(0).max(1),
  blockType: BlockTypeSchema,
  boundingBox: BoundingBoxSchema.optional(),
});

export const PageAIMetadataSchema = z.object({
  detectedLanguage: z.string().optional(),
  overallConfidence: z.number().min(0).max(1),
  blocks: z.array(TranscribedBlockSchema),
  inputTokens: z.number().optional(),
  outputTokens: z.number().optional(),
  extractedEntities: z.record(z.string(), z.unknown()).optional(),
});

export type PageAIMetadata = z.infer<typeof PageAIMetadataSchema>;

// ==========================================
// 3. TABLE ENTITY SCHEMAS
// ==========================================

// --- PERSON TABLE ---
export const PersonSchema = z.object({
  id: UUIDSchema,
  fullName: z.string().min(1),
  displayName: z.string().nullable().optional(),
  maidenName: z.string().nullable().optional(),
  birthDate: z.string().nullable().optional(),
  birthDateRaw: z.string().nullable().optional(),
  birthPlace: z.string().nullable().optional(),
  deathDate: z.string().nullable().optional(),
  deathDateRaw: z.string().nullable().optional(),
  deathPlace: z.string().nullable().optional(),
  biography: z.string().nullable().optional(),
  portraitPath: z.string().nullable().optional(),
  metadata: z.record(z.string(), z.unknown()).default({}),
  createdAt: ISODateTimeSchema,
  updatedAt: ISODateTimeSchema,
});

// --- DOCUMENT TABLE ---
export const DocumentSchema = z.object({
  id: UUIDSchema,
  name: z.string().min(1),
  documentType: z.string().nullable().optional(),
  documentDate: z.string().nullable().optional(),
  documentDateRaw: z.string().nullable().optional(),
  locationCreated: z.string().nullable().optional(),
  notes: z.string().nullable().optional(),
  archiveIdentifier: z.string().nullable().optional(),
  createdAt: ISODateTimeSchema,
  updatedAt: ISODateTimeSchema,
});

// --- DOCUMENT_PERSON JUNCTION ---
export const PersonRoleSchema = z.enum(["author", "recipient"]);

export const DocumentPersonSchema = z.object({
  id: UUIDSchema,
  documentId: UUIDSchema,
  personId: UUIDSchema,
  role: PersonRoleSchema,
  createdAt: ISODateTimeSchema,
});

// --- JOB TABLE ---
export const JobStatusSchema = z.enum([
  "queued",
  "processing",
  "completed",
  "partial_success",
  "failed",
]);

export const JobSchema = z.object({
  id: UUIDSchema,
  documentId: UUIDSchema,
  status: JobStatusSchema.default("queued"),
  retryCount: z.number().int().nonnegative().default(0),
  provider: z.string(),
  model: z.string(),
  promptName: z.string().nullable().optional(),
  dateCreated: ISODateTimeSchema,
  dateUpdated: ISODateTimeSchema,
});

// --- SOURCE TABLE ---
export const SourceSchema = z.object({
  id: UUIDSchema,
  documentId: UUIDSchema,
  pageNumber: z.number().int().positive().default(1),
  uploadName: z.string(),
  filename: z.string(),
  filePath: z.string(),
  rawTranscription: z.string().nullable().optional(),
  revisedText: z.string().nullable().optional(),
  dateUploaded: ISODateTimeSchema,
  dateRevised: ISODateTimeSchema.nullable().optional(),
});

// --- JOB_SOURCE JUNCTION (Page Execution Output) ---
export const JobSourceStatusSchema = z.enum([
  "pending",
  "transcribed",
  "failed",
]);

export const JobSourceSchema = z.object({
  id: UUIDSchema,
  jobId: UUIDSchema,
  sourceId: UUIDSchema,
  status: JobSourceStatusSchema.default("pending"),
  rawTranscription: z.string().nullable().optional(),
  aiMetadata: PageAIMetadataSchema.nullable().optional(),
  rawApiResponse: z.record(z.string(), z.unknown()).nullable().optional(),
  errorDetail: z.string().nullable().optional(),
  executedAt: ISODateTimeSchema,
});

export type Person = z.infer<typeof PersonSchema>;
export type Document = z.infer<typeof DocumentSchema>;
export type DocumentPerson = z.infer<typeof DocumentPersonSchema>;
export type Job = z.infer<typeof JobSchema>;
export type Source = z.infer<typeof SourceSchema>;
export type JobSource = z.infer<typeof JobSourceSchema>;
```
