import { z } from "zod"; import { HISTORICAL_FOLD_LIMITS, HISTORICAL_STUDY_LIMITS, isDiagnosticToken, resolveHistoricalInformationSet, runHistoricalFold, runHistoricalStudy, validateHistoricalFoldRequest, validateHistoricalStudyRequest, type HistoricalFoldPreflight, type HistoricalStudyPreflight, type HistoricalStudy, type HistoricalStudyRequest, type HistoricalFold, type HistoricalFoldRequest, type HistoricalFoldPolicy, type HistoricalModelTemplate } from "@actuarial-ts/core"; import { historicalPreparationRequestSchema } from "./historicalPreparationContracts.js"; import { modelRequestSchema, bfPriorCalibrationRequestSchema } from "./reviewContracts.js"; import { recipeProjectionSchemaForPreparation } from "./recipeDevelopmentContracts.js"; import { recipeExposureSchemaForPreparation, exposurePopulationRelationshipSchema } from "./recipeExposureContracts.js"; import { modelForecastSchemaForRun, modelForecastScoringSchemaForForecast } from "./modelForecastContracts.js"; import { modelReplayLimitsSchema } from "./modelCalibrationContracts.js"; import { parseReviewBoundary } from "./reviewBoundary.js"; import { reviewOnLevelRequestSchema } from "./reviewOnLevelContracts.js"; const token = z.string().refine(isDiagnosticToken); const catalog = z.object({ kind: z.literal("catalog"), slot: token }).strict(); const binding = z.discriminatedUnion("kind", [catalog, z.object({ kind: z.literal("fold-prior") }).strict()]); const window = z.discriminatedUnion("kind", [z.object({ kind: z.literal("external-assumptions") }).strict(), z.object({ kind: z.literal("model-input-origins"), origins: z.array(token).min(1), rationale: token }).strict()]); const onLevel = z.object({ kind: z.literal("on-level"), historySlot: token, calculation: reviewOnLevelRequestSchema.omit({ context: true, premium: true, history: true, snapshots: true, application: true, declaration: true }), application: z.object({ kind: z.enum(["model-prior", "expected-loss-calibration"]), rationale: token }).strict(), declaration: z.object({ actorId: token, rationale: token, clock: z.literal("fold-cutoffs") }).strict(), }).strict(); // Derive method-specific options from the existing model contract; replace only the bound inputs. const templates = modelRequestSchema.options.map((variant) => { const shape: Record = { ...variant.shape.options.shape }; if (shape.adjustments) shape.adjustments = z.union([shape.adjustments, onLevel]); for (const key of ["development", "prior", "countDevelopment", "severityDevelopment", "tail", "exposure", "counts"]) { const original = shape[key]; if (!original || key === "tail" && original instanceof z.ZodLiteral) continue; const replacement = key === "exposure" ? z.literal("prepared") : key === "counts" ? catalog.extend({ clock: z.literal("fold-cutoffs") }) : binding; shape[key] = original.isNullable() ? replacement.nullable() : replacement; } return z.object({ method: variant.shape.method, options: z.object(shape).strict(), policies: variant.shape.policies, limits: variant.shape.limits, calibrationWindow: window }).strict(); }); export const historicalModelTemplateSchema: z.ZodType = z.discriminatedUnion("method", templates as [typeof templates[number], ...typeof templates[number][]]) as unknown as z.ZodType; const forecast = modelForecastSchemaForRun(z.unknown()).omit({ run: true, pattern: true }).extend({ pattern: z.discriminatedUnion("kind", [z.object({ kind: z.literal("model") }).strict(), catalog, z.object({ kind: z.literal("frequency-severity-product"), rationale: token }).strict()]) }); export const historicalFoldPolicySchema: z.ZodType = z.object({ refit: z.enum(["fixed-assumptions", "recalibrated-per-fold"]), projection: recipeProjectionSchemaForPreparation(z.unknown()).omit({ preparations: true, valuationDate: true, knowledgeDate: true }), exposure: z.object({ projection: recipeExposureSchemaForPreparation(z.unknown()).omit({ preparation: true, knownDate: true }), relationship: exposurePopulationRelationshipSchema }).strict().nullable(), model: historicalModelTemplateSchema, priorCalibration: z.object({ model: historicalModelTemplateSchema, calibration: z.object({ id: token, version: token, selection: bfPriorCalibrationRequestSchema.shape.selection, assumption: bfPriorCalibrationRequestSchema.shape.assumption, limits: modelReplayLimitsSchema }).strict() }).strict().nullable(), forecast, }).strict(); const structure = z.object({ id: token, version: token, preparation: historicalPreparationRequestSchema, policySlot: token, scoring: modelForecastScoringSchemaForForecast(z.unknown()).omit({ forecast: true }).nullable(), limits: z.object({ maximumInputCharacters: z.number().int().positive().max(HISTORICAL_FOLD_LIMITS.maximumInputCharacters), maximumOutputCharacters: z.number().int().positive().max(HISTORICAL_FOLD_LIMITS.maximumOutputCharacters) }).strict(), }).strict(); export const historicalFoldRequestSchema: z.ZodType = structure.superRefine((request, context) => { const information = resolveHistoricalInformationSet(request.preparation.information); const selected = information.decisions.find((decision) => decision.slot === request.policySlot)?.selected; if (selected) { const parsed = historicalFoldPolicySchema.safeParse(selected.snapshot.content); if (!parsed.success) for (const issue of parsed.error.issues) context.addIssue({ code: z.ZodIssueCode.custom, path: ["preparation", "information", "selectedPolicy", ...issue.path], message: issue.message }); } }); export function parseHistoricalFoldRequest(input: unknown): HistoricalFoldRequest { return parseReviewBoundary(historicalFoldRequestSchema, input); } export function validateHistoricalFoldFromInput(input: unknown): HistoricalFoldPreflight { return validateHistoricalFoldRequest(parseHistoricalFoldRequest(input)); } export function runHistoricalFoldFromInput(input: unknown): HistoricalFold { return runHistoricalFold(parseHistoricalFoldRequest(input)); } export const historicalStudyRequestSchema: z.ZodType = z.object({ id: token, version: token, refit: z.enum(["fixed-assumptions", "recalibrated-per-fold"]), folds: z.array(z.object({ id: token, role: z.enum(["exploratory", "tuning", "validation"]), request: historicalFoldRequestSchema }).strict()).min(1).max(HISTORICAL_STUDY_LIMITS.maximumFolds), decisionUses: z.array(z.object({ decision: z.enum(["method-selection", "parameter-tuning", "assumption-selection"]), evaluatedFolds: z.array(token).min(1), appliesToFolds: z.array(token).min(1), rationale: token }).strict()).max(HISTORICAL_STUDY_LIMITS.maximumDecisionUses), limits: z.object({ maximumFolds: z.number().int().positive().max(HISTORICAL_STUDY_LIMITS.maximumFolds), maximumDecisionUses: z.number().int().positive().max(HISTORICAL_STUDY_LIMITS.maximumDecisionUses), maximumInputCharacters: z.number().int().positive().max(HISTORICAL_STUDY_LIMITS.maximumInputCharacters), maximumOutputCharacters: z.number().int().positive().max(HISTORICAL_STUDY_LIMITS.maximumOutputCharacters) }).strict(), }).strict(); export function parseHistoricalStudyRequest(input: unknown): HistoricalStudyRequest { return parseReviewBoundary(historicalStudyRequestSchema, input); } export function validateHistoricalStudyFromInput(input: unknown): HistoricalStudyPreflight { return validateHistoricalStudyRequest(parseHistoricalStudyRequest(input)); } export function runHistoricalStudyFromInput(input: unknown): HistoricalStudy { return runHistoricalStudy(parseHistoricalStudyRequest(input)); }