/** * Pure reads of the Case snapshot payload for the rectification chat surface: * the current question, its source, and the initial state that arrives with * the reveal. Moved out of rectification-agentic-chat.tsx unchanged * (TASK-rectification-code-split-20260926); no React hooks here. */ import { parseRectificationCandidateResult, type RectificationCandidateResult, } from "@/lib/rectification-candidate-result"; import { searchWindowFromSnapshot, caseStageFromSnapshot, interviewStopReasonFromSnapshot, interviewSessionOutcomeFromSnapshot, birthTimeSourceFromSnapshot, type RectificationCaseSnapshotPayload, } from "@/lib/rectification-surface-state"; import type { RectificationTimelineStage } from "@/lib/rectification-timeline-scale"; import { isRectificationCaseStatus, type RectificationCaseStatus } from "@/lib/rectification-agentic/v9/case-status"; import { parseRectificationChoiceCard, type RectificationChoiceCard as ChoiceCardModel } from "@/lib/rectification-agentic/v9/choice-card"; export type CandidateResult = RectificationCandidateResult | null; export type CurrentQuestionModel = Readonly<{ kind: "choice" | "collect_spoken"; prompt: string | null; focus_id: string | null; question_id: string | null; }>; export function currentQuestionFromSnapshot(value: unknown): CurrentQuestionModel | null { if (!value || typeof value !== "object" || Array.isArray(value)) return null; const question = value as { kind?: unknown; prompt?: unknown; focus_id?: unknown; question_id?: unknown }; if (question.kind !== "choice" && question.kind !== "collect_spoken" && question.kind !== "reverse_verify") { return null; } return { kind: question.kind === "collect_spoken" ? "collect_spoken" : "choice", prompt: typeof question.prompt === "string" && question.prompt.trim() ? question.prompt.trim() : null, focus_id: typeof question.focus_id === "string" ? question.focus_id : null, question_id: typeof question.question_id === "string" ? question.question_id : null, }; } export function questionSourceFromSnapshot(value: unknown): "focus" | "unavailable" | null { if (value === "unavailable") return "unavailable"; if (value === "focus") return "focus"; return null; } export type CaseSnapshotState = Readonly<{ candidate: CandidateResult; question: CurrentQuestionModel | null; questionSource: "focus" | "unavailable" | null; choice: ChoiceCardModel | null; caseStatus: RectificationCaseStatus | null; savedTime: string | null; savedStatus: "accepted" | "confirmed" | null; nextUserActionId: string | null; searchWindow: readonly [string, string] | null; stage: RectificationTimelineStage | null; interviewStopReason: string | null; interviewSessionOutcome: string | null; birthTimeSource: ReturnType; }>; export function nextUserActionIdFromSnapshot(payload: RectificationCaseSnapshotPayload | null): string | null { const id = payload?.next_user_action?.id; return typeof id === "string" && id.trim() ? id.trim() : null; } /** The snapshot that arrived with the reveal, as initial state; nothing is fetched on mount. */ export function caseSnapshotState(payload: RectificationCaseSnapshotPayload | null): CaseSnapshotState | null { if (!payload) return null; const confirmedTime = typeof payload.case?.confirmed_time === "string" ? payload.case.confirmed_time : null; const acceptedTime = typeof payload.case?.accepted_time === "string" ? payload.case.accepted_time : null; return { candidate: parseRectificationCandidateResult(payload.latest_result), question: currentQuestionFromSnapshot(payload.current_question), questionSource: questionSourceFromSnapshot(payload.question_source), choice: parseRectificationChoiceCard(payload.choice_card), caseStatus: isRectificationCaseStatus(payload.case?.status) ? payload.case.status : null, savedTime: confirmedTime ?? acceptedTime, savedStatus: confirmedTime ? "confirmed" : acceptedTime ? "accepted" : null, nextUserActionId: nextUserActionIdFromSnapshot(payload), searchWindow: searchWindowFromSnapshot(payload.case?.candidate_range), stage: caseStageFromSnapshot(payload.case?.stage), interviewStopReason: interviewStopReasonFromSnapshot(payload), interviewSessionOutcome: interviewSessionOutcomeFromSnapshot(payload), birthTimeSource: birthTimeSourceFromSnapshot(payload), }; }