Showing a choice card is no longer treated as completion. Distinguish probes require real candidate groups, holdout stays out of scoring, and ordinary sessions can finish with a credible range instead of an exact-minute gate. Co-authored-by: Cursor <cursoragent@cursor.com>
53 lines
2.2 KiB
TypeScript
53 lines
2.2 KiB
TypeScript
import { MIN_SEPARATION_LEAD } from "./candidate-separation.ts";
|
|
import type { InferenceCandidate } from "./types.ts";
|
|
|
|
function rankActive(candidates: readonly InferenceCandidate[]): InferenceCandidate[] {
|
|
return [...candidates]
|
|
.filter((item) => item.status !== "eliminated")
|
|
.sort((left, right) => {
|
|
if (right.probability !== left.probability) return right.probability - left.probability;
|
|
if (right.posterior_score !== left.posterior_score) return right.posterior_score - left.posterior_score;
|
|
return left.time.localeCompare(right.time);
|
|
});
|
|
}
|
|
|
|
function toMinutes(value: string): number | null {
|
|
if (!/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(value)) return null;
|
|
return Number(value.slice(0, 2)) * 60 + Number(value.slice(3, 5));
|
|
}
|
|
|
|
function fromMinutes(value: number): string {
|
|
const wrapped = ((value % 1440) + 1440) % 1440;
|
|
return `${String(Math.floor(wrapped / 60)).padStart(2, "0")}:${String(wrapped % 60).padStart(2, "0")}`;
|
|
}
|
|
|
|
/**
|
|
* Union every still-valid parallel candidate or cluster, not only rank=1.
|
|
* A time stays valid while it is within the separation lead of the peak.
|
|
*/
|
|
export function unionStillValidRange(
|
|
candidates: readonly InferenceCandidate[],
|
|
lead = MIN_SEPARATION_LEAD,
|
|
): readonly [string, string] | null {
|
|
const active = rankActive(candidates);
|
|
if (active.length === 0) return null;
|
|
const peak = active[0]!.posterior_score;
|
|
const stillValid = active.filter((item) => peak - item.posterior_score < lead);
|
|
const points = stillValid.flatMap((item) => [item.cluster_range[0], item.cluster_range[1], item.time]);
|
|
const minutes = points
|
|
.map(toMinutes)
|
|
.filter((value): value is number => value !== null)
|
|
.sort((left, right) => left - right);
|
|
if (minutes.length === 0) return null;
|
|
return [fromMinutes(minutes[0]!), fromMinutes(minutes[minutes.length - 1]!)];
|
|
}
|
|
|
|
export function rangeFromTimes(times: readonly string[]): readonly [string, string] | null {
|
|
const minutes = times
|
|
.map(toMinutes)
|
|
.filter((value): value is number => value !== null)
|
|
.sort((left, right) => left - right);
|
|
if (minutes.length === 0) return null;
|
|
return [fromMinutes(minutes[0]!), fromMinutes(minutes[minutes.length - 1]!)];
|
|
}
|