fix(rectification): share decideFromDossier across projections and treat a sole candidate as a close, not a tie
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user