9958e00abc
Rectification stays optional. Reported minutes can consult and generate reports; date-plus-period uses a declared window instead of a midpoint or 00:00. Updates BUG-341. Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
4.8 KiB
TypeScript
121 lines
4.8 KiB
TypeScript
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> = T extends (...args: never[]) => unknown
|
|
? T
|
|
: T extends readonly (infer Item)[]
|
|
? readonly DeepReadonly<Item>[]
|
|
: T extends object
|
|
? { readonly [Key in keyof T]: DeepReadonly<T[Key]> }
|
|
: T;
|
|
|
|
export type ConsultationPlan = DeepReadonly<z.infer<typeof consultationPlanSchema>>;
|
|
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<T>(value: T): DeepReadonly<T> {
|
|
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
for (const child of Object.values(value)) deepFreeze(child);
|
|
Object.freeze(value);
|
|
}
|
|
return value as DeepReadonly<T>;
|
|
}
|
|
|
|
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 });
|
|
}
|