A superseded collect row still occupied the unique question id, so the last "没有" skipped the unasked domain and spoke delivery copy while can_adopt stayed false. Co-authored-by: Cursor <cursoragent@cursor.com>
781 lines
26 KiB
TypeScript
781 lines
26 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 { EFFECTIVE_ANSWER_SAFETY_CAP } from "../../birth-time-dynamic-stop-policy.ts";
|
|
import { RECTIFICATION_POLICY } from "../../rectification-policy.ts";
|
|
import {
|
|
DEFAULT_MAX_DISCRIMINATION_ROUNDS,
|
|
type RectificationPhase,
|
|
type ResultStatus,
|
|
} from "./types.ts";
|
|
import {
|
|
REPRESENTATIVE_MINUTE_DISCLAIMER,
|
|
nonConvergingRangeNarration,
|
|
} from "../user-copy.ts";
|
|
|
|
export { REPRESENTATIVE_MINUTE_DISCLAIMER, nonConvergingRangeNarration };
|
|
|
|
export type RectificationNextActionType =
|
|
| "ask_fact_collection"
|
|
| "ask_block_choice"
|
|
| "ask_window_widen"
|
|
| "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 EvidenceStopReason =
|
|
| "insufficient_dated_events"
|
|
| "insufficient_domains"
|
|
| "tied_first"
|
|
| "user_uncertainty_too_high"
|
|
| "probe_pool_exhausted";
|
|
|
|
export type StopClass =
|
|
| Readonly<{ kind: "keep_collecting"; reason: "insufficient_dated_events" | "insufficient_domains" }>
|
|
| Readonly<{ kind: "exhausted"; reason: "tied_first" | "user_uncertainty_too_high" }>
|
|
| Readonly<{ kind: "user_stopped" }>;
|
|
|
|
export type EngineCapabilityCeiling = Readonly<{
|
|
acceptanceAllowed: boolean;
|
|
selectionAllowed: boolean;
|
|
proposeAllowed: boolean;
|
|
confirmationAllowed: boolean;
|
|
}>;
|
|
|
|
type DeliveryCapability = Readonly<{
|
|
canAdopt: boolean;
|
|
selectionAllowed: boolean;
|
|
proposeAllowed: boolean;
|
|
canConfirmExactMinute: boolean;
|
|
}>;
|
|
|
|
const CLOSED_ENGINE_CAPABILITY_CEILING: EngineCapabilityCeiling = {
|
|
acceptanceAllowed: false,
|
|
selectionAllowed: false,
|
|
proposeAllowed: false,
|
|
confirmationAllowed: false,
|
|
};
|
|
|
|
export function engineCapabilityCeilingFromReceipt(value: unknown): EngineCapabilityCeiling {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
return CLOSED_ENGINE_CAPABILITY_CEILING;
|
|
}
|
|
const row = value as Readonly<Record<string, unknown>>;
|
|
const acceptanceAllowed = row.acceptance_allowed;
|
|
const selectionAllowed = row.selection_allowed;
|
|
const proposeAllowed = row.propose_allowed;
|
|
const confirmationAllowed = row.confirmation_allowed;
|
|
if (
|
|
typeof acceptanceAllowed !== "boolean"
|
|
|| typeof selectionAllowed !== "boolean"
|
|
|| typeof proposeAllowed !== "boolean"
|
|
|| typeof confirmationAllowed !== "boolean"
|
|
|| acceptanceAllowed !== selectionAllowed
|
|
|| (row.accept_allowed !== undefined
|
|
&& (typeof row.accept_allowed !== "boolean" || row.accept_allowed !== selectionAllowed))
|
|
|| (row.confirm_allowed !== undefined
|
|
&& (typeof row.confirm_allowed !== "boolean" || row.confirm_allowed !== confirmationAllowed))
|
|
|| (row.display_allowed !== undefined
|
|
&& (typeof row.display_allowed !== "boolean" || (selectionAllowed && !row.display_allowed)))
|
|
|| (proposeAllowed && !selectionAllowed)
|
|
|| (confirmationAllowed && !selectionAllowed)
|
|
) {
|
|
return CLOSED_ENGINE_CAPABILITY_CEILING;
|
|
}
|
|
return { acceptanceAllowed, selectionAllowed, proposeAllowed, confirmationAllowed };
|
|
}
|
|
|
|
export const RECTIFICATION_TERMINATION_COPY = "当前最优结果是候选时间段,而不是已经确认的唯一出生分钟。临时代表时间仅用于下一轮验证与比较。";
|
|
|
|
/** Standalone range-delivery floor; the exact-minute confirmation gate remains 4/3. */
|
|
export const MIN_STANDALONE_DATED_EVENTS = 3;
|
|
export const MIN_STANDALONE_DATED_DOMAINS = 2;
|
|
|
|
export type DecisionSessionOutcome =
|
|
| "collect_evidence"
|
|
| "compare_blocks"
|
|
| "widen_window"
|
|
| "discriminate_candidates"
|
|
| "validate_holdout"
|
|
| "provisional_range"
|
|
| "provisional_range_user_stopped"
|
|
| "completed_with_range"
|
|
| "validated_range"
|
|
| "exact_minute_confirmed"
|
|
| "adopt_representative"
|
|
| "awaiting_confirmation";
|
|
|
|
/** Public `can_adopt` is true only in these outcomes. Internal `decision.canAdopt` may still be true during collect. */
|
|
export const ADOPT_OUTCOMES: ReadonlySet<DecisionSessionOutcome> = new Set([
|
|
"adopt_representative",
|
|
"provisional_range_user_stopped",
|
|
"awaiting_confirmation",
|
|
"validated_range",
|
|
]);
|
|
|
|
export function sessionOutcomeAllowsAdopt(
|
|
outcome: DecisionSessionOutcome | string | null | undefined,
|
|
): outcome is DecisionSessionOutcome {
|
|
return typeof outcome === "string" && ADOPT_OUTCOMES.has(outcome as DecisionSessionOutcome);
|
|
}
|
|
|
|
export function publicCanAdopt(decision: Pick<RectificationDecision, "canAdopt" | "sessionOutcome">): boolean {
|
|
return decision.canAdopt && sessionOutcomeAllowsAdopt(decision.sessionOutcome);
|
|
}
|
|
|
|
/**
|
|
* Delivery copy (`这次给出的范围` / representative-candidate close) is only
|
|
* allowed when the public adopt flag is on, or the session is explicitly
|
|
* offering a selectable range. Internal `canAdopt` during collect is not enough.
|
|
*/
|
|
export function deliveryNarrationAllowed(
|
|
decision: Pick<RectificationDecision, "canAdopt" | "sessionOutcome" | "nextAction" | "selectionAllowed">,
|
|
nextAction?: string | null,
|
|
): boolean {
|
|
if (publicCanAdopt(decision)) return true;
|
|
const action = nextAction ?? decision.nextAction;
|
|
return decision.selectionAllowed === true && action === "offer_provisional_range";
|
|
}
|
|
|
|
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[];
|
|
stopReason?: EvidenceStopReason | null;
|
|
terminationCopy?: string | null;
|
|
}>;
|
|
|
|
export type DecideRectificationInput = Readonly<{
|
|
methodCoverageAll: boolean;
|
|
confirmationAllowed?: boolean;
|
|
userStopped?: boolean;
|
|
snapshotCurrent?: boolean;
|
|
trainingGateOpen?: boolean;
|
|
candidateScores: readonly CandidateScoreRow[];
|
|
discriminatorProbe?: CandidateDiscriminatorProbe | null;
|
|
nakshatraBoundaryProbe?: CandidateDiscriminatorProbe | null;
|
|
holdoutValidation?: HoldoutValidationStatus;
|
|
accepted?: boolean;
|
|
inferenceCredibleRange?: readonly [string, string] | null;
|
|
engineCeiling: EngineCapabilityCeiling;
|
|
datedMethodCollectOpen?: boolean;
|
|
/** Server-persisted inference counters; never infer these from chat text. */
|
|
inferenceRounds?: number;
|
|
effectiveAnswerCount?: number;
|
|
plateauRounds?: number;
|
|
/** Minimum standalone delivery floor; distinct from the 4/3 confirmation gate. */
|
|
datedEventCount?: number;
|
|
datedDomainCount?: number;
|
|
userUncertaintyHigh?: boolean;
|
|
caseStage?: "minute" | "block_scan";
|
|
blockScanDeclined?: boolean;
|
|
windowWidenSuggested?: boolean;
|
|
}>;
|
|
|
|
function classifyStop(
|
|
input: DecideRectificationInput,
|
|
separation: CandidateSeparation,
|
|
): StopClass | null {
|
|
if (
|
|
input.datedEventCount !== undefined
|
|
&& input.datedEventCount < MIN_STANDALONE_DATED_EVENTS
|
|
) {
|
|
return { kind: "keep_collecting", reason: "insufficient_dated_events" };
|
|
}
|
|
if (
|
|
input.datedDomainCount !== undefined
|
|
&& input.datedDomainCount < MIN_STANDALONE_DATED_DOMAINS
|
|
) {
|
|
return { kind: "keep_collecting", reason: "insufficient_domains" };
|
|
}
|
|
if (separation.ranked.length === 0 && !input.discriminatorProbe) {
|
|
return { kind: "keep_collecting", reason: "insufficient_dated_events" };
|
|
}
|
|
if (input.userStopped === true) return { kind: "user_stopped" };
|
|
if (separation.tiedForFirst) return { kind: "exhausted", reason: "tied_first" };
|
|
if (input.userUncertaintyHigh === true) {
|
|
return { kind: "exhausted", reason: "user_uncertainty_too_high" };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function deliveryCapability(input: {
|
|
stopClass: StopClass | null;
|
|
separation: CandidateSeparation;
|
|
holdout: HoldoutValidationStatus;
|
|
engineCeiling: EngineCapabilityCeiling;
|
|
confirmationAllowed: boolean;
|
|
trainingGateOpen: boolean;
|
|
}): DeliveryCapability {
|
|
const locallySelectable = input.separation.ranked.length > 0
|
|
&& input.trainingGateOpen
|
|
&& input.stopClass?.kind !== "keep_collecting";
|
|
return {
|
|
canAdopt: locallySelectable && input.engineCeiling.acceptanceAllowed,
|
|
selectionAllowed: locallySelectable && input.engineCeiling.selectionAllowed,
|
|
proposeAllowed: locallySelectable && input.engineCeiling.proposeAllowed,
|
|
canConfirmExactMinute: locallySelectable
|
|
&& input.separation.sufficient
|
|
&& input.holdout === "passed"
|
|
&& input.confirmationAllowed
|
|
&& input.engineCeiling.confirmationAllowed,
|
|
};
|
|
}
|
|
|
|
export function decideRectification(input: DecideRectificationInput): RectificationDecision {
|
|
if (input.caseStage === "block_scan") {
|
|
return decideBlockScan(input);
|
|
}
|
|
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);
|
|
const stopClass = classifyStop(input, separation);
|
|
const stopReason = stopClass && "reason" in stopClass ? stopClass.reason : null;
|
|
const canDiscriminateDespiteCoverage = Boolean(probe)
|
|
&& !separation.sufficient
|
|
&& input.trainingGateOpen !== false;
|
|
const coverageBlocks = (!input.methodCoverageAll && !canDiscriminateDespiteCoverage)
|
|
|| input.trainingGateOpen === false;
|
|
const capability = deliveryCapability({
|
|
stopClass,
|
|
separation,
|
|
holdout,
|
|
engineCeiling: input.engineCeiling,
|
|
confirmationAllowed,
|
|
trainingGateOpen: input.trainingGateOpen !== false,
|
|
});
|
|
|
|
if (input.windowWidenSuggested === true && input.trainingGateOpen !== false && !userStopped) {
|
|
return askWindowWiden(separation, range);
|
|
}
|
|
|
|
if (userStopped && separation.ranked.length > 0) {
|
|
return completeWithRange(separation, holdout, range, "user_stopped", capability);
|
|
}
|
|
|
|
if (input.snapshotCurrent === false) {
|
|
if (probe && !userStopped && input.trainingGateOpen !== false) {
|
|
return discriminateOrExhaust(input, separation, holdout, range, probe, capability, stopReason);
|
|
}
|
|
return collect(separation, holdout, range, probe, capability, stopReason);
|
|
}
|
|
if (coverageBlocks) {
|
|
const engineOffers = input.engineCeiling.acceptanceAllowed
|
|
|| input.engineCeiling.proposeAllowed;
|
|
if (
|
|
stopClass?.kind !== "keep_collecting"
|
|
&& input.trainingGateOpen !== false
|
|
&& separation.ranked.length > 0
|
|
&& !probe
|
|
&& engineOffers
|
|
&& input.datedMethodCollectOpen !== true
|
|
) {
|
|
return offerRangeWithoutAdopt(separation, holdout, range, capability);
|
|
}
|
|
return collect(separation, holdout, range, probe, capability, stopReason);
|
|
}
|
|
if (stopClass?.kind === "keep_collecting") {
|
|
return collect(separation, holdout, range, probe, capability, stopClass.reason);
|
|
}
|
|
if (stopClass?.kind === "exhausted" && stopClass.reason === "user_uncertainty_too_high") {
|
|
return completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason);
|
|
}
|
|
if (!separation.sufficient) {
|
|
if (probe) {
|
|
return discriminateOrExhaust(input, separation, holdout, range, probe, capability, stopReason);
|
|
}
|
|
if (holdout === "not_started" && !capability.canAdopt) {
|
|
return holdoutValidation(separation, range, capability);
|
|
}
|
|
if (input.datedMethodCollectOpen === true) {
|
|
return collect(separation, holdout, range, null, capability, stopReason);
|
|
}
|
|
if (input.nakshatraBoundaryProbe) {
|
|
return discriminateOrExhaust(
|
|
input,
|
|
separation,
|
|
holdout,
|
|
range,
|
|
input.nakshatraBoundaryProbe,
|
|
capability,
|
|
stopReason,
|
|
);
|
|
}
|
|
if (stopClass?.kind === "exhausted") {
|
|
return completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason);
|
|
}
|
|
if (capability.canAdopt && input.methodCoverageAll) {
|
|
return finish("adopt_representative", {
|
|
input,
|
|
separation,
|
|
holdout,
|
|
range,
|
|
probe: null,
|
|
capability,
|
|
stopReason: "probe_pool_exhausted",
|
|
});
|
|
}
|
|
return completeWithRange(separation, holdout, range, "offer", capability);
|
|
}
|
|
if (stopClass?.kind === "exhausted") {
|
|
return completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason);
|
|
}
|
|
if (input.accepted) {
|
|
return finish(confirmationAllowed ? "awaiting_confirmation" : "adopt_representative", {
|
|
input,
|
|
separation,
|
|
holdout,
|
|
range,
|
|
probe: null,
|
|
capability,
|
|
});
|
|
}
|
|
if (input.datedMethodCollectOpen === true && !input.userStopped) {
|
|
return collect(separation, holdout, range, null, capability, stopReason);
|
|
}
|
|
if (confirmationAllowed) {
|
|
return finish("awaiting_confirmation", {
|
|
input,
|
|
separation,
|
|
holdout,
|
|
range,
|
|
probe: null,
|
|
capability,
|
|
});
|
|
}
|
|
if (holdout === "not_started" && !capability.canAdopt) {
|
|
return holdoutValidation(separation, range, capability);
|
|
}
|
|
if (holdout === "failed") {
|
|
if (probe) {
|
|
return discriminateOrExhaust(input, separation, holdout, range, probe, capability);
|
|
}
|
|
return completeWithRange(separation, holdout, range, "exhausted", capability);
|
|
}
|
|
if (holdout === "unavailable") {
|
|
return offerRangeWithoutAdopt(separation, holdout, range, capability);
|
|
}
|
|
return finish("adopt_representative", {
|
|
input,
|
|
separation,
|
|
holdout,
|
|
range,
|
|
probe: null,
|
|
capability,
|
|
});
|
|
}
|
|
|
|
function budgetExhausted(input: DecideRectificationInput): boolean {
|
|
return (input.inferenceRounds ?? 0) >= DEFAULT_MAX_DISCRIMINATION_ROUNDS
|
|
|| (input.effectiveAnswerCount ?? 0) >= EFFECTIVE_ANSWER_SAFETY_CAP
|
|
|| (input.plateauRounds ?? 0) >= RECTIFICATION_POLICY.maxPlateauRounds;
|
|
}
|
|
|
|
function discriminateOrExhaust(
|
|
input: DecideRectificationInput,
|
|
separation: CandidateSeparation,
|
|
holdout: HoldoutValidationStatus,
|
|
range: readonly [string, string] | null,
|
|
probe: CandidateDiscriminatorProbe,
|
|
capability: DeliveryCapability,
|
|
stopReason: EvidenceStopReason | null = null,
|
|
): RectificationDecision {
|
|
if (budgetExhausted(input)) {
|
|
return completeWithRange(separation, holdout, range, "exhausted", capability, stopReason);
|
|
}
|
|
return discriminate(separation, holdout, range, probe, capability, stopReason);
|
|
}
|
|
|
|
function decideBlockScan(input: DecideRectificationInput): RectificationDecision {
|
|
const closed: DeliveryCapability = {
|
|
canAdopt: false,
|
|
selectionAllowed: false,
|
|
proposeAllowed: false,
|
|
canConfirmExactMinute: false,
|
|
};
|
|
const separation = evaluateCandidateSeparation([]);
|
|
const range = input.inferenceCredibleRange ?? (["00:00", "23:59"] as const);
|
|
if (input.blockScanDeclined === true) {
|
|
return collect(separation, "unavailable", range, null, closed, "insufficient_dated_events");
|
|
}
|
|
if (input.trainingGateOpen === false) {
|
|
const stopReason: EvidenceStopReason = (input.datedDomainCount ?? 0) < MIN_STANDALONE_DATED_DOMAINS
|
|
&& (input.datedEventCount ?? 0) >= MIN_STANDALONE_DATED_EVENTS
|
|
? "insufficient_domains"
|
|
: "insufficient_dated_events";
|
|
return collect(separation, "unavailable", range, null, closed, stopReason);
|
|
}
|
|
return askBlockChoice(separation, range, closed);
|
|
}
|
|
|
|
function askBlockChoice(
|
|
separation: CandidateSeparation,
|
|
range: readonly [string, string] | null,
|
|
capability: DeliveryCapability,
|
|
): RectificationDecision {
|
|
return {
|
|
phase: "event_collection",
|
|
nextAction: "ask_block_choice",
|
|
sessionOutcome: "compare_blocks",
|
|
resultStatus: "insufficient_evidence",
|
|
canOfferRange: false,
|
|
...capability,
|
|
precisionStage: "collect_events",
|
|
activeFocusPolicy: "keep",
|
|
completionStatus: null,
|
|
validated: false,
|
|
credibleRange: range,
|
|
representativeTime: null,
|
|
separation,
|
|
probe: null,
|
|
holdoutValidation: "unavailable",
|
|
droppedProbes: [],
|
|
};
|
|
}
|
|
|
|
function askWindowWiden(
|
|
separation: CandidateSeparation,
|
|
range: readonly [string, string] | null,
|
|
): RectificationDecision {
|
|
const closed: DeliveryCapability = {
|
|
canAdopt: false,
|
|
selectionAllowed: false,
|
|
proposeAllowed: false,
|
|
canConfirmExactMinute: false,
|
|
};
|
|
return {
|
|
phase: "discrimination",
|
|
nextAction: "ask_window_widen",
|
|
sessionOutcome: "widen_window",
|
|
resultStatus: "discriminating",
|
|
canOfferRange: false,
|
|
...closed,
|
|
precisionStage: "theme_refine",
|
|
activeFocusPolicy: "keep",
|
|
completionStatus: null,
|
|
validated: false,
|
|
credibleRange: range,
|
|
representativeTime: separation.representativeTime,
|
|
separation,
|
|
probe: null,
|
|
holdoutValidation: "unavailable",
|
|
droppedProbes: [],
|
|
};
|
|
}
|
|
|
|
function collect(
|
|
separation: CandidateSeparation,
|
|
holdout: HoldoutValidationStatus,
|
|
range: readonly [string, string] | null,
|
|
probe: CandidateDiscriminatorProbe | null,
|
|
capability: DeliveryCapability,
|
|
stopReason: EvidenceStopReason | null = null,
|
|
): RectificationDecision {
|
|
return {
|
|
phase: "event_collection",
|
|
nextAction: "ask_fact_collection",
|
|
sessionOutcome: "collect_evidence",
|
|
resultStatus: "insufficient_evidence",
|
|
canOfferRange: false,
|
|
...capability,
|
|
precisionStage: "collect_events",
|
|
activeFocusPolicy: "keep",
|
|
completionStatus: null,
|
|
validated: false,
|
|
credibleRange: range,
|
|
representativeTime: separation.representativeTime,
|
|
separation,
|
|
probe,
|
|
holdoutValidation: holdout,
|
|
droppedProbes: [],
|
|
stopReason,
|
|
};
|
|
}
|
|
|
|
function offerRangeWithoutAdopt(
|
|
separation: CandidateSeparation,
|
|
holdout: HoldoutValidationStatus,
|
|
range: readonly [string, string] | null,
|
|
capability: DeliveryCapability,
|
|
): RectificationDecision {
|
|
return {
|
|
phase: "discrimination",
|
|
nextAction: "offer_provisional_range",
|
|
sessionOutcome: capability.canAdopt ? "adopt_representative" : "provisional_range",
|
|
resultStatus: "insufficient_evidence",
|
|
canOfferRange: true,
|
|
...capability,
|
|
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";
|
|
}
|
|
|
|
function discriminate(
|
|
separation: CandidateSeparation,
|
|
holdout: HoldoutValidationStatus,
|
|
range: readonly [string, string] | null,
|
|
probe: CandidateDiscriminatorProbe,
|
|
capability: DeliveryCapability,
|
|
stopReason: EvidenceStopReason | null = null,
|
|
): RectificationDecision {
|
|
return {
|
|
phase: "discrimination",
|
|
nextAction: "ask_candidate_discriminator",
|
|
sessionOutcome: "discriminate_candidates",
|
|
resultStatus: "discriminating",
|
|
canOfferRange: false,
|
|
...capability,
|
|
precisionStage: "theme_refine",
|
|
activeFocusPolicy: "keep",
|
|
completionStatus: null,
|
|
validated: false,
|
|
credibleRange: range,
|
|
representativeTime: separation.representativeTime,
|
|
separation,
|
|
probe,
|
|
holdoutValidation: holdout,
|
|
droppedProbes: [],
|
|
...(stopReason ? { stopReason } : {}),
|
|
};
|
|
}
|
|
|
|
function holdoutValidation(
|
|
separation: CandidateSeparation,
|
|
range: readonly [string, string] | null,
|
|
capability: DeliveryCapability,
|
|
): RectificationDecision {
|
|
return {
|
|
phase: "holdout_validation",
|
|
nextAction: "ask_holdout_validation",
|
|
sessionOutcome: "validate_holdout",
|
|
resultStatus: "discriminating",
|
|
canOfferRange: false,
|
|
...capability,
|
|
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",
|
|
capability: DeliveryCapability,
|
|
stopReason: EvidenceStopReason | null = null,
|
|
): RectificationDecision {
|
|
const userStopped = kind === "user_stopped";
|
|
const terminal = kind !== "offer";
|
|
const nextAction = terminal ? "complete_with_range" : "offer_provisional_range";
|
|
const sessionOutcome = capability.canAdopt
|
|
? "adopt_representative"
|
|
: userStopped
|
|
? "provisional_range_user_stopped"
|
|
: terminal
|
|
? "completed_with_range"
|
|
: "provisional_range";
|
|
return {
|
|
phase: terminal ? "completed" : "discrimination",
|
|
nextAction,
|
|
sessionOutcome,
|
|
resultStatus: "completed_with_range",
|
|
canOfferRange: true,
|
|
...capability,
|
|
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: [],
|
|
stopReason,
|
|
terminationCopy: terminal ? RECTIFICATION_TERMINATION_COPY : null,
|
|
};
|
|
}
|
|
|
|
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;
|
|
capability: DeliveryCapability;
|
|
stopReason?: EvidenceStopReason | null;
|
|
},
|
|
): RectificationDecision {
|
|
const completionStatus: CompletionStatus | null = input.capability.canConfirmExactMinute && input.input.accepted
|
|
? "exact_minute_confirmed"
|
|
: input.holdout === "passed"
|
|
? "validated_range"
|
|
: input.input.userStopped === true
|
|
? "provisional_range_user_stopped"
|
|
: null;
|
|
const kind = input.capability.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,
|
|
...input.capability,
|
|
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: [],
|
|
...(input.stopReason ? { stopReason: input.stopReason } : {}),
|
|
};
|
|
}
|
|
|
|
export function sessionKindFromNextAction(
|
|
type: RectificationNextActionType,
|
|
): DecisionSessionOutcome {
|
|
if (type === "ask_fact_collection") return "collect_evidence";
|
|
if (type === "ask_block_choice") return "compare_blocks";
|
|
if (type === "ask_window_widen") return "widen_window";
|
|
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;
|
|
stop_reason: EvidenceStopReason | null;
|
|
}> {
|
|
return {
|
|
type: decision.nextAction,
|
|
session_outcome: decision.sessionOutcome,
|
|
completion_status: decision.completionStatus,
|
|
validated: decision.validated,
|
|
can_offer_range: decision.canOfferRange,
|
|
can_adopt: publicCanAdopt(decision),
|
|
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,
|
|
stop_reason: decision.stopReason ?? null,
|
|
};
|
|
}
|
|
|
|
export function publicDecisionFields(
|
|
decision: RectificationDecision,
|
|
): ReturnType<typeof publicNextAction> {
|
|
return publicNextAction(decision);
|
|
}
|