import { z } from "zod"; import { isDiagnosticToken, isRealIsoDate, MODEL_FORECAST_LIMITS, createModelForecast, scoreModelForecast, calculateEmergence, type ModelForecastRequest, type ModelForecastScoringRequest, type ModelForecast, type ModelForecastScore, type EmergenceResult } from "@actuarial-ts/core"; import { parseReviewBoundary } from "./reviewBoundary.js"; import { retainedModelInputSchema } from "./modelCalibrationContracts.js"; import { analysisContextSchema, quantitySemanticsSchema, analysisSnapshotSchema, analysisReferenceSchema, assumptionSnapshotSchema, modelDevelopmentSourceSchema } from "./reviewContracts.js"; const token = z.string().refine(isDiagnosticToken), date = z.string().refine(isRealIsoDate); const characters = z.number().int().positive().max(MODEL_FORECAST_LIMITS.maximumInputCharacters); export const emergenceInputSchema = z.object({ priorUltimate: z.number().finite().nullable(), priorLatest: z.number().finite().nullable(), currentLatest: z.number().finite().nullable(), expectedPercentAtPrior: z.number().finite().nullable(), expectedPercentAtCurrent: z.number().finite().nullable() }).strict(); export function calculateEmergenceFromInput(input: unknown): EmergenceResult { return calculateEmergence(parseReviewBoundary(emergenceInputSchema, input)); } /** The retained model is checked by core replay, not certified by this structural boundary. */ export function modelForecastSchemaForRun(run: T) { return z.object({ id: token, version: token, run, target: z.discriminatedUnion("kind", [ z.object({ kind: z.literal("valuation"), date, ages: z.array(z.object({ origin: token, months: z.number().int().nonnegative() }).strict()).min(1).max(MODEL_FORECAST_LIMITS.maximumOrigins) }).strict(), z.object({ kind: z.literal("ultimate"), date, rationale: token }).strict(), ]), pattern: z.discriminatedUnion("kind", [z.object({ kind: z.literal("model") }).strict(), z.object({ kind: z.literal("assumption"), assumption: assumptionSnapshotSchema }).strict(), z.object({ kind: z.literal("frequency-severity-product"), rationale: token }).strict()]), convention: z.enum(["ultimate-pattern-difference", "remaining-reserve-proportion"]), rationale: token, limits: z.object({ maximumOrigins: z.number().int().positive().max(MODEL_FORECAST_LIMITS.maximumOrigins), maximumInputCharacters: characters }).strict() }).strict(); } export const forecastScoringVintageSchema = z.object({ source: modelDevelopmentSourceSchema, context: analysisContextSchema, quantity: quantitySemanticsSchema, snapshots: z.array(analysisSnapshotSchema), rationale: token, maturity: z.discriminatedUnion("kind", [z.object({ kind: z.literal("not-declared") }).strict(), z.object({ kind: z.literal("declared-complete"), origins: z.array(token).min(1), knownDate: date, rationale: token, evidence: z.array(analysisReferenceSchema).min(1) }).strict()]), }).strict(); export function modelForecastScoringSchemaForForecast(forecast: T) { return z.object({ id: token, version: token, forecast, truth: forecastScoringVintageSchema, population: z.object({ relationship: z.literal("same"), rationale: token }).strict().nullable(), scales: z.enum(["require-identical", "convert-to-forecast"]), normalization: z.enum(["none", "positive-expected-emergence", "positive-prior-cumulative"]), limits: z.object({ maximumInputCharacters: characters }).strict() }).strict(); } export const modelForecastRequestSchema = modelForecastSchemaForRun(retainedModelInputSchema); const retainedForecastInputSchema = z.unknown().superRefine((value, context) => { const parsed = z.object({ request: modelForecastRequestSchema }).passthrough().safeParse(value); if (!parsed.success) for (const issue of parsed.error.issues) context.addIssue(issue); }); export const modelForecastScoringRequestSchema = modelForecastScoringSchemaForForecast(retainedForecastInputSchema); export function parseModelForecastRequest(input: unknown): ModelForecastRequest { return parseReviewBoundary(modelForecastRequestSchema, input) as ModelForecastRequest; } export function parseModelForecastScoringRequest(input: unknown): ModelForecastScoringRequest { return parseReviewBoundary(modelForecastScoringRequestSchema, input) as ModelForecastScoringRequest; } export function createModelForecastFromInput(input: unknown): ModelForecast { return createModelForecast(parseModelForecastRequest(input)); } export function scoreModelForecastFromInput(input: unknown): ModelForecastScore { return scoreModelForecast(parseModelForecastScoringRequest(input)); }