51 lines
2.4 KiB
TypeScript
51 lines
2.4 KiB
TypeScript
import type { CandidateResult } from "./birth-time-evidence.ts";
|
|
|
|
export type DynamicStopInput = {
|
|
readonly result: CandidateResult | null;
|
|
readonly effectiveAnswer: boolean;
|
|
readonly previousResult: CandidateResult | null;
|
|
readonly priorPlateauCount: number;
|
|
readonly usefulOpportunityCount: number;
|
|
readonly repeatedOnly: boolean;
|
|
readonly effectiveAnswerCount: number;
|
|
readonly forcedReason: "user_finished" | "generation_unavailable" | null;
|
|
};
|
|
|
|
export type DynamicStopDecision =
|
|
| {
|
|
readonly kind: "finish";
|
|
readonly reason: "user_finished" | "generation_unavailable" | "high_confidence" | "safety_cap" | "plateau" | "no_information_gain" | "repeated_partition";
|
|
readonly plateauCount: number;
|
|
}
|
|
| { readonly kind: "continue"; readonly plateauCount: number };
|
|
|
|
export function materiallyChanged(
|
|
previousResult: CandidateResult | null,
|
|
result: CandidateResult,
|
|
): boolean {
|
|
if (previousResult === null) return true;
|
|
const previousRange = previousResult.winningSegment;
|
|
const nextRange = result.winningSegment;
|
|
const rangeChanged = previousRange === null || nextRange === null
|
|
? previousRange !== nextRange
|
|
: previousRange.startTime !== nextRange.startTime
|
|
|| previousRange.endTime !== nextRange.endTime
|
|
|| previousRange.representativeTime !== nextRange.representativeTime;
|
|
return rangeChanged || Math.abs(previousResult.marginPercent - result.marginPercent) >= 2;
|
|
}
|
|
|
|
export function decideDynamicStop(input: DynamicStopInput): DynamicStopDecision {
|
|
const plateauCount = input.effectiveAnswer && input.result !== null
|
|
? materiallyChanged(input.previousResult, input.result) ? 0 : input.priorPlateauCount + 1
|
|
: input.priorPlateauCount;
|
|
if (input.forcedReason !== null) return { kind: "finish", reason: input.forcedReason, plateauCount };
|
|
if (input.result?.confidence === "high" && input.result.canApply) {
|
|
return { kind: "finish", reason: "high_confidence", plateauCount };
|
|
}
|
|
if (input.effectiveAnswerCount >= 10) return { kind: "finish", reason: "safety_cap", plateauCount };
|
|
if (plateauCount >= 2) return { kind: "finish", reason: "plateau", plateauCount };
|
|
if (input.usefulOpportunityCount === 0) return { kind: "finish", reason: "no_information_gain", plateauCount };
|
|
if (input.repeatedOnly) return { kind: "finish", reason: "repeated_partition", plateauCount };
|
|
return { kind: "continue", plateauCount };
|
|
}
|