fix(rectification): share decideFromDossier across projections and treat a sole candidate as a close, not a tie
Independent Staging Quality Gate / validate (push) Successful in 11m43s
Independent Staging Quality Gate / publish (push) Successful in 40m51s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-28 19:21:28 +08:00
co-authored by Cursor
parent 30d8c78d19
commit 69c3e94920
10 changed files with 271 additions and 300 deletions
@@ -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",
@@ -64,7 +64,6 @@ import {
datedDomainsFromEvidence,
isStructuredDiscriminator,
mentionedVargaKeysFromLedgerEvidence,
selectDiscriminatorProbe,
vargaLayerCovered,
vargaLayerFromSemanticKey,
type CandidateContrastPacket,
@@ -1116,7 +1115,7 @@ export function buildMethodFollowupPlan(input: {
coverage("horary", horaryStatus),
];
const sessionOutcome = input.sessionOutcome ?? "collect_evidence";
const sessionOutcome = input.sessionOutcome;
const candidatesSeparated = input.candidatesSeparated === true;
const contrastProbes = candidatesSeparated ? [] : [...(input.contrastPacket?.probes ?? [])];
// Legacy known-event quality cards were never backed by an inference probe.
@@ -1221,7 +1220,7 @@ export function buildMethodFollowupPlan(input: {
methods,
next_followup: keepNext,
deferred_followup: null,
session_outcome: sessionOutcome,
session_outcome: sessionOutcome ?? "collect_evidence",
stop_domain_rotation: true,
do_not_poll: DO_NOT_POLL,
not_in_rotation: NOT_IN_ROTATION,
@@ -1257,7 +1256,7 @@ export function buildMethodFollowupPlan(input: {
methods,
next_followup: holdoutNext,
deferred_followup: null,
session_outcome: sessionOutcome,
session_outcome: sessionOutcome ?? "collect_evidence",
stop_domain_rotation: true,
do_not_poll: DO_NOT_POLL,
not_in_rotation: NOT_IN_ROTATION,
@@ -1285,7 +1284,7 @@ export function buildMethodFollowupPlan(input: {
methods,
next_followup: next,
deferred_followup: null,
session_outcome: sessionOutcome,
session_outcome: sessionOutcome ?? "collect_evidence",
stop_domain_rotation: true,
do_not_poll: DO_NOT_POLL,
not_in_rotation: NOT_IN_ROTATION,
@@ -1714,7 +1713,7 @@ export function buildMethodFollowupPlan(input: {
methods,
next_followup: deferAdoption ? null : next,
deferred_followup: deferAdoption ? next : null,
session_outcome: sessionOutcome,
session_outcome: sessionOutcome ?? "collect_evidence",
stop_domain_rotation: true,
do_not_poll: DO_NOT_POLL,
not_in_rotation: NOT_IN_ROTATION,
@@ -1732,24 +1731,8 @@ export function projectRectificationChoiceCard(
latestAssistantText?: string | null;
},
): RectificationChoiceCard | null {
let plan = buildMethodFollowupPlan(input);
const sessionOutcome = conversationalSessionOutcome({
selectionAllowed: input.selectionAllowed === true,
proposeAllowed: input.proposeAllowed === true,
confirmationAllowed: input.confirmationAllowed === true,
nextFollowup: plan.next_followup,
methods: plan.methods,
userStopped: input.userStopped,
candidateScores: input.candidateScores,
discriminatorProbe: selectDiscriminatorProbe(input.contrastPacket ?? null, {
mentionedKeys: mentionedVargaKeysFromLedgerEvidence(input.evidence),
}) ?? undefined,
holdoutValidation: input.holdoutValidation,
evidence: input.evidence,
});
if (sessionOutcome !== (input.sessionOutcome ?? "collect_evidence")) {
plan = buildMethodFollowupPlan({ ...input, sessionOutcome });
}
const sessionOutcome = input.sessionOutcome;
const plan = buildMethodFollowupPlan(input);
if (
!input.accepted
&& (sessionOutcome === "adopt_representative"
+95 -246
View File
@@ -38,6 +38,7 @@ import {
scorableEvidence,
RectificationToolServiceError,
type V9CaseDossier,
type V9ComputeProjection,
} from "@/lib/rectification-agentic/v9/tool-service";
import {
evidenceDomainSchema,
@@ -63,10 +64,6 @@ import { parseRectificationHouseTable } from "@/lib/rectification-candidate-resu
import {
buildMethodFollowupPlan,
buildNextUserAction,
conversationalSessionOutcome,
decideConversationalSession,
type MethodCoverage,
type MethodFollowup,
} from "@/lib/rectification-agentic/v9/method-followup";
import { refinementFromDecisionReceipt } from "@/lib/rectification-agentic/v9/refinement-packet";
import {
@@ -89,8 +86,12 @@ import {
import { projectTurnDecision } from "@/lib/rectification-agentic/v9/turn-decision";
import {
contrastPacketFromLatestResult,
decideFromDossier,
overlayPublicDecision,
rectificationFollowupCatalog,
} from "@/lib/rectification-agentic/v9/decision-from-dossier";
import type { RectificationDecision } from "@/lib/rectification-agentic/core/rectification-decision";
import { publicDecisionFields } from "@/lib/rectification-agentic/core/rectification-decision";
import { QUESTION_CONTRACT_VERSION } from "@/lib/rectification-agentic/v9/probe-question-contract";
import {
posteriorMap,
@@ -100,11 +101,8 @@ import {
buildCandidateContrastPacket,
conflictProbesFromContrast,
datedDomainsFromEvidence,
mentionedVargaKeysFromLedgerEvidence,
selectDiscriminatorProbe,
volunteeredDomainsFromEvidence,
} from "@/lib/rectification-agentic/core/candidate-contrast-packet";
import { evaluateCandidateSeparation } from "@/lib/rectification-agentic/core/candidate-separation";
import { offerSessionKinds } from "@/lib/rectification-agentic/core/decide-next-action";
import {
candidateSnapshotSource,
@@ -112,7 +110,6 @@ import {
storedSnapshotIsCurrent,
SNAPSHOT_STALE_COPY,
} from "@/lib/rectification-agentic/core/snapshot-source";
import type { HoldoutValidationStatus } from "@/lib/rectification-agentic/core/decide-next-action";
import { buildSkillVerificationReport } from "@/lib/rectification-agentic/v9/skill-verification-report";
import {
internalObservationsFromWindowScan,
@@ -170,22 +167,6 @@ function safeBirthContext(compute: Awaited<ReturnType<typeof loadV9CaseCompute>>
};
}
function candidateScoresFromLatest(latest: NonNullable<DossierForTools["latestResult"]> | null | undefined) {
return authoritativeCandidateProjection(latest).scores;
}
function holdoutStatusFromLatest(latest: NonNullable<DossierForTools["latestResult"]> | null | undefined): HoldoutValidationStatus {
const inference = previousInferenceFromReceipt(latest?.decisionReceipt ?? null);
if (!inference) return "unavailable";
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";
}
return "not_started";
}
function contrastPacketFromLatest(
latest: NonNullable<DossierForTools["latestResult"]> | null | undefined,
evidence: DossierForTools["evidence"] = [],
@@ -223,104 +204,53 @@ function storedSnapshotSource(latest: NonNullable<DossierForTools["latestResult"
});
}
function selectLiveDiscriminator(
packet: Parameters<typeof selectDiscriminatorProbe>[0],
evidence: readonly Readonly<{
status?: string | null;
domain?: string | null;
eventKind?: string | null;
summary?: string | null;
}>[],
) {
return selectDiscriminatorProbe(packet, {
mentionedKeys: mentionedVargaKeysFromLedgerEvidence(evidence),
});
function sessionOutcomeKind(value: unknown): string {
if (typeof value === "string") return value;
if (value && typeof value === "object" && "kind" in value) {
const kind = (value as { kind?: unknown }).kind;
if (typeof kind === "string") return kind;
}
return "";
}
function safeCaseProjection(
dossier: ReturnType<typeof parseDossierForTools>,
compute: Awaited<ReturnType<typeof loadV9CaseCompute>>,
export function safeCaseProjection(
dossier: V9CaseDossier,
compute: V9ComputeProjection,
): Record<string, unknown> {
const caseRow = dossier.case;
const latest = dossier.latestResult;
const parsed = parseDossierForTools(dossier);
const caseRow = parsed.case;
const latest = parsed.latestResult;
const decision = decideFromDossier(dossier, {
currentEvidenceFingerprint: evidenceLedgerFingerprint(dossier.evidence),
});
const windowScan = windowScanFromDecisionReceipt(latest?.decisionReceipt ?? null);
const observations = internalObservationsFromWindowScan(windowScan);
const catalog = rectificationFollowupCatalog(latest, dossier.evidence);
const catalog = rectificationFollowupCatalog(latest, parsed.evidence);
const contrastPacket = catalog.contrastPacket;
const accepted = Boolean(caseRow.acceptedTime);
const candidateScores = candidateScoresFromLatest(latest);
const holdoutValidation = holdoutStatusFromLatest(latest);
const separation = evaluateCandidateSeparation(candidateScores);
const currentSnapshot = snapshotSourceFromDossier(dossier, compute);
const currentSnapshot = snapshotSourceFromDossier(parsed, compute);
const storedSnapshot = latest ? storedSnapshotSource(latest) : null;
const snapshotCurrent = storedSnapshotIsCurrent(storedSnapshot, currentSnapshot);
const collectingPlan = buildMethodFollowupPlan({
evidence: dossier.evidence,
activeFocus: dossier.conversationSummary.activeFocus,
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
const methodFollowupPlan = buildMethodFollowupPlan({
evidence: parsed.evidence,
activeFocus: parsed.conversationSummary.activeFocus,
declinedTopics: parsed.conversationSummary.declinedSkippedTopics,
observations,
sessionOutcome: "collect_evidence",
sessionOutcome: decision.sessionOutcome,
...catalog,
birthDate: String(compute.baselineBirthSnapshot.birth_date ?? "") || null,
accepted,
candidatesSeparated: separation.sufficient,
holdoutValidation,
candidatesSeparated: decision.separation.sufficient,
holdoutValidation: decision.holdoutValidation,
});
const userStopped = dossier.case.status === "paused";
const confirmationGate = buildConfirmationGate({
engineConfirmationAllowed: latest?.confirmationAllowed === true,
candidates: latest?.candidates ?? [],
decisionReceipt: latest?.decisionReceipt ?? null,
});
const decision = decideConversationalSession({
selectionAllowed: false,
proposeAllowed: false,
confirmationAllowed: confirmationGate.confirmation_allowed,
nextFollowup: collectingPlan.next_followup,
methods: collectingPlan.methods,
userStopped,
candidateScores,
discriminatorProbe: selectLiveDiscriminator(contrastPacket, dossier.evidence),
holdoutValidation,
snapshotCurrent,
evidence: dossier.evidence,
accepted,
});
const sessionOutcome = decision.sessionOutcome;
const latestProjection = latest
? latestResultToolProjection(latest, {
proposeAllowed: decision.proposeAllowed,
nextFollowup: collectingPlan.next_followup,
methods: collectingPlan.methods,
userStopped,
candidateScores,
holdoutValidation,
discriminatorProbe: selectLiveDiscriminator(contrastPacket, dossier.evidence),
snapshotCurrent,
evidence: dossier.evidence,
})
: null;
const methodFollowupPlan = sessionOutcome === "collect_evidence"
? { ...collectingPlan, session_outcome: sessionOutcome }
: buildMethodFollowupPlan({
evidence: dossier.evidence,
activeFocus: dossier.conversationSummary.activeFocus,
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
observations,
sessionOutcome,
...catalog,
birthDate: String(compute.baselineBirthSnapshot.birth_date ?? "") || null,
accepted,
candidatesSeparated: separation.sufficient,
holdoutValidation,
});
const latestProjection = latest ? latestResultToolProjection(latest, decision) : null;
const birthContext = safeBirthContext(compute);
const nextUserAction = buildNextUserAction({
scorableCount: dossier.scorable.length,
evidenceCount: dossier.evidence.length,
scorableCount: parsed.scorable.length,
evidenceCount: parsed.evidence.length,
hasLatestResult: Boolean(latestProjection),
selectionAllowed: decision.selectionAllowed,
sessionOutcome,
sessionOutcome: decision.sessionOutcome,
nextFollowup: methodFollowupPlan.next_followup,
workingTime: caseRow.acceptedTime
?? (typeof birthContext.active_birth_time === "string" ? birthContext.active_birth_time : null)
@@ -331,8 +261,7 @@ function safeCaseProjection(
const latestResult = latestProjection
? {
...latestProjection,
session_outcome: sessionOutcomeView(sessionOutcome),
candidate_separation: separation,
candidate_separation: decision.separation,
candidate_contrast_packet: contrastPacket,
candidate_snapshot: {
is_current: snapshotCurrent,
@@ -355,15 +284,15 @@ function safeCaseProjection(
evidence_count: caseRow.evidenceCount,
turn_count: caseRow.turnCount,
evidence_summary: {
confirmed: dossier.confirmedEvidence.length,
pending: dossier.pendingEvidence.length,
by_domain: dossier.domainCounts,
by_kind: dossier.kindCounts,
scorable: dossier.scorable.length,
confirmed: parsed.confirmedEvidence.length,
pending: parsed.pendingEvidence.length,
by_domain: parsed.domainCounts,
by_kind: parsed.kindCounts,
scorable: parsed.scorable.length,
},
evidence_context: safeEvidenceContext(dossier),
conversation_context: safeConversationContext(dossier),
conversation_summary: safeConversationSummary(dossier),
evidence_context: safeEvidenceContext(parsed),
conversation_context: safeConversationContext(parsed),
conversation_summary: safeConversationSummary(parsed),
birth_context: birthContext,
latest_result: latestResult,
method_followup_plan: methodFollowupPlan,
@@ -441,18 +370,7 @@ function safeEvidenceContext(dossier: DossierForTools) {
export function latestResultToolProjection(
latest: NonNullable<DossierForTools["latestResult"]>,
session?: {
proposeAllowed?: boolean;
nextFollowup?: MethodFollowup | null;
methods?: readonly MethodCoverage[];
userStopped?: boolean;
candidateScores?: readonly Readonly<{ time: string; score: number }>[];
holdoutValidation?: HoldoutValidationStatus;
discriminatorProbe?: ReturnType<typeof selectDiscriminatorProbe>;
snapshotCurrent?: boolean;
evidence?: DossierForTools["evidence"];
accepted?: boolean;
},
decision: RectificationDecision,
): Record<string, unknown> {
const width = indistinguishableWidthMinutes(latest.candidates);
const windowScan = windowScanFromDecisionReceipt(latest.decisionReceipt ?? null);
@@ -461,37 +379,14 @@ export function latestResultToolProjection(
candidates: latest.candidates,
decisionReceipt: latest.decisionReceipt ?? null,
});
const candidateScores = session?.candidateScores ?? candidateScoresFromLatest(latest);
const decision = session
? decideConversationalSession({
selectionAllowed: false,
proposeAllowed: false,
confirmationAllowed: confirmationGate.confirmation_allowed,
nextFollowup: session.nextFollowup ?? null,
methods: session.methods,
userStopped: session.userStopped,
candidateScores,
discriminatorProbe: session.discriminatorProbe,
holdoutValidation: session.holdoutValidation,
snapshotCurrent: session.snapshotCurrent,
evidence: session.evidence,
accepted: session.accepted,
})
: null;
const sessionOutcome = decision?.sessionOutcome ?? "collect_evidence";
const houseTable = parseRectificationHouseTable(latest.decisionReceipt?.house_table);
const refinement = refinementFromDecisionReceipt(latest.decisionReceipt ?? null);
const separation = evaluateCandidateSeparation(candidateScores);
const contrastPacket = contrastPacketFromLatest(latest, session?.evidence);
const contrastPacket = contrastPacketFromLatest(latest);
const candidateProjection = authoritativeCandidateProjection(latest);
return {
const base = {
result_id: latest.resultId,
candidates: candidateProjection.fromInference ? candidateProjection.candidates : latest.candidates,
selection_allowed: candidateProjection.consistent && decision ? decision.selectionAllowed : false,
propose_allowed: decision ? decision.proposeAllowed : false,
confirmation_allowed: confirmationGate.confirmation_allowed,
completion_status: decision?.completionStatus ?? null,
validated: decision?.validated ?? false,
representative_time: candidateProjection.fromInference
? candidateProjection.representativeTime
: latest.representativeTime,
@@ -501,8 +396,7 @@ export function latestResultToolProjection(
indistinguishable_width_minutes: width,
window_scan: windowScan,
confirmation_gate: confirmationGate,
session_outcome: sessionOutcomeView(sessionOutcome),
candidate_separation: separation,
candidate_separation: decision.separation,
candidate_contrast_packet: contrastPacket,
event_dasha_ledger: refinement.event_dasha_ledger,
event_fit_rate: refinement.event_fit_rate,
@@ -536,7 +430,7 @@ export function latestResultToolProjection(
calculation_result_id?: string;
}>
: [],
separation,
separation: decision.separation,
engineResultId: latest.resultId,
}),
...(houseTable ? { house_table: houseTable } : {}),
@@ -548,6 +442,16 @@ export function latestResultToolProjection(
: {}),
inference_state: compactInferenceProjection(previousInferenceFromReceipt(latest.decisionReceipt ?? null)),
};
const overlaid = overlayPublicDecision(latest, decision);
return {
...base,
...publicDecisionFields(decision),
candidates: overlaid.candidates,
representative_time: overlaid.representativeTime ?? overlaid.representative_time ?? base.representative_time,
selection_allowed: overlaid.selectionAllowed,
confirmation_allowed: confirmationGate.confirmation_allowed,
session_outcome_view: sessionOutcomeView(decision.sessionOutcome),
};
}
function agentVisibleLatestProjection(
@@ -601,27 +505,32 @@ function agentVisibleLatestProjection(
};
}
function collectingFollowupForParsed(
function followupPlanForParsed(
parsed: DossierForTools,
latest: NonNullable<DossierForTools["latestResult"]>,
decision: RectificationDecision,
birthDate?: string | null,
) {
const windowScan = windowScanFromDecisionReceipt(latest.decisionReceipt ?? null);
const observations = internalObservationsFromWindowScan(windowScan);
const catalog = rectificationFollowupCatalog(latest, parsed.evidence);
const separation = evaluateCandidateSeparation(candidateScoresFromLatest(latest));
return buildMethodFollowupPlan({
evidence: parsed.evidence,
activeFocus: parsed.conversationSummary.activeFocus,
declinedTopics: parsed.conversationSummary.declinedSkippedTopics,
observations,
sessionOutcome: "collect_evidence",
...catalog,
birthDate,
accepted: Boolean(parsed.case.acceptedTime),
candidatesSeparated: separation.sufficient,
holdoutValidation: holdoutStatusFromLatest(latest),
});
return {
plan: buildMethodFollowupPlan({
evidence: parsed.evidence,
activeFocus: parsed.conversationSummary.activeFocus,
declinedTopics: parsed.conversationSummary.declinedSkippedTopics,
observations,
sessionOutcome: decision.sessionOutcome,
...catalog,
birthDate: birthDate ?? null,
accepted: Boolean(parsed.case.acceptedTime),
candidatesSeparated: decision.separation.sufficient,
holdoutValidation: decision.holdoutValidation,
}),
contrastPacket: catalog.contrastPacket,
sessionOutcome: decision.sessionOutcome,
decision,
};
}
function sessionAwareFollowupForParsed(
@@ -629,55 +538,16 @@ function sessionAwareFollowupForParsed(
latest: NonNullable<DossierForTools["latestResult"]>,
options?: { birthDate?: string | null; snapshotCurrent?: boolean },
) {
const collectingPlan = collectingFollowupForParsed(parsed, latest, options?.birthDate);
const catalog = rectificationFollowupCatalog(latest, parsed.evidence);
const contrastPacket = catalog.contrastPacket;
const candidateScores = candidateScoresFromLatest(latest);
const holdoutValidation = holdoutStatusFromLatest(latest);
const sessionOutcome = conversationalSessionOutcome({
selectionAllowed: false,
proposeAllowed: false,
confirmationAllowed: buildConfirmationGate({
engineConfirmationAllowed: latest.confirmationAllowed,
candidates: latest.candidates,
decisionReceipt: latest.decisionReceipt ?? null,
}).confirmation_allowed,
nextFollowup: collectingPlan.next_followup,
methods: collectingPlan.methods,
userStopped: parsed.case.status === "paused",
candidateScores,
discriminatorProbe: selectLiveDiscriminator(contrastPacket, parsed.evidence),
holdoutValidation,
snapshotCurrent: options?.snapshotCurrent,
const decision = decideFromDossier({
evidence: parsed.evidence,
accepted: Boolean(parsed.case.acceptedTime),
conversationSummary: parsed.conversationSummary,
latestResult: latest,
case: parsed.case,
turns: parsed.turns,
}, {
currentEvidenceFingerprint: evidenceLedgerFingerprint(parsed.evidence),
});
if (sessionOutcome === "collect_evidence") {
return {
plan: { ...collectingPlan, session_outcome: sessionOutcome },
contrastPacket,
sessionOutcome,
};
}
const windowScan = windowScanFromDecisionReceipt(latest.decisionReceipt ?? null);
const observations = internalObservationsFromWindowScan(windowScan);
const separation = evaluateCandidateSeparation(candidateScores);
return {
plan: buildMethodFollowupPlan({
evidence: parsed.evidence,
activeFocus: parsed.conversationSummary.activeFocus,
declinedTopics: parsed.conversationSummary.declinedSkippedTopics,
observations,
sessionOutcome,
...catalog,
birthDate: options?.birthDate ?? null,
accepted: Boolean(parsed.case.acceptedTime),
candidatesSeparated: separation.sufficient,
holdoutValidation,
}),
contrastPacket,
sessionOutcome,
};
return followupPlanForParsed(parsed, latest, decision, options?.birthDate);
}
function safeConversationSummary(dossier: DossierForTools) {
@@ -826,7 +696,7 @@ export function createRectificationV9ReadOnlyTools(ctx: RectificationV9Context)
const dossier = await loadV9CaseDossier(accounting, userId, input.caseId);
if ((input.projection ?? "turn_decision") === "full_diagnostics") {
const compute = await loadV9CaseCompute(accounting, userId, input.caseId);
return safeCaseProjection(parseDossierForTools(dossier), compute);
return safeCaseProjection(dossier, compute);
}
return projectTurnDecision(dossier);
},
@@ -1057,7 +927,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
} catch {
// Ranking stays fail-open when compute is unavailable.
}
const { plan: collectingPlan, contrastPacket } = sessionAwareFollowupForParsed(parsed, latest, { birthDate });
const { plan: collectingPlan, contrastPacket, decision } = sessionAwareFollowupForParsed(parsed, latest, { birthDate });
const persistedFocus = await persistServerOwnedFocus({
accounting,
userId,
@@ -1077,6 +947,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
: { ...collectingPlan, next_followup: null },
persistedFocus,
contrastPacket,
decision,
};
};
@@ -1173,7 +1044,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
const projectionKind = input.projection ?? "turn_decision";
const projection = projectionKind === "full_diagnostics"
? safeCaseProjection(
parseDossierForTools(refreshed),
refreshed,
await loadV9CaseCompute(accounting, userId, input.caseId),
)
: projectTurnDecision(refreshed);
@@ -1184,7 +1055,7 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
const projectionKind = input.projection ?? "turn_decision";
const projection = projectionKind === "full_diagnostics"
? safeCaseProjection(
parseDossierForTools(dossier),
dossier,
await loadV9CaseCompute(accounting, userId, input.caseId),
)
: projectTurnDecision(dossier);
@@ -1768,18 +1639,8 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
algorithmVersion: scored.persisted.algorithmVersion,
decisionReceipt: scored.decisionReceipt,
};
const { collectingPlan, persistedFocus, contrastPacket } = await persistPlanFocus(scored.parsed, latest);
const latestProjection = latestResultToolProjection(latest, {
nextFollowup: collectingPlan.next_followup,
methods: collectingPlan.methods,
userStopped: scored.parsed.case.status === "paused",
candidateScores: candidateScoresFromLatest(latest),
holdoutValidation: holdoutStatusFromLatest(latest),
discriminatorProbe: selectLiveDiscriminator(contrastPacket, scored.parsed.evidence),
snapshotCurrent: true,
evidence: scored.parsed.evidence,
accepted: Boolean(scored.parsed.case.acceptedTime),
});
const { collectingPlan, persistedFocus, decision } = await persistPlanFocus(scored.parsed, latest);
const latestProjection = latestResultToolProjection(latest, decision);
const projection = {
...agentVisibleLatestProjection(latestProjection, {
openQuestion: openQuestionFromPersistedFocus(persistedFocus),
@@ -1879,27 +1740,15 @@ export function createRectificationV9Tools(ctx: RectificationV9Context) {
throw new RectificationToolServiceError("no_candidate_result");
}
const latest = parsed.latestResult;
const { plan: collectingPlan, contrastPacket } = sessionAwareFollowupForParsed(parsed, latest);
const userStopped = parsed.case.status === "paused";
const candidateScores = candidateScoresFromLatest(latest);
const { decision } = sessionAwareFollowupForParsed(parsed, latest);
const currentSnapshot = snapshotSourceFromDossier(parsed, null);
const storedSnapshot = storedSnapshotSource(latest);
const scoreableCurrent = storedSnapshotIsCurrent(storedSnapshot, currentSnapshot);
if (!scoreableCurrent) {
throw new RectificationToolServiceError("offer_not_allowed");
}
const projection = latestResultToolProjection(latest, {
nextFollowup: collectingPlan.next_followup,
methods: collectingPlan.methods,
userStopped,
candidateScores,
holdoutValidation: holdoutStatusFromLatest(latest),
discriminatorProbe: selectLiveDiscriminator(contrastPacket, dossier.evidence),
snapshotCurrent: true,
evidence: parsed.evidence,
accepted: Boolean(parsed.case.acceptedTime),
});
const sessionKind = (projection.session_outcome as { kind?: string }).kind ?? "";
const projection = latestResultToolProjection(latest, decision);
const sessionKind = sessionOutcomeKind(projection.session_outcome);
if (!offerSessionKinds().includes(sessionKind)) {
throw new RectificationToolServiceError("offer_not_allowed");
}
@@ -11,6 +11,7 @@ import {
} from "../src/lib/rectification-agentic/v9/confirmation-gate.ts";
import { RECTIFICATION_POLICY } from "../src/lib/rectification-policy.ts";
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import {
createRectificationV9Tools,
latestResultToolProjection,
@@ -62,6 +63,14 @@ const PLATEAU = [
{ candidateId: THIRD_CANDIDATE_ID, time: "04:47", rank: 3, relativeSupport: 25, tiedMinuteCount: 25 },
];
function collectDecision(candidates: readonly { time: string; relativeSupport: number }[]) {
return decideRectification({
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport })),
});
}
function blocker(
gate: ReturnType<typeof buildConfirmationGate>,
id: string,
@@ -125,7 +134,7 @@ test("a 25-minute plateau stays confirmation-blocked as an indistinguishable ran
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
});
}, collectDecision(PLATEAU));
const gate = projection.confirmation_gate as ReturnType<typeof buildConfirmationGate>;
assert.ok(Number(projection.indistinguishable_width_minutes) >= 25);
assert.equal(projection.confirmation_allowed, false);
@@ -136,7 +145,7 @@ test("a 25-minute plateau stays confirmation-blocked as an indistinguishable ran
RECTIFICATION_POLICY.maxConfirmationWidthMinutes,
);
assert.match(blocker(gate, "adjacent_minutes_indistinguishable").user_meaning, /不可分区间|代表性候选/);
assert.equal((projection.session_outcome as { kind: string }).kind, "collect_evidence");
assert.equal(projection.session_outcome, "collect_evidence");
assert.equal(sessionOutcomeFromGate({
selectionAllowed: true,
confirmationAllowed: false,
@@ -183,7 +192,7 @@ test("holdout not_ready forbids unique-minute copy and still blocks confirm", as
decisionReceipt: {
gates: { exact_confirmation: { external_validation_status: "not_evaluated" } },
},
});
}, collectDecision(UNIQUE_MINUTE));
const serialized = JSON.stringify(projection);
assert.equal(projection.confirmation_allowed, false);
assert.equal(
@@ -22,6 +22,7 @@ import {
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import { buildSkillVerificationReport } from "../src/lib/rectification-agentic/v9/skill-verification-report.ts";
const TIED = [
{ id: "c0", time: "05:00", score: 34 },
{ id: "c1", time: "05:01", score: 33 },
@@ -499,6 +500,31 @@ test("stale snapshot keeps discrimination instead of returning to collection", (
}).type, "ask_fact_collection");
});
test("a single candidate is sole_candidate, not a parallel range", () => {
const separation = evaluateCandidateSeparation([{ time: "05:00", score: 80 }]);
assert.equal(separation.status, "sole_candidate");
assert.equal(separation.sufficient, true);
assert.equal(separation.lead, 0);
assert.deepEqual(separation.credibleRange, ["05:00"]);
const decision = decideRectification({
methodCoverageAll: true,
trainingGateOpen: true,
candidateScores: [{ time: "05:00", score: 80 }],
holdoutValidation: "unavailable",
});
assert.equal(decision.sessionOutcome, "adopt_representative");
assert.equal(decision.canConfirmExactMinute, false);
assert.doesNotMatch(
buildSkillVerificationReport({
representativeTime: "05:00",
widthMinutes: 0,
candidates: [{ time: "05:00", rank: 1, relativeSupport: 80 }],
separation,
}),
/基本并列/,
);
});
test("34/33/33 is a tie, not a recommended winner", () => {
const separation = evaluateCandidateSeparation(TIED);
assert.equal(separation.status, "not_separated");
@@ -10,6 +10,14 @@ import { decideNextAction } from "../src/lib/rectification-agentic/core/decide-n
import { selectDiscriminatorProbe } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
import { contrastPacketFromDossier, decideFromDossier, overlayPublicDecision } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
import { buildMethodFollowupPlan, conversationalSessionOutcome } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { projectTurnDecision } from "../src/lib/rectification-agentic/v9/turn-decision.ts";
import { parseV9CaseDossier, parseV9ComputeProjection } from "../src/lib/rectification-agentic/v9/tool-service.ts";
import { safeCaseProjection } from "../src/mastra/rectification-v9-tools.ts";
import {
candidateSnapshotFixture,
computeFixture,
dossierFixture,
} from "./rectification-v9-test-support.ts";
const SEPARATED = [
{ time: "04:48", score: 58 },
@@ -643,6 +651,28 @@ test("public candidate cards follow the inference ranking and hide an inconsiste
}
});
test("turn decision and safe case projection share session_outcome and selection_allowed", () => {
const dossier = parseV9CaseDossier(dossierFixture({
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "05:00",
}),
}));
const compute = parseV9ComputeProjection(computeFixture());
assert.ok(dossier);
assert.ok(compute);
const turn = projectTurnDecision(dossier);
const safe = safeCaseProjection(dossier, compute);
assert.equal(
(turn.next_action as { session_outcome: unknown }).session_outcome,
(safe.latest_result as { session_outcome: unknown }).session_outcome,
);
assert.equal(
(turn.candidate_summary as { selection_allowed: unknown }).selection_allowed,
(safe.latest_result as { selection_allowed: unknown }).selection_allowed,
);
});
test("interview, choice, refresh and next-action all call the same reducer", () => {
const interview = readSource("../src/lib/rectification-agentic/v9/interview-state.ts");
const adapter = readSource("../src/lib/rectification-agentic/v9/decision-from-dossier.ts");
@@ -650,13 +680,20 @@ test("interview, choice, refresh and next-action all call the same reducer", ()
const refresh = readSource("../src/lib/rectification-agentic/v9/turn-decision.ts");
const followup = readSource("../src/lib/rectification-agentic/v9/method-followup.ts");
const tools = readSource("../src/mastra/rectification-v9-tools.ts");
const separation = readSource("../src/lib/rectification-agentic/core/candidate-separation.ts");
const caseRoute = readSource("../src/app/api/rectification/cases/[caseId]/route.ts");
assert.match(interview, /decideFromDossier\(/);
assert.match(refresh, /decideFromDossier\(/);
assert.match(choice, /decideAfterInferenceChange\(/);
assert.match(adapter, /decideRectification\(/);
assert.match(followup, /decideRectification\(/);
assert.match(tools, /decideConversationalSession\(/);
assert.match(tools, /decideFromDossier\(/);
assert.match(tools, /overlayPublicDecision/);
assert.match(separation, /sole_candidate/);
assert.doesNotMatch(separation, /top \? MIN_SEPARATION_LEAD/);
assert.doesNotMatch(tools, /decideConversationalSession\(/);
assert.doesNotMatch(tools, /sessionOutcome \?\? "collect_evidence"/);
assert.doesNotMatch(followup, /input\.sessionOutcome \?\? "collect_evidence"/);
assert.match(caseRoute, /overlayPublicDecision/);
assert.match(caseRoute, /publicDecisionFields\(decision\)/);
assert.doesNotMatch(interview, /sessionOutcomeFromGate/);
@@ -14,6 +14,7 @@ import {
} from "../src/lib/rectification-agentic/v9/varga-observations.ts";
import { WINDOW_SCAN_DISPLAY_LAYER_ORDER } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { readVedastroMinuteSensitiveStatus } from "../src/lib/rectification-agentic/v9/confirmation-gate.ts";
import { authoritativeCandidateProjection } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
import { createRectificationV9Tools, latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts";
@@ -49,6 +50,14 @@ const FAMILY_ID = "44444444-4444-4444-8444-444444444443";
const CAREER_ID = "44444444-4444-4444-8444-444444444444";
const UNIQUE_MINUTE_COPY = /±5 分钟确定性/;
function collectDecision(candidates: readonly { time: string; relativeSupport?: number }[]) {
return decideRectification({
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport ?? 0 })),
});
}
const DYNAMIC_STYLE_OPTIONS = [
{ label: "明确发生且时间吻合", answer_class: "yes" as const },
{ label: "发生过但程度较弱", answer_class: "weak_yes" as const },
@@ -864,7 +873,7 @@ test("read-case follows method plan and keeps D9/D10 type tables when SQL missin
confirmation_allowed: boolean;
indistinguishable_width_minutes: number;
window_scan: { d9_candidates_differ: boolean } | null;
session_outcome: { kind: string };
session_outcome: string;
};
}>;
}).execute({ caseId: CASE_ID, projection: "full_diagnostics" });
@@ -881,7 +890,7 @@ test("read-case follows method plan and keeps D9/D10 type tables when SQL missin
(projection as { next_user_action?: { on_user_stop?: { id?: string } } }).next_user_action?.on_user_stop?.id,
"offer_provisional_range",
);
assert.equal(projection.latest_result.session_outcome.kind, "collect_evidence");
assert.equal(projection.latest_result.session_outcome, "collect_evidence");
assert.equal(projection.internal_observations.find((item) => item.layer === "d9")?.ask_theme, "relationship_style");
assert.equal(projection.latest_result.confirmation_allowed, false);
assert.ok(projection.latest_result.indistinguishable_width_minutes >= 25);
@@ -1316,26 +1325,27 @@ test("public tool surface stays at 14 and new cases bind 10.0.13", () => {
"8d7aa2d4bea0414e9a89ef908ccbc8c708c98f79f5b78ae4f7dc229b5f7dbb30",
);
assert.equal(deprecated.status, "deprecated");
const plateau = latestResultToolProjection({
resultId: RESULT_ID,
candidates: [
const plateauCandidates = [
{ candidateId: CANDIDATE_ID, time: "04:45", rank: 1, relativeSupport: 40, tiedMinuteCount: 25 },
{ candidateId: SECOND_CANDIDATE_ID, time: "04:46", rank: 2, relativeSupport: 35, tiedMinuteCount: 25 },
{ candidateId: THIRD_CANDIDATE_ID, time: "04:47", rank: 3, relativeSupport: 25, tiedMinuteCount: 25 },
],
];
const plateau = latestResultToolProjection({
resultId: RESULT_ID,
candidates: plateauCandidates,
selectionAllowed: true,
confirmationAllowed: true,
representativeTime: "04:45",
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
});
}, collectDecision(plateauCandidates));
assert.equal(plateau.confirmation_allowed, false);
assert.equal(plateau.unique_minute_claim, false);
assert.match(String(plateau.skill_verification_report), /Dasha \+ Gochara/);
assert.match(String(plateau.skill_verification_report), /candidate_range_not_birth_time_truth/);
assert.doesNotMatch(String(plateau.skill_verification_report), UNIQUE_MINUTE_COPY);
assert.equal((plateau.session_outcome as { kind: string }).kind, "collect_evidence");
assert.equal(plateau.session_outcome, "collect_evidence");
const skill = readFileSync(new URL("../../skills/jyotish-birth-time-rectification/SKILL.md", import.meta.url), "utf8");
assert.match(skill, /method_followup_plan/);
assert.match(skill, /感情 → 事业 → 家人 → 职业 → 占问/);
@@ -1383,14 +1393,14 @@ test("Mastra hides active candidates when the receipt range excludes one of them
algorithmVersion: "rectification-v5",
decisionReceipt: { inference_state: inferenceState },
};
const session = {
methods: buildMethodFollowupPlan({ evidence: CLASSIC_COVERAGE }).methods,
const session = decideRectification({
methodCoverageAll: true,
userStopped: true,
candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport })),
holdoutValidation: "passed" as const,
holdoutValidation: "passed",
snapshotCurrent: true,
trainingGateOpen: true,
};
});
const valid = latestResultToolProjection(latest, session);
assert.deepEqual((valid.candidates as typeof candidates).map((item) => item.time), ["05:00", "05:07"]);
assert.equal(valid.selection_allowed, true);
@@ -2836,9 +2846,9 @@ test("paused case with selection_allowed may offer the escape hatch", async () =
accounting: accounting.client as never,
});
const projection = await (tools["rectification-offer-candidates"] as unknown as {
execute(input: unknown): Promise<{ session_outcome: { kind: string } }>;
execute(input: unknown): Promise<{ session_outcome: string }>;
}).execute({ caseId: CASE_ID });
assert.equal(projection.session_outcome.kind, "provisional_range_user_stopped");
assert.equal(projection.session_outcome, "provisional_range_user_stopped");
assert.equal(
accounting.calls.some((call) =>
call.fn === "transition_agentic_rectification_case_status"
@@ -2949,9 +2959,9 @@ test("offer-candidates allows a 34/33/33 tie after method coverage when remainin
accounting: accounting.client as never,
});
const projection = await (tools["rectification-offer-candidates"] as unknown as {
execute(input: unknown): Promise<{ session_outcome: { kind: string } }>;
execute(input: unknown): Promise<{ session_outcome: string }>;
}).execute({ caseId: CASE_ID });
assert.equal(projection.session_outcome.kind, "provisional_range");
assert.equal(projection.session_outcome, "provisional_range");
assert.equal(
accounting.calls.some((call) =>
call.fn === "transition_agentic_rectification_case_status"
@@ -18,6 +18,7 @@ import {
} from "../src/lib/rectification-agentic/v9/candidate-plateau.ts";
import { RECTIFICATION_POLICY } from "../src/lib/rectification-policy.ts";
import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { createRectificationV9Tools, latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts";
import {
CASE_ID,
@@ -126,7 +127,11 @@ test("a 25-minute tied plateau projects width and forbids unique-minute confirma
selectedTime: null,
selectionKind: null,
algorithmVersion: "rectification-v5",
});
}, decideRectification({
methodCoverageAll: false,
trainingGateOpen: false,
candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport })),
}));
assert.equal(projection.indistinguishable_width_minutes, width);
assert.equal(projection.confirmation_allowed, false);
assert.equal(