feat: add evidence-checked document exports #411

Merged
admin merged 1 commits from codex/document-validated-export into main 2026-11-02 01:05:40 +00:00
2 changed files with 93 additions and 0 deletions
@@ -0,0 +1,52 @@
import { validateStructuredOutput, type FieldEvidence, type OutputFieldContract, type TextSource, type ValidationIssue } from './structured-output-validation';
export type ValidatedExport =
| { status: 'ready'; mediaType: 'application/json' | 'text/csv'; content: string }
| { status: 'needs_review'; issues: ValidationIssue[] };
/**
* Serialize a single, flat record after rechecking its evidence against the
* supplied source text. This is an internal serializer, not an authorization,
* completeness, semantic-correctness or source-authenticity check.
* CSV rejects formula-like text rather than silently changing customer data;
* JSON preserves it exactly. CSV also rejects absent optional fields so that
* absence is never silently converted into an empty string.
*/
export function exportValidatedOutput(input: {
format: 'json' | 'csv';
fields: readonly OutputFieldContract[];
value: unknown;
sources: readonly TextSource[];
evidence: readonly FieldEvidence[];
}): ValidatedExport {
if (input.format !== 'json' && input.format !== 'csv') {
return { status: 'needs_review', issues: [{ field: '', code: 'unsupported_export_format' }] };
}
const validation = validateStructuredOutput(input);
if (validation.status !== 'mechanically_validated') return validation;
const value = input.value as Record<string, string | number | boolean>;
const ordered: Record<string, string | number | boolean> = Object.create(null) as Record<string, string | number | boolean>;
for (const field of input.fields) {
if (Object.prototype.hasOwnProperty.call(value, field.name)) ordered[field.name] = value[field.name]!;
else if (input.format === 'csv') {
return { status: 'needs_review', issues: [{ field: field.name, code: 'csv_missing_value' }] };
}
}
if (input.format === 'json') {
return { status: 'ready', mediaType: 'application/json', content: `${JSON.stringify(ordered, null, 2)}\n` };
}
// Spreadsheet applications may interpret these prefixes as formulas even
// in quoted CSV cells. Treat leading control characters as unsafe as well.
const unsafe = (cell: string): boolean => /^[\t\r\n]/u.test(cell) || /^\s*[=+@-]/u.test(cell);
for (const field of input.fields) {
if (unsafe(field.name) || (field.type === 'string' && unsafe(ordered[field.name] as string))) {
return { status: 'needs_review', issues: [{ field: field.name, code: 'csv_unsafe_cell' }] };
}
}
const quote = (cell: string): string => `"${cell.replaceAll('"', '""')}"`;
const header = input.fields.map((field) => quote(field.name)).join(',');
const row = input.fields.map((field) => quote(String(ordered[field.name]))).join(',');
return { status: 'ready', mediaType: 'text/csv', content: `${header}\r\n${row}\r\n` };
}
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest';
import { exportValidatedOutput } from '../../src/document-workbench/export-validated-output';
const fields = [
{ name: 'name', type: 'string' as const, required: true },
{ name: 'count', type: 'integer' as const, required: true },
];
const sources = [{ id: 'source', text: 'Acme, "North"\n-2' }];
const evidence = [
{ field: 'name', sourceId: 'source', start: 0, end: 13, transform: 'exact' as const },
{ field: 'count', sourceId: 'source', start: 14, end: 16, transform: 'integer' as const },
];
const input = { fields, sources, evidence, value: { name: 'Acme, "North"', count: -2 } };
describe('validated document exports', () => {
it('preserves JSON values and produces correctly escaped, ordered CSV', () => {
const json = exportValidatedOutput({ ...input, format: 'json' });
expect(json.status).toBe('ready');
if (json.status === 'ready') expect(JSON.parse(json.content)).toEqual(input.value);
expect(exportValidatedOutput({ ...input, format: 'csv' })).toEqual({
status: 'ready', mediaType: 'text/csv', content: '"name","count"\r\n"Acme, ""North""","-2"\r\n',
});
});
it('revalidates evidence and refuses unsupported or absent data', () => {
expect(exportValidatedOutput({ ...input, value: { ...input.value, name: 'invented' }, format: 'json' }).status).toBe('needs_review');
expect(exportValidatedOutput({ ...input, evidence: [], format: 'csv' }).status).toBe('needs_review');
const optional = { ...input, fields: [...fields, { name: 'note', type: 'string' as const, required: false }] };
expect(exportValidatedOutput({ ...optional, format: 'csv' })).toMatchObject({ status: 'needs_review', issues: [{ field: 'note', code: 'csv_missing_value' }] });
expect(exportValidatedOutput({ ...optional, format: 'json' }).status).toBe('ready');
expect(exportValidatedOutput({ ...input, format: 'xml' as 'json' })).toMatchObject({ status: 'needs_review', issues: [{ code: 'unsupported_export_format' }] });
});
it('rejects formula-like CSV values and headers without changing JSON', () => {
const bad = { fields: [{ name: 'cell', type: 'string' as const, required: true }], sources: [{ id: 's', text: ' =1+1' }], evidence: [{ field: 'cell', sourceId: 's', start: 0, end: 6, transform: 'exact' as const }], value: { cell: ' =1+1' } };
expect(exportValidatedOutput({ ...bad, format: 'csv' })).toMatchObject({ status: 'needs_review', issues: [{ code: 'csv_unsafe_cell' }] });
expect(exportValidatedOutput({ ...bad, format: 'json' }).status).toBe('ready');
const header = { fields: [{ name: '=header', type: 'string' as const, required: true }], sources: [{ id: 's', text: 'safe' }], evidence: [{ field: '=header', sourceId: 's', start: 0, end: 4, transform: 'exact' as const }], value: { '=header': 'safe' } };
expect(exportValidatedOutput({ ...header, format: 'csv' })).toMatchObject({ status: 'needs_review', issues: [{ code: 'csv_unsafe_cell' }] });
});
});