Files
Jyotisha/frontend/src/lib/rectification-agentic/core/decide-next-action.ts
T
Jesse_Chen 3f1998bdb6
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
fix(web): stop adopting tied rectification candidates after method coverage
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>
2026-08-24 12:19:36 +08:00

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"];
}