fix(rectification): require a renderable card before holdout validation

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-29 00:30:04 +08:00
parent e41eaace8d
commit fe0afdaddb
4 changed files with 323 additions and 65 deletions
@@ -17,6 +17,7 @@ import {
import {
decideRectification,
publicDecisionFields,
type HoldoutValidationStatus,
type RectificationDecision,
} from "../core/rectification-decision.ts";
import type { InferenceState } from "../core/types.ts";
@@ -110,25 +111,51 @@ export function discriminatorCandidateTimes(
return candidateScoresFromDossier(latest).map((item) => item.time);
}
function holdoutStatusFromInference(inference: ReturnType<typeof previousInferenceFromReceipt>) {
if (!inference) return "unavailable" as const;
const hasHoldout = inference.events.some((item) => item.usage === "holdout");
if (!hasHoldout) return "unavailable" as const;
if (inference.holdout_passed === true) return "passed" as const;
if (inference.holdout_passed === false || inference.result_status === "validation_failed") {
return "failed" as const;
}
return "not_started" as const;
function canAskHoldout(input: {
events?: readonly Readonly<{ usage: string; year: number | null }>[];
oosBlindPrompts?: readonly unknown[] | null;
}): boolean {
if ((input.oosBlindPrompts?.length ?? 0) > 0) return true;
return Boolean(input.events?.some((item) => item.usage === "holdout" && item.year !== null));
}
function holdoutStatusFromState(state: InferenceState) {
const hasHoldout = state.events.some((item) => item.usage === "holdout");
if (state.holdout_passed === true) return "passed" as const;
if (state.holdout_passed === false || state.result_status === "validation_failed") {
return "failed" as const;
function holdoutValidationStatus(input: {
events?: readonly Readonly<{ usage: string; year: number | null }>[];
holdoutPassed?: boolean | null;
resultStatus?: string | null;
oosBlindPrompts?: readonly unknown[] | null;
}): HoldoutValidationStatus {
if (input.holdoutPassed === true) return "passed";
if (input.holdoutPassed === false || input.resultStatus === "validation_failed") {
return "failed";
}
if (hasHoldout) return "not_started" as const;
return "unavailable" as const;
if (canAskHoldout(input)) return "not_started";
return "unavailable";
}
function holdoutStatusFromInference(
inference: ReturnType<typeof previousInferenceFromReceipt>,
oosBlindPrompts?: readonly unknown[] | null,
) {
if (!inference) return "unavailable" as const;
return holdoutValidationStatus({
events: inference.events,
holdoutPassed: inference.holdout_passed,
resultStatus: inference.result_status,
oosBlindPrompts,
});
}
function holdoutStatusFromState(
state: InferenceState,
oosBlindPrompts?: readonly unknown[] | null,
) {
return holdoutValidationStatus({
events: state.events,
holdoutPassed: state.holdout_passed,
resultStatus: state.result_status,
oosBlindPrompts,
});
}
export function contrastPacketFromLatestResult(
@@ -294,6 +321,9 @@ export function decideFromDossier(
options?: { currentEvidenceFingerprint?: string | null },
): RectificationDecision {
const inference = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
const oosBlindPrompts = refinementFromDecisionReceipt(
dossier.latestResult?.decisionReceipt ?? null,
).oos_blind_prompts;
const trainingGate = trainingScoreableGate(dossier.evidence);
const collecting = buildMethodFollowupPlan({
evidence: dossier.evidence,
@@ -326,7 +356,7 @@ export function decideFromDossier(
snapshotCurrent,
candidateScores: candidateScoresFromDossier(dossier.latestResult),
discriminatorProbe: inspected.selected,
holdoutValidation: holdoutStatusFromInference(inference),
holdoutValidation: holdoutStatusFromInference(inference, oosBlindPrompts),
accepted: Boolean(dossier.case.acceptedTime),
inferenceCredibleRange: inference?.credible_range ?? null,
}),
@@ -366,7 +396,10 @@ export function decideAfterInferenceChange(input: {
.filter((item) => item.status !== "eliminated")
.map((item) => ({ time: item.time, score: item.posterior_score })),
discriminatorProbe: inspected.selected,
holdoutValidation: holdoutStatusFromState(input.state),
holdoutValidation: holdoutStatusFromState(
input.state,
refinementFromDecisionReceipt(input.dossier.latestResult?.decisionReceipt ?? null).oos_blind_prompts,
),
inferenceCredibleRange: input.state.credible_range,
userStopped: input.userStopped,
accepted: Boolean(input.dossier.case.acceptedTime),
@@ -74,6 +74,7 @@ import {
completeStyleOptions,
isRenderableProbe,
rankDiscriminatorScore,
EXISTENCE_STYLE_OPTIONS,
type ProbeStyleOption,
} from "./probe-question-contract.ts";
import type { SessionOutcomeKind } from "./confirmation-gate.ts";
@@ -1032,6 +1033,39 @@ export function buildNextUserAction(input: {
return { id: explain.id, user_meaning: explain.user_meaning, on_user_stop: explain };
}
function holdoutAskFields(
prompt: OosBlindPrompt | null | undefined,
reserved: Readonly<{ domain: string; year: number | null }> | null,
): Omit<MethodFollowup, "must_not_label" | "choice_frame"> | null {
if (prompt) {
return {
method_id: "oos_blind",
intent: "out_of_sample_check",
ask_theme: "holdout",
domain: prompt.domain,
kind_hint: null,
user_prompt_hint: prompt.user_meaning,
source: "oos_blind",
choice_kind: "existence",
style_options: EXISTENCE_STYLE_OPTIONS,
};
}
if (reserved?.year == null) return null;
return {
method_id: "holdout_validation",
intent: "out_of_sample_check",
ask_theme: "holdout",
domain: reserved.domain,
kind_hint: null,
user_prompt_hint: `${reserved.year} 年前后这件事还要单独核对一次,不计入候选分数。`,
source: "oos_blind",
probe_year: reserved.year,
year_label: `${reserved.year} 年前后`,
choice_kind: "existence",
style_options: EXISTENCE_STYLE_OPTIONS,
};
}
export function buildMethodFollowupPlan(input: {
evidence: readonly MethodFollowupEvidence[];
activeFocus?: MethodFollowupFocus | null;
@@ -1230,32 +1264,13 @@ export function buildMethodFollowupPlan(input: {
}
if (sessionOutcome === "validate_holdout") {
const prompt = input.oosBlindPrompts?.[0] ?? null;
const reserved = (input.holdoutEvents ?? []).find((item) => item.year !== null) ?? null;
const holdoutNext = prompt
? makeFollowup({
method_id: "oos_blind",
intent: "out_of_sample_check",
ask_theme: "holdout",
domain: prompt.domain,
kind_hint: null,
user_prompt_hint: prompt.user_meaning,
source: "oos_blind",
}, false, true)
: reserved
? makeFollowup({
method_id: "holdout_validation",
intent: "out_of_sample_check",
ask_theme: "holdout",
domain: reserved.domain,
kind_hint: null,
user_prompt_hint: `${reserved.year} 年前后这件事还要单独核对一次,不计入候选分数。`,
source: "oos_blind",
}, false, true)
: null;
const fields = holdoutAskFields(
input.oosBlindPrompts?.[0],
(input.holdoutEvents ?? []).find((item) => item.year !== null) ?? null,
);
return {
methods,
next_followup: holdoutNext,
next_followup: fields ? makeFollowup(fields, false, true) : null,
deferred_followup: null,
session_outcome: sessionOutcome ?? "collect_evidence",
stop_domain_rotation: true,
@@ -1663,29 +1678,11 @@ export function buildMethodFollowupPlan(input: {
source: "nakshatra_boundary",
});
} else if (input.holdoutValidation === "not_started") {
const prompt = input.oosBlindPrompts?.[0];
const reserved = (input.holdoutEvents ?? []).find((item) => item.year !== null);
if (prompt) {
next = makeFollowup({
method_id: "oos_blind",
intent: "out_of_sample_check",
ask_theme: "holdout",
domain: prompt.domain,
kind_hint: null,
user_prompt_hint: prompt.user_meaning,
source: "oos_blind",
}, false, true);
} else if (reserved) {
next = makeFollowup({
method_id: "holdout_validation",
intent: "out_of_sample_check",
ask_theme: "holdout",
domain: reserved.domain,
kind_hint: null,
user_prompt_hint: `${reserved.year} 年前后这件事还要单独核对一次,不计入候选分数。`,
source: "oos_blind",
}, false, true);
}
const fields = holdoutAskFields(
input.oosBlindPrompts?.[0],
(input.holdoutEvents ?? []).find((item) => item.year !== null) ?? null,
);
if (fields) next = makeFollowup(fields, false, true);
} else if (horaryStatus === "uncovered") {
next = makeFollowup({
method_id: "horary",