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