Files
Jyotisha/frontend/src/lib/rectification-agentic/core/candidate-separation.ts
T
Jesse_Chen 3a4396a4b7
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
fix(rectification): adopt evidence-based stop semantics
2026-08-31 12:50:30 +08:00

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