3f1998bdb6
Coverage complete only unlocks discrimination. A 34/33/33 window plus an occupation note must ask a D9/D10 contrast probe instead of offering a stale winner card. Co-authored-by: Cursor <cursoragent@cursor.com>
90 lines
3.2 KiB
TypeScript
90 lines
3.2 KiB
TypeScript
import {
|
|
evaluateCandidateSeparation,
|
|
type CandidateScoreRow,
|
|
type CandidateSeparation,
|
|
} from "./candidate-separation.ts";
|
|
import type { CandidateDiscriminatorProbe } from "./candidate-contrast-packet.ts";
|
|
|
|
export type RectificationNextActionType =
|
|
| "ask_fact_collection"
|
|
| "ask_candidate_discriminator"
|
|
| "ask_holdout_validation"
|
|
| "offer_provisional_range"
|
|
| "ready_to_adopt";
|
|
|
|
export type HoldoutValidationStatus = "not_started" | "passed" | "failed" | "unavailable";
|
|
|
|
export type DecideNextActionInput = Readonly<{
|
|
methodCoverageAll: boolean;
|
|
proposeAllowed: boolean;
|
|
confirmationAllowed?: boolean;
|
|
userStopped?: boolean;
|
|
selectionAllowed?: boolean;
|
|
snapshotCurrent?: boolean;
|
|
candidateScores: readonly CandidateScoreRow[];
|
|
discriminatorProbe?: CandidateDiscriminatorProbe | null;
|
|
holdoutValidation?: HoldoutValidationStatus;
|
|
}>;
|
|
|
|
export type RectificationNextAction = Readonly<{
|
|
type: RectificationNextActionType;
|
|
separation: CandidateSeparation;
|
|
probe: CandidateDiscriminatorProbe | null;
|
|
}>;
|
|
|
|
/**
|
|
* Coverage complete only unlocks discrimination. It never grants adoption.
|
|
*/
|
|
export function decideNextAction(input: DecideNextActionInput): RectificationNextAction {
|
|
const separation = evaluateCandidateSeparation(input.candidateScores);
|
|
const probe = input.discriminatorProbe ?? null;
|
|
const holdout = input.holdoutValidation ?? "unavailable";
|
|
const userStopped = input.userStopped === true;
|
|
|
|
if (input.confirmationAllowed === true) {
|
|
return { type: "ready_to_adopt", separation, probe: null };
|
|
}
|
|
if (userStopped && input.selectionAllowed === true) {
|
|
if (!separation.sufficient) {
|
|
return { type: "offer_provisional_range", separation, probe: null };
|
|
}
|
|
return { type: "ready_to_adopt", separation, probe: null };
|
|
}
|
|
if (!input.methodCoverageAll || input.snapshotCurrent === false) {
|
|
return { type: "ask_fact_collection", separation, probe: null };
|
|
}
|
|
if (!separation.sufficient) {
|
|
if (probe) {
|
|
return { type: "ask_candidate_discriminator", separation, probe };
|
|
}
|
|
return { type: "offer_provisional_range", separation, probe: null };
|
|
}
|
|
if (holdout === "not_started") {
|
|
return { type: "ask_holdout_validation", separation, probe: null };
|
|
}
|
|
if (holdout === "failed") {
|
|
if (probe) {
|
|
return { type: "ask_candidate_discriminator", separation, probe };
|
|
}
|
|
return { type: "offer_provisional_range", separation, probe: null };
|
|
}
|
|
if (input.proposeAllowed !== true) {
|
|
return { type: "ask_fact_collection", separation, probe: null };
|
|
}
|
|
return { type: "ready_to_adopt", separation, probe: null };
|
|
}
|
|
|
|
export function sessionKindFromNextAction(
|
|
type: RectificationNextActionType,
|
|
): "collect_evidence" | "discriminate_candidates" | "validate_holdout" | "provisional_range" | "adopt_representative" {
|
|
if (type === "ask_fact_collection") return "collect_evidence";
|
|
if (type === "ask_candidate_discriminator") return "discriminate_candidates";
|
|
if (type === "ask_holdout_validation") return "validate_holdout";
|
|
if (type === "offer_provisional_range") return "provisional_range";
|
|
return "adopt_representative";
|
|
}
|
|
|
|
export function offerSessionKinds(): readonly string[] {
|
|
return ["adopt_representative", "awaiting_confirmation", "provisional_range"];
|
|
}
|