fix(rectification): ask from the scored probe catalog, not snapshot leftovers
Independent Staging Quality Gate / validate (push) Successful in 9m30s
Independent Staging Quality Gate / publish (push) Has been cancelled

Empty snapshot candidates were starving remaining D24 splits, so the
TypeScript follow-up chain asked the low-gain Python career probe.
Read paths now share one inference+engine catalog and yield a stale
low-gain distinguish card to the current winner.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-28 09:48:57 +08:00
co-authored by Cursor
parent 4bf074b2bf
commit ca6252ecb6
9 changed files with 417 additions and 139 deletions
@@ -21,16 +21,15 @@ import { resolveSessionLanguageModel } from "@/lib/model-catalog";
import { createAdminSupabaseClient } from "@/lib/supabase/admin";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import { defaultMessageOrigin, isRectificationMessageOrigin } from "@/lib/rectification-agentic/v9/message-origin";
import { previousInferenceFromReceipt, askedDiscriminatorKeys } from "@/lib/rectification-agentic/v9/inference-adapter";
import { previousInferenceFromReceipt } from "@/lib/rectification-agentic/v9/inference-adapter";
import { parseAgentChoiceCopy } from "@/lib/rectification-agentic/v9/choice-card";
import {
classifyRectificationTurnIntent,
optionIdForAnswerClass,
} from "@/lib/rectification-agentic/v9/turn-intent-classifier";
import { decideFromDossier, contrastPacketFromDossier } from "@/lib/rectification-agentic/v9/decision-from-dossier";
import { decideFromDossier, rectificationFollowupCatalog } from "@/lib/rectification-agentic/v9/decision-from-dossier";
import { persistServerOwnedFocus, openQuestionFromPersistedFocus } from "@/lib/rectification-agentic/v9/server-focus";
import { buildMethodFollowupPlan } from "@/lib/rectification-agentic/v9/method-followup";
import { refinementFromDecisionReceipt } from "@/lib/rectification-agentic/v9/refinement-packet";
export const runtime = "nodejs";
export const maxDuration = 240;
@@ -383,17 +382,15 @@ export async function POST(request: Request) {
} else {
const decision = decideFromDossier(dossier);
if (decision.nextAction === "ask_candidate_discriminator") {
const refinement = refinementFromDecisionReceipt(dossier.latestResult?.decisionReceipt ?? null);
const catalog = rectificationFollowupCatalog(
dossier.latestResult,
dossier.evidence,
);
const plan = buildMethodFollowupPlan({
evidence: dossier.evidence,
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
sessionOutcome: "discriminate_candidates",
eventProbes: refinement.discriminating_event_probes,
askedProbeKeys: askedDiscriminatorKeys(
dossier.latestResult?.decisionReceipt,
dossier.evidence,
),
contrastPacket: contrastPacketFromDossier(dossier),
...catalog,
candidatesSeparated: false,
});
const persisted = await persistServerOwnedFocus({
@@ -80,8 +80,11 @@ export type EngineContrastProbe = Readonly<{
information_gain?: number;
expected_outcomes?: readonly Readonly<{
answer_class?: string;
outcomeId?: string;
supports?: readonly string[];
supportsCandidateIds?: readonly string[];
conflicts?: readonly string[];
conflictsCandidateIds?: readonly string[];
}>[];
left_time?: string;
right_time?: string;
@@ -312,11 +315,12 @@ export function buildCandidateContrastPacket(input: {
candidateTimes: input.candidateTimes ?? [],
transitions: input.transitions ?? [],
});
const presentKeys = new Set(fromEngine.map((item) => item.semanticKey));
const fromVarga = vargaProbe(
remainingSplits,
input.candidateSetVersion,
input.calculationResultId ?? null,
asked,
new Set([...asked, ...presentKeys]),
);
const probes = [...fromEngine, ...(fromVarga ? [fromVarga] : [])]
.sort((left, right) => right.informationGain - left.informationGain);
@@ -515,15 +519,30 @@ export function conflictProbesFromContrast(
});
}
function inferredContrastChoiceKind(
semanticKey: string,
explicit?: ContrastChoiceKind,
): ContrastChoiceKind {
if (explicit === "varga_style" || explicit === "event_quality" || explicit === "existence") {
return explicit;
}
const layer = semanticKey.match(/^varga\.(d\d+)/)?.[1];
if (layer === "d9" || layer === "d10") return "varga_style";
if (layer === "d24" || layer === "d5") return "event_quality";
return "existence";
}
function probeFromEngine(
probe: EngineContrastProbe,
candidateSetVersion: string,
calculationResultId: string | null,
): CandidateDiscriminatorProbe | null {
const outcomes = (probe.expected_outcomes ?? []).flatMap((row) => {
const outcomeId = typeof row.answer_class === "string" ? row.answer_class : "";
const supports = row.supports ?? [];
const conflicts = row.conflicts ?? [];
const outcomeId = typeof row.answer_class === "string" && row.answer_class.trim()
? row.answer_class
: typeof row.outcomeId === "string" ? row.outcomeId : "";
const supports = row.supports ?? row.supportsCandidateIds ?? [];
const conflicts = row.conflicts ?? row.conflictsCandidateIds ?? [];
if (!outcomeId) return [];
return [{ outcomeId, supportsCandidateIds: supports, conflictsCandidateIds: conflicts }];
});
@@ -553,7 +572,7 @@ function probeFromEngine(
domain: probe.domain ?? null,
year: probe.year ?? null,
semanticKey,
choiceKind: probe.choice_kind,
choiceKind: inferredContrastChoiceKind(semanticKey, probe.choice_kind),
styleOptions: styleOptionsFromEngine(probe.style_options),
};
}
@@ -10,6 +10,7 @@ import {
selectDiscriminatorProbe,
volunteeredDomainsFromEvidence,
type CandidateContrastPacket,
type EngineContrastProbe,
} from "../core/candidate-contrast-packet.ts";
import {
decideRectification,
@@ -84,6 +85,22 @@ export function candidateScoresFromDossier(latest: DecisionDossier["latestResult
return authoritativeCandidateProjection(latest).scores;
}
const CLOCK_TIME = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
export function discriminatorCandidateTimes(
latest: DecisionDossier["latestResult"],
): string[] {
const inference = previousInferenceFromReceipt(latest?.decisionReceipt ?? null);
const fromInference = [...new Set(
(inference?.candidates ?? [])
.filter((item) => item.status !== "eliminated")
.map((item) => item.time)
.filter((time) => CLOCK_TIME.test(time)),
)];
if (fromInference.length >= 2) return fromInference;
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");
@@ -105,15 +122,47 @@ function holdoutStatusFromState(state: InferenceState) {
return "unavailable" as const;
}
export function contrastPacketFromDossier(dossier: DecisionDossier): CandidateContrastPacket {
const windowScan = windowScanFromDecisionReceipt(dossier.latestResult?.decisionReceipt ?? null);
const refinement = refinementFromDecisionReceipt(dossier.latestResult?.decisionReceipt ?? null);
const inference = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
const candidateScores = candidateScoresFromDossier(dossier.latestResult);
export function contrastPacketFromLatestResult(
latest: DecisionDossier["latestResult"],
evidence: DecisionDossier["evidence"] = [],
): CandidateContrastPacket {
const windowScan = windowScanFromDecisionReceipt(latest?.decisionReceipt ?? null);
const refinement = refinementFromDecisionReceipt(latest?.decisionReceipt ?? null);
const inference = previousInferenceFromReceipt(latest?.decisionReceipt ?? null);
const answered = new Set((inference?.answered_probes ?? []).map((item) => item.id));
const fromInference: EngineContrastProbe[] = (inference?.probes ?? []).flatMap((probe) => {
if (answered.has(probe.id) || probe.information_gain <= 0) return [];
if (probe.source === "known_event_quality") return [];
return [{
semantic_key: probe.semantic_key,
candidate_split_hash: probe.candidate_split_hash,
domain: probe.domain,
year: probe.year > 0 ? probe.year : undefined,
user_meaning: probe.question,
information_gain: probe.information_gain,
expected_outcomes: probe.expected_outcomes,
candidate_ids: probe.candidate_ids,
}];
});
const merged = mergeEngineProbes(
fromInference,
refinement.discriminating_event_probes.map((probe) => ({
semantic_key: probe.semantic_key,
candidate_split_hash: probe.candidate_split_hash,
domain: probe.domain,
year: probe.year,
user_meaning: probe.user_meaning,
information_gain: probe.information_gain,
expected_outcomes: probe.expected_outcomes,
candidate_ids: probe.candidate_ids,
choice_kind: probe.choice_kind,
style_options: probe.style_options,
})),
);
return buildCandidateContrastPacket({
candidateSetVersion: inference?.candidate_set_id ?? dossier.latestResult?.resultId ?? "none",
calculationResultId: dossier.latestResult?.resultId ?? null,
engineProbes: refinement.discriminating_event_probes,
candidateSetVersion: inference?.candidate_set_id ?? latest?.resultId ?? "none",
calculationResultId: latest?.resultId ?? null,
engineProbes: merged,
vargaDifferences: [
...(windowScan?.d9_candidates_differ && windowScan.d9_sign_names.length >= 2
? [{ layer: "d9", signs: windowScan.d9_sign_names }]
@@ -122,16 +171,57 @@ export function contrastPacketFromDossier(dossier: DecisionDossier): CandidateCo
? [{ layer: "d10", signs: windowScan.d10_sign_names }]
: []),
],
candidateTimes: candidateScores.map((item) => item.time),
candidateTimes: discriminatorCandidateTimes(latest),
transitions: windowScan?.transitions ?? [],
askedKeys: askedDiscriminatorKeys(
dossier.latestResult?.decisionReceipt,
dossier.evidence,
),
volunteeredDomains: volunteeredDomainsFromEvidence(dossier.evidence),
askedKeys: askedDiscriminatorKeys(latest?.decisionReceipt, evidence),
volunteeredDomains: volunteeredDomainsFromEvidence(evidence),
});
}
function mergeEngineProbes(
...groups: ReadonlyArray<readonly EngineContrastProbe[] | undefined>
): EngineContrastProbe[] {
const byKey = new Map<string, EngineContrastProbe>();
for (const group of groups) {
for (const probe of group ?? []) {
const key = probe.semantic_key?.trim() ?? "";
if (!key) continue;
const current = byKey.get(key);
if (!current || (probe.information_gain ?? 0) > (current.information_gain ?? 0)) {
byKey.set(key, probe);
}
}
}
return [...byKey.values()];
}
export function contrastPacketFromDossier(dossier: DecisionDossier): CandidateContrastPacket {
return contrastPacketFromLatestResult(dossier.latestResult, dossier.evidence);
}
export function rectificationFollowupCatalog(
latest: DecisionDossier["latestResult"],
evidence: DecisionDossier["evidence"] = [],
) {
const receipt = latest?.decisionReceipt ?? null;
const refinement = refinementFromDecisionReceipt(receipt);
const inference = previousInferenceFromReceipt(receipt);
return {
contrastPacket: contrastPacketFromLatestResult(latest, evidence),
topCandidateTimes: discriminatorCandidateTimes(latest),
askedProbeKeys: askedDiscriminatorKeys(receipt, evidence),
eventProbes: refinement.discriminating_event_probes,
eventClarificationProbes: refinement.event_clarification_probes,
evidenceCollectionProbes: refinement.evidence_collection_probes,
precisionStage: refinement.precision_stage?.current ?? null,
nakshatraBoundary: refinement.nakshatra_boundary,
oosBlindPrompts: refinement.oos_blind_prompts,
holdoutEvents: (inference?.events ?? [])
.filter((item) => item.usage === "holdout")
.map((item) => ({ domain: item.domain, year: item.year })),
};
}
function contrastPacketFromState(state: InferenceState): CandidateContrastPacket {
const answered = new Set(state.answered_probes.map((item) => item.probe_id));
return buildCandidateContrastPacket({
@@ -6,15 +6,13 @@
* Card identity is the persisted focus UUID plus the inference revision.
*/
import { askedKeysFromLedgerEvidence } from "../core/candidate-contrast-packet.ts";
import { askedProbeKeysFromReceipt, previousInferenceFromReceipt } from "./inference-adapter";
import { previousInferenceFromReceipt } from "./inference-adapter";
import {
contrastPacketFromDossier,
decideFromDossier,
rectificationFollowupCatalog,
} from "./decision-from-dossier";
import { evidenceLedgerFingerprint } from "./tool-service";
import { projectRectificationChoiceCard } from "./method-followup";
import { refinementFromDecisionReceipt } from "./refinement-packet";
import {
internalObservationsFromWindowScan,
windowScanFromDecisionReceipt,
@@ -62,11 +60,11 @@ export function choiceCardFromCaseDossier(dossier: {
};
turns?: readonly Readonly<{ role: string; text: string | null }>[];
}): RectificationChoiceCard | null {
const windowScan = windowScanFromDecisionReceipt(dossier.latestResult?.decisionReceipt ?? null);
const observations = internalObservationsFromWindowScan(windowScan);
const refinement = refinementFromDecisionReceipt(dossier.latestResult?.decisionReceipt ?? null);
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
const observations = internalObservationsFromWindowScan(
windowScanFromDecisionReceipt(dossier.latestResult?.decisionReceipt ?? null),
);
const inference = previousInferenceFromReceipt(dossier.latestResult?.decisionReceipt ?? null);
const contrastPacket = contrastPacketFromDossier(dossier);
const decision = decideFromDossier(dossier, {
currentEvidenceFingerprint: evidenceLedgerFingerprint(dossier.evidence as never),
});
@@ -74,35 +72,23 @@ export function choiceCardFromCaseDossier(dossier: {
.reverse()
.find((turn) => turn.role === "assistant")
?.text ?? null;
const holdoutEvents = (inference?.events ?? [])
.filter((item) => item.usage === "holdout")
.map((item) => ({ domain: item.domain, year: item.year }));
return projectRectificationChoiceCard({
evidence: dossier.evidence,
activeFocus: dossier.conversationSummary.activeFocus,
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
observations,
sessionOutcome: decision.sessionOutcome,
...catalog,
precisionStage: decision.precisionStage === "collect_events"
? "collect_events"
: decision.precisionStage === "ready_to_adopt"
? "ready_to_adopt"
: refinement.precision_stage?.current,
nakshatraBoundary: refinement.nakshatra_boundary,
oosBlindPrompts: refinement.oos_blind_prompts,
eventProbes: refinement.discriminating_event_probes,
eventClarificationProbes: refinement.event_clarification_probes,
evidenceCollectionProbes: refinement.evidence_collection_probes,
askedProbeKeys: [
...askedProbeKeysFromReceipt(dossier.latestResult?.decisionReceipt),
...askedKeysFromLedgerEvidence(dossier.evidence),
],
: catalog.precisionStage,
accepted: Boolean(dossier.case.acceptedTime),
selectionAllowed: decision.selectionAllowed,
proposeAllowed: decision.proposeAllowed,
confirmationAllowed: decision.canConfirmExactMinute,
caseRevision: inference?.revision ?? 0,
contrastPacket,
candidateScores: decision.separation.ranked.map((item) => ({
time: item.time,
score: item.score,
@@ -111,7 +97,6 @@ export function choiceCardFromCaseDossier(dossier: {
latestAssistantText,
candidatesSeparated: decision.separation.sufficient,
holdoutValidation: decision.holdoutValidation,
holdoutEvents,
});
}
@@ -36,6 +36,8 @@
* Coverage complete never means adopt. Horary does not block cards.
* A/B/C/D choice frames attach only when candidates already diverge
* (event probes, precision stage, varga observation, nakshatra, or holdout).
* An already-open distinguish card yields if the live catalog winner is a
* different probe. Do not keep a low-gain Python event card over D24.
*/
import {
@@ -499,9 +501,19 @@ function followupOwnedProbe(
};
}
function persistedFocusProbeKey(focus: MethodFollowupFocus | null | undefined): string {
const key = focus?.expectedAnswerSchema?.semantic_key;
return typeof key === "string" && key.trim() ? key.trim() : "";
}
function rankedDiscriminatorKey(row: RankedDiscriminator | null): string {
if (!row) return "";
return row.eventProbe?.semantic_key ?? row.contrastProbe?.semanticKey ?? "";
}
function rankRenderableDiscriminators(input: {
eventProbes: readonly DiscriminatingEventProbe[];
contrastProbe: CandidateDiscriminatorProbe | null;
contrastProbes: readonly CandidateDiscriminatorProbe[];
askedKeys: ReadonlySet<string>;
topCandidateTimes?: readonly string[];
}): RankedDiscriminator[] {
@@ -520,7 +532,9 @@ function rankRenderableDiscriminators(input: {
for (const probe of input.eventProbes) {
push(renderableEventProbe(probe, input.askedKeys, top));
}
push(input.contrastProbe ? renderableContrastProbe(input.contrastProbe, input.askedKeys, top) : null);
for (const probe of input.contrastProbes) {
push(renderableContrastProbe(probe, input.askedKeys, top));
}
return rows.sort((left, right) => right.score - left.score || (right.eventProbe?.information_gain ?? right.contrastProbe?.informationGain ?? 0) - (left.eventProbe?.information_gain ?? left.contrastProbe?.informationGain ?? 0));
}
@@ -811,6 +825,7 @@ export function buildMethodFollowupPlan(input: {
accepted?: boolean;
candidatesSeparated?: boolean;
contrastPacket?: CandidateContrastPacket | null;
topCandidateTimes?: readonly string[];
holdoutValidation?: HoldoutValidationStatus;
holdoutEvents?: readonly Readonly<{ domain: string; year: number | null }>[];
}): MethodFollowupPlan {
@@ -879,7 +894,7 @@ export function buildMethodFollowupPlan(input: {
const sessionOutcome = input.sessionOutcome ?? "collect_evidence";
const candidatesSeparated = input.candidatesSeparated === true;
const contrastProbe = selectDiscriminatorProbe(input.contrastPacket ?? null);
const contrastProbes = candidatesSeparated ? [] : [...(input.contrastPacket?.probes ?? [])];
// Legacy known-event quality cards were never backed by an inference probe.
// Ignore them so existing cases resume evidence collection instead of exposing a stale card.
const focus = input.activeFocus?.intent === "clarify_event" ? null : input.activeFocus ?? null;
@@ -887,6 +902,20 @@ export function buildMethodFollowupPlan(input: {
focus && (focus.intent === "reverse_verify" || focus.intent === "out_of_sample_check"),
);
const coverageComplete = blockingMethodsCovered(methods);
const askedKeys = new Set([
...(input.askedProbeKeys ?? []),
...askedKeysFromLedgerEvidence(input.evidence),
]);
const rankedDiscriminators = dashaCovered && meetsAcceptanceEventQuality(input.evidence)
? rankRenderableDiscriminators({
eventProbes: remainingConflictProbes(input.eventProbes, input.evidence, declined, askedKeys),
contrastProbes,
askedKeys,
topCandidateTimes: input.topCandidateTimes,
})
: [];
const bestDiscriminator = rankedDiscriminators[0] ?? null;
const catalogWinnerKey = rankedDiscriminatorKey(bestDiscriminator);
const staleCollectFocus = Boolean(
focus
&& focus.intent === "collect_method_evidence"
@@ -898,9 +927,17 @@ export function buildMethodFollowupPlan(input: {
|| (focus.targetDomain === "horary" && horaryStatus !== "uncovered")
),
);
const staleDiscriminatorFocus = Boolean(
focus
&& focus.intent === "distinguish_candidates"
&& catalogWinnerKey
&& persistedFocusProbeKey(focus)
&& persistedFocusProbeKey(focus) !== catalogWinnerKey
);
if (
focus
&& !staleCollectFocus
&& !staleDiscriminatorFocus
&& (sessionOutcome !== "adopt_representative"
&& sessionOutcome !== "validated_range"
&& sessionOutcome !== "exact_minute_confirmed"
@@ -1018,18 +1055,6 @@ export function buildMethodFollowupPlan(input: {
let next: MethodFollowup | null = null;
const stage = input.precisionStage ?? null;
const askedKeys = new Set([
...(input.askedProbeKeys ?? []),
...askedKeysFromLedgerEvidence(input.evidence),
]);
const rankedDiscriminators = dashaCovered && meetsAcceptanceEventQuality(input.evidence)
? rankRenderableDiscriminators({
eventProbes: remainingConflictProbes(input.eventProbes, input.evidence, declined, askedKeys),
contrastProbe: !candidatesSeparated ? contrastProbe : null,
askedKeys,
})
: [];
const bestDiscriminator = rankedDiscriminators[0] ?? null;
const followupFromRanked = (ranked: RankedDiscriminator): MethodFollowup => {
if (ranked.kind === "event" && ranked.eventProbe) {
const conflictProbe = ranked.eventProbe;
+17 -71
View File
@@ -87,6 +87,10 @@ import {
resolveEvidenceQuote,
} from "@/lib/rectification-agentic/v9/evidence-quote";
import { projectTurnDecision } from "@/lib/rectification-agentic/v9/turn-decision";
import {
contrastPacketFromLatestResult,
rectificationFollowupCatalog,
} from "@/lib/rectification-agentic/v9/decision-from-dossier";
import { QUESTION_CONTRACT_VERSION } from "@/lib/rectification-agentic/v9/probe-question-contract";
import {
posteriorMap,
@@ -174,43 +178,17 @@ function holdoutStatusFromLatest(latest: NonNullable<DossierForTools["latestResu
const hasHoldout = inference.events.some((item) => item.usage === "holdout");
if (!hasHoldout) return "unavailable";
if (inference.holdout_passed === true) return "passed";
if (inference.holdout_passed === false || inference.result_status === "validation_failed") return "failed";
if (inference.holdout_passed === false || inference.result_status === "validation_failed") {
return "failed";
}
return "not_started";
}
function holdoutEventsFromLatest(latest: NonNullable<DossierForTools["latestResult"]> | null | undefined) {
const inference = previousInferenceFromReceipt(latest?.decisionReceipt ?? null);
return (inference?.events ?? [])
.filter((item) => item.usage === "holdout")
.map((item) => ({ domain: item.domain, year: item.year }));
}
function contrastPacketFromLatest(
latest: NonNullable<DossierForTools["latestResult"]> | null | undefined,
evidence: DossierForTools["evidence"] = [],
) {
const inference = previousInferenceFromReceipt(latest?.decisionReceipt ?? null);
const windowScan = windowScanFromDecisionReceipt(latest?.decisionReceipt ?? null);
const refinement = refinementFromDecisionReceipt(latest?.decisionReceipt ?? null);
const candidateTimes = candidateScoresFromLatest(latest).map((item) => item.time);
const vargaDifferences = [
...(windowScan?.d9_candidates_differ && windowScan.d9_sign_names.length >= 2
? [{ layer: "d9", signs: windowScan.d9_sign_names }]
: []),
...(windowScan?.d10_candidates_differ && windowScan.d10_sign_names.length >= 2
? [{ layer: "d10", signs: windowScan.d10_sign_names }]
: []),
];
return buildCandidateContrastPacket({
candidateSetVersion: inference?.candidate_set_id ?? latest?.resultId ?? "none",
calculationResultId: latest?.resultId ?? null,
engineProbes: refinement.discriminating_event_probes,
vargaDifferences,
candidateTimes,
transitions: windowScan?.transitions ?? [],
askedKeys: askedDiscriminatorKeys(latest?.decisionReceipt, evidence),
volunteeredDomains: volunteeredDomainsFromEvidence(evidence),
});
return contrastPacketFromLatestResult(latest, evidence);
}
function snapshotSourceFromDossier(
@@ -251,9 +229,9 @@ function safeCaseProjection(
const latest = dossier.latestResult;
const windowScan = windowScanFromDecisionReceipt(latest?.decisionReceipt ?? null);
const observations = internalObservationsFromWindowScan(windowScan);
const refinement = refinementFromDecisionReceipt(latest?.decisionReceipt ?? null);
const catalog = rectificationFollowupCatalog(latest, dossier.evidence);
const contrastPacket = catalog.contrastPacket;
const accepted = Boolean(caseRow.acceptedTime);
const contrastPacket = contrastPacketFromLatest(latest, dossier.evidence);
const candidateScores = candidateScoresFromLatest(latest);
const holdoutValidation = holdoutStatusFromLatest(latest);
const separation = evaluateCandidateSeparation(candidateScores);
@@ -268,19 +246,11 @@ function safeCaseProjection(
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
observations,
sessionOutcome: "collect_evidence",
precisionStage: refinement.precision_stage?.current,
nakshatraBoundary: refinement.nakshatra_boundary,
oosBlindPrompts: refinement.oos_blind_prompts,
eventProbes: refinement.discriminating_event_probes,
eventClarificationProbes: refinement.event_clarification_probes,
evidenceCollectionProbes: refinement.evidence_collection_probes,
askedProbeKeys: askedDiscriminatorKeys(latest?.decisionReceipt, dossier.evidence),
...catalog,
birthDate: String(compute.baselineBirthSnapshot.birth_date ?? "") || null,
accepted,
candidatesSeparated: separation.sufficient,
contrastPacket,
holdoutValidation,
holdoutEvents: holdoutEventsFromLatest(latest),
});
const userStopped = dossier.case.status === "paused";
const confirmationGate = buildConfirmationGate({
@@ -324,17 +294,10 @@ function safeCaseProjection(
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
observations,
sessionOutcome,
precisionStage: refinement.precision_stage?.current,
nakshatraBoundary: refinement.nakshatra_boundary,
oosBlindPrompts: refinement.oos_blind_prompts,
eventProbes: refinement.discriminating_event_probes,
eventClarificationProbes: refinement.event_clarification_probes,
evidenceCollectionProbes: refinement.evidence_collection_probes,
askedProbeKeys: askedDiscriminatorKeys(latest?.decisionReceipt, dossier.evidence),
...catalog,
birthDate: String(compute.baselineBirthSnapshot.birth_date ?? "") || null,
accepted,
candidatesSeparated: separation.sufficient,
contrastPacket,
holdoutValidation,
});
const birthContext = safeBirthContext(compute);
@@ -630,8 +593,7 @@ function collectingFollowupForParsed(
) {
const windowScan = windowScanFromDecisionReceipt(latest.decisionReceipt ?? null);
const observations = internalObservationsFromWindowScan(windowScan);
const refinement = refinementFromDecisionReceipt(latest.decisionReceipt ?? null);
const contrastPacket = contrastPacketFromLatest(latest, parsed.evidence);
const catalog = rectificationFollowupCatalog(latest, parsed.evidence);
const separation = evaluateCandidateSeparation(candidateScoresFromLatest(latest));
return buildMethodFollowupPlan({
evidence: parsed.evidence,
@@ -639,18 +601,10 @@ function collectingFollowupForParsed(
declinedTopics: parsed.conversationSummary.declinedSkippedTopics,
observations,
sessionOutcome: "collect_evidence",
precisionStage: refinement.precision_stage?.current,
nakshatraBoundary: refinement.nakshatra_boundary,
oosBlindPrompts: refinement.oos_blind_prompts,
eventProbes: refinement.discriminating_event_probes,
eventClarificationProbes: refinement.event_clarification_probes,
evidenceCollectionProbes: refinement.evidence_collection_probes,
askedProbeKeys: askedDiscriminatorKeys(latest.decisionReceipt, parsed.evidence),
...catalog,
accepted: Boolean(parsed.case.acceptedTime),
candidatesSeparated: separation.sufficient,
contrastPacket,
holdoutValidation: holdoutStatusFromLatest(latest),
holdoutEvents: holdoutEventsFromLatest(latest),
});
}
@@ -660,7 +614,8 @@ function sessionAwareFollowupForParsed(
options?: { birthDate?: string | null; snapshotCurrent?: boolean },
) {
const collectingPlan = collectingFollowupForParsed(parsed, latest);
const contrastPacket = contrastPacketFromLatest(latest, parsed.evidence);
const catalog = rectificationFollowupCatalog(latest, parsed.evidence);
const contrastPacket = catalog.contrastPacket;
const candidateScores = candidateScoresFromLatest(latest);
const holdoutValidation = holdoutStatusFromLatest(latest);
const sessionOutcome = conversationalSessionOutcome({
@@ -690,7 +645,6 @@ function sessionAwareFollowupForParsed(
}
const windowScan = windowScanFromDecisionReceipt(latest.decisionReceipt ?? null);
const observations = internalObservationsFromWindowScan(windowScan);
const refinement = refinementFromDecisionReceipt(latest.decisionReceipt ?? null);
const separation = evaluateCandidateSeparation(candidateScores);
return {
plan: buildMethodFollowupPlan({
@@ -699,19 +653,11 @@ function sessionAwareFollowupForParsed(
declinedTopics: parsed.conversationSummary.declinedSkippedTopics,
observations,
sessionOutcome,
precisionStage: refinement.precision_stage?.current,
nakshatraBoundary: refinement.nakshatra_boundary,
oosBlindPrompts: refinement.oos_blind_prompts,
eventProbes: refinement.discriminating_event_probes,
eventClarificationProbes: refinement.event_clarification_probes,
evidenceCollectionProbes: refinement.evidence_collection_probes,
askedProbeKeys: askedDiscriminatorKeys(latest.decisionReceipt, parsed.evidence),
...catalog,
birthDate: options?.birthDate ?? null,
accepted: Boolean(parsed.case.acceptedTime),
candidatesSeparated: separation.sufficient,
contrastPacket,
holdoutValidation,
holdoutEvents: holdoutEventsFromLatest(latest),
}),
contrastPacket,
sessionOutcome,
@@ -7,8 +7,9 @@ import {
publicDecisionFields,
} from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { decideNextAction } from "../src/lib/rectification-agentic/core/decide-next-action.ts";
import { selectDiscriminatorProbe } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
import { contrastPacketFromDossier, overlayPublicDecision } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
import { conversationalSessionOutcome } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { buildMethodFollowupPlan, conversationalSessionOutcome } from "../src/lib/rectification-agentic/v9/method-followup.ts";
const SEPARATED = [
{ time: "04:48", score: 58 },
@@ -145,6 +146,158 @@ test("recorded education evidence does not suppress an unasked D24 discriminator
assert.equal(packet.probes[0]?.expectedOutcomes.at(-1)?.outcomeId, "unsure");
});
test("scored inference catalog outranks a low-gain Python career probe when snapshot candidates are empty", () => {
const careerOutcomes = [
{ answer_class: "yes", supports: ["04:45", "05:00", "05:14"], conflicts: ["05:15"] },
{ answer_class: "weak_yes", supports: ["04:45", "05:00", "05:14"], conflicts: ["05:15"] },
{ answer_class: "no", supports: ["05:15"], conflicts: ["04:45", "05:00", "05:14"] },
{ answer_class: "unsure", supports: [], conflicts: [] },
];
const d24Outcomes = [
{ answer_class: "yes", supports: ["04:47"], conflicts: ["04:51", "04:53", "04:59", "05:00", "05:07", "05:12", "05:14", "05:15"] },
{ answer_class: "weak_yes", supports: ["04:51", "04:53"], conflicts: ["04:47", "04:59", "05:00", "05:07", "05:12", "05:14", "05:15"] },
{ answer_class: "no", supports: ["04:59"], conflicts: ["04:47", "04:51", "04:53", "05:00", "05:07", "05:12", "05:14", "05:15"] },
{ answer_class: "unsure", supports: [], conflicts: [] },
];
const inferenceCandidates = [
{ id: "05:00", time: "05:00", cluster_range: ["05:00", "05:00"], prior_score: 22, posterior_score: 22, probability: 0.51, status: "active", rank: 1, strong_conflict_count: 0 },
{ id: "05:07", time: "05:07", cluster_range: ["05:07", "05:07"], prior_score: 17, posterior_score: 17, probability: 0.15, status: "active", rank: 2, strong_conflict_count: 0 },
{ id: "05:12", time: "05:12", cluster_range: ["05:12", "05:14"], prior_score: 17, posterior_score: 17, probability: 0.15, status: "equivalent", rank: 3, strong_conflict_count: 0 },
{ id: "05:14", time: "05:14", cluster_range: ["05:12", "05:14"], prior_score: 17, posterior_score: 17, probability: 0.15, status: "equivalent", rank: 4, strong_conflict_count: 0 },
{ id: "04:47", time: "04:47", cluster_range: ["04:47", "04:47"], prior_score: 6, posterior_score: 6, probability: 0.01, status: "active", rank: 5, strong_conflict_count: 0 },
{ id: "04:51", time: "04:51", cluster_range: ["04:51", "04:51"], prior_score: 6, posterior_score: 6, probability: 0.01, status: "active", rank: 6, strong_conflict_count: 0 },
{ id: "04:53", time: "04:53", cluster_range: ["04:53", "04:53"], prior_score: 6, posterior_score: 6, probability: 0.01, status: "active", rank: 7, strong_conflict_count: 0 },
{ id: "04:59", time: "04:59", cluster_range: ["04:59", "04:59"], prior_score: 6, posterior_score: 6, probability: 0.01, status: "active", rank: 8, strong_conflict_count: 0 },
{ id: "05:15", time: "05:15", cluster_range: ["05:15", "05:15"], prior_score: 3, posterior_score: 3, probability: 0.004, status: "active", rank: 9, strong_conflict_count: 0 },
];
const evidence = [
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null, eventKind: "education_start" },
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-08-08", occurredTo: null, eventKind: "relationship_end" },
{ status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2020-04-01", occurredTo: null, eventKind: "career_entry" },
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null, eventKind: "family_event" },
];
const dossier = {
evidence,
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
latestResult: {
resultId: "result-empty-snapshot",
candidates: [],
decisionReceipt: {
discriminating_event_probes: [{
role: "distinguish",
phase: "candidate_discriminator",
year: 2023,
year_label: "2023 年前后",
domain: "career",
event_family: "入职、升职或职责明显加重",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: false,
unique_minute_claim: false,
user_meaning: "时间范围锁定 2023 年前后;领域锁定 career。",
choice_kind: "existence",
information_gain: 0.56,
semantic_key: "career.2023.dasha_activation",
candidate_split_hash: "500ce694938305201fbab9ba",
candidate_ids: ["04:45", "05:00", "05:14", "05:15"],
expected_outcomes: careerOutcomes,
style_options: [
{ label: "明确发生且时间吻合", answer_class: "yes" },
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
{ label: "明确没有发生", answer_class: "no" },
{ label: "这段记不清楚", answer_class: "unsure" },
],
}],
inference_state: {
algorithm_version: "rectification-inference-v1",
candidate_set_id: "04:45-05:15:04:47,04:51,04:53,04:59,05:00,05:07,05:12,05:14,05:15",
revision: 1,
phase: "discrimination",
result_status: "discriminating",
range_start: "04:45",
range_end: "05:15",
candidates: inferenceCandidates,
events: [],
probes: [{
id: "probe:career.2023.dasha_activation:500ce694938305201fbab9ba",
year: 2023,
domain: "career",
source: "dasha_activation",
question: "时间范围锁定 2023 年前后;领域锁定 career。",
semantic_key: "career.2023.dasha_activation",
candidate_ids: ["04:45", "05:00", "05:14", "05:15"],
information_gain: 0.56,
expected_outcomes: careerOutcomes,
candidate_split_hash: "500ce694938305201fbab9ba",
}, {
id: "contrast:varga.d24.04:47/04:51|04:53/04:59/05:00/05:07|05:12/05:14|05:15",
year: 0,
domain: "education",
source: "varga_contrast",
question: "引擎给出的区分机会绑定 D24。",
semantic_key: "varga.d24.04:47/04:51|04:53/04:59/05:00/05:07|05:12/05:14|05:15",
candidate_ids: ["04:47", "04:51", "04:53", "04:59", "05:00", "05:07", "05:12", "05:14", "05:15"],
information_gain: 2.503258334775646,
expected_outcomes: d24Outcomes,
candidate_split_hash: "04:45-05:15:varga.d24",
}],
answered_probes: [],
rounds: [],
entropy: 2.0,
representative_time: "05:00",
credible_range: ["05:00", "05:14"],
},
window_scan: {
scanned: true,
d24_lagna_count: 6,
d24_candidates_differ: true,
transitions: [
{ layer: "d24", at: "04:48", from_sign: "白羊座", to_sign: "金牛座" },
{ layer: "d24", at: "04:54", from_sign: "金牛座", to_sign: "双子座" },
{ layer: "d24", at: "05:00", from_sign: "双子座", to_sign: "巨蟹座" },
{ layer: "d24", at: "05:06", from_sign: "巨蟹座", to_sign: "狮子座" },
{ layer: "d24", at: "05:13", from_sign: "狮子座", to_sign: "处女座" },
],
},
},
},
case: { acceptedTime: null },
};
const packet = contrastPacketFromDossier(dossier);
const selected = selectDiscriminatorProbe(packet);
assert.match(selected?.semanticKey ?? "", /^varga\.d24\./);
assert.ok((selected?.informationGain ?? 0) > 2);
assert.doesNotMatch(selected?.semanticKey ?? "", /career\.2023/);
const plan = buildMethodFollowupPlan({
evidence,
eventProbes: packet.probes.flatMap((probe) => probe.semanticKey.startsWith("career.")
? [{
year: 2023,
year_label: "2023 年前后",
domain: "career",
event_family: "入职、升职或职责明显加重",
source: "dasha_activation",
tracks: ["vimshottari", "narayana"],
tracks_agree: false,
unique_minute_claim: false,
user_meaning: "时间范围锁定 2023 年前后。",
role: "distinguish",
phase: "candidate_discriminator",
information_gain: 0.56,
semantic_key: "career.2023.dasha_activation",
candidate_split_hash: "500ce694938305201fbab9ba",
candidate_ids: ["04:45", "05:00", "05:14", "05:15"],
expected_outcomes: careerOutcomes,
}]
: []),
contrastPacket: packet,
candidatesSeparated: false,
});
assert.match(plan.next_followup?.semantic_key ?? "", /^varga\.d24\./);
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /career\.2023/);
});
test("public candidate cards follow the inference ranking and hide an inconsistent state", () => {
const decision = decideRectification({
methodCoverageAll: true,
@@ -1825,6 +1825,52 @@ test("low-gain career event probe does not outrank a renderable high-gain D24 co
assert.ok((plan.next_followup?.selection_score ?? 0) > 0.56);
});
test("already-open low-gain career card yields to the high-gain D24 catalog winner", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
eventProbes: [{
...CAREER_CONFLICT_PROBE,
year: 2023,
year_label: "2023 年前后",
semantic_key: "career.2023.dasha_activation",
information_gain: 0.56,
candidate_split_hash: "set-test:career:2023",
}],
contrastPacket: {
candidateSetVersion: "05:00-05:14",
vargaDifferences: [],
probes: [{
probeId: "contrast:varga.d24.05:00/05:07|05:10|05:14",
candidateSetVersion: "05:00-05:14",
question: "当前几个候选在学业盘上还分得开。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:07", "05:10", "05:14"] },
{ outcomeId: "no", supportsCandidateIds: ["05:07", "05:10", "05:14"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d24.05:00/05:07|05:10|05:14",
informationGain: 2.503258,
sourceFeatures: [{ technique: "D24", calculationResultId: RESULT_ID }],
domain: "education",
year: null,
semanticKey: "varga.d24.05:00/05:07|05:10|05:14",
choiceKind: "event_quality",
}],
},
candidatesSeparated: false,
activeFocus: {
intent: "distinguish_candidates",
targetDomain: "career",
targetKind: "career_entry",
expectedAnswerSchema: {
semantic_key: "career.2023.dasha_activation",
candidate_split_hash: "set-test:career:2023",
},
},
});
assert.equal(plan.next_followup?.semantic_key, "varga.d24.05:00/05:07|05:10|05:14");
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /career\.2023/);
});
const DUMP_COVERAGE = [
{
status: "confirmed" as const,