import { z } from "zod"; import type { ConsultationBirthTimeMode } from "./consultation-birth-time-mode.ts"; import { consultationDomainDefinition, consultationDomainIds, consultationDomainSchema, type ConsultationTheme, } from "./consultation-domain-registry.ts"; export const consultationDomainValues = consultationDomainIds; export const consultationDepthValues = ["concise", "standard", "deep", "research"] as const; export const consultationTimingHorizonValues = ["next_3_months", "next_12_months", "next_24_months", "long_term"] as const; export const consultationEvidenceCategoryValues = ["natal_foundation", "domain", "timing", "validation"] as const; export const consultationPlanVersion = "consultation-plan-v2" as const; export const consultationPlanMaxCreditCost = 1 as const; const consultationPrecisionBoundaryValues = ["server_evidence_required", "precise_timing_blocked"] as const; export const consultationPlanSchema = z.object({ plan_version: z.literal(consultationPlanVersion), userIntent: z.string().trim().min(1).max(500), requestedDomains: z.array(consultationDomainSchema).min(1).max(6), depth: z.enum(consultationDepthValues), timingHorizon: z.enum(consultationTimingHorizonValues).nullable(), requiredEvidenceCategories: z.array(z.enum(consultationEvidenceCategoryValues)).min(1).max(4), birthTimeMode: z.enum([ "verified_chart", "unverified_birth_time", "declared_birth_window", "general_no_birth_time", ]), precisionBoundary: z.enum(consultationPrecisionBoundaryValues), maxCreditCost: z.literal(consultationPlanMaxCreditCost), }).strict(); type DeepReadonly = T extends (...args: never[]) => unknown ? T : T extends readonly (infer Item)[] ? readonly DeepReadonly[] : T extends object ? { readonly [Key in keyof T]: DeepReadonly } : T; export type ConsultationPlan = DeepReadonly>; export type ConsultationPlanTheme = ConsultationTheme; export type ConsultationPlanValidationCode = | "plan_mode_mismatch" | "plan_precision_mismatch" | "plan_cost_ceiling_exceeded"; export class ConsultationPlanValidationError extends Error { readonly code: ConsultationPlanValidationCode; constructor(code: ConsultationPlanValidationCode) { super(`Consultation plan rejected: ${code}`); this.name = "ConsultationPlanValidationError"; this.code = code; } } function domainsForTheme(theme: ConsultationPlanTheme): ConsultationPlan["requestedDomains"] { return [...consultationDomainDefinition(theme).workflowThemes]; } function deepFreeze(value: T): DeepReadonly { if (value && typeof value === "object" && !Object.isFrozen(value)) { for (const child of Object.values(value)) deepFreeze(child); Object.freeze(value); } return value as DeepReadonly; } function precisionBoundaryFor(mode: ConsultationBirthTimeMode) { return mode === "general_no_birth_time" || mode === "declared_birth_window" ? "precise_timing_blocked" as const : "server_evidence_required" as const; } export function validateConsultationPlan( plan: ConsultationPlan, input: Readonly<{ consultationMode: ConsultationBirthTimeMode; modelCreditCost: number }>, ) { if (plan.birthTimeMode !== input.consultationMode) { throw new ConsultationPlanValidationError("plan_mode_mismatch"); } if (plan.precisionBoundary !== precisionBoundaryFor(input.consultationMode)) { throw new ConsultationPlanValidationError("plan_precision_mismatch"); } if (!Number.isInteger(input.modelCreditCost) || input.modelCreditCost < 1 || input.modelCreditCost > plan.maxCreditCost) { throw new ConsultationPlanValidationError("plan_cost_ceiling_exceeded"); } return plan; } export function createConsultationPlan(input: { userIntent: string; theme: ConsultationPlanTheme; consultationMode?: ConsultationBirthTimeMode; modelCreditCost?: number; depth?: ConsultationPlan["depth"]; timingHorizon?: ConsultationPlan["timingHorizon"]; }): ConsultationPlan { const consultationMode = input.consultationMode ?? "verified_chart"; const theme = consultationDomainSchema.parse(input.theme); const plan = deepFreeze(consultationPlanSchema.parse({ plan_version: consultationPlanVersion, userIntent: input.userIntent, requestedDomains: domainsForTheme(theme), depth: input.depth ?? "standard", timingHorizon: input.timingHorizon ?? (theme === "timing" || theme === "annual" ? "next_12_months" : null), requiredEvidenceCategories: [...consultationEvidenceCategoryValues], birthTimeMode: consultationMode, precisionBoundary: precisionBoundaryFor(consultationMode), maxCreditCost: consultationPlanMaxCreditCost, })); return input.modelCreditCost === undefined ? plan : validateConsultationPlan(plan, { consultationMode, modelCreditCost: input.modelCreditCost }); }