Files
Jyotisha/frontend/src/lib/rectification-agentic/v9/choice-action.ts
T
Jesse_Chen 518119acbd
Independent Staging Quality Gate / validate (push) Successful in 12m35s
Independent Staging Quality Gate / publish (push) Successful in 15m16s
fix(rectification): continue after structured choices
2026-08-27 09:23:32 +08:00

107 lines
3.8 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 type { ChoiceKey, 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 const OPTION_ANSWER_CLASS: Readonly<Record<ChoiceKey, AnswerClass>> = {
A: "yes",
B: "weak_yes",
C: "no",
D: "unsure",
};
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 {
return classifyChoiceAnswer(optionId, schema);
}
export function focusStatusForOption(optionId: ChoiceOptionId): "resolved" | "declined" | "skipped" {
if (optionId === "C") return "declined";
if (optionId === "D" || optionId === "stop") return "skipped";
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();
}
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;
}): string {
if (input.optionId === "stop") {
return "已按你的选择先看到当前范围。独立核对尚未完成,这不是最终校正结果。";
}
if (!input.scoring) {
return "已记录你的选择。这是盘外核对,不会改候选分数。";
}
if (input.appliedInference) {
return "已记录你的选择,并更新了候选比较。";
}
return "已记录你的选择。";
}
export function shouldContinueAfterStructuredChoice(nextAction: unknown): boolean {
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}`;
}