87 lines
2.4 KiB
TypeScript
87 lines
2.4 KiB
TypeScript
/**
|
|
* Candidate separation is not event-fit and not method coverage.
|
|
* A tie means the top two engine scores are exactly equal; a 1-point lead is not a winner.
|
|
* One remaining candidate is a sole candidate, not a parallel range.
|
|
*/
|
|
|
|
export const MIN_SEPARATION_LEAD = 8;
|
|
|
|
export type SeparationStatus = "not_separated" | "weak_lead" | "separated" | "sole_candidate";
|
|
|
|
export type CandidateScoreRow = Readonly<{
|
|
id?: string;
|
|
time: string;
|
|
score: number;
|
|
}>;
|
|
|
|
export type CandidateSeparation = Readonly<{
|
|
sufficient: boolean;
|
|
status: SeparationStatus;
|
|
lead: number;
|
|
topShare: number;
|
|
representativeTime: string | null;
|
|
credibleRange: readonly string[];
|
|
ranked: readonly CandidateScoreRow[];
|
|
tiedForFirst: boolean;
|
|
}>;
|
|
|
|
export function evaluateCandidateSeparation(
|
|
candidates: readonly CandidateScoreRow[],
|
|
): CandidateSeparation {
|
|
const ranked = [...candidates]
|
|
.filter((item) => Number.isFinite(item.score))
|
|
.sort((left, right) => {
|
|
if (right.score !== left.score) return right.score - left.score;
|
|
return left.time.localeCompare(right.time);
|
|
});
|
|
const top = ranked[0] ?? null;
|
|
const runnerUp = ranked[1] ?? null;
|
|
const total = ranked.reduce((sum, item) => sum + Math.max(item.score, 0), 0);
|
|
if (!top) {
|
|
return {
|
|
sufficient: false,
|
|
status: "not_separated",
|
|
lead: 0,
|
|
topShare: 0,
|
|
representativeTime: null,
|
|
credibleRange: [],
|
|
ranked,
|
|
tiedForFirst: false,
|
|
};
|
|
}
|
|
if (!runnerUp) {
|
|
return {
|
|
sufficient: true,
|
|
status: "sole_candidate",
|
|
lead: 0,
|
|
topShare: 1,
|
|
representativeTime: top.time,
|
|
credibleRange: [top.time],
|
|
ranked,
|
|
tiedForFirst: false,
|
|
};
|
|
}
|
|
const lead = top.score - runnerUp.score;
|
|
const tiedForFirst = top.score === runnerUp.score;
|
|
const topShare = total > 0 ? Math.max(top.score, 0) / total : 0;
|
|
const status: SeparationStatus = lead < MIN_SEPARATION_LEAD
|
|
? "not_separated"
|
|
: lead >= 20
|
|
? "separated"
|
|
: "weak_lead";
|
|
const peak = top.score;
|
|
const credibleRange = ranked
|
|
.filter((item) => peak - item.score < MIN_SEPARATION_LEAD)
|
|
.map((item) => item.time);
|
|
return {
|
|
sufficient: status !== "not_separated",
|
|
status,
|
|
lead,
|
|
topShare,
|
|
representativeTime: top.time,
|
|
credibleRange: credibleRange.length > 0 ? credibleRange : [top.time],
|
|
ranked,
|
|
tiedForFirst,
|
|
};
|
|
}
|