import { z } from "zod"; import { AnalysisReviewError, DiagnosticValidationError, canonicalJson, isDiagnosticToken, isRealIsoDate, REVIEW_PREPARATION_CONTRACT_VERSION, REVIEW_PREPARATION_LIMITS, RECIPE_DEVELOPMENT_LIMITS, replayReviewPreparation, runAnalysisRecipe, runReviewPreparation, projectRecipeDevelopment, type ReviewRecipePreparation, type RecipeDevelopmentProjectionRequest, type RecipeDevelopmentProjection } from "@actuarial-ts/core"; import { analysisReferenceSchema, analysisSnapshotSchema, quantitySemanticsSchema } from "./reviewContracts.js"; import { historicalDatasetInputSchema, analysisDefinitionSchema, analysisRecipeSchema, analysisResultSchema, customizationExposureObservationSchema, exposureAllocationTargetSchema, exposureRevisionSelectionSchema } from "./customizationContracts.js"; import { parseReviewBoundary } from "./reviewBoundary.js"; const token = z.string().refine(isDiagnosticToken, "Expected a valid nonempty identifier"); const date = z.string().refine(isRealIsoDate, "Expected a real ISO date"); /** Internal shared shape builder for recipe and controlled-scenario boundaries. */ export function recipeExecutionRequestSchema() { return z.object({ history: historicalDatasetInputSchema, definition: analysisDefinitionSchema, recipe: analysisRecipeSchema, exposureObservations: z.array(customizationExposureObservationSchema).optional(), exposureTargets: z.array(exposureAllocationTargetSchema).optional(), exposureRevisionSelection: exposureRevisionSelectionSchema.optional(), evaluationDate: date.optional(), }).strict(); } // Lazy boundaries avoid evaluating the shared review schemas while their module initializes. export const reviewPreparationRequestSchema: z.ZodType = z.lazy(recipeExecutionRequestSchema); export const reviewRecipePreparationSchema: z.ZodType = z.lazy(() => z.object({ contractVersion: z.literal(REVIEW_PREPARATION_CONTRACT_VERSION), sdkVersion: token, id: token, version: token, request: reviewPreparationRequestSchema, result: analysisResultSchema, reference: analysisReferenceSchema, }).strict()); export function parseAnalysisRecipeRequest(input: unknown): Parameters[0] { const request = parseReviewBoundary(reviewPreparationRequestSchema, input); if (canonicalJson(request).length > REVIEW_PREPARATION_LIMITS.maximumInputCharacters) throw new AnalysisReviewError([{ code: "RESOURCE_LIMIT_EXCEEDED", path: "$", message: "Recipe input exceeds the standalone preparation boundary's character ceiling", remedies: ["Use a bounded declared analysis population or the host's bounded recipe execution"] }]); return request; } export function runAnalysisRecipeFromInput(input: unknown): ReturnType { return runAnalysisRecipe(parseAnalysisRecipeRequest(input)); } /** Internal schema builder shared with the graph's typed preparation links. */ export function recipeProjectionSchemaForPreparation(preparation: T) { return z.object({ id: token, version: token, preparations: z.array(z.object({ preparation, knownDate: date }).strict()).min(1).max(RECIPE_DEVELOPMENT_LIMITS.maximumPreparations), measureId: token, kind: z.enum(["paid", "incurred"]), origins: z.array(z.object({ id: token, startDate: date, endDate: date }).strict()).min(1).max(RECIPE_DEVELOPMENT_LIMITS.maximumOrigins), ages: z.array(z.number().int().safe().positive()), ageConvention: z.enum(["calendar-period-end", "elapsed-months"]), coverage: z.enum(["exact-universe", "declared-subset"]), valuationDate: date, knowledgeDate: date, accountingDate: date.nullable(), meaning: z.object({ quantity: quantitySemanticsSchema, financialBasis: analysisSnapshotSchema, actorId: token, knownDate: date, rationale: token }).strict(), limits: z.object({ maximumPreparations: z.number().int().positive().max(RECIPE_DEVELOPMENT_LIMITS.maximumPreparations), maximumOrigins: z.number().int().positive().max(RECIPE_DEVELOPMENT_LIMITS.maximumOrigins), maximumCells: z.number().int().positive().max(RECIPE_DEVELOPMENT_LIMITS.maximumCells), maximumInputCharacters: z.number().int().positive().max(RECIPE_DEVELOPMENT_LIMITS.maximumInputCharacters) }).strict(), }).strict(); } export const recipeDevelopmentProjectionRequestSchema: z.ZodType = z.lazy(() => recipeProjectionSchemaForPreparation(reviewRecipePreparationSchema)); export function parseReviewRecipePreparation(input: unknown): ReviewRecipePreparation { return replayReviewPreparation(parseReviewBoundary(reviewRecipePreparationSchema, input)); } export const reviewPreparationExecutionRequestSchema = z.lazy(() => z.object({ id: token, version: token, request: reviewPreparationRequestSchema }).strict()); export function parseReviewPreparationRequest(input: unknown): Parameters[0] { return parseReviewBoundary(reviewPreparationExecutionRequestSchema, input); } export function parseRecipeDevelopmentProjectionRequest(input: unknown): RecipeDevelopmentProjectionRequest { return parseReviewBoundary(recipeDevelopmentProjectionRequestSchema, input); } export function runReviewPreparationFromInput(input: unknown): ReviewRecipePreparation { const parsed = parseReviewPreparationRequest(input); try { return runReviewPreparation(parsed); } catch (error) { if (!(error instanceof DiagnosticValidationError)) throw error; throw new DiagnosticValidationError(error.issues.map((issue) => { const root = /^\$\.([A-Za-z][A-Za-z0-9]*)/.exec(issue.path)?.[1]; const prefix = root && Object.hasOwn(parsed.request, root) ? "$.request" : root && Object.hasOwn(parsed.request.history, root) ? "$.request.history" : root && Object.hasOwn(parsed.request.definition, root) ? "$.request.definition" : "$.request"; return { ...issue, path: issue.path.startsWith("$") ? prefix + issue.path.slice(1) : issue.path }; })); } } export function projectRecipeDevelopmentFromInput(input: unknown): RecipeDevelopmentProjection { return projectRecipeDevelopment(parseRecipeDevelopmentProjectionRequest(input)); }