Files
Jyotisha/frontend/src/lib/rectification-agentic/core/rectification-decision.ts
T
Jesse_ChenandCursor a3a51c32c2 fix(rectification): re-ask superseded collect ids instead of fake delivery (BUG-644/645)
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>
2026-09-10 21:03:12 +08:00

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);
}