/** * Type definitions for the backfill pipeline * * This module defines the three-stage type system for book metadata processing: * 1. GeminiBookMetadata - Raw Gemini API output (no ISBN yet) * 2. ResolvedCandidate - After ISBNdb resolution attempt (ISBN optional) * 3. EnrichmentCandidate - For enrichment pipeline (ISBN required) * * @see https://github.com/jukasdrj/alexandria/issues/157 */ /** * Stage 1: Raw Gemini API output * * Represents book metadata generated by Gemini AI before ISBN resolution. * All fields come directly from Gemini's structured output. */ export interface GeminiBookMetadata { /** Book title (required from Gemini) */ title: string; /** Primary author */ author: string; /** Full author list (if multiple) */ authors?: string[]; /** Publisher name */ publisher?: string; /** Book format (hardcover, paperback, etc.) */ format?: string; /** Publication year */ year?: number; /** Historical/cultural significance description */ significance?: string; /** Gemini's confidence score (0-1) */ confidence?: number; } /** * Stage 2: After ISBNdb resolution attempt * * Extends GeminiBookMetadata with ISBN resolution results. * ISBN may be missing if ISBNdb quota exhausted or book not found. */ export interface ResolvedCandidate extends GeminiBookMetadata { /** ISBN-13 (optional - ISBNdb may fail) */ isbn?: string; /** Confidence in ISBN resolution */ resolution_confidence?: 'high' | 'medium' | 'low' | 'not_found'; /** Source of ISBN resolution */ resolution_source?: 'isbndb' | 'fallback'; } /** * Stage 3: For enrichment pipeline * * Represents a book candidate ready for database enrichment. * ISBN is required - this type guarantees downstream code can safely use ISBN. */ export interface EnrichmentCandidate { /** ISBN-13 (required!) */ isbn: string; /** Book title */ title: string; /** Author list (guaranteed non-empty) */ authors: string[]; /** Publisher name */ publisher?: string; /** Publication year */ year?: number; /** Source of enrichment data */ source: string; } /** * Validation result for Gemini output */ export interface ValidationResult { success: boolean; data?: T; error?: string; } /** * Validates raw Gemini API output and constructs GeminiBookMetadata * * @param raw - Raw object from Gemini API * @returns Validation result with GeminiBookMetadata or error */ export function validateGeminiOutput(raw: unknown): ValidationResult { const data = raw as any; // Required fields if (!data.title || typeof data.title !== 'string') { return { success: false, error: 'Missing or invalid title' }; } if (!data.author || typeof data.author !== 'string') { return { success: false, error: 'Missing or invalid author' }; } // Construct validated metadata const metadata: GeminiBookMetadata = { title: data.title.trim(), author: data.author.trim(), authors: Array.isArray(data.authors) ? data.authors.map((a: any) => String(a).trim()) : undefined, publisher: data.publisher ? String(data.publisher).trim() : undefined, format: data.format ? String(data.format).trim() : undefined, year: typeof data.year === 'number' ? data.year : undefined, significance: data.significance ? String(data.significance).trim() : undefined, confidence: typeof data.confidence === 'number' ? data.confidence : undefined, }; return { success: true, data: metadata }; } /** * Converts ResolvedCandidate to EnrichmentCandidate * * Fails (returns null) if: * - ISBN is missing * - Title is missing * - No authors available * * @param resolved - Candidate after ISBNdb resolution * @returns EnrichmentCandidate or null if validation fails */ export function toEnrichmentCandidate( resolved: ResolvedCandidate ): ValidationResult { // ISBN is required for enrichment if (!resolved.isbn) { return { success: false, error: 'ISBN required for enrichment' }; } // Title is required if (!resolved.title) { return { success: false, error: 'Title required for enrichment' }; } // Build authors array const authors = resolved.authors?.length ? resolved.authors : [resolved.author]; if (authors.length === 0) { return { success: false, error: 'At least one author required for enrichment' }; } const candidate: EnrichmentCandidate = { isbn: resolved.isbn, title: resolved.title, authors, publisher: resolved.publisher, year: resolved.year, source: 'gemini-backfill' }; return { success: true, data: candidate }; } /** * Splits ResolvedCandidate array into enrichment-ready and synthetic-only * * @param resolved - Array of candidates after ISBNdb resolution * @returns Object with enrichment candidates (have ISBN) and synthetic candidates (no ISBN) */ export function splitResolvedCandidates(resolved: ResolvedCandidate[]): { forEnrichment: EnrichmentCandidate[]; forSynthetic: ResolvedCandidate[]; validationErrors: Array<{ candidate: ResolvedCandidate; error: string }>; } { const forEnrichment: EnrichmentCandidate[] = []; const forSynthetic: ResolvedCandidate[] = []; const validationErrors: Array<{ candidate: ResolvedCandidate; error: string }> = []; for (const candidate of resolved) { if (!candidate.isbn) { // No ISBN - goes to synthetic works forSynthetic.push(candidate); continue; } // Has ISBN - try to convert to EnrichmentCandidate const result = toEnrichmentCandidate(candidate); if (result.success && result.data) { forEnrichment.push(result.data); } else { // Validation failed - log but still try synthetic validationErrors.push({ candidate, error: result.error || 'Unknown validation error' }); forSynthetic.push(candidate); } } return { forEnrichment, forSynthetic, validationErrors }; }