import { reviewPremiumAdjustmentRequestSchema, reviewPremiumTrendSchemaForRate, adjustedModelPriorCalibrationSchemaForInputs, reviewPremiumAdjustmentResultSchema } from "./reviewPremiumAdjustmentContracts.js"; import { reviewSettlementAdjustmentSchemaForInput } from "./reviewSettlementAdjustmentContracts.js"; import { z } from "zod"; import { compileReviewPlan, isDiagnosticToken, isRealIsoDate, REVIEW_PLAN_CONTRACT_VERSION, REVIEW_PLAN_LIMITS, REVIEW_RESERVE_CONTROL_LIMIT, type ReviewPlan, type ReviewPlanPort, type CompiledReviewPlan } from "@actuarial-ts/core"; import { parseReviewBoundary } from "./reviewBoundary.js"; import { developmentSelectionSchemaForInput } from "./developmentSelectionContracts.js"; import { reviewTrendFitRequestSchema, reviewTrendSelectionRequestSchema, reviewTrendApplicationRequestSchema } from "./reviewTrendContracts.js"; import { reviewOnLevelRequestSchema } from "./reviewOnLevelContracts.js"; import { reviewLayerApplicationRequestSchema, reviewIlfCalibrationRequestSchema, reviewIlfApplicationRequestSchema } from "./reviewCappingIlfContracts.js"; import { reviewUlaeCalibrationRequestSchema, reviewUlaeReserveRequestSchema } from "./reviewUlaeContracts.js"; import { nominalCashflowSourceDefinitionSchema, reviewNominalCashflowRequestSchema } from "./reviewNominalCashflowContracts.js"; import { reviewDiscountingRequestSchema } from "./reviewDiscountingContracts.js"; import { reviewNetRecoveriesPaidBalanceRequestSchema, reviewNetRecoveriesRequestSchema } from "./reviewNetRecoveriesContracts.js"; import { declaredReviewOriginSeriesRequestSchema, modelReviewOriginSeriesRequestSchema } from "./reviewOriginSeriesContracts.js"; import { reviewDevelopmentAdjustmentSchemaForInput } from "./reviewDevelopmentAdjustmentContracts.js"; import { analysisSnapshotSchema, analysisReferenceSchema, analysisContextSchema, reviewAssumptionDefinitionSchema, assumptionSnapshotSchema, developmentCalendarInputSchema, developmentGridInputSchema, alignedExposureInputSchema, modelRequestSchema, modelComparisonCandidateSchema, boundedGroupedProjectionRequestSchema, modelComparisonRequestSchema, declaredEstimateInputSchema, estimateSelectionCandidateSchema, estimateSelectionCoordinateSchema, estimateSelectionRuleSchema, estimateSelectionRequestSchema, developmentReserveBalanceInputSchema, reserveControlInputSchema, reserveReconciliationRequestSchema, } from "./reviewContracts.js"; import { reviewPreparationRequestSchema, recipeProjectionSchemaForPreparation } from "./recipeDevelopmentContracts.js"; import { recipeExposureSchemaForPreparation, exposurePopulationRelationshipSchema } from "./recipeExposureContracts.js"; import { modelPriorCalibrationSchemaForRun } from "./modelCalibrationContracts.js"; import { modelDiagnosticSchemaForRun } from "./modelDiagnosticsContracts.js"; import { observedDiagnosticRequestSchema } from "./observedDiagnosticsContracts.js"; import { modelForecastSchemaForRun, modelForecastScoringSchemaForForecast } from "./modelForecastContracts.js"; import { historicalPreparationRequestSchema } from "./historicalPreparationContracts.js"; import { historicalFoldRequestSchema, historicalStudyRequestSchema } from "./historicalValidationContracts.js"; import { reviewDevelopmentAccumulationSchemaForInput, reviewDevelopmentSchemaForInput, reviewDevelopmentTransformOperationSchema, reviewDevelopmentViewOperationSchema } from "./reviewDevelopmentContracts.js"; const token = z.string().refine(isDiagnosticToken, "Expected a valid nonempty identifier"); const date = z.string().refine(isRealIsoDate, "Expected a real ISO date"); const link = (port: K) => z.object({ kind: z.literal("review-output"), node: token, port: z.literal(port) }).strict(); const value = (schema: z.ZodTypeAny, port: ReviewPlanPort) => z.union([schema, link(port)]); const reference = value(analysisReferenceSchema, "reference"), snapshot = value(analysisSnapshotSchema, "snapshot"); const contextReference = z.union([reference, z.literal("not-applicable"), z.null()]); const context = z.union([link("context"), analysisContextSchema.extend({ populations: z.object({ claims: contextReference, financial: contextReference, reporting: contextReference, exposure: contextReference }).strict(), financialBasis: contextReference, sourceRevisions: z.array(reference), adjustments: z.array(reference), })]); const union = (schemas: z.ZodTypeAny[]) => z.union(schemas as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]]); // Extend the existing method schemas at the same documented slots as the pure graph mapper. const model = union((modelRequestSchema.options as z.AnyZodObject[]).map((schema) => { const options = schema.shape.options as z.AnyZodObject; const lifted: z.ZodRawShape = {}; for (const key of ["development", "prior", "pattern", "countDevelopment", "severityDevelopment", "ultimateCounts", "disposalPattern", "severityTrend", "exposure", "tail", "adjustments"]) { const original: z.ZodTypeAny | undefined = options.shape[key]; if (original && (key !== "tail" || schema.shape.method.value === "mack")) lifted[key] = value(original, key === "exposure" ? "exposure" : key === "adjustments" ? "modelAdjustments" : "assumption"); } if (schema.shape.method.value === "frequencySeverity") { const counts = options.shape.counts as z.AnyZodObject; lifted.counts = counts.extend({ source: value(counts.shape.source, "development"), context, quantity: value(counts.shape.quantity, "quantity") }); } if (schema.shape.method.value === "munichChainLadder") { const counterpart = options.shape.counterpart as z.AnyZodObject; lifted.counterpart = counterpart.extend({ source: value(counterpart.shape.source, "development"), quantity: value(counterpart.shape.quantity, "quantity") }); } if (schema.shape.method.value === "caseOutstanding") { const caseOperand = options.shape.case as z.AnyZodObject; lifted.case = caseOperand.extend({ source: value(caseOperand.shape.source, "development"), quantity: value(caseOperand.shape.quantity, "quantity") }); } if (schema.shape.method.value === "fisherLange") { const counts = options.shape.closedCounts as z.AnyZodObject; lifted.closedCounts = counts.extend({ source: value(counts.shape.source, "development"), context, quantity: value(counts.shape.quantity, "quantity") }); } const quantityKey = schema.shape.method.value === "salvageSubro" ? "recoveryQuantity" : "lossQuantity"; return schema.extend({ context, [quantityKey]: value(schema.shape[quantityKey], "quantity"), source: value(schema.shape.source, "development"), snapshots: z.union([z.array(snapshot), link("snapshots")]), options: options.extend(lifted) }); })); const selectionRules = union(estimateSelectionRuleSchema.options.map((schema) => schema.shape.kind.value === "manual" ? schema.extend({ evidence: z.array(reference) }) : schema)); const selection = estimateSelectionRequestSchema.extend({ coordinates: z.array(estimateSelectionCoordinateSchema.extend({ context })).min(1), candidates: z.array(value(estimateSelectionCandidateSchema, "selectionCandidate")), snapshots: z.union([z.array(snapshot), link("snapshots")]), rules: z.array(selectionRules), }); const reconciliation = reserveReconciliationRequestSchema.extend({ selection: link("selection"), snapshots: z.array(snapshot), entries: z.array(reserveReconciliationRequestSchema.shape.entries.element.extend({ paid: reference.nullable(), case: reference.nullable(), reportedIncurred: reference.nullable(), bookedUnpaid: reference.nullable() })).min(1), controls: reserveReconciliationRequestSchema.shape.controls.unwrap().element.extend({ source: snapshot }).array().max(REVIEW_RESERVE_CONTROL_LIMIT).optional(), }); const modelSelection = estimateSelectionCandidateSchema.options.find((schema) => schema.shape.kind.value === "model")! as z.AnyZodObject; const calculatedSelection = estimateSelectionCandidateSchema.options.find((schema) => schema.shape.kind.value === "calculated")! as z.AnyZodObject; const discountRates = z.discriminatedUnion("kind", [ z.object({ kind: z.literal("flat"), rate: value(assumptionSnapshotSchema, "assumption") }).strict(), z.object({ kind: z.literal("curve"), points: z.array(z.object({ year: z.number().int().safe().positive(), rate: value(assumptionSnapshotSchema, "assumption") }).strict()).min(1) }).strict(), ]); const development = reviewDevelopmentSchemaForInput(); const developmentSelection = developmentSelectionSchemaForInput(); const developmentInputs = development.extend({ source: value(development.shape.source, "development"), context, quantity: value(development.shape.quantity, "quantity"), snapshots: z.union([z.array(snapshot), link("snapshots")]) }); const recipeDevelopment = recipeProjectionSchemaForPreparation(link("preparedRecipe")); const adjustment = reviewDevelopmentAdjustmentSchemaForInput(); const adjustmentOperand = (schema: T) => schema.extend({ source: value(schema.shape.source, "development"), context, quantity: value(schema.shape.quantity, "quantity") }); const selectionSubject = developmentSelection.shape.subject.unwrap(); const selectionRatio = selectionSubject.options[1]; const selectionSubjectInput = z.discriminatedUnion("kind", [selectionSubject.options[0], selectionRatio.extend({ counts: adjustmentOperand(selectionRatio.shape.counts) })]); const adjustmentInput = adjustment.extend({ inputs: adjustment.shape.inputs.extend({ paid: adjustmentOperand(adjustment.shape.inputs.shape.paid), incurred: adjustmentOperand(adjustment.shape.inputs.shape.incurred), openCounts: adjustmentOperand(adjustment.shape.inputs.shape.openCounts) }), severityTrend: value(adjustment.shape.severityTrend, "assumption"), snapshots: z.union([z.array(snapshot), link("snapshots")]) }); const settlement = reviewSettlementAdjustmentSchemaForInput(); const settlementInput = settlement.extend({ inputs: settlement.shape.inputs.extend({ paid: adjustmentOperand(settlement.shape.inputs.shape.paid), closedCounts: adjustmentOperand(settlement.shape.inputs.shape.closedCounts) }), ultimateCounts: value(settlement.shape.ultimateCounts, "assumption"), snapshots: z.union([z.array(snapshot), link("snapshots")]) }); const nominalSourceMeaning = z.object({ quantity: value(nominalCashflowSourceDefinitionSchema.options[0].shape.meaning.shape.quantity, "quantity"), financialBasis: reference, financialPopulation: reference, }).strict(); const nominalSourceInput = z.discriminatedUnion("kind", [ nominalCashflowSourceDefinitionSchema.options[0].extend({ meaning: nominalSourceMeaning }), nominalCashflowSourceDefinitionSchema.options[1].extend({ meaning: nominalSourceMeaning }), ]); const inputSchemas: Record = { "context-origins": z.object({ context, origins: z.array(token).min(1).max(1000) }).strict(), "premium-adjustment": reviewPremiumAdjustmentRequestSchema.extend({ onLevel: value(reviewPremiumAdjustmentRequestSchema.shape.onLevel, "onLevel"), trend: reviewPremiumTrendSchemaForRate(value(reviewPremiumAdjustmentRequestSchema.shape.trend.options[0].shape.rate, "assumption")), snapshots: z.union([z.array(snapshot), link("snapshots")]), application: z.discriminatedUnion("kind", [reviewPremiumAdjustmentRequestSchema.shape.application.options[0], reviewPremiumAdjustmentRequestSchema.shape.application.options[1], reviewPremiumAdjustmentRequestSchema.shape.application.options[2].extend({ assumption: z.union([reference, link("priorReference")]) })]) }), "adjusted-prior-calibration": adjustedModelPriorCalibrationSchemaForInputs(link("model"), value(reviewPremiumAdjustmentResultSchema, "premiumAdjustment")).extend({ snapshots: z.union([z.array(snapshot), link("snapshots")]) }), "on-level": reviewOnLevelRequestSchema.extend({ context, premium: value(reviewOnLevelRequestSchema.shape.premium, "exposure"), snapshots: z.union([z.array(snapshot), link("snapshots")]), application: z.discriminatedUnion("kind", [reviewOnLevelRequestSchema.shape.application.options[0], reviewOnLevelRequestSchema.shape.application.options[1], reviewOnLevelRequestSchema.shape.application.options[2].extend({ assumption: reference })]) }), "layer-application": reviewLayerApplicationRequestSchema.extend({ context, quantity: value(reviewLayerApplicationRequestSchema.shape.quantity, "quantity"), snapshots: z.union([z.array(snapshot), link("snapshots")]) }), "ilf-calibration": reviewIlfCalibrationRequestSchema.extend({ layer: link("layerApplication"), data: z.array(reference), snapshots: z.union([z.array(snapshot), link("snapshots")]) }), "ilf-application": reviewIlfApplicationRequestSchema.extend({ series: link("originSeries"), calibration: link("ilfCalibration"), targetFinancialBasis: snapshot, snapshots: z.union([z.array(snapshot), link("snapshots")]) }), "ulae-calibration": reviewUlaeCalibrationRequestSchema.extend({ measures: reviewUlaeCalibrationRequestSchema.shape.measures.extend(Object.fromEntries((["ulaePaid", "reportedUltimate", "paid", "closedUltimate"] as const).map(key => { const original = reviewUlaeCalibrationRequestSchema.shape.measures.shape[key]; return [key, original.extend({ source: reference, quantity: value(original.shape.quantity, "quantity") })]; }))), financialBases: z.object({ expense: reference, loss: reference }).strict(), populations: z.object({ expense: reference, loss: reference }).strict(), weights: z.object({ opening: value(reviewUlaeCalibrationRequestSchema.shape.weights.shape.opening, "assumption"), maintaining: value(reviewUlaeCalibrationRequestSchema.shape.weights.shape.maintaining, "assumption"), closing: value(reviewUlaeCalibrationRequestSchema.shape.weights.shape.closing, "assumption") }).strict(), snapshots: z.union([z.array(snapshot), link("snapshots")]) }), "ulae-reserve": reviewUlaeReserveRequestSchema.extend({ calibration: link("ulaeCalibration"), selectedRatio: value(reviewUlaeReserveRequestSchema.shape.selectedRatio, "assumption"), inputs: z.object({ ultimateLosses: link("originSeries"), reportedToDate: link("originSeries").nullable(), paidToDate: link("originSeries").nullable(), closedToDate: link("originSeries").nullable(), ulaePaidToDate: link("originSeries").nullable() }).strict(), context, quantity: value(reviewUlaeReserveRequestSchema.shape.quantity, "quantity"), snapshots: z.union([z.array(snapshot), link("snapshots")]) }), "nominal-cashflow-source": nominalSourceInput, "nominal-cashflows": reviewNominalCashflowRequestSchema.extend({source:value(reviewNominalCashflowRequestSchema.shape.source,"snapshot"),context,quantity:value(reviewNominalCashflowRequestSchema.shape.quantity,"quantity"),snapshots:z.union([z.array(snapshot),link("snapshots")])}), discounting: reviewDiscountingRequestSchema.extend({ nominal: link("originSeries"), cashflowProvenance: reviewDiscountingRequestSchema.shape.cashflowProvenance.extend({ source: reference }), rates: discountRates, rateProvenance: reviewDiscountingRequestSchema.shape.rateProvenance.extend({ source: reference }), riskMargin: z.discriminatedUnion("kind", [reviewDiscountingRequestSchema.shape.riskMargin.options[0], reviewDiscountingRequestSchema.shape.riskMargin.options[1].extend({ source: reference })]), discountedQuantity: value(reviewDiscountingRequestSchema.shape.discountedQuantity, "quantity"), snapshots: z.union([z.array(snapshot), link("snapshots")]) }), "net-recoveries": reviewNetRecoveriesRequestSchema.extend({ gross: z.object({ ultimate: link("originSeries"), unpaid: link("originSeries") }).strict(), recoveries: z.object({ ultimate: link("originSeries"), future: link("originSeries") }).strict(), targetFinancialBasis: snapshot, snapshots: z.union([z.array(snapshot), link("snapshots")]) }), "trend-fit": reviewTrendFitRequestSchema.extend({ series: link("originSeries") }), "trend-selection": reviewTrendSelectionRequestSchema.extend({ fit: link("trendFit"), snapshots: z.union([z.array(snapshot), link("snapshots")]) }), "trend-application": reviewTrendApplicationRequestSchema.extend({ series: link("originSeries"), rate: value(reviewTrendApplicationRequestSchema.shape.rate, "assumption"), snapshots: z.union([z.array(snapshot), link("snapshots")]) }), "origin-series": z.discriminatedUnion("kind", [declaredReviewOriginSeriesRequestSchema.extend({ context, quantity: value(declaredReviewOriginSeriesRequestSchema.shape.quantity, "quantity"), snapshots: z.union([z.array(snapshot), link("snapshots")]) }), modelReviewOriginSeriesRequestSchema.extend({ run: link("model") })]), snapshot: analysisSnapshotSchema, assumption: reviewAssumptionDefinitionSchema.omit({ contractVersion: true }), "snapshot-bundle": z.object({ bundles: z.array(z.union([z.array(analysisSnapshotSchema), link("snapshots")])), snapshots: z.array(snapshot) }).strict(), "development-selection": developmentSelection.extend({ source: value(developmentSelection.shape.source, "development"), context, quantity: value(developmentSelection.shape.quantity, "quantity"), subject: selectionSubjectInput.optional(), snapshots: z.union([z.array(snapshot), link("snapshots")]) }), development: z.object({ calendar: developmentCalendarInputSchema, knownDate: date, id: token, version: token }).strict(), exposure: alignedExposureInputSchema, "development-import": z.object({ grid: developmentGridInputSchema, calendar: developmentCalendarInputSchema.omit({ development: true }), knownDate: date, id: token, version: token }).strict(), preparation: reviewPreparationRequestSchema, "recipe-development": recipeDevelopment.extend({ preparations: z.union([recipeDevelopment.shape.preparations, link("preparationFrames")]) }), "bounded-grouped-development": boundedGroupedProjectionRequestSchema, model, "historical-preparation": historicalPreparationRequestSchema, "historical-fold": historicalFoldRequestSchema, "historical-study": historicalStudyRequestSchema, "historical-frames": z.object({ preparation: link("historicalPreparation") }).strict(), "recipe-exposure": recipeExposureSchemaForPreparation(link("preparedRecipe")), "attach-exposure": z.object({ id: token, version: token, context, snapshots: z.union([z.array(snapshot), link("snapshots")]), projection: link("recipeExposure"), relationship: exposurePopulationRelationshipSchema }).strict(), "comparison-candidate": z.object({ id: token, run: link("model"), seriesKey: modelComparisonCandidateSchema.shape.seriesKey }).strict(), "prior-calibration": modelPriorCalibrationSchemaForRun(link("model")).extend({ exposure: value(alignedExposureInputSchema, "exposure"), snapshots: z.union([z.array(snapshot), link("snapshots")]) }), "model-diagnostics": modelDiagnosticSchemaForRun(link("model")), "observed-diagnostics": observedDiagnosticRequestSchema.extend({ source: value(observedDiagnosticRequestSchema.shape.source, "development"), context, quantity: value(observedDiagnosticRequestSchema.shape.quantity, "quantity"), snapshots: z.union([z.array(snapshot), link("snapshots")]) }), "model-forecast": modelForecastSchemaForRun(link("model")), "forecast-score": modelForecastScoringSchemaForForecast(link("forecast")), "development-accumulation": reviewDevelopmentAccumulationSchemaForInput().extend({ context, quantity: developmentInputs.shape.quantity, snapshots: developmentInputs.shape.snapshots }), "development-transform": developmentInputs.extend({ operation: reviewDevelopmentTransformOperationSchema }), "development-adjustment": adjustmentInput, "settlement-adjustment": settlementInput, "development-view": developmentInputs.extend({ operation: reviewDevelopmentViewOperationSchema }), "model-selection-candidate": modelSelection.omit({ kind: true }).extend({ model: value(modelComparisonCandidateSchema, "comparisonCandidate") }), "calculated-selection-candidate": calculatedSelection.omit({ kind: true, calculation: true }).extend({ application: link("ilfApplication") }), "ulae-selection-candidate": calculatedSelection.omit({ kind: true, calculation: true }).extend({ reserve: link("ulaeReserve") }), "discount-selection-candidate": calculatedSelection.omit({ kind: true, calculation: true }).extend({ discounting: link("discounting") }), "net-recoveries-selection-candidate": calculatedSelection.omit({ kind: true, calculation: true }).extend({ netting: link("netRecoveries"), series: z.enum(["net-ultimate", "net-unpaid"]) }), "net-recoveries-paid-balance": reviewNetRecoveriesPaidBalanceRequestSchema.omit({ netting: true }).extend({ netting: link("netRecoveries") }), "declared-selection-candidate": z.object({ id: token, definition: declaredEstimateInputSchema }).strict(), comparison: modelComparisonRequestSchema.extend({ candidates: z.array(value(modelComparisonCandidateSchema, "comparisonCandidate")).min(2) }), selection, "development-balance": developmentReserveBalanceInputSchema.extend({ context, source: value(developmentReserveBalanceInputSchema.shape.source, "development"), snapshots: z.union([z.array(snapshot), link("snapshots")]) }), control: reserveControlInputSchema, reconciliation, }; const variants = Object.entries(inputSchemas).map(([operation, input]) => z.object({ id: token, version: token, operation: z.literal(operation), input }).strict()); const nodes = z.discriminatedUnion("operation", variants as [typeof variants[number], ...typeof variants[number][]]); const port = z.enum(["priorReference", "premiumAdjustment", "adjustedPriorCalibration", "onLevel", "modelAdjustments", "layerApplication", "ilfCalibration", "ilfApplication", "ulaeCalibration", "ulaeReserve", "discounting", "nominalCashflows", "netRecoveries", "reference", "snapshot", "assumption", "originSeries", "trendFit", "trendSelection", "trendApplication", "developmentSelection", "development", "exposure", "model", "diagnostics", "observedDiagnostics", "forecast", "forecastScore", "priorCalibration", "developmentTransform", "developmentView", "developmentAccumulation", "developmentAdjustment", "settlementAdjustment", "comparisonCandidate", "selectionCandidate", "comparison", "selection", "reconciliation", "preparation", "preparedRecipe", "historicalPreparation", "historicalFold", "historicalStudy", "preparationFrames", "recipeDevelopment", "boundedGroupedProjection", "recipeExposure", "context", "quantity", "snapshots"]); /** Structural boundary. parseReviewPlan also checks dependency topology; execution checks resolved domain meaning. */ export const reviewPlanSchema: z.ZodType = z.object({ contractVersion: z.literal(REVIEW_PLAN_CONTRACT_VERSION), id: token, version: token, nodes: z.array(nodes).min(1).max(REVIEW_PLAN_LIMITS.maximumNodes), outputs: z.array(z.object({ id: token, value: z.object({ kind: z.literal("review-output"), node: token, port }).strict() }).strict()).min(1), limits: z.object({ maximumNodes: z.number().int().positive().max(REVIEW_PLAN_LIMITS.maximumNodes), maximumLinks: z.number().int().positive().max(REVIEW_PLAN_LIMITS.maximumLinks), maximumInputCharacters: z.number().int().positive().max(REVIEW_PLAN_LIMITS.maximumInputCharacters), maximumOutputCharacters: z.number().int().positive().max(REVIEW_PLAN_LIMITS.maximumOutputCharacters) }).strict(), }).strict() as z.ZodType; export function parseReviewPlan(input: unknown): CompiledReviewPlan { return compileReviewPlan(parseReviewBoundary(reviewPlanSchema, input)); }