Files
0pt/src/document-workbench/structured-batch.ts
T
admin d56ccbb542
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (push) Successful in 14s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / security (push) Successful in 29s
Security Scan (SIAX Cloud) / security (push) Successful in 37s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 27s
CI (SIAX Cloud) / security (pull_request) Successful in 43s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m0s
CI (SIAX Cloud) / quality (push) Successful in 1m46s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m34s
feat: validate and export evidenced document batches
2026-11-02 08:37:11 +01:00

69 lines
3.7 KiB
TypeScript

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<string, unknown>; 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<string, unknown>, 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` };
}