fix(rectification): share decideFromDossier across projections and treat a sole candidate as a close, not a tie
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* Candidate separation is not event-fit and not method coverage.
|
||||
* 34/33/33 is a tie. A 1-point engine 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";
|
||||
export type SeparationStatus = "not_separated" | "weak_lead" | "separated" | "sole_candidate";
|
||||
|
||||
export type CandidateScoreRow = Readonly<{
|
||||
id?: string;
|
||||
@@ -35,14 +36,36 @@ export function evaluateCandidateSeparation(
|
||||
const top = ranked[0] ?? null;
|
||||
const runnerUp = ranked[1] ?? null;
|
||||
const total = ranked.reduce((sum, item) => sum + Math.max(item.score, 0), 0);
|
||||
const lead = top && runnerUp ? top.score - runnerUp.score : (top ? MIN_SEPARATION_LEAD : 0);
|
||||
const topShare = top && total > 0 ? Math.max(top.score, 0) / total : 0;
|
||||
const status: SeparationStatus = !top || ranked.length < 2 || lead < MIN_SEPARATION_LEAD
|
||||
if (!top) {
|
||||
return {
|
||||
sufficient: false,
|
||||
status: "not_separated",
|
||||
lead: 0,
|
||||
topShare: 0,
|
||||
representativeTime: null,
|
||||
credibleRange: [],
|
||||
ranked,
|
||||
};
|
||||
}
|
||||
if (!runnerUp) {
|
||||
return {
|
||||
sufficient: true,
|
||||
status: "sole_candidate",
|
||||
lead: 0,
|
||||
topShare: 1,
|
||||
representativeTime: top.time,
|
||||
credibleRange: [top.time],
|
||||
ranked,
|
||||
};
|
||||
}
|
||||
const lead = 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 ?? 0;
|
||||
const peak = top.score;
|
||||
const credibleRange = ranked
|
||||
.filter((item) => peak - item.score < MIN_SEPARATION_LEAD)
|
||||
.map((item) => item.time);
|
||||
@@ -51,8 +74,8 @@ export function evaluateCandidateSeparation(
|
||||
status,
|
||||
lead,
|
||||
topShare,
|
||||
representativeTime: top?.time ?? null,
|
||||
credibleRange: credibleRange.length > 0 ? credibleRange : (top ? [top.time] : []),
|
||||
representativeTime: top.time,
|
||||
credibleRange: credibleRange.length > 0 ? credibleRange : [top.time],
|
||||
ranked,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -141,6 +141,19 @@ export function decideRectification(input: DecideRectificationInput): Rectificat
|
||||
}
|
||||
return completeWithRange(separation, holdout, range, "exhausted");
|
||||
}
|
||||
if (holdout === "unavailable") {
|
||||
if (userStopped) {
|
||||
return completeWithRange(separation, holdout, range, "user_stopped");
|
||||
}
|
||||
return finish("adopt_representative", {
|
||||
input,
|
||||
separation,
|
||||
holdout,
|
||||
range,
|
||||
probe: null,
|
||||
canConfirmExactMinute: false,
|
||||
});
|
||||
}
|
||||
if (userStopped && holdout !== "passed") {
|
||||
return completeWithRange(separation, holdout, range, "user_stopped");
|
||||
}
|
||||
@@ -273,7 +286,7 @@ function completeWithRange(
|
||||
}
|
||||
|
||||
function finish(
|
||||
sessionOutcome: "adopt_representative" | "awaiting_confirmation" | "validated_range" | "exact_minute_confirmed",
|
||||
fallbackOutcome: "adopt_representative" | "awaiting_confirmation" | "validated_range" | "exact_minute_confirmed",
|
||||
input: {
|
||||
input: DecideRectificationInput;
|
||||
separation: CandidateSeparation;
|
||||
@@ -294,7 +307,7 @@ function finish(
|
||||
? (input.input.accepted ? "exact_minute_confirmed" : "awaiting_confirmation")
|
||||
: input.holdout === "passed"
|
||||
? "validated_range"
|
||||
: sessionOutcome;
|
||||
: fallbackOutcome;
|
||||
return {
|
||||
phase: "completed",
|
||||
nextAction: "ready_to_adopt",
|
||||
|
||||
Reference in New Issue
Block a user