0c0df42679
One "一时说不好" no longer ends the interview. Plateau streaks use the same sample floor. Delivery copy states why the range stopped. Co-authored-by: Cursor <cursoragent@cursor.com>
129 lines
4.9 KiB
TypeScript
129 lines
4.9 KiB
TypeScript
/**
|
||
* Structured choice / stop commands for birth-time rectification.
|
||
*
|
||
* A/B/C/D taps and “先这样” are server-owned state transitions. They are not
|
||
* chat messages and must not be interpreted by the language model.
|
||
*/
|
||
|
||
import type { AnswerClass } from "../core/types";
|
||
import { classifyChoiceAnswer } from "../core/build-state";
|
||
import { RECTIFICATION_TERMINATION_COPY } from "../core/rectification-decision";
|
||
import {
|
||
CHOICE_STOP_LABEL,
|
||
CHOICE_STOP_MESSAGE,
|
||
CHOICE_SKIP_QUESTION_LABEL,
|
||
CHOICE_SKIP_QUESTION_MESSAGE,
|
||
HOLDOUT_MESSAGE_PREFIX,
|
||
type ChoiceKey,
|
||
type RectificationChoiceCard,
|
||
} from "./choice-card";
|
||
|
||
export const CHOICE_ACTION = "answer_choice" as const;
|
||
export const STOP_ACTION = "stop_and_review" as const;
|
||
export const DETERMINISTIC_CHOICE_MODEL = "deterministic:choice";
|
||
|
||
export type ChoiceActionKind = typeof CHOICE_ACTION | typeof STOP_ACTION;
|
||
export type ChoiceOptionId = ChoiceKey | "stop";
|
||
export type ChoiceActionStatus = "received" | "applied" | "narrated";
|
||
|
||
export type StructuredProbeDerivedContext = Readonly<{
|
||
sourceType: "structured_probe_answer";
|
||
derivedFromProbeId: string | null;
|
||
referencedDateRange: Readonly<{ start: string; end: string }> | null;
|
||
answerClass: AnswerClass | null;
|
||
}>;
|
||
|
||
export function outcomeIdForOption(optionId: ChoiceKey, schema?: unknown): AnswerClass | null {
|
||
return classifyChoiceAnswer(optionId, schema);
|
||
}
|
||
|
||
export function focusStatusForAnswer(answerClass: AnswerClass | null, optionId: ChoiceOptionId): "resolved" | "declined" | "skipped" {
|
||
if (optionId === "stop" || answerClass === "unsure") return "skipped";
|
||
if (answerClass === "no") return "declined";
|
||
return "resolved";
|
||
}
|
||
|
||
export function choiceOptionQuote(card: RectificationChoiceCard, key: ChoiceKey): string {
|
||
return (card.options.find((item) => item.key === key)?.label ?? "").trim();
|
||
}
|
||
|
||
export function userVisibleChoiceLine(card: RectificationChoiceCard, key: ChoiceKey): string {
|
||
const label = choiceOptionQuote(card, key);
|
||
return `${key}. ${label}`.trim();
|
||
}
|
||
|
||
const STRUCTURED_CHOICE_LINE = /^(?:盘外核对(不计分):)?[A-D]\. \S/;
|
||
|
||
/** Taps are state transitions. Hide the synthetic A/B/C/D or 先这样 chat line. */
|
||
export function isStructuredChoiceUserText(text: string | null | undefined): boolean {
|
||
const trimmed = text?.trim() ?? "";
|
||
if (!trimmed) return false;
|
||
if (trimmed === CHOICE_STOP_MESSAGE || trimmed === CHOICE_STOP_LABEL) return true;
|
||
if (trimmed === CHOICE_SKIP_QUESTION_MESSAGE || trimmed === CHOICE_SKIP_QUESTION_LABEL) return true;
|
||
if (trimmed.startsWith(`${HOLDOUT_MESSAGE_PREFIX}:`) && STRUCTURED_CHOICE_LINE.test(trimmed)) return true;
|
||
return STRUCTURED_CHOICE_LINE.test(trimmed) && trimmed.length <= 90;
|
||
}
|
||
|
||
export function derivedProbeDateRange(year: number | null | undefined): { start: string; end: string } | null {
|
||
if (!year || year < 1900 || year > 2100) return null;
|
||
return { start: `${year}-01-01`, end: `${year}-12-31` };
|
||
}
|
||
|
||
export function structuredProbeContext(input: {
|
||
probeId: string | null;
|
||
year?: number | null;
|
||
answerClass: AnswerClass | null;
|
||
}): StructuredProbeDerivedContext {
|
||
return {
|
||
sourceType: "structured_probe_answer",
|
||
derivedFromProbeId: input.probeId,
|
||
referencedDateRange: derivedProbeDateRange(input.year),
|
||
answerClass: input.answerClass,
|
||
};
|
||
}
|
||
|
||
export function quoteIsFromAssistantQuestion(quote: string, question: string): boolean {
|
||
const normalizedQuote = quote.replace(/\s+/g, "");
|
||
const normalizedQuestion = question.replace(/\s+/g, "");
|
||
if (!normalizedQuote || normalizedQuote.length < 2) return false;
|
||
if (normalizedQuote === normalizedQuestion) return true;
|
||
return /(?:19|20)\d{2}年/.test(normalizedQuote) && normalizedQuestion.includes(normalizedQuote);
|
||
}
|
||
|
||
export function composeChoiceNarration(input: {
|
||
optionId: ChoiceOptionId;
|
||
scoring: boolean;
|
||
appliedInference: boolean;
|
||
answerClass?: AnswerClass | null;
|
||
}): string {
|
||
if (input.optionId === "stop") {
|
||
return `已记录你的选择,并结束本次校正,交付当前可信区间和代表性工作时间。${RECTIFICATION_TERMINATION_COPY}`;
|
||
}
|
||
if (input.answerClass === "unsure") {
|
||
return "已记录。这题先不计分,换一件事问。";
|
||
}
|
||
if (!input.scoring) {
|
||
return "已记录你的选择。这是盘外核对,不会改候选分数。";
|
||
}
|
||
if (input.appliedInference) {
|
||
return "已记录你的选择,并更新了候选比较。";
|
||
}
|
||
return "已记录你的选择。";
|
||
}
|
||
|
||
export function shouldContinueAfterStructuredChoice(
|
||
nextAction: unknown,
|
||
receipt?: { nextInterviewPersisted?: boolean } | null,
|
||
): boolean {
|
||
if (receipt?.nextInterviewPersisted) return false;
|
||
if (!nextAction || typeof nextAction !== "object") return false;
|
||
const type = (nextAction as { type?: unknown }).type;
|
||
return type === "ask_fact_collection"
|
||
|| type === "ask_candidate_discriminator"
|
||
|| type === "ask_holdout_validation";
|
||
}
|
||
|
||
export function stableChoiceActionKey(focusId: string, optionId: ChoiceOptionId): string {
|
||
return `${focusId}:${optionId}`;
|
||
}
|