When coverage still blocks adopt and no renderable discriminator remains, narrate the numeric range and persist a spoken collect instead of promising a time card that cannot be clicked. Recompute probe information gain on active candidates and drop zero-split probes explicitly. Co-authored-by: Cursor <cursoragent@cursor.com>
475 lines
15 KiB
TypeScript
475 lines
15 KiB
TypeScript
/**
|
||
* Single authoritative rectification decision.
|
||
*
|
||
* phase, nextAction, canOfferRange, canAdopt, and canConfirmExactMinute are
|
||
* computed here. precision_stage, selection_allowed, and active_focus policy
|
||
* are derived from this object and must not be judged independently.
|
||
*/
|
||
|
||
import type { CandidateDiscriminatorProbe } from "./candidate-contrast-packet.ts";
|
||
import {
|
||
evaluateCandidateSeparation,
|
||
type CandidateScoreRow,
|
||
type CandidateSeparation,
|
||
} from "./candidate-separation.ts";
|
||
import { rangeFromTimes } from "./credible-range.ts";
|
||
import type { DroppedProbe } from "../v9/probe-question-contract.ts";
|
||
import type { RectificationPhase, ResultStatus } from "./types.ts";
|
||
|
||
export type RectificationNextActionType =
|
||
| "ask_fact_collection"
|
||
| "ask_candidate_discriminator"
|
||
| "ask_holdout_validation"
|
||
| "offer_provisional_range"
|
||
| "complete_with_range"
|
||
| "ready_to_adopt";
|
||
|
||
export type HoldoutValidationStatus = "not_started" | "passed" | "failed" | "unavailable";
|
||
|
||
export type DecisionSessionOutcome =
|
||
| "collect_evidence"
|
||
| "discriminate_candidates"
|
||
| "validate_holdout"
|
||
| "provisional_range"
|
||
| "provisional_range_user_stopped"
|
||
| "completed_with_range"
|
||
| "validated_range"
|
||
| "exact_minute_confirmed"
|
||
| "adopt_representative"
|
||
| "awaiting_confirmation";
|
||
|
||
export type CompletionStatus =
|
||
| "provisional_range_user_stopped"
|
||
| "validated_range"
|
||
| "exact_minute_confirmed";
|
||
|
||
export type DerivedPrecisionStage = "collect_events" | "theme_refine" | "ready_to_adopt";
|
||
|
||
export type RectificationDecision = Readonly<{
|
||
phase: RectificationPhase;
|
||
nextAction: RectificationNextActionType;
|
||
sessionOutcome: DecisionSessionOutcome;
|
||
resultStatus: ResultStatus;
|
||
canOfferRange: boolean;
|
||
canAdopt: boolean;
|
||
canConfirmExactMinute: boolean;
|
||
selectionAllowed: boolean;
|
||
proposeAllowed: boolean;
|
||
precisionStage: DerivedPrecisionStage;
|
||
activeFocusPolicy: "keep" | "close";
|
||
completionStatus: CompletionStatus | null;
|
||
validated: boolean;
|
||
credibleRange: readonly [string, string] | null;
|
||
representativeTime: string | null;
|
||
separation: CandidateSeparation;
|
||
probe: CandidateDiscriminatorProbe | null;
|
||
holdoutValidation: HoldoutValidationStatus;
|
||
droppedProbes: readonly DroppedProbe[];
|
||
}>;
|
||
|
||
export type DecideRectificationInput = Readonly<{
|
||
methodCoverageAll: boolean;
|
||
confirmationAllowed?: boolean;
|
||
userStopped?: boolean;
|
||
snapshotCurrent?: boolean;
|
||
trainingGateOpen?: boolean;
|
||
candidateScores: readonly CandidateScoreRow[];
|
||
discriminatorProbe?: CandidateDiscriminatorProbe | null;
|
||
holdoutValidation?: HoldoutValidationStatus;
|
||
accepted?: boolean;
|
||
inferenceCredibleRange?: readonly [string, string] | null;
|
||
engineAcceptAllowed?: boolean;
|
||
engineProposeAllowed?: boolean;
|
||
datedMethodCollectOpen?: boolean;
|
||
}>;
|
||
|
||
export function decideRectification(input: DecideRectificationInput): RectificationDecision {
|
||
const separation = evaluateCandidateSeparation(input.candidateScores);
|
||
const probe = input.discriminatorProbe ?? null;
|
||
const holdout = input.holdoutValidation ?? "unavailable";
|
||
const userStopped = input.userStopped === true;
|
||
const confirmationAllowed = input.confirmationAllowed === true;
|
||
const range = input.inferenceCredibleRange
|
||
?? rangeFromTimes(separation.credibleRange)
|
||
?? (separation.representativeTime
|
||
? [separation.representativeTime, separation.representativeTime] as const
|
||
: null);
|
||
|
||
if (input.accepted) {
|
||
return finish(confirmationAllowed ? "awaiting_confirmation" : "adopt_representative", {
|
||
input,
|
||
separation,
|
||
holdout,
|
||
range,
|
||
probe: null,
|
||
canConfirmExactMinute: confirmationAllowed,
|
||
});
|
||
}
|
||
if (confirmationAllowed) {
|
||
return finish("awaiting_confirmation", {
|
||
input,
|
||
separation,
|
||
holdout,
|
||
range,
|
||
probe: null,
|
||
canConfirmExactMinute: true,
|
||
});
|
||
}
|
||
const canDiscriminateDespiteCoverage = Boolean(probe)
|
||
&& !separation.sufficient
|
||
&& input.trainingGateOpen !== false;
|
||
const coverageBlocks = (!input.methodCoverageAll && !canDiscriminateDespiteCoverage)
|
||
|| input.trainingGateOpen === false;
|
||
if (coverageBlocks && !(userStopped && input.candidateScores.length > 0)) {
|
||
const trainingOpen = input.trainingGateOpen !== false;
|
||
const engineOffers = input.engineAcceptAllowed === true || input.engineProposeAllowed === true;
|
||
if (
|
||
trainingOpen
|
||
&& input.candidateScores.length > 0
|
||
&& !probe
|
||
&& engineOffers
|
||
&& input.datedMethodCollectOpen !== true
|
||
) {
|
||
return offerRangeWithoutAdopt(separation, holdout, range);
|
||
}
|
||
return collect(separation, holdout, range, probe);
|
||
}
|
||
if (input.snapshotCurrent === false) {
|
||
if (probe && !userStopped && input.trainingGateOpen !== false) {
|
||
return discriminate(separation, holdout, range, probe);
|
||
}
|
||
if (!(userStopped && input.candidateScores.length > 0)) {
|
||
return collect(separation, holdout, range, probe);
|
||
}
|
||
}
|
||
if (!separation.sufficient) {
|
||
if (probe && !userStopped) {
|
||
return discriminate(separation, holdout, range, probe);
|
||
}
|
||
return completeWithRange(separation, holdout, range, userStopped ? "user_stopped" : "offer");
|
||
}
|
||
if (holdout === "not_started" && !userStopped) {
|
||
return holdoutValidation(separation, range);
|
||
}
|
||
if (holdout === "failed") {
|
||
if (probe && !userStopped) {
|
||
return discriminate(separation, holdout, range, probe);
|
||
}
|
||
return completeWithRange(separation, holdout, range, "exhausted");
|
||
}
|
||
if (holdout === "unavailable") {
|
||
if (userStopped) {
|
||
return completeWithRange(separation, holdout, range, "user_stopped");
|
||
}
|
||
return finish("adopt_representative", {
|
||
input,
|
||
separation,
|
||
holdout,
|
||
range,
|
||
probe: null,
|
||
canConfirmExactMinute: false,
|
||
});
|
||
}
|
||
if (userStopped && holdout !== "passed") {
|
||
return completeWithRange(separation, holdout, range, "user_stopped");
|
||
}
|
||
return finish("adopt_representative", {
|
||
input,
|
||
separation,
|
||
holdout,
|
||
range,
|
||
probe: null,
|
||
canConfirmExactMinute: false,
|
||
});
|
||
}
|
||
|
||
function collect(
|
||
separation: CandidateSeparation,
|
||
holdout: HoldoutValidationStatus,
|
||
range: readonly [string, string] | null,
|
||
probe: CandidateDiscriminatorProbe | null,
|
||
): RectificationDecision {
|
||
return {
|
||
phase: "event_collection",
|
||
nextAction: "ask_fact_collection",
|
||
sessionOutcome: "collect_evidence",
|
||
resultStatus: "insufficient_evidence",
|
||
canOfferRange: false,
|
||
canAdopt: false,
|
||
canConfirmExactMinute: false,
|
||
selectionAllowed: false,
|
||
proposeAllowed: false,
|
||
precisionStage: "collect_events",
|
||
activeFocusPolicy: "keep",
|
||
completionStatus: null,
|
||
validated: false,
|
||
credibleRange: range,
|
||
representativeTime: separation.representativeTime,
|
||
separation,
|
||
probe,
|
||
holdoutValidation: holdout,
|
||
droppedProbes: [],
|
||
};
|
||
}
|
||
|
||
function offerRangeWithoutAdopt(
|
||
separation: CandidateSeparation,
|
||
holdout: HoldoutValidationStatus,
|
||
range: readonly [string, string] | null,
|
||
): RectificationDecision {
|
||
return {
|
||
phase: "discrimination",
|
||
nextAction: "offer_provisional_range",
|
||
sessionOutcome: "provisional_range",
|
||
resultStatus: "insufficient_evidence",
|
||
canOfferRange: true,
|
||
canAdopt: false,
|
||
canConfirmExactMinute: false,
|
||
selectionAllowed: false,
|
||
proposeAllowed: false,
|
||
precisionStage: "theme_refine",
|
||
activeFocusPolicy: "close",
|
||
completionStatus: null,
|
||
validated: false,
|
||
credibleRange: range,
|
||
representativeTime: separation.representativeTime,
|
||
separation,
|
||
probe: null,
|
||
holdoutValidation: holdout,
|
||
droppedProbes: [],
|
||
};
|
||
}
|
||
|
||
export function isNonConvergingRangeOffer(decision: Pick<
|
||
RectificationDecision,
|
||
"canOfferRange" | "canAdopt" | "canConfirmExactMinute" | "nextAction"
|
||
>): boolean {
|
||
return decision.canOfferRange
|
||
&& !decision.canAdopt
|
||
&& !decision.canConfirmExactMinute
|
||
&& decision.nextAction === "offer_provisional_range";
|
||
}
|
||
|
||
export const REPRESENTATIVE_MINUTE_DISCLAIMER = "代表分钟只是代表性候选,不是已确认的唯一出生分钟。";
|
||
|
||
export function nonConvergingRangeNarration(input: {
|
||
credibleRange?: readonly [string, string] | null;
|
||
representativeTime?: string | null;
|
||
} = {}): string {
|
||
const range = input.credibleRange;
|
||
const representative = input.representativeTime?.trim() || null;
|
||
const rangeText = range?.[0] && range[1]
|
||
? range[0] === range[1] ? range[0] : `${range[0]}–${range[1]}`
|
||
: null;
|
||
if (rangeText && representative) {
|
||
return `当前可信区间是 ${rangeText},代表分钟 ${representative}。${REPRESENTATIVE_MINUTE_DISCLAIMER}`;
|
||
}
|
||
if (rangeText) {
|
||
return `当前可信区间是 ${rangeText}。${REPRESENTATIVE_MINUTE_DISCLAIMER}`;
|
||
}
|
||
if (representative) {
|
||
return `当前代表分钟 ${representative}。${REPRESENTATIVE_MINUTE_DISCLAIMER}`;
|
||
}
|
||
return `当前几个候选还分不开。${REPRESENTATIVE_MINUTE_DISCLAIMER}`;
|
||
}
|
||
|
||
function discriminate(
|
||
separation: CandidateSeparation,
|
||
holdout: HoldoutValidationStatus,
|
||
range: readonly [string, string] | null,
|
||
probe: CandidateDiscriminatorProbe,
|
||
): RectificationDecision {
|
||
return {
|
||
phase: "discrimination",
|
||
nextAction: "ask_candidate_discriminator",
|
||
sessionOutcome: "discriminate_candidates",
|
||
resultStatus: "discriminating",
|
||
canOfferRange: false,
|
||
canAdopt: false,
|
||
canConfirmExactMinute: false,
|
||
selectionAllowed: false,
|
||
proposeAllowed: false,
|
||
precisionStage: "theme_refine",
|
||
activeFocusPolicy: "keep",
|
||
completionStatus: null,
|
||
validated: false,
|
||
credibleRange: range,
|
||
representativeTime: separation.representativeTime,
|
||
separation,
|
||
probe,
|
||
holdoutValidation: holdout,
|
||
droppedProbes: [],
|
||
};
|
||
}
|
||
|
||
function holdoutValidation(
|
||
separation: CandidateSeparation,
|
||
range: readonly [string, string] | null,
|
||
): RectificationDecision {
|
||
return {
|
||
phase: "holdout_validation",
|
||
nextAction: "ask_holdout_validation",
|
||
sessionOutcome: "validate_holdout",
|
||
resultStatus: "discriminating",
|
||
canOfferRange: false,
|
||
canAdopt: false,
|
||
canConfirmExactMinute: false,
|
||
selectionAllowed: false,
|
||
proposeAllowed: false,
|
||
precisionStage: "theme_refine",
|
||
activeFocusPolicy: "keep",
|
||
completionStatus: null,
|
||
validated: false,
|
||
credibleRange: range,
|
||
representativeTime: separation.representativeTime,
|
||
separation,
|
||
probe: null,
|
||
holdoutValidation: "not_started",
|
||
droppedProbes: [],
|
||
};
|
||
}
|
||
|
||
function completeWithRange(
|
||
separation: CandidateSeparation,
|
||
holdout: HoldoutValidationStatus,
|
||
range: readonly [string, string] | null,
|
||
kind: "user_stopped" | "offer" | "exhausted",
|
||
): RectificationDecision {
|
||
const userStopped = kind === "user_stopped";
|
||
const terminal = kind !== "offer";
|
||
const nextAction = terminal ? "complete_with_range" : "offer_provisional_range";
|
||
const sessionOutcome = userStopped
|
||
? "provisional_range_user_stopped"
|
||
: terminal
|
||
? "completed_with_range"
|
||
: "provisional_range";
|
||
return {
|
||
phase: terminal ? "completed" : "discrimination",
|
||
nextAction,
|
||
sessionOutcome,
|
||
resultStatus: "completed_with_range",
|
||
canOfferRange: true,
|
||
canAdopt: true,
|
||
canConfirmExactMinute: false,
|
||
selectionAllowed: true,
|
||
proposeAllowed: true,
|
||
precisionStage: "ready_to_adopt",
|
||
activeFocusPolicy: "close",
|
||
completionStatus: userStopped ? "provisional_range_user_stopped" : null,
|
||
validated: false,
|
||
credibleRange: range,
|
||
representativeTime: separation.representativeTime,
|
||
separation,
|
||
probe: null,
|
||
holdoutValidation: holdout,
|
||
droppedProbes: [],
|
||
};
|
||
}
|
||
|
||
function finish(
|
||
fallbackOutcome: "adopt_representative" | "awaiting_confirmation" | "validated_range" | "exact_minute_confirmed",
|
||
input: {
|
||
input: DecideRectificationInput;
|
||
separation: CandidateSeparation;
|
||
holdout: HoldoutValidationStatus;
|
||
range: readonly [string, string] | null;
|
||
probe: CandidateDiscriminatorProbe | null;
|
||
canConfirmExactMinute: boolean;
|
||
},
|
||
): RectificationDecision {
|
||
const completionStatus: CompletionStatus | null = input.canConfirmExactMinute && input.input.accepted
|
||
? "exact_minute_confirmed"
|
||
: input.holdout === "passed"
|
||
? "validated_range"
|
||
: input.input.userStopped === true
|
||
? "provisional_range_user_stopped"
|
||
: null;
|
||
const kind = input.canConfirmExactMinute
|
||
? (input.input.accepted ? "exact_minute_confirmed" : "awaiting_confirmation")
|
||
: input.holdout === "passed"
|
||
? "validated_range"
|
||
: fallbackOutcome;
|
||
return {
|
||
phase: "completed",
|
||
nextAction: "ready_to_adopt",
|
||
sessionOutcome: kind,
|
||
resultStatus: kind === "awaiting_confirmation" || kind === "exact_minute_confirmed"
|
||
? "converged"
|
||
: "completed_with_range",
|
||
canOfferRange: true,
|
||
canAdopt: true,
|
||
canConfirmExactMinute: input.canConfirmExactMinute,
|
||
selectionAllowed: true,
|
||
proposeAllowed: true,
|
||
precisionStage: "ready_to_adopt",
|
||
activeFocusPolicy: "close",
|
||
completionStatus,
|
||
validated: completionStatus === "validated_range" || completionStatus === "exact_minute_confirmed",
|
||
credibleRange: input.range,
|
||
representativeTime: input.separation.representativeTime,
|
||
separation: input.separation,
|
||
probe: input.probe,
|
||
holdoutValidation: input.holdout,
|
||
droppedProbes: [],
|
||
};
|
||
}
|
||
|
||
export function sessionKindFromNextAction(
|
||
type: RectificationNextActionType,
|
||
): DecisionSessionOutcome {
|
||
if (type === "ask_fact_collection") return "collect_evidence";
|
||
if (type === "ask_candidate_discriminator") return "discriminate_candidates";
|
||
if (type === "ask_holdout_validation") return "validate_holdout";
|
||
if (type === "offer_provisional_range") return "provisional_range";
|
||
if (type === "complete_with_range") return "completed_with_range";
|
||
return "adopt_representative";
|
||
}
|
||
|
||
export function offerSessionKinds(): readonly string[] {
|
||
return [
|
||
"adopt_representative",
|
||
"awaiting_confirmation",
|
||
"provisional_range",
|
||
"provisional_range_user_stopped",
|
||
"completed_with_range",
|
||
"validated_range",
|
||
"exact_minute_confirmed",
|
||
];
|
||
}
|
||
|
||
export function publicNextAction(decision: RectificationDecision): Readonly<{
|
||
type: RectificationNextActionType;
|
||
session_outcome: DecisionSessionOutcome;
|
||
completion_status: CompletionStatus | null;
|
||
validated: boolean;
|
||
can_offer_range: boolean;
|
||
can_adopt: boolean;
|
||
can_confirm_exact_minute: boolean;
|
||
selection_allowed: boolean;
|
||
propose_allowed: boolean;
|
||
precision_stage: DerivedPrecisionStage;
|
||
representative_time: string | null;
|
||
credible_range: readonly [string, string] | null;
|
||
}> {
|
||
return {
|
||
type: decision.nextAction,
|
||
session_outcome: decision.sessionOutcome,
|
||
completion_status: decision.completionStatus,
|
||
validated: decision.validated,
|
||
can_offer_range: decision.canOfferRange,
|
||
can_adopt: decision.canAdopt,
|
||
can_confirm_exact_minute: decision.canConfirmExactMinute,
|
||
selection_allowed: decision.selectionAllowed,
|
||
propose_allowed: decision.proposeAllowed,
|
||
precision_stage: decision.precisionStage,
|
||
representative_time: decision.representativeTime,
|
||
credible_range: decision.credibleRange,
|
||
};
|
||
}
|
||
|
||
export function publicDecisionFields(
|
||
decision: RectificationDecision,
|
||
): ReturnType<typeof publicNextAction> {
|
||
return publicNextAction(decision);
|
||
}
|