/** * 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>; 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 = 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): 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, 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 { return publicNextAction(decision); }