import { z } from "zod"; import { AnalysisReviewError, DiagnosticValidationError, ReservingError, canonicalJson, snapshotDiagnosticJson, isDiagnosticToken, isRealIsoDate, normalizeDevelopmentInput, remapDevelopmentAges, developmentIntervalView, accumulateDevelopmentIntervals, truncateDevelopmentAtValuation, calendarDevelopmentView, sameMaturityDevelopment, aggregateDevelopmentOrigins, computeDevelopmentFactors, factorVolatility, triangleFromGrid, DEFAULT_AVERAGES, type AverageSpec, type DevelopmentFactors } from "@actuarial-ts/core"; import { developmentGridInputSchema, normalizedDevelopmentInputSchema, developmentAgeMappingSchema, developmentIntervalViewSchema, developmentCalendarInputSchema, developmentOriginAggregationRequestSchema } from "./reviewContracts.js"; import { parseReviewBoundary } from "./reviewBoundary.js"; import type { DevelopmentGridInput, DevelopmentAgeMapping, NormalizedDevelopmentInput, DevelopmentOpening, DevelopmentIntervalView, DevelopmentCalendarInput, Triangle } from "@actuarial-ts/core"; import { MAX_DIAGNOSTIC_JSON_DEPTH, MAX_DIAGNOSTIC_JSON_NODES } from "@actuarial-ts/core"; type RemappingRequest = { source: DevelopmentGridInput; mapping: DevelopmentAgeMapping }; type IntervalsRequest = { development: NormalizedDevelopmentInput; opening?: DevelopmentOpening }; type ValuationRequest = { source: DevelopmentCalendarInput; cutoff: string }; type MaturityRequest = { development: NormalizedDevelopmentInput; ageMonths: number }; type FactorsRequest = { triangle: Triangle; averageSpecs?: AverageSpec[] }; /** Standalone boundary ceilings; these do not assert measured latency or peak memory. */ const boundaryLimits = Object.freeze({ maximumOrigins: 1000, maximumCells: 100000, maximumInputCharacters: 20000000, maximumOutputCharacters: 80000000, maximumJsonDepth: MAX_DIAGNOSTIC_JSON_DEPTH, maximumJsonNodes: MAX_DIAGNOSTIC_JSON_NODES }); export const NATIVE_DEVELOPMENT_INPUT_LIMITS = Object.freeze({ ...boundaryLimits, maximumTargetOrigins: 1000, maximumOutputCells: 100000, maximumContributionCells: 1000000 }); export const NATIVE_FACTOR_INPUT_LIMITS = Object.freeze({ ...boundaryLimits, maximumAverages: 64, maximumOutputCells: 100000 }); const token = z.string().refine(isDiagnosticToken), date = z.string().refine(isRealIsoDate); export const developmentNormalizationRequestSchema: z.ZodType = developmentGridInputSchema; export const developmentRemappingRequestSchema: z.ZodType = z.object({ source: developmentGridInputSchema, mapping: developmentAgeMappingSchema }).strict(); export const developmentIntervalsRequestSchema: z.ZodType = z.object({ development: normalizedDevelopmentInputSchema, opening: developmentIntervalViewSchema.shape.opening.optional() }).strict(); export const developmentIntervalAccumulationRequestSchema: z.ZodType = developmentIntervalViewSchema; export const developmentValuationRequestSchema: z.ZodType = z.object({ source: developmentCalendarInputSchema, cutoff: date }).strict(); export const developmentCalendarViewRequestSchema: z.ZodType = developmentCalendarInputSchema; export const developmentMaturityRequestSchema: z.ZodType = z.object({ development: normalizedDevelopmentInputSchema, ageMonths: z.number().int().safe().nonnegative() }).strict(); export const developmentAggregationRequestSchema: z.ZodType[0]> = developmentOriginAggregationRequestSchema; const average = z.object({ key: token, label: token, kind: z.enum(["straight", "weighted", "medial", "geometric"]), years: z.number().int().safe().positive().optional() }).strict(); const averages = z.array(average).max(NATIVE_FACTOR_INPUT_LIMITS.maximumAverages).superRefine((items, context) => { if (new Set(items.map((item) => item.key)).size !== items.length) context.addIssue({ code: z.ZodIssueCode.custom, message: "Average keys must be unique" }); }); export const developmentFactorsRequestSchema: z.ZodType = z.object({ triangle: developmentGridInputSchema.extend({ ages: z.array(z.number().int().safe().positive()) }), averageSpecs: averages.optional() }).strict(); export const developmentFactorVolatilityRequestSchema: z.ZodType = z.object({ fromAges: z.array(z.number().int().safe().positive()), toAges: z.array(z.number().int().safe().positive()), individual: z.array(z.array(z.number().finite().nullable())), averages: z.array(z.object({ spec: average, values: z.array(z.number().finite().nullable()) }).strict()).max(NATIVE_FACTOR_INPUT_LIMITS.maximumAverages), }).strict().superRefine((value, context) => { const n = value.fromAges.length; if (value.toAges.length !== n || value.fromAges.some((age, i) => value.toAges[i]! <= age || (i > 0 && age !== value.toAges[i - 1]))) context.addIssue({ code: z.ZodIssueCode.custom, path: ["fromAges"], message: "Factor intervals must be positive, contiguous and ordered" }); if (value.individual.some((row) => row.length !== n) || value.averages.some((row) => row.values.length !== n)) context.addIssue({ code: z.ZodIssueCode.custom, path: ["individual"], message: "Every individual and average row must match factor intervals" }); if (new Set(value.averages.map((row) => row.spec.key)).size !== value.averages.length) context.addIssue({ code: z.ZodIssueCode.custom, path: ["averages"], message: "Average keys must be unique" }); }); function refuse(path: string, message: string): never { throw new AnalysisReviewError([{ code: "RESOURCE_LIMIT_EXCEEDED", path, message, remedies: ["Reduce the declared source, target or average expansion, or use an explicitly bounded host plan"] }]); } function bounded(schema: z.ZodType, input: unknown): T { const request = parseReviewBoundary(schema, input); if (canonicalJson(request).length > boundaryLimits.maximumInputCharacters) refuse("$", "Native development input exceeds the standalone character ceiling"); return request; } function grid(input: { readonly origins: readonly unknown[]; readonly values: readonly (readonly unknown[])[] }, columns: number, path: string): void { if (input.origins.length > boundaryLimits.maximumOrigins || Math.max(columns, input.origins.length * columns, input.values.reduce((sum, row) => sum + row.length, 0)) > boundaryLimits.maximumCells) refuse(path, "Native development input exceeds its origin/grid ceiling"); } function result(calculate: () => T): T { try { const output = snapshotDiagnosticJson(calculate()); if (canonicalJson(output).length > boundaryLimits.maximumOutputCharacters) refuse("$", "Native development output exceeds its serialized character ceiling"); return output; } catch (error) { if (!(error instanceof DiagnosticValidationError) || !error.issues.every((issue) => issue.code === "invalid-json-value")) throw error; throw new AnalysisReviewError(error.issues.map((issue) => ({ code: "DEVELOPMENT_NUMERIC_FAILURE", path: "$", message: issue.message, coordinates: { nativeOutputPath: issue.path }, remedies: ["Inspect native arithmetic and input magnitudes; no automatic rescaling or replacement value is applied"] }))); } } export function parseDevelopmentNormalizationRequest(input: unknown) { const value = bounded(developmentNormalizationRequestSchema, input); grid(value, value.ages.length, "$"); return value; } export function parseDevelopmentRemappingRequest(input: unknown) { const value = bounded(developmentRemappingRequestSchema, input); grid(value.source, value.source.ages.length, "$.source"); return value; } export function parseDevelopmentIntervalsRequest(input: unknown) { const value = bounded(developmentIntervalsRequestSchema, input); grid(value.development, value.development.ages.length, "$.development"); return value; } export function parseDevelopmentIntervalAccumulationRequest(input: unknown) { const value = bounded(developmentIntervalAccumulationRequestSchema, input); grid(value, value.intervals.length, "$"); return value; } export function parseDevelopmentValuationRequest(input: unknown) { const value = bounded(developmentValuationRequestSchema, input); grid(value.source.development, value.source.development.ages.length, "$.source"); return value; } export function parseDevelopmentCalendarViewRequest(input: unknown) { const value = bounded(developmentCalendarViewRequestSchema, input); grid(value.development, value.development.ages.length, "$.development"); return value; } export function parseDevelopmentMaturityRequest(input: unknown) { const value = bounded(developmentMaturityRequestSchema, input); grid(value.development, value.development.ages.length, "$.development"); return value; } export function parseDevelopmentAggregationRequest(input: unknown) { const value = bounded(developmentAggregationRequestSchema, input); grid(value.source.development, value.source.development.ages.length, "$.source"); const columns = new Set(value.targets.flatMap((target) => target.observations.map((observation) => observation.ageMonths))).size; if (value.targets.length > NATIVE_DEVELOPMENT_INPUT_LIMITS.maximumTargetOrigins || value.targets.length * columns > NATIVE_DEVELOPMENT_INPUT_LIMITS.maximumOutputCells || value.targets.reduce((sum, target) => sum + target.members.length * target.observations.length, 0) > NATIVE_DEVELOPMENT_INPUT_LIMITS.maximumContributionCells) refuse("$.targets", "Aggregation expansion exceeds its target/output/contribution ceiling"); return value; } export function parseDevelopmentFactorsRequest(input: unknown) { const value = bounded(developmentFactorsRequestSchema, input); grid(value.triangle, value.triangle.ages.length, "$.triangle"); try { triangleFromGrid(value.triangle.kind, value.triangle.origins, value.triangle.ages, value.triangle.values); } catch (error) { if (!(error instanceof ReservingError)) throw error; throw new AnalysisReviewError([{ code: "INVALID_FACTOR_TRIANGLE", path: "$.triangle", message: error.message, coordinates: { nativeCode: error.code }, remedies: ["Supply the native positive-age rectangular triangle; normalize/remap source ages before factor calculation"] }]); } // Validate the actual native default too; the public menu remains the sole default source. const averageSpecs: AverageSpec[] = parseReviewBoundary(averages, value.averageSpecs ?? DEFAULT_AVERAGES); if (Math.max(0, value.triangle.ages.length - 1) * (value.triangle.origins.length + averageSpecs.length) > NATIVE_FACTOR_INPUT_LIMITS.maximumOutputCells) refuse("$.averageSpecs", "Individual-factor and average expansion exceeds its standalone output-cell ceiling"); return { ...value, averageSpecs }; } export function parseDevelopmentFactorVolatilityRequest(input: unknown) { const value = bounded(developmentFactorVolatilityRequestSchema, input); grid({ origins: value.individual, values: value.individual }, value.fromAges.length, "$.individual"); if (value.averages.reduce((sum, row) => sum + row.values.length, 0) > NATIVE_FACTOR_INPUT_LIMITS.maximumCells) refuse("$.averages", "Supplied average evidence exceeds its standalone cell ceiling"); return value; } export function normalizeDevelopmentFromInput(input: unknown) { const value = parseDevelopmentNormalizationRequest(input); return result(() => normalizeDevelopmentInput(value)); } export function remapDevelopmentAgesFromInput(input: unknown) { const value = parseDevelopmentRemappingRequest(input); return result(() => remapDevelopmentAges(value.source, value.mapping)); } export function developmentIntervalsFromInput(input: unknown) { const value = parseDevelopmentIntervalsRequest(input); return result(() => developmentIntervalView(value.development, value.opening)); } export function accumulateDevelopmentIntervalsFromInput(input: unknown) { const value = parseDevelopmentIntervalAccumulationRequest(input); return result(() => accumulateDevelopmentIntervals(value)); } export function truncateDevelopmentFromInput(input: unknown) { const value = parseDevelopmentValuationRequest(input); return result(() => truncateDevelopmentAtValuation(value.source, value.cutoff)); } export function calendarDevelopmentFromInput(input: unknown) { const value = parseDevelopmentCalendarViewRequest(input); return result(() => calendarDevelopmentView(value)); } export function sameMaturityDevelopmentFromInput(input: unknown) { const value = parseDevelopmentMaturityRequest(input); return result(() => sameMaturityDevelopment(value.development, value.ageMonths)); } export function aggregateDevelopmentFromInput(input: unknown) { const value = parseDevelopmentAggregationRequest(input); return result(() => aggregateDevelopmentOrigins(value)); } export function computeDevelopmentFactorsFromInput(input: unknown) { const value = parseDevelopmentFactorsRequest(input); value.triangle.values.forEach((row, i) => row.slice(0, -1).forEach((denominator, j) => { const numerator = row[j + 1] ?? null; if (denominator !== null && denominator > 0 && numerator !== null && !Number.isFinite(numerator / denominator)) throw new AnalysisReviewError([{ code: "DEVELOPMENT_NUMERIC_FAILURE", path: "$.triangle", message: "Native factor arithmetic would produce a nonfinite value", coordinates: { originIndex: i, fromAgeMonths: value.triangle.ages[j]!, toAgeMonths: value.triangle.ages[j + 1]! }, remedies: ["Inspect native arithmetic and input magnitudes; no automatic rescaling or replacement value is applied"] }]); })); return result(() => computeDevelopmentFactors(value.triangle, value.averageSpecs)); } export function factorVolatilityFromInput(input: unknown) { const value = parseDevelopmentFactorVolatilityRequest(input); return result(() => factorVolatility(value)); }