Files
Jyotisha/frontend/src/lib/birth-time-dynamic-choice-internal.ts
T
2026-07-19 09:12:45 +08:00

256 lines
9.8 KiB
TypeScript

import { z } from "zod";
import { candidateResultSchema } from "./birth-time-evidence.ts";
import { dynamicActionReceiptSchema } from "./birth-time-dynamic-action-receipt.ts";
import {
DYNAMIC_QUESTION_LABEL_MAX_LENGTH,
DYNAMIC_QUESTION_PROMPT_MAX_LENGTH,
} from "./birth-time-dynamic-question-limits.ts";
import {
publicChoiceKindSchema,
publicDynamicChoiceQuestionSchema,
timeRangeSchema,
validateOptionSet,
} from "./birth-time-dynamic-choice.ts";
import type { CandidateResult } from "./birth-time-evidence.ts";
import type { DynamicActionReceipt } from "./birth-time-dynamic-action-receipt.ts";
import type { PublicChoiceKind, PublicDynamicChoiceQuestion, TimeRange } from "./birth-time-dynamic-choice.ts";
const finiteScoresSchema = z.record(z.number().finite());
export type EvidencePartition = {
readonly partitionId: string;
readonly descriptor: string;
readonly fallbackLabel: string;
};
export type ScoredEvidencePartition = EvidencePartition & {
readonly candidateScores: Readonly<Record<string, number>>;
};
export type QuestionOpportunity = {
readonly opportunityId: string;
readonly dimensionCode: string;
readonly neutralContext: string;
readonly estimatedInformationGain: number;
readonly candidatePartitionFingerprint: string;
readonly fallbackPrompt: string;
readonly partitions: readonly EvidencePartition[];
};
export type CandidateDifferencePacket = {
readonly caseId: string;
readonly scoringVersion: "birth-time-choice-scoring-v2";
readonly currentRange: TimeRange;
readonly opportunities: readonly QuestionOpportunity[];
readonly askedQuestionFingerprints: readonly string[];
readonly candidatePartitionFingerprints: readonly string[];
readonly recentRangeHistory: readonly TimeRange[];
};
export type CandidateDifferenceBuild = {
readonly packet: CandidateDifferencePacket;
readonly candidateModel: Readonly<Record<string, unknown>>;
readonly scoringPartitions: Readonly<Record<string, readonly ScoredEvidencePartition[]>>;
};
export type PersistedDynamicChoiceOption =
| {
readonly optionId: string;
readonly label: string;
readonly kind: "primary";
readonly partitionId: string;
readonly candidateScores: Readonly<Record<string, number>>;
}
| {
readonly optionId: string;
readonly label: string;
readonly kind: "unknown" | "unmatched";
readonly partitionId: null;
readonly candidateScores: null;
};
export type PersistedDynamicChoiceQuestion = Omit<PublicDynamicChoiceQuestion, "options"> & {
readonly opportunityId: string;
readonly dimensionCode: string;
readonly estimatedInformationGain: number;
readonly scoringVersion: string;
readonly source: "agent" | "fallback";
readonly questionFingerprint: string;
readonly candidatePartitionFingerprint: string;
readonly options: readonly PersistedDynamicChoiceOption[];
};
export type StoredChoiceAnswer = {
readonly questionId: string;
readonly optionId: string;
readonly kind: PublicChoiceKind;
readonly opportunityId: string;
readonly answeredAt: string;
readonly unmatchedContext?: string;
};
export type ServerChoiceEvidence = {
readonly questionId: string;
readonly opportunityId: string;
readonly partitionId: string;
readonly dimensionCode: string;
readonly candidateScores: Readonly<Record<string, number>>;
readonly informationGain: number;
};
export type DynamicChoiceScoringResult = {
readonly candidate: CandidateResult;
readonly evidenceMode: "dynamic_choice";
readonly effectiveAnswerCount: number;
readonly dimensionCount: number;
};
export type PausedDynamicAction =
| { readonly kind: "generate_dynamic_question" }
| { readonly kind: "ask_dynamic_choice"; readonly questionId: string }
| { readonly kind: "clarify_unmatched_answer"; readonly questionId: string }
| { readonly kind: "retry_question_generation" }
| { readonly kind: "score_pending"; readonly jobId: string }
| { readonly kind: "retry_scoring"; readonly jobId: string };
export type DynamicControlState = {
readonly asOfDate: string;
readonly answeredCount: number;
readonly effectiveAnswerCount: number;
readonly plateauCount: number;
readonly questionFingerprints: readonly string[];
readonly partitionFingerprints: readonly string[];
readonly dismissedOpportunityIds: readonly string[];
readonly recentRanges: readonly TimeRange[];
readonly pausedAction: PausedDynamicAction | null;
readonly lastActionReceipt?: DynamicActionReceipt | null;
};
const evidencePartitionBaseSchema = z.object({
partitionId: z.string().trim().min(1),
descriptor: z.string().trim().min(1),
fallbackLabel: z.string().trim().min(1).max(DYNAMIC_QUESTION_LABEL_MAX_LENGTH),
}).strict();
export const evidencePartitionSchema = evidencePartitionBaseSchema.readonly();
export const scoredEvidencePartitionSchema = evidencePartitionBaseSchema.extend({
candidateScores: finiteScoresSchema,
}).strict().readonly();
export const questionOpportunitySchema = z.object({
opportunityId: z.string().trim().min(1),
dimensionCode: z.string().trim().min(1),
neutralContext: z.string().trim().min(1),
estimatedInformationGain: z.number().finite().nonnegative(),
candidatePartitionFingerprint: z.string().trim().min(1),
fallbackPrompt: z.string().trim().min(1).max(DYNAMIC_QUESTION_PROMPT_MAX_LENGTH),
partitions: z.array(evidencePartitionSchema).min(2).max(4).readonly(),
}).strict().readonly();
export const candidateDifferencePacketSchema = z.object({
caseId: z.string().trim().min(1),
scoringVersion: z.literal("birth-time-choice-scoring-v2"),
currentRange: timeRangeSchema,
opportunities: z.array(questionOpportunitySchema).readonly(),
askedQuestionFingerprints: z.array(z.string().trim().min(1)).readonly(),
candidatePartitionFingerprints: z.array(z.string().trim().min(1)).readonly(),
recentRangeHistory: z.array(timeRangeSchema).readonly(),
}).strict().readonly();
export const candidateDifferenceBuildSchema = z.object({
packet: candidateDifferencePacketSchema,
candidateModel: z.record(z.unknown()),
scoringPartitions: z.record(z.array(scoredEvidencePartitionSchema).readonly()),
}).strict().readonly();
const persistedPrimaryChoiceSchema = z.object({
optionId: z.string().trim().min(1),
label: z.string().trim().min(1).max(DYNAMIC_QUESTION_LABEL_MAX_LENGTH),
kind: z.literal("primary"),
partitionId: z.string().trim().min(1),
candidateScores: finiteScoresSchema,
}).strict().readonly();
const persistedSpecialChoiceSchema = (kind: "unknown" | "unmatched") => z.object({
optionId: z.string().trim().min(1),
label: z.string().trim().min(1).max(DYNAMIC_QUESTION_LABEL_MAX_LENGTH),
kind: z.literal(kind),
partitionId: z.null(),
candidateScores: z.null(),
}).strict().readonly();
export const persistedDynamicChoiceQuestionSchema = z.object({
questionId: z.string().trim().min(1),
opportunityId: z.string().trim().min(1),
dimensionCode: z.string().trim().min(1),
estimatedInformationGain: z.number().finite().nonnegative(),
scoringVersion: z.string().trim().min(1),
source: z.enum(["agent", "fallback"]),
questionFingerprint: z.string().trim().min(1),
candidatePartitionFingerprint: z.string().trim().min(1),
prompt: z.string().trim().min(1).max(DYNAMIC_QUESTION_PROMPT_MAX_LENGTH),
options: z.array(z.union([
persistedPrimaryChoiceSchema,
persistedSpecialChoiceSchema("unknown"),
persistedSpecialChoiceSchema("unmatched"),
])).readonly(),
}).strict().superRefine(validateOptionSet).readonly();
export const storedChoiceAnswerSchema = z.object({
questionId: z.string().trim().min(1),
optionId: z.string().trim().min(1),
kind: publicChoiceKindSchema,
opportunityId: z.string().trim().min(1),
answeredAt: z.string().datetime({ offset: true }),
unmatchedContext: z.string().max(240).optional(),
}).strict().readonly();
export const serverChoiceEvidenceSchema = z.object({
questionId: z.string().trim().min(1),
opportunityId: z.string().trim().min(1),
partitionId: z.string().trim().min(1),
dimensionCode: z.string().trim().min(1),
candidateScores: finiteScoresSchema,
informationGain: z.number().finite().nonnegative(),
}).strict().readonly();
export const dynamicChoiceScoringResultSchema = z.object({
candidate: candidateResultSchema,
evidenceMode: z.literal("dynamic_choice"),
effectiveAnswerCount: z.number().int().min(0),
dimensionCount: z.number().int().min(0),
}).strict().readonly();
export const pausedDynamicActionSchema = z.discriminatedUnion("kind", [
z.object({ kind: z.literal("generate_dynamic_question") }).strict(),
z.object({ kind: z.literal("ask_dynamic_choice"), questionId: z.string().trim().min(1) }).strict(),
z.object({ kind: z.literal("clarify_unmatched_answer"), questionId: z.string().trim().min(1) }).strict(),
z.object({ kind: z.literal("retry_question_generation") }).strict(),
z.object({ kind: z.literal("score_pending"), jobId: z.string().trim().min(1) }).strict(),
z.object({ kind: z.literal("retry_scoring"), jobId: z.string().trim().min(1) }).strict(),
]).readonly();
export const dynamicControlStateSchema = z.object({
asOfDate: z.string().date(),
answeredCount: z.number().int().min(0),
effectiveAnswerCount: z.number().int().min(0),
plateauCount: z.number().int().min(0),
questionFingerprints: z.array(z.string().trim().min(1)).readonly(),
partitionFingerprints: z.array(z.string().trim().min(1)).readonly(),
dismissedOpportunityIds: z.array(z.string().trim().min(1)).readonly(),
recentRanges: z.array(timeRangeSchema).readonly(),
pausedAction: pausedDynamicActionSchema.nullable(),
lastActionReceipt: dynamicActionReceiptSchema.nullable().optional(),
}).strict().readonly();
export function toPublicDynamicChoiceQuestion(
question: PersistedDynamicChoiceQuestion,
): PublicDynamicChoiceQuestion {
return publicDynamicChoiceQuestionSchema.parse({
questionId: question.questionId,
prompt: question.prompt,
options: question.options.map(({ optionId, label, kind }) => ({ optionId, label, kind })),
});
}