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
@@ -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(