feat(rectification): add adaptive question selector
This commit is contained in:
@@ -45,6 +45,7 @@ const optionSchema = z.object({
|
||||
const questionSchema = z.object({
|
||||
id: z.string().trim().min(1),
|
||||
prompt: z.string().trim().min(1),
|
||||
domain: z.string().trim().min(1).optional(),
|
||||
round: z.number().int().min(1).optional(),
|
||||
options: z.array(optionSchema).optional(),
|
||||
}).passthrough();
|
||||
@@ -90,6 +91,9 @@ const scoringSchema = z.object({
|
||||
})),
|
||||
next_round: z.number().int().min(1).nullable().default(null),
|
||||
next_round_questions: z.array(questionSchema).default([]),
|
||||
next_round_selection: z.object({
|
||||
selected_questions: z.array(questionSchema).default([]),
|
||||
}).nullable().optional(),
|
||||
}).passthrough();
|
||||
|
||||
const eventDomainSchema = z.enum([
|
||||
@@ -232,6 +236,7 @@ function normalizeQuestion(question: z.infer<typeof questionSchema>): Rectificat
|
||||
return {
|
||||
id: question.id,
|
||||
prompt: question.prompt,
|
||||
...(question.domain ? { domain: question.domain } : {}),
|
||||
...(question.round ? { round: question.round } : {}),
|
||||
...(question.options ? { options: question.options } : {}),
|
||||
};
|
||||
@@ -239,11 +244,14 @@ function normalizeQuestion(question: z.infer<typeof questionSchema>): Rectificat
|
||||
|
||||
export function parseRectificationScoring(value: unknown): RectificationScoringResult {
|
||||
const parsed = scoringSchema.parse(value);
|
||||
const selectedQuestions = parsed.next_round_selection?.selected_questions ?? [];
|
||||
return {
|
||||
answeredCount: parsed.answered_count,
|
||||
candidateClusterRankings: parsed.candidate_cluster_rankings,
|
||||
nextRound: parsed.next_round,
|
||||
nextRoundQuestions: parsed.next_round_questions.map(normalizeQuestion),
|
||||
nextRoundQuestions: (selectedQuestions.length > 0
|
||||
? selectedQuestions
|
||||
: parsed.next_round_questions).map(normalizeQuestion),
|
||||
raw: parsed,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ export function projectJourneyResponse(
|
||||
questionnaire: response.questionnaire,
|
||||
persistedProgress,
|
||||
candidateResult: response.candidateResult,
|
||||
serverSelectedQuestion: response.scoring?.nextRoundQuestions[0] ?? null,
|
||||
lifeEvents: response.lifeEvents,
|
||||
}),
|
||||
};
|
||||
@@ -93,6 +94,7 @@ function projectStoredTurn(
|
||||
questionnaire: stored.questionnaire,
|
||||
persistedProgress: stored.persistedProgress,
|
||||
candidateResult: stored.candidateResult ?? null,
|
||||
serverSelectedQuestion: stored.scoring?.nextRoundQuestions[0] ?? null,
|
||||
lifeEvents: stored.lifeEvents ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ function completedTurn(
|
||||
questionnaire: stored.questionnaire,
|
||||
persistedProgress: stored.persistedProgress,
|
||||
candidateResult,
|
||||
serverSelectedQuestion: stored.scoring?.nextRoundQuestions[0] ?? null,
|
||||
lifeEvents: stored.lifeEvents ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export type RectificationAnswer = "A" | "B" | "C" | "D";
|
||||
export type RectificationQuestion = {
|
||||
readonly id: string;
|
||||
readonly prompt: string;
|
||||
readonly domain?: string;
|
||||
readonly round?: number;
|
||||
readonly options?: readonly {
|
||||
readonly key: RectificationAnswer;
|
||||
|
||||
@@ -174,9 +174,40 @@ export type JourneyTurnProjectionInput = {
|
||||
readonly askedDomains: readonly EvidenceDomain[];
|
||||
} | null;
|
||||
readonly candidateResult: CandidateResult | null;
|
||||
readonly serverSelectedQuestion?: { readonly id: string; readonly domain?: string } | null;
|
||||
readonly lifeEvents: readonly LifeEvent[];
|
||||
};
|
||||
|
||||
const serverDomainMap: Readonly<Record<string, EvidenceDomain>> = {
|
||||
education: "education",
|
||||
relocation: "relocation",
|
||||
residence: "relocation",
|
||||
relationship: "relationship",
|
||||
career: "career",
|
||||
career_learning: "career",
|
||||
public_work: "career",
|
||||
finance: "finance",
|
||||
health_pressure: "health_pressure",
|
||||
};
|
||||
|
||||
function serverAdaptiveQuestion(
|
||||
question: JourneyTurnProjectionInput["serverSelectedQuestion"],
|
||||
adaptiveRound: number,
|
||||
): QuestionSpec | null {
|
||||
if (!question?.domain) return null;
|
||||
const domain = serverDomainMap[question.domain];
|
||||
if (!domain) return null;
|
||||
return {
|
||||
questionId: question.id,
|
||||
phase: "adaptive",
|
||||
domain,
|
||||
requestedPrecision: ["year", "month"],
|
||||
allowUnknown: true,
|
||||
purposeCode: `candidate_difference_${domain}`,
|
||||
plannerVersion: "server-next-round-selection-v1",
|
||||
};
|
||||
}
|
||||
|
||||
function projectionPhase(input: JourneyTurnProjectionInput, progress: JourneyProgress): JourneyProgress["phase"] {
|
||||
if (input.snapshot.state === "ready") return "ready";
|
||||
if (input.candidateResult) {
|
||||
@@ -219,7 +250,7 @@ export function projectJourneyTurn(input: JourneyTurnProjectionInput): JourneyTu
|
||||
const decisionProgress = input.candidateResult?.confidence === "low"
|
||||
? { ...projectedProgress, adaptiveRound }
|
||||
: projectedProgress;
|
||||
const nextQuestion = phase === "baseline" || phase === "adaptive"
|
||||
const plannedQuestion = phase === "baseline" || phase === "adaptive"
|
||||
? planEvidenceQuestion({
|
||||
phase,
|
||||
samples: input.questionnaire?.samples ?? [],
|
||||
@@ -228,6 +259,9 @@ export function projectJourneyTurn(input: JourneyTurnProjectionInput): JourneyTu
|
||||
adaptiveRound,
|
||||
})
|
||||
: null;
|
||||
const nextQuestion = phase === "adaptive"
|
||||
? serverAdaptiveQuestion(input.serverSelectedQuestion, adaptiveRound) ?? plannedQuestion
|
||||
: plannedQuestion;
|
||||
return {
|
||||
turnVersion: input.turnVersion,
|
||||
nextAction: deriveNextAction({
|
||||
|
||||
Reference in New Issue
Block a user