import { z } from "zod"; import { AnalysisReviewError, isDiagnosticPlainRecord, snapshotDiagnosticJson, type JsonValue } from "@actuarial-ts/core"; import { parseCsv } from "./csv.js"; import { historicalSourceMappingSchema, mapHistoricalSourceRows, type HistoricalSourceMapping } from "./historyMapping.js"; export const REVIEW_SOURCE_DECODING_CONTRACT_VERSION = "actuarial-review-source-decoding/1" as const; export const REVIEW_SOURCE_DECODING_LIMITS = Object.freeze({ maximumCharacters: 64000000, maximumRows: 1000000 }); export type ReviewSourcePath = readonly (string | number)[]; export type ReviewSourceDecoding = | { readonly kind: "json-path"; readonly path: ReviewSourcePath } | { readonly kind: "historical-csv"; readonly mapping: HistoricalSourceMapping }; export const reviewSourcePathSchema = z.array(z.union([z.string(), z.number().int().nonnegative()])).max(64); export const reviewSourceDecodingSchema: z.ZodType = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("json-path"), path: reviewSourcePathSchema }).strict(), z.object({ kind: z.literal("historical-csv"), mapping: historicalSourceMappingSchema }).strict(), ]); export interface DecodedReviewSource { readonly contractVersion: typeof REVIEW_SOURCE_DECODING_CONTRACT_VERSION; readonly value: JsonValue; readonly evidence: { readonly kind: "json-path" } | { readonly kind: "historical-csv"; readonly mappingId: string; readonly mappingVersion: string; readonly inputRows: number; readonly acceptedRows: number; readonly rejectedRows: 0; }; } function invalid(path: string, message: string): never { throw new AnalysisReviewError([{ code: "REVIEW_SOURCE_DECODING_INVALID", path, message, remedies: ["Supply the exact supported decoding/mapping and correct the source structure before verifying it"] }]); } /** Own-member traversal only. A path declares the exact compared input, never code to execute. */ export function reviewSourceValueAt(value: unknown, path: ReviewSourcePath): unknown { const checked = reviewSourcePathSchema.safeParse(snapshotDiagnosticJson(path)); if (!checked.success) invalid("$.path", "A source path requires bounded string members or nonnegative array indices"); let selected = value; for (const part of checked.data) { if ((Array.isArray(selected) ? typeof part !== "number" : !isDiagnosticPlainRecord(selected) || typeof part !== "string")) invalid("$.path", "Source path component does not match its container"); const member = Object.getOwnPropertyDescriptor(selected as object, part); if (!member || !("value" in member) || !member.enumerable) invalid("$.path", "Source path does not resolve to an own JSON data member"); selected = member.value; } return selected; } /** Decodes supplied text; performs no I/O and does not authenticate its source. */ export function decodeReviewSource(input: { readonly text: string; readonly decoding: ReviewSourceDecoding; readonly limits: { readonly maximumCharacters: number; readonly maximumRows: number }; }): DecodedReviewSource { if (!isDiagnosticPlainRecord(input)) invalid("$", "Source decoding input must be a plain object"); const fields = Object.getOwnPropertyDescriptors(input); for (const key of Reflect.ownKeys(input)) if (typeof key !== "string" || !["text", "decoding", "limits"].includes(key) || !fields[key] || !("value" in fields[key]!) || !fields[key]!.enumerable) invalid("$", "Source decoding requires only own enumerable data fields"); const text = fields.text?.value; const parsed = z.object({ decoding: reviewSourceDecodingSchema, limits: z.object({ maximumCharacters: z.number().int().positive().max(REVIEW_SOURCE_DECODING_LIMITS.maximumCharacters), maximumRows: z.number().int().positive().max(REVIEW_SOURCE_DECODING_LIMITS.maximumRows), }).strict() }).strict().safeParse(snapshotDiagnosticJson({ decoding: fields.decoding?.value, limits: fields.limits?.value })); if (!parsed.success) invalid("$", `Invalid source decoding contract: ${parsed.error.issues[0]?.message ?? "schema failure"}`); const { decoding, limits } = parsed.data; if (typeof text !== "string" || text.length > limits.maximumCharacters) invalid("$.text", "Source text must fit the declared character budget"); if (decoding.kind === "json-path") { let value: unknown; try { value = JSON.parse(text.charCodeAt(0) === 0xfeff ? text.slice(1) : text); } catch { invalid("$.text", "Source is not valid JSON text"); } const selected = reviewSourceValueAt(value, decoding.path); return snapshotDiagnosticJson({ contractVersion: REVIEW_SOURCE_DECODING_CONTRACT_VERSION, value: selected as JsonValue, evidence: { kind: "json-path" } }); } const csv = parseCsv(text), header = csv.rows[0]; if (csv.warnings.length) invalid("$.text", csv.warnings.join("; ")); if (!header?.length || header.some((name) => !name.trim()) || new Set(header).size !== header.length) invalid("$.text", "CSV requires distinct nonblank header names"); if (csv.rows.length - 1 > limits.maximumRows) invalid("$.text", "CSV exceeds the declared row budget"); const rows = csv.rows.slice(1).map((row, i) => { if (row.length !== header.length) invalid(`$.text.rows[${i + 1}]`, "CSV row width differs from its header"); return { rowNumber: csv.rowLines[i + 1]!, values: Object.fromEntries(header.map((name, j) => [name, row[j]!])) }; }); const mapped = mapHistoricalSourceRows(decoding.mapping, rows); if (mapped.rejected.length) invalid("$.text", `Historical mapping rejected ${mapped.rejected.length} source rows; review them through mapHistoricalSourceRows before source verification`); return snapshotDiagnosticJson({ contractVersion: REVIEW_SOURCE_DECODING_CONTRACT_VERSION, value: mapped.records as unknown as JsonValue, evidence: { kind: "historical-csv", mappingId: mapped.mappingId, mappingVersion: mapped.mappingVersion, inputRows: rows.length, acceptedRows: mapped.records.length, rejectedRows: 0 } }); }