import { validateStructuredOutput, type FieldEvidence, type OutputFieldContract, type TextSource, type ValidationIssue } from './structured-output-validation'; import { exportValidatedOutput } from './export-validated-output'; /** Each row is independently supported by spans in the original source texts. */ export interface EvidencedRecord { value: Record; evidence: FieldEvidence[] } export interface BatchIssue extends ValidationIssue { row?: number } export type BatchValidation = | { status: 'mechanically_validated'; records: EvidencedRecord[] } | { status: 'needs_review'; issues: BatchIssue[] }; const MAX_RECORDS = 200; /** This gate cannot establish that the model found every source record. */ export function validateStructuredBatch(input: { fields: readonly OutputFieldContract[]; sources: readonly TextSource[]; records: unknown; }): BatchValidation { if (!Array.isArray(input.records) || input.records.length === 0 || input.records.length > MAX_RECORDS) { return { status: 'needs_review', issues: [{ field: '', code: 'invalid_record_count' }] }; } const issues: BatchIssue[] = []; const records: EvidencedRecord[] = []; for (const [row, candidate] of input.records.entries()) { if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate) || ![Object.prototype, null].includes(Object.getPrototypeOf(candidate)) || Object.keys(candidate).some((key) => key !== 'value' && key !== 'evidence')) { issues.push({ row, field: '', code: 'invalid_record' }); continue; } const record = candidate as { value?: unknown; evidence?: unknown }; if (!Array.isArray(record.evidence)) { issues.push({ row, field: '', code: 'invalid_evidence' }); continue; } const validation = validateStructuredOutput({ fields: input.fields, sources: input.sources, value: record.value, evidence: record.evidence }); if (validation.status === 'needs_review') issues.push(...validation.issues.map((issue) => ({ row, ...issue }))); else records.push({ value: record.value as Record, evidence: record.evidence as FieldEvidence[] }); } return issues.length ? { status: 'needs_review', issues } : { status: 'mechanically_validated', records }; } /** Revalidate every row immediately before export; never return partial output. */ export function exportValidatedBatch(input: { format: 'json' | 'csv'; fields: readonly OutputFieldContract[]; sources: readonly TextSource[]; records: unknown; }): | { status: 'ready'; mediaType: 'application/json' | 'text/csv'; content: string } | { status: 'needs_review'; issues: BatchIssue[] } { if (input.format !== 'json' && input.format !== 'csv') { return { status: 'needs_review', issues: [{ field: '', code: 'unsupported_export_format' }] }; } const validation = validateStructuredBatch(input); if (validation.status !== 'mechanically_validated') return validation; const serialized: string[] = []; for (const [row, record] of validation.records.entries()) { const result = exportValidatedOutput({ ...input, ...record }); if (result.status !== 'ready') return { status: 'needs_review', issues: result.issues.map((issue) => ({ row, ...issue })) }; serialized.push(result.content); } if (input.format === 'json') { return { status: 'ready', mediaType: 'application/json', content: `[\n${serialized.map((s) => s.trim()).join(',\n')}\n]\n` }; } const quote = (cell: string) => `"${cell.replaceAll('"', '""')}"`; const header = input.fields.map((field) => quote(field.name)).join(','); const rows = validation.records.map((record) => input.fields.map((field) => quote(String(record.value[field.name]))).join(',')); return { status: 'ready', mediaType: 'text/csv', content: `${header}\r\n${rows.join('\r\n')}\r\n` }; }