ed9497e76a
Natal answers were opening on parameter tables, and rectification turns were one-sentence legal copy. Centralize user-facing strings, keep representative-minute and question-slot red lines, and stop duplicating the opening collect prompt as a second assistant message. Co-authored-by: Cursor <cursoragent@cursor.com>
1336 lines
58 KiB
TypeScript
1336 lines
58 KiB
TypeScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import test from "node:test";
|
||
|
||
import {
|
||
decideRectification,
|
||
engineCapabilityCeilingFromReceipt,
|
||
publicDecisionFields,
|
||
} from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
||
import { decideNextAction } from "../src/lib/rectification-agentic/core/decide-next-action.ts";
|
||
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||
import {
|
||
inspectDiscriminatorProbes,
|
||
selectDiscriminatorProbe,
|
||
type CandidateDiscriminatorProbe,
|
||
} 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 {
|
||
applyChoiceWithoutEvidence,
|
||
nakshatraBoundaryProbe,
|
||
stampChoiceSchemaWithProbe,
|
||
withNakshatraBoundaryProbe,
|
||
} from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
|
||
import { buildMethodFollowupPlan, conversationalSessionOutcome } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||
import { awaitTurnExitBeforeResponse, finalizeSuccessfulTurnExit } from "../src/lib/rectification-agentic/v9/turn-exit.ts";
|
||
import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||
import { projectCurrentQuestion, projectTurnDecision } from "../src/lib/rectification-agentic/v9/turn-decision.ts";
|
||
import {
|
||
evidenceLedgerFingerprint,
|
||
parseV9CaseDossier,
|
||
parseV9ComputeProjection,
|
||
} from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||
import { safeCaseProjection } from "../src/mastra/rectification-v9-tools.ts";
|
||
import {
|
||
CASE_ID,
|
||
FOCUS_ID,
|
||
TURN_ID,
|
||
USER_ID,
|
||
candidateSnapshotFixture,
|
||
computeFixture,
|
||
dossierFixture,
|
||
fakeAccounting,
|
||
} from "./rectification-v9-test-support.ts";
|
||
|
||
const SEPARATED = [
|
||
{ time: "04:48", score: 58 },
|
||
{ time: "04:49", score: 42 },
|
||
];
|
||
|
||
type TestEngineCapabilityCeiling = Readonly<{
|
||
acceptanceAllowed: boolean;
|
||
selectionAllowed: boolean;
|
||
proposeAllowed: boolean;
|
||
confirmationAllowed: boolean;
|
||
}>;
|
||
|
||
const ENGINE_OPEN: TestEngineCapabilityCeiling = {
|
||
acceptanceAllowed: true,
|
||
selectionAllowed: true,
|
||
proposeAllowed: true,
|
||
confirmationAllowed: true,
|
||
};
|
||
|
||
function capabilityCombinations(): TestEngineCapabilityCeiling[] {
|
||
return Array.from({ length: 16 }, (_, bits) => ({
|
||
acceptanceAllowed: Boolean(bits & 1),
|
||
selectionAllowed: Boolean(bits & 2),
|
||
proposeAllowed: Boolean(bits & 4),
|
||
confirmationAllowed: Boolean(bits & 8),
|
||
}));
|
||
}
|
||
|
||
function decideWithEngineCeiling(
|
||
engineCeiling: TestEngineCapabilityCeiling,
|
||
overrides: Partial<Parameters<typeof decideRectification>[0]> = {},
|
||
) {
|
||
return decideRectification({
|
||
methodCoverageAll: true,
|
||
trainingGateOpen: true,
|
||
snapshotCurrent: true,
|
||
candidateScores: SEPARATED,
|
||
holdoutValidation: "passed",
|
||
confirmationAllowed: true,
|
||
datedEventCount: 3,
|
||
datedDomainCount: 2,
|
||
...overrides,
|
||
engineCeiling,
|
||
} as Parameters<typeof decideRectification>[0]);
|
||
}
|
||
|
||
test("invariant 1: TypeScript delivery capabilities never exceed the engine ceiling", () => {
|
||
for (const ceiling of capabilityCombinations()) {
|
||
const decision = decideWithEngineCeiling(ceiling);
|
||
assert.equal(decision.canAdopt && !ceiling.acceptanceAllowed, false, JSON.stringify(ceiling));
|
||
assert.equal(decision.selectionAllowed && !ceiling.selectionAllowed, false, JSON.stringify(ceiling));
|
||
assert.equal(decision.proposeAllowed && !ceiling.proposeAllowed, false, JSON.stringify(ceiling));
|
||
assert.equal(decision.canConfirmExactMinute && !ceiling.confirmationAllowed, false, JSON.stringify(ceiling));
|
||
}
|
||
});
|
||
|
||
test("invariant 2: insufficient dated evidence never permits selection or adoption", () => {
|
||
for (const datedEventCount of [0, 2, 3]) {
|
||
for (const datedDomainCount of [0, 1, 2]) {
|
||
if (datedEventCount >= 3 && datedDomainCount >= 2) continue;
|
||
const decision = decideWithEngineCeiling(ENGINE_OPEN, {
|
||
confirmationAllowed: false,
|
||
datedEventCount,
|
||
datedDomainCount,
|
||
});
|
||
assert.equal(decision.canAdopt, false, `${datedEventCount} events / ${datedDomainCount} domains`);
|
||
assert.equal(decision.selectionAllowed, false, `${datedEventCount} events / ${datedDomainCount} domains`);
|
||
}
|
||
}
|
||
});
|
||
|
||
test("invariant 3: tied leading candidates can provisionally adopt, but exact-minute confirm stays closed", () => {
|
||
const tiedScores = [
|
||
{ time: "04:48", score: 12 },
|
||
{ time: "04:49", score: 12 },
|
||
{ time: "04:50", score: 11 },
|
||
];
|
||
const stopped = decideWithEngineCeiling(ENGINE_OPEN, {
|
||
candidateScores: tiedScores,
|
||
confirmationAllowed: false,
|
||
userStopped: true,
|
||
});
|
||
assert.equal(stopped.completionStatus, "provisional_range_user_stopped");
|
||
// 原断言 canAdopt=false(并列不得采用)→ 新断言 canAdopt=true。
|
||
// 产品 2026-09-01 授权:并列只挡唯一分钟确认门,不挡代表性采用。
|
||
assert.equal(stopped.canAdopt, true);
|
||
assert.equal(stopped.canConfirmExactMinute, false);
|
||
|
||
const continuing = decideWithEngineCeiling(ENGINE_OPEN, {
|
||
candidateScores: tiedScores,
|
||
confirmationAllowed: false,
|
||
userStopped: false,
|
||
});
|
||
assert.equal(continuing.separation.tiedForFirst, true);
|
||
assert.equal(continuing.canAdopt, true);
|
||
assert.equal(continuing.canConfirmExactMinute, false);
|
||
});
|
||
|
||
test("invariant 4: unavailable holdout still allows provisional adopt; exact-minute confirm stays closed", () => {
|
||
for (const userStopped of [false, true]) {
|
||
const decision = decideWithEngineCeiling(ENGINE_OPEN, {
|
||
confirmationAllowed: false,
|
||
holdoutValidation: "unavailable",
|
||
userStopped,
|
||
});
|
||
// 原断言 canAdopt=false(holdout unavailable 不放行)→ 新断言 canAdopt=true。
|
||
// holdout 只保留给唯一分钟确认门;引擎 ceiling 仍是硬上限。
|
||
assert.equal(decision.canAdopt, true, `userStopped=${userStopped}`);
|
||
assert.equal(decision.canConfirmExactMinute, false, `userStopped=${userStopped}`);
|
||
}
|
||
});
|
||
|
||
test("raw engine receipt contradictions fail closed before delivery", () => {
|
||
const openReceipt = {
|
||
acceptance_allowed: true,
|
||
selection_allowed: true,
|
||
propose_allowed: true,
|
||
confirmation_allowed: false,
|
||
};
|
||
const closed = {
|
||
acceptanceAllowed: false,
|
||
selectionAllowed: false,
|
||
proposeAllowed: false,
|
||
confirmationAllowed: false,
|
||
};
|
||
for (const receipt of [
|
||
{ ...openReceipt, accept_allowed: false },
|
||
{ ...openReceipt, confirm_allowed: true },
|
||
{ ...openReceipt, display_allowed: false },
|
||
{ ...openReceipt, confirmation_allowed: undefined },
|
||
]) {
|
||
assert.deepEqual(engineCapabilityCeilingFromReceipt(receipt), closed);
|
||
}
|
||
});
|
||
|
||
test("invariant 5: public overlay intersects all delivery gates with the engine receipt", () => {
|
||
const decision = decideWithEngineCeiling(ENGINE_OPEN);
|
||
const candidates = [
|
||
{ candidateId: "c-0507", time: "05:07", rank: 1, relativeSupport: 13, tiedMinuteCount: 1 },
|
||
{ candidateId: "c-0500", time: "05:00", rank: 2, relativeSupport: 12, tiedMinuteCount: 1 },
|
||
{ candidateId: "c-0515", time: "05:15", rank: 3, relativeSupport: 10, tiedMinuteCount: 1 },
|
||
];
|
||
const inferenceState = {
|
||
algorithm_version: "rectification-inference-v1",
|
||
candidate_set_id: "05:00-05:15:05:00,05:07,05:15",
|
||
revision: 2,
|
||
phase: "discrimination",
|
||
result_status: "discriminating",
|
||
range_start: "05:00",
|
||
range_end: "05:15",
|
||
candidates: [
|
||
{ id: "05:07", time: "05:07", cluster_range: ["05:07", "05:07"], prior_score: 13, posterior_score: 15, probability: 0.4, status: "active", rank: 2, strong_conflict_count: 0 },
|
||
{ id: "05:00", time: "05:00", cluster_range: ["05:00", "05:00"], prior_score: 12, posterior_score: 20, probability: 0.6, status: "active", rank: 1, strong_conflict_count: 0 },
|
||
{ id: "05:15", time: "05:15", cluster_range: ["05:15", "05:15"], prior_score: 10, posterior_score: 30, probability: 0, status: "eliminated", rank: 3, strong_conflict_count: 1 },
|
||
],
|
||
events: [],
|
||
probes: [],
|
||
answered_probes: [],
|
||
rounds: [],
|
||
entropy: 0.67,
|
||
representative_time: "05:00",
|
||
credible_range: ["05:00", "05:07"],
|
||
};
|
||
const validEngineCeilings = capabilityCombinations().filter((ceiling) => (
|
||
ceiling.acceptanceAllowed === ceiling.selectionAllowed
|
||
&& (!ceiling.proposeAllowed || ceiling.selectionAllowed)
|
||
&& (!ceiling.confirmationAllowed || ceiling.selectionAllowed)
|
||
));
|
||
|
||
for (const ceiling of validEngineCeilings) {
|
||
const overlaid = overlayPublicDecision({
|
||
candidates,
|
||
representativeTime: "05:07",
|
||
decisionReceipt: {
|
||
acceptance_allowed: ceiling.acceptanceAllowed,
|
||
selection_allowed: ceiling.selectionAllowed,
|
||
propose_allowed: ceiling.proposeAllowed,
|
||
confirmation_allowed: ceiling.confirmationAllowed,
|
||
inference_state: inferenceState,
|
||
},
|
||
}, decision);
|
||
assert.equal(overlaid.can_adopt, ceiling.acceptanceAllowed, JSON.stringify(ceiling));
|
||
assert.equal(overlaid.selection_allowed, ceiling.selectionAllowed, JSON.stringify(ceiling));
|
||
assert.equal(overlaid.propose_allowed, ceiling.proposeAllowed, JSON.stringify(ceiling));
|
||
assert.equal(overlaid.can_confirm_exact_minute, ceiling.confirmationAllowed, JSON.stringify(ceiling));
|
||
}
|
||
});
|
||
|
||
function readSource(relative: string) {
|
||
return readFileSync(new URL(relative, import.meta.url), "utf8");
|
||
}
|
||
|
||
const ADVANCING_RECTIFICATION_ACTIONS = [
|
||
"opening",
|
||
"message",
|
||
"answer_choice",
|
||
"stop_and_review",
|
||
] as const;
|
||
|
||
function routeExitContract(route: string, turnExit: string) {
|
||
const immediateStart = route.indexOf("if (immediateResponse)");
|
||
const immediateEnd = route.indexOf("const requestTime", immediateStart);
|
||
const immediateExit = immediateStart >= 0 && immediateEnd > immediateStart
|
||
? route.slice(immediateStart, immediateEnd)
|
||
: "";
|
||
const awaitIndex = immediateExit.indexOf("await awaitTurnExitBeforeResponse");
|
||
const returnIndex = immediateExit.indexOf("return response", awaitIndex);
|
||
const immediateAwaited = awaitIndex >= 0
|
||
&& returnIndex > awaitIndex
|
||
&& /finalizeSuccessfulTurnExit\s*\(/.test(immediateExit.slice(awaitIndex, returnIndex));
|
||
const readOnlyExplicitlyExcluded = /input\.action\s*===\s*["']read_only["']/.test(turnExit);
|
||
|
||
const resultStart = route.indexOf("if (!result.ok)");
|
||
const doneIndex = route.indexOf('send({ type: "done"', resultStart);
|
||
const streamedSuccess = resultStart >= 0 && doneIndex > resultStart
|
||
? route.slice(resultStart, doneIndex)
|
||
: "";
|
||
const streamingAwaited = /await\s+finalizeSuccessfulTurnExit\s*\(/.test(streamedSuccess);
|
||
const executionBody = /RECTIFICATION_ACTION_EXECUTION\s*=\s*\{([\s\S]*?)\}\s*as const/.exec(turnExit)?.[1] ?? "";
|
||
const executionActions = [...executionBody.matchAll(/^\s*([a-z_]+):/gm)].map((match) => match[1]);
|
||
|
||
return { executionActions, immediateAwaited, readOnlyExplicitlyExcluded, streamingAwaited };
|
||
}
|
||
|
||
test("nonterminal invariant 1: every advancing action exits through an awaited common gate", () => {
|
||
const route = readSource("../src/app/api/rectification/agent/route.ts");
|
||
const turnExit = readSource("../src/lib/rectification-agentic/v9/turn-exit.ts");
|
||
const declared = /action:\s*z\.enum\(\[([^\]]+)\]\)/.exec(route)?.[1]
|
||
?.match(/["']([^"']+)["']/g)
|
||
?.map((value) => value.slice(1, -1)) ?? [];
|
||
assert.deepEqual(
|
||
declared.filter((action) => action !== "read_only"),
|
||
[...ADVANCING_RECTIFICATION_ACTIONS],
|
||
"new advancing actions must enter this invariant instead of silently bypassing the exit gate",
|
||
);
|
||
|
||
const contract = routeExitContract(route, turnExit);
|
||
const failures: string[] = [];
|
||
if (contract.executionActions.join(",") !== declared.join(",")) {
|
||
failures.push("schema actions and execution declarations differ");
|
||
}
|
||
for (const action of ADVANCING_RECTIFICATION_ACTIONS) {
|
||
const immediate = action === "message" || action === "answer_choice" || action === "stop_and_review";
|
||
const streamed = action === "opening" || action === "message";
|
||
if (immediate && !contract.immediateAwaited) {
|
||
failures.push(`${action}: HTTP 200 response can return without awaiting the common exit gate`);
|
||
}
|
||
if (streamed && !contract.streamingAwaited) {
|
||
failures.push(`${action}: stream can emit done before awaiting the common exit gate`);
|
||
}
|
||
}
|
||
if (!contract.readOnlyExplicitlyExcluded) {
|
||
failures.push("read_only: common gate exclusion is not explicit at the shared exit");
|
||
}
|
||
assert.deepEqual(failures, []);
|
||
});
|
||
|
||
test("nonterminal invariant 2: exhausted probes take the next available server-owned exit", () => {
|
||
const nakshatraProbe: CandidateDiscriminatorProbe = {
|
||
probeId: "probe:nakshatra-boundary:incident",
|
||
candidateSetVersion: "incident",
|
||
question: "哪一组日常节奏更像你?",
|
||
informationGain: 0.01,
|
||
semanticKey: "nakshatra-boundary:incident",
|
||
candidateSplitHash: "nakshatra-boundary:incident:early|late",
|
||
expectedOutcomes: [
|
||
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:07"] },
|
||
{ outcomeId: "weak_yes", supportsCandidateIds: ["05:07"], conflictsCandidateIds: ["05:00"] },
|
||
{ outcomeId: "no", supportsCandidateIds: [], conflictsCandidateIds: [] },
|
||
{ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] },
|
||
],
|
||
sourceFeatures: [],
|
||
domain: "appearance",
|
||
year: null,
|
||
choiceKind: "varga_style",
|
||
styleOptions: [
|
||
{ label: "A 组:直接、外放", answerClass: "yes" },
|
||
{ label: "B 组:克制、内敛", answerClass: "weak_yes" },
|
||
{ label: "两组都不太像", answerClass: "no" },
|
||
{ label: "一时说不好", answerClass: "unsure" },
|
||
],
|
||
};
|
||
const cases = [
|
||
{
|
||
name: "holdout",
|
||
holdoutValidation: "not_started" as const,
|
||
datedMethodCollectOpen: false,
|
||
nakshatraBoundaryProbe: null,
|
||
nextAction: "offer_provisional_range",
|
||
},
|
||
{
|
||
name: "dated collect",
|
||
holdoutValidation: "unavailable" as const,
|
||
datedMethodCollectOpen: true,
|
||
nakshatraBoundaryProbe: null,
|
||
nextAction: "ask_fact_collection",
|
||
},
|
||
{
|
||
name: "nakshatra boundary",
|
||
holdoutValidation: "unavailable" as const,
|
||
datedMethodCollectOpen: false,
|
||
nakshatraBoundaryProbe: nakshatraProbe,
|
||
nextAction: "ask_candidate_discriminator",
|
||
},
|
||
{
|
||
name: "explicit range exit",
|
||
holdoutValidation: "unavailable" as const,
|
||
datedMethodCollectOpen: false,
|
||
nakshatraBoundaryProbe: null,
|
||
nextAction: "offer_provisional_range",
|
||
},
|
||
];
|
||
|
||
for (const fixture of cases) {
|
||
const decision = decideRectification({
|
||
methodCoverageAll: true,
|
||
trainingGateOpen: true,
|
||
snapshotCurrent: true,
|
||
candidateScores: [
|
||
{ time: "05:00", score: 34 },
|
||
{ time: "05:06", score: 33 },
|
||
{ time: "05:07", score: 33 },
|
||
],
|
||
discriminatorProbe: null,
|
||
holdoutValidation: fixture.holdoutValidation,
|
||
datedMethodCollectOpen: fixture.datedMethodCollectOpen,
|
||
nakshatraBoundaryProbe: fixture.nakshatraBoundaryProbe,
|
||
userStopped: false,
|
||
datedEventCount: 7,
|
||
datedDomainCount: 3,
|
||
engineCeiling: ENGINE_OPEN,
|
||
});
|
||
assert.equal(decision.separation.sufficient, false, fixture.name);
|
||
// 原断言 canAdopt=false(分离不足不得采用)→ 新断言 canAdopt=true。
|
||
// 分离不足只挡确认门;coverage/证据下限仍挡采用。本夹具覆盖已齐且事件够数。
|
||
assert.equal(decision.canAdopt, true, fixture.name);
|
||
assert.equal(decision.canConfirmExactMinute, false, fixture.name);
|
||
assert.equal(decision.nextAction, fixture.nextAction, fixture.name);
|
||
}
|
||
});
|
||
|
||
test("nakshatra boundary is a consumable four-answer probe and is not asked twice", () => {
|
||
const base = buildInferenceState({
|
||
range_start: "04:51",
|
||
range_end: "05:15",
|
||
candidates: [
|
||
{ id: "04:51", time: "04:51", relative_support: 34 },
|
||
{ id: "05:03", time: "05:03", relative_support: 33 },
|
||
{ id: "05:15", time: "05:15", relative_support: 32 },
|
||
],
|
||
events: Array.from({ length: 7 }, (_, index) => ({
|
||
id: `incident-event-${index + 1}`,
|
||
domain: ["career", "relationship", "education"][index % 3]!,
|
||
year: 2016 + index,
|
||
precision: "year" as const,
|
||
})),
|
||
probes: [],
|
||
});
|
||
const boundary = {
|
||
near_boundary: true,
|
||
user_meaning: "平时做决定时,哪一组节奏更像你?",
|
||
options: [
|
||
{ key: "A" as const, time_bias: "earlier" as const, traits: ["直接", "行动快"] },
|
||
{ key: "B" as const, time_bias: "later" as const, traits: ["克制", "先观察"] },
|
||
],
|
||
};
|
||
const probe = nakshatraBoundaryProbe(base, boundary);
|
||
assert.ok(probe);
|
||
assert.deepEqual(probe.style_options?.map((item) => item.answer_class), ["yes", "weak_yes", "no", "unsure"]);
|
||
assert.match(probe.style_options?.[0]?.label ?? "", /直接.*行动快/);
|
||
assert.match(probe.style_options?.[1]?.label ?? "", /克制.*先观察/);
|
||
|
||
const withProbe = withNakshatraBoundaryProbe(base, boundary);
|
||
assert.ok(withProbe);
|
||
const schema = stampChoiceSchemaWithProbe({
|
||
choice: {
|
||
options: [
|
||
{ key: "A", answer_class: "yes" },
|
||
{ key: "B", answer_class: "weak_yes" },
|
||
{ key: "C", answer_class: "no" },
|
||
{ key: "D", answer_class: "unsure" },
|
||
],
|
||
},
|
||
}, withProbe, "nakshatra-boundary:incident", {
|
||
probe_id: probe.id,
|
||
semantic_key: probe.semantic_key,
|
||
candidate_split_hash: probe.candidate_split_hash,
|
||
});
|
||
const applied = applyChoiceWithoutEvidence(withProbe, {
|
||
choiceKey: "A",
|
||
schema,
|
||
questionId: "nakshatra-boundary:incident",
|
||
domain: "appearance",
|
||
classifiedFrom: "choice",
|
||
});
|
||
assert.equal(applied.applied, true);
|
||
assert.equal(applied.reason, "applied");
|
||
assert.equal(applied.state.answered_probes.at(-1)?.probe_id, probe.id);
|
||
assert.equal(applied.state.answered_probes.at(-1)?.answer_class, "yes");
|
||
|
||
const deduplicated = withNakshatraBoundaryProbe(applied.state, boundary);
|
||
assert.ok(deduplicated);
|
||
assert.equal(deduplicated.probes.some((item) => item.source === "nakshatra_boundary"), false);
|
||
});
|
||
|
||
test("nonterminal invariant 3: answer_choice response waits until incident fallback focus is persisted", async () => {
|
||
const evidence = Array.from({ length: 7 }, (_, index) => ({
|
||
id: `44444444-4444-4444-8444-${String(index + 10).padStart(12, "0")}`,
|
||
source_turn_id: "33333333-3333-4333-8333-333333333333",
|
||
subject: "self",
|
||
event_kind: "dated_event",
|
||
domain: ["career", "relationship", "education"][index % 3]!,
|
||
occurred_from: `${2016 + index}-01-01`,
|
||
occurred_to: null,
|
||
date_precision: "year",
|
||
summary: `incident evidence ${index + 1}`,
|
||
status: "confirmed",
|
||
supersedes_evidence_id: null,
|
||
created_at: "2026-09-01T00:00:00.000Z",
|
||
}));
|
||
const answeredProbes = Array.from({ length: 6 }, (_, index) => ({
|
||
id: `incident-answered-${index + 1}`,
|
||
semantic_key: `incident.answered.${index + 1}`,
|
||
candidate_split_hash: `incident-answered-split-${index + 1}`,
|
||
domain: ["career", "relationship", "education"][index % 3]!,
|
||
year: 2016 + index,
|
||
question: `incident answered question ${index + 1}`,
|
||
candidate_ids: ["04:51", "05:03", "05:15"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes" as const, supports: ["04:51"], conflicts: ["05:15"] },
|
||
{ answer_class: "no" as const, supports: ["05:15"], conflicts: ["04:51"] },
|
||
{ answer_class: "unsure" as const, supports: [], conflicts: [] },
|
||
],
|
||
information_gain: 0.5,
|
||
source: "dasha_boundary",
|
||
}));
|
||
const droppedProbes = Array.from({ length: 5 }, (_, index) => ({
|
||
id: `incident-dropped-${index + 1}`,
|
||
semantic_key: `varga.d${index + 2}.incident`,
|
||
candidate_split_hash: `incident-dropped-split-${index + 1}`,
|
||
domain: "career",
|
||
year: 0,
|
||
question: `yearless varga contrast ${index + 1}`,
|
||
candidate_ids: ["04:51", "05:03", "05:15"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes" as const, supports: ["04:51"], conflicts: ["05:15"] },
|
||
{ answer_class: "no" as const, supports: ["05:15"], conflicts: ["04:51"] },
|
||
],
|
||
information_gain: 1 + index / 10,
|
||
source: "varga_contrast",
|
||
}));
|
||
const incidentInference = buildInferenceState({
|
||
range_start: "04:51",
|
||
range_end: "05:15",
|
||
candidates: [
|
||
{ id: "04:51", time: "04:51", relative_support: 34 },
|
||
{ id: "05:03", time: "05:03", relative_support: 33 },
|
||
{ id: "05:15", time: "05:15", relative_support: 32 },
|
||
],
|
||
events: evidence.map((item) => ({
|
||
id: item.id,
|
||
domain: item.domain,
|
||
year: Number(item.occurred_from.slice(0, 4)),
|
||
precision: "year" as const,
|
||
})),
|
||
probes: [...answeredProbes, ...droppedProbes],
|
||
answered_probes: answeredProbes.map((probe) => ({
|
||
probe_id: probe.id,
|
||
semantic_key: probe.semantic_key,
|
||
candidate_split_hash: probe.candidate_split_hash,
|
||
answer_class: "unsure" as const,
|
||
classified_from: "choice" as const,
|
||
})),
|
||
});
|
||
const raw = dossierFixture({
|
||
evidence,
|
||
evidenceCount: evidence.length,
|
||
latestResult: candidateSnapshotFixture({
|
||
selectionAllowed: true,
|
||
decisionReceipt: {
|
||
inference_state: incidentInference,
|
||
diagnostic_quality: { passed: false, margin_percent: 4.476 },
|
||
date_sensitivity_retention_rate: 0.2857,
|
||
oos_blind_prompts: [
|
||
{ domain: "family", user_meaning: "家里有没有结婚、添丁或住院这类记得住时间的事?", used_for_scoring: false },
|
||
{ domain: "health_pressure", user_meaning: "有没有记得住时间的健康压力事件?", used_for_scoring: false },
|
||
],
|
||
nakshatra_boundary: {
|
||
near_boundary: true,
|
||
user_meaning: "平时做决定时,哪一组节奏更像你?",
|
||
options: [
|
||
{ key: "A", time_bias: "earlier", traits: ["直接", "行动快"] },
|
||
{ key: "B", time_bias: "later", traits: ["克制", "先观察"] },
|
||
],
|
||
},
|
||
},
|
||
candidates: [
|
||
{ candidate_id: "88888888-8888-4888-8888-888888888881", rank: 1, time: "04:51", relative_support: 34, tied_minute_count: 1 },
|
||
{ candidate_id: "88888888-8888-4888-8888-888888888882", rank: 2, time: "05:03", relative_support: 33, tied_minute_count: 1 },
|
||
{ candidate_id: "88888888-8888-4888-8888-888888888883", rank: 3, time: "05:15", relative_support: 32, tied_minute_count: 1 },
|
||
],
|
||
}),
|
||
});
|
||
const parsedIncident = parseV9CaseDossier(raw);
|
||
assert.ok(parsedIncident);
|
||
const decision = decideFromDossier(parsedIncident);
|
||
assert.equal(decision.canAdopt, false);
|
||
assert.notEqual(decision.nextAction, "offer_provisional_range");
|
||
|
||
let activeFocus: Record<string, unknown> | null = null;
|
||
let releaseWrite!: () => void;
|
||
const writeGate = new Promise<void>((resolve) => { releaseWrite = resolve; });
|
||
let markWriteStarted!: () => void;
|
||
const writeStarted = new Promise<void>((resolve) => { markWriteStarted = resolve; });
|
||
const accounting = fakeAccounting({
|
||
get_agentic_rectification_case_dossier: () => ({
|
||
...raw,
|
||
conversation_summary: {
|
||
...raw.conversation_summary,
|
||
active_focus: activeFocus,
|
||
},
|
||
}),
|
||
get_agentic_rectification_case_compute: () => computeFixture(),
|
||
set_agentic_rectification_conversation_focus: async (_fn, args) => {
|
||
markWriteStarted();
|
||
await writeGate;
|
||
activeFocus = {
|
||
id: FOCUS_ID,
|
||
case_id: CASE_ID,
|
||
question_id: args.p_question_id,
|
||
intent: args.p_intent,
|
||
target_evidence_id: args.p_target_evidence_id,
|
||
target_domain: args.p_target_domain,
|
||
target_kind: args.p_target_kind,
|
||
expected_answer_schema: args.p_expected_answer_schema,
|
||
status: "active",
|
||
asked_at: "2026-09-01T00:00:00.000Z",
|
||
resolved_at: null,
|
||
};
|
||
return { focus: activeFocus, idempotent: false };
|
||
},
|
||
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID, idempotent: false }),
|
||
});
|
||
|
||
let responseVisible = false;
|
||
const responsePromise = awaitTurnExitBeforeResponse(
|
||
new Response(null, { status: 200 }),
|
||
() => finalizeSuccessfulTurnExit({
|
||
accounting: accounting.client,
|
||
userId: USER_ID,
|
||
caseId: CASE_ID,
|
||
action: "answer_choice",
|
||
}),
|
||
);
|
||
void responsePromise.then(() => { responseVisible = true; });
|
||
await writeStarted;
|
||
await Promise.resolve();
|
||
assert.equal(responseVisible, false, "answer_choice response resolved before focus persistence completed");
|
||
|
||
releaseWrite();
|
||
const response = await responsePromise;
|
||
const refreshed = parseV9CaseDossier({
|
||
...raw,
|
||
conversation_summary: {
|
||
...raw.conversation_summary,
|
||
active_focus: activeFocus,
|
||
},
|
||
});
|
||
assert.ok(refreshed);
|
||
const currentQuestion = projectCurrentQuestion(refreshed.conversationSummary.activeFocus);
|
||
assert.equal(response.status, 200);
|
||
assert.equal(decision.canAdopt, false);
|
||
assert.ok(currentQuestion?.prompt);
|
||
assert.equal(currentQuestion.prompt, "家里如果有结婚、添丁或住院这类事,记得大概哪年就行。");
|
||
const historyTurn = accounting.calls.find((call) => call.fn === "append_agentic_rectification_turn");
|
||
assert.equal(historyTurn?.args.p_request_id, FOCUS_ID);
|
||
assert.equal(historyTurn?.args.p_user_message, null);
|
||
assert.equal(historyTurn?.args.p_assistant_message, currentQuestion.prompt);
|
||
});
|
||
|
||
test("public decision fields are derived from decideRectification", () => {
|
||
const fixtures = [
|
||
{
|
||
methodCoverageAll: false,
|
||
trainingGateOpen: false,
|
||
candidateScores: SEPARATED,
|
||
},
|
||
{
|
||
methodCoverageAll: true,
|
||
trainingGateOpen: true,
|
||
candidateScores: [
|
||
{ time: "05:00", score: 34 },
|
||
{ time: "05:01", score: 33 },
|
||
{ time: "05:02", score: 33 },
|
||
],
|
||
},
|
||
{
|
||
methodCoverageAll: true,
|
||
trainingGateOpen: true,
|
||
candidateScores: SEPARATED,
|
||
holdoutValidation: "passed" as const,
|
||
},
|
||
{
|
||
methodCoverageAll: true,
|
||
userStopped: true,
|
||
candidateScores: SEPARATED,
|
||
holdoutValidation: "not_started" as const,
|
||
},
|
||
];
|
||
for (const input of fixtures) {
|
||
const decision = decideRectification({ ...input, engineCeiling: ENGINE_OPEN });
|
||
const fields = publicDecisionFields(decision);
|
||
assert.deepEqual(fields, {
|
||
type: decision.nextAction,
|
||
session_outcome: decision.sessionOutcome,
|
||
completion_status: decision.completionStatus,
|
||
validated: decision.validated,
|
||
can_offer_range: decision.canOfferRange,
|
||
can_adopt: decision.canAdopt,
|
||
can_confirm_exact_minute: decision.canConfirmExactMinute,
|
||
selection_allowed: decision.selectionAllowed,
|
||
propose_allowed: decision.proposeAllowed,
|
||
precision_stage: decision.precisionStage,
|
||
representative_time: decision.representativeTime,
|
||
credible_range: decision.credibleRange,
|
||
});
|
||
const publicOverlay = overlayPublicDecision({ selectionAllowed: true }, decision);
|
||
assert.equal(publicOverlay.selectionAllowed, false);
|
||
assert.equal(publicOverlay.validated, fields.validated);
|
||
assert.equal(decideNextAction(input).type, decision.nextAction);
|
||
assert.equal(conversationalSessionOutcome({
|
||
selectionAllowed: decision.selectionAllowed,
|
||
proposeAllowed: decision.proposeAllowed,
|
||
confirmationAllowed: false,
|
||
nextFollowup: null,
|
||
userStopped: input.userStopped,
|
||
candidateScores: input.candidateScores,
|
||
holdoutValidation: input.holdoutValidation,
|
||
trainingGateOpen: input.trainingGateOpen,
|
||
}), decision.sessionOutcome);
|
||
}
|
||
});
|
||
|
||
test("recorded education evidence does not suppress an unasked D24 discriminator", () => {
|
||
const packet = contrastPacketFromDossier({
|
||
evidence: [{
|
||
status: "confirmed",
|
||
domain: "education",
|
||
datePrecision: "year",
|
||
occurredFrom: "2016-01-01",
|
||
occurredTo: null,
|
||
eventKind: "education_exam",
|
||
summary: "2016 年考试发挥失常",
|
||
}],
|
||
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
|
||
latestResult: {
|
||
resultId: "result-d24",
|
||
candidates: [
|
||
{ candidateId: "c-0500", time: "05:00", relativeSupport: 20 },
|
||
{ candidateId: "c-0507", time: "05:07", relativeSupport: 15 },
|
||
{ candidateId: "c-0512", time: "05:12", relativeSupport: 15 },
|
||
],
|
||
decisionReceipt: {
|
||
inference_state: {
|
||
algorithm_version: "rectification-inference-v1",
|
||
candidate_set_id: "05:00-05:12:05:00,05:07,05:12",
|
||
revision: 0,
|
||
phase: "discrimination",
|
||
result_status: "discriminating",
|
||
range_start: "05:00",
|
||
range_end: "05:12",
|
||
candidates: [
|
||
{ id: "05:00", time: "05:00", cluster_range: ["05:00", "05:00"], prior_score: 20, posterior_score: 20, probability: 0.4, status: "active", rank: 1, strong_conflict_count: 0 },
|
||
{ id: "05:07", time: "05:07", cluster_range: ["05:07", "05:07"], prior_score: 15, posterior_score: 15, probability: 0.3, status: "active", rank: 2, strong_conflict_count: 0 },
|
||
{ id: "05:12", time: "05:12", cluster_range: ["05:12", "05:12"], prior_score: 15, posterior_score: 15, probability: 0.3, status: "active", rank: 3, strong_conflict_count: 0 },
|
||
],
|
||
events: [], probes: [], answered_probes: [], rounds: [], entropy: 1,
|
||
representative_time: "05:00",
|
||
credible_range: ["05:00", "05:12"],
|
||
},
|
||
window_scan: {
|
||
scanned: true,
|
||
d9_lagna_count: 1,
|
||
d10_lagna_count: 1,
|
||
d24_lagna_count: 3,
|
||
transitions: [
|
||
{ layer: "d24", at: "05:07", from_sign: "白羊座", to_sign: "金牛座" },
|
||
{ layer: "d24", at: "05:12", from_sign: "金牛座", to_sign: "双子座" },
|
||
],
|
||
},
|
||
},
|
||
},
|
||
case: { acceptedTime: null },
|
||
});
|
||
|
||
assert.equal(packet.probes[0]?.semanticKey.startsWith("varga.d24."), true);
|
||
assert.equal(packet.probes[0]?.informationGain > 1, true);
|
||
assert.deepEqual(packet.probes[0]?.expectedOutcomes.map((row) => row.supportsCandidateIds), [
|
||
["05:00"],
|
||
["05:07"],
|
||
["05:12"],
|
||
[],
|
||
]);
|
||
assert.equal(packet.probes[0]?.expectedOutcomes.at(-1)?.outcomeId, "unsure");
|
||
});
|
||
|
||
test("answered inference probes drop out of the catalog by probe_id", () => {
|
||
const outcomes = [
|
||
{ answer_class: "yes" as const, supports: ["05:00"], conflicts: ["05:12"] },
|
||
{ answer_class: "no" as const, supports: ["05:12"], conflicts: ["05:00"] },
|
||
{ answer_class: "unsure" as const, supports: [], conflicts: [] },
|
||
];
|
||
const packet = contrastPacketFromDossier({
|
||
evidence: [],
|
||
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
|
||
latestResult: {
|
||
resultId: "result-answered-probe",
|
||
candidates: [
|
||
{ candidateId: "c-0500", time: "05:00", relativeSupport: 20 },
|
||
{ candidateId: "c-0512", time: "05:12", relativeSupport: 15 },
|
||
],
|
||
decisionReceipt: {
|
||
inference_state: {
|
||
algorithm_version: "rectification-inference-v1",
|
||
candidate_set_id: "05:00-05:12:05:00,05:12",
|
||
revision: 1,
|
||
phase: "discrimination",
|
||
result_status: "discriminating",
|
||
range_start: "05:00",
|
||
range_end: "05:12",
|
||
candidates: [
|
||
{ id: "05:00", time: "05:00", cluster_range: ["05:00", "05:00"], prior_score: 20, posterior_score: 20, probability: 0.6, status: "active", rank: 1, strong_conflict_count: 0 },
|
||
{ id: "05:12", time: "05:12", cluster_range: ["05:12", "05:12"], prior_score: 15, posterior_score: 15, probability: 0.4, status: "active", rank: 2, strong_conflict_count: 0 },
|
||
],
|
||
events: [],
|
||
probes: [{
|
||
id: "probe:career.2018.dasha_boundary:hash",
|
||
year: 2018,
|
||
domain: "career",
|
||
source: "dasha_boundary",
|
||
question: "2018 年 3 月前后 career",
|
||
semantic_key: "career.2018.dasha_boundary",
|
||
candidate_ids: ["05:00", "05:12"],
|
||
information_gain: 1.2,
|
||
expected_outcomes: outcomes,
|
||
candidate_split_hash: "hash",
|
||
}],
|
||
answered_probes: [{
|
||
probe_id: "probe:career.2018.dasha_boundary:hash",
|
||
semantic_key: "career.2018.dasha_boundary",
|
||
candidate_split_hash: "hash",
|
||
answer_class: "yes",
|
||
classified_from: "choice",
|
||
}],
|
||
rounds: [],
|
||
entropy: 1,
|
||
representative_time: "05:00",
|
||
credible_range: ["05:00", "05:12"],
|
||
},
|
||
},
|
||
},
|
||
case: { acceptedTime: null },
|
||
});
|
||
assert.equal(packet.probes.some((probe) => probe.semanticKey === "career.2018.dasha_boundary"), false);
|
||
|
||
const adapter = readSource("../src/lib/rectification-agentic/v9/decision-from-dossier.ts");
|
||
assert.match(adapter, /answered_probes[\s\S]{0,120}item\.probe_id/);
|
||
assert.doesNotMatch(adapter, /answered_probes[\s\S]{0,120}item\.id\)/);
|
||
});
|
||
|
||
test("scored inference catalog outranks a low-gain Python career probe when snapshot candidates are empty", () => {
|
||
const careerOutcomes = [
|
||
{ answer_class: "yes", supports: ["04:45", "05:00", "05:14"], conflicts: ["05:15"] },
|
||
{ answer_class: "weak_yes", supports: ["04:45", "05:00", "05:14"], conflicts: ["05:15"] },
|
||
{ answer_class: "no", supports: ["05:15"], conflicts: ["04:45", "05:00", "05:14"] },
|
||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||
];
|
||
const d24Outcomes = [
|
||
{ answer_class: "yes", supports: ["04:47"], conflicts: ["04:51", "04:53", "04:59", "05:00", "05:07", "05:12", "05:14", "05:15"] },
|
||
{ answer_class: "weak_yes", supports: ["04:51", "04:53"], conflicts: ["04:47", "04:59", "05:00", "05:07", "05:12", "05:14", "05:15"] },
|
||
{ answer_class: "no", supports: ["04:59"], conflicts: ["04:47", "04:51", "04:53", "05:00", "05:07", "05:12", "05:14", "05:15"] },
|
||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||
];
|
||
const inferenceCandidates = [
|
||
{ id: "05:00", time: "05:00", cluster_range: ["05:00", "05:00"], prior_score: 22, posterior_score: 22, probability: 0.51, status: "active", rank: 1, strong_conflict_count: 0 },
|
||
{ id: "05:07", time: "05:07", cluster_range: ["05:07", "05:07"], prior_score: 17, posterior_score: 17, probability: 0.15, status: "active", rank: 2, strong_conflict_count: 0 },
|
||
{ id: "05:12", time: "05:12", cluster_range: ["05:12", "05:14"], prior_score: 17, posterior_score: 17, probability: 0.15, status: "equivalent", rank: 3, strong_conflict_count: 0 },
|
||
{ id: "05:14", time: "05:14", cluster_range: ["05:12", "05:14"], prior_score: 17, posterior_score: 17, probability: 0.15, status: "equivalent", rank: 4, strong_conflict_count: 0 },
|
||
{ id: "04:47", time: "04:47", cluster_range: ["04:47", "04:47"], prior_score: 6, posterior_score: 6, probability: 0.01, status: "active", rank: 5, strong_conflict_count: 0 },
|
||
{ id: "04:51", time: "04:51", cluster_range: ["04:51", "04:51"], prior_score: 6, posterior_score: 6, probability: 0.01, status: "active", rank: 6, strong_conflict_count: 0 },
|
||
{ id: "04:53", time: "04:53", cluster_range: ["04:53", "04:53"], prior_score: 6, posterior_score: 6, probability: 0.01, status: "active", rank: 7, strong_conflict_count: 0 },
|
||
{ id: "04:59", time: "04:59", cluster_range: ["04:59", "04:59"], prior_score: 6, posterior_score: 6, probability: 0.01, status: "active", rank: 8, strong_conflict_count: 0 },
|
||
{ id: "05:15", time: "05:15", cluster_range: ["05:15", "05:15"], prior_score: 3, posterior_score: 3, probability: 0.004, status: "active", rank: 9, strong_conflict_count: 0 },
|
||
];
|
||
const evidence = [
|
||
{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null, eventKind: "education_start" },
|
||
{ status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-08-08", occurredTo: null, eventKind: "relationship_end" },
|
||
{ status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2020-04-01", occurredTo: null, eventKind: "career_entry" },
|
||
{ status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null, eventKind: "family_event" },
|
||
];
|
||
const dossier = {
|
||
evidence,
|
||
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
|
||
latestResult: {
|
||
resultId: "result-empty-snapshot",
|
||
candidates: [],
|
||
decisionReceipt: {
|
||
discriminating_event_probes: [{
|
||
role: "distinguish",
|
||
phase: "candidate_discriminator",
|
||
year: 2023,
|
||
year_label: "2023 年前后",
|
||
domain: "career",
|
||
event_family: "入职、升职或职责明显加重",
|
||
source: "dasha_activation",
|
||
tracks: ["vimshottari", "narayana"],
|
||
tracks_agree: false,
|
||
unique_minute_claim: false,
|
||
user_meaning: "时间范围锁定 2023 年前后;领域锁定 career。",
|
||
choice_kind: "existence",
|
||
information_gain: 0.56,
|
||
semantic_key: "career.2023.dasha_activation",
|
||
candidate_split_hash: "500ce694938305201fbab9ba",
|
||
candidate_ids: ["04:45", "05:00", "05:14", "05:15"],
|
||
expected_outcomes: careerOutcomes,
|
||
style_options: [
|
||
{ label: "明确发生且时间吻合", answer_class: "yes" },
|
||
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
|
||
{ label: "明确没有发生", answer_class: "no" },
|
||
{ label: "这段记不清楚", answer_class: "unsure" },
|
||
],
|
||
}],
|
||
inference_state: {
|
||
algorithm_version: "rectification-inference-v1",
|
||
candidate_set_id: "04:45-05:15:04:47,04:51,04:53,04:59,05:00,05:07,05:12,05:14,05:15",
|
||
revision: 1,
|
||
phase: "discrimination",
|
||
result_status: "discriminating",
|
||
range_start: "04:45",
|
||
range_end: "05:15",
|
||
candidates: inferenceCandidates,
|
||
events: [],
|
||
probes: [{
|
||
id: "probe:career.2023.dasha_activation:500ce694938305201fbab9ba",
|
||
year: 2023,
|
||
domain: "career",
|
||
source: "dasha_activation",
|
||
question: "时间范围锁定 2023 年前后;领域锁定 career。",
|
||
semantic_key: "career.2023.dasha_activation",
|
||
candidate_ids: ["04:45", "05:00", "05:14", "05:15"],
|
||
information_gain: 0.56,
|
||
expected_outcomes: careerOutcomes,
|
||
candidate_split_hash: "500ce694938305201fbab9ba",
|
||
}, {
|
||
id: "contrast:varga.d24.04:47/04:51|04:53/04:59/05:00/05:07|05:12/05:14|05:15",
|
||
year: 0,
|
||
domain: "education",
|
||
source: "varga_contrast",
|
||
question: "引擎给出的区分机会绑定 D24。",
|
||
semantic_key: "varga.d24.04:47/04:51|04:53/04:59/05:00/05:07|05:12/05:14|05:15",
|
||
candidate_ids: ["04:47", "04:51", "04:53", "04:59", "05:00", "05:07", "05:12", "05:14", "05:15"],
|
||
information_gain: 2.503258334775646,
|
||
expected_outcomes: d24Outcomes,
|
||
candidate_split_hash: "04:45-05:15:varga.d24",
|
||
}],
|
||
answered_probes: [],
|
||
rounds: [],
|
||
entropy: 2.0,
|
||
representative_time: "05:00",
|
||
credible_range: ["05:00", "05:14"],
|
||
},
|
||
window_scan: {
|
||
scanned: true,
|
||
d24_lagna_count: 6,
|
||
d24_candidates_differ: true,
|
||
transitions: [
|
||
{ layer: "d24", at: "04:48", from_sign: "白羊座", to_sign: "金牛座" },
|
||
{ layer: "d24", at: "04:54", from_sign: "金牛座", to_sign: "双子座" },
|
||
{ layer: "d24", at: "05:00", from_sign: "双子座", to_sign: "巨蟹座" },
|
||
{ layer: "d24", at: "05:06", from_sign: "巨蟹座", to_sign: "狮子座" },
|
||
{ layer: "d24", at: "05:13", from_sign: "狮子座", to_sign: "处女座" },
|
||
],
|
||
},
|
||
},
|
||
},
|
||
case: { acceptedTime: null },
|
||
};
|
||
const packet = contrastPacketFromDossier(dossier);
|
||
const inspected = inspectDiscriminatorProbes(packet);
|
||
assert.equal(packet.probes.some((probe) => probe.semanticKey.includes("career.2023")), true);
|
||
assert.ok(packet.probes.some((probe) => probe.semanticKey.startsWith("varga.d24.")));
|
||
assert.equal(inspected.selected?.semanticKey.includes("career.2023"), true);
|
||
assert.equal(inspected.dropped.some((item) => (
|
||
item.semantic_key.startsWith("varga.d24.") && item.reason === "yearless_ungrounded_contrast"
|
||
)), true);
|
||
|
||
const plan = buildMethodFollowupPlan({
|
||
evidence,
|
||
eventProbes: [{
|
||
year: 2023,
|
||
year_label: "2023 年前后",
|
||
domain: "career",
|
||
event_family: "入职、升职或职责明显加重",
|
||
source: "dasha_activation",
|
||
tracks: ["vimshottari", "narayana"],
|
||
tracks_agree: false,
|
||
unique_minute_claim: false,
|
||
user_meaning: "时间范围锁定 2023 年前后。",
|
||
role: "distinguish",
|
||
phase: "candidate_discriminator",
|
||
information_gain: 0.56,
|
||
semantic_key: "career.2023.dasha_activation",
|
||
candidate_split_hash: "500ce694938305201fbab9ba",
|
||
candidate_ids: ["04:45", "05:00", "05:14", "05:15"],
|
||
expected_outcomes: careerOutcomes,
|
||
style_options: [
|
||
{ label: "明确发生且时间吻合", answer_class: "yes" },
|
||
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
|
||
{ label: "明确没有发生", answer_class: "no" },
|
||
{ label: "这段记不清楚", answer_class: "unsure" },
|
||
],
|
||
}],
|
||
contrastPacket: packet,
|
||
candidatesSeparated: false,
|
||
});
|
||
assert.equal(plan.next_followup?.semantic_key, "career.2023.dasha_activation");
|
||
assert.match(plan.next_followup?.choice_frame?.period ?? "", /2023 年前后/);
|
||
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d24/);
|
||
});
|
||
|
||
test("career and relationship training still discriminates before family or occupation coverage", () => {
|
||
const d24Outcomes = [
|
||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:07", "05:12"] },
|
||
{ answer_class: "weak_yes", supports: ["05:07"], conflicts: ["05:00", "05:12"] },
|
||
{ answer_class: "no", supports: ["05:12"], conflicts: ["05:00", "05:07"] },
|
||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||
];
|
||
const inferenceCandidates = [
|
||
{ id: "05:00", time: "05:00", cluster_range: ["05:00", "05:00"], prior_score: 23, posterior_score: 23, probability: 0.58, status: "active", rank: 1, strong_conflict_count: 0 },
|
||
{ id: "05:07", time: "05:07", cluster_range: ["05:07", "05:07"], prior_score: 17, posterior_score: 17, probability: 0.21, status: "active", rank: 2, strong_conflict_count: 0 },
|
||
{ id: "05:12", time: "05:12", cluster_range: ["05:12", "05:12"], prior_score: 17, posterior_score: 17, probability: 0.21, status: "active", rank: 3, strong_conflict_count: 0 },
|
||
];
|
||
const evidence = [
|
||
{ id: "edu-2016", status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null, eventKind: "education_milestone" },
|
||
{ id: "career-exit", status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2020-10-01", occurredTo: null, eventKind: "career_exit" },
|
||
{ id: "career-entry", status: "confirmed", domain: "career", datePrecision: "month", occurredFrom: "2020-04-01", occurredTo: null, eventKind: "career_entry" },
|
||
{ id: "rel-start", status: "confirmed", domain: "relationship", datePrecision: "month", occurredFrom: "2024-05-01", occurredTo: null, eventKind: "relationship_start" },
|
||
{ id: "rel-end", status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-08-08", occurredTo: null, eventKind: "relationship_end" },
|
||
];
|
||
const familyProbe = {
|
||
year: 2013,
|
||
year_label: "2013 年 3 月前后",
|
||
domain: "family" as const,
|
||
event_family: "家人结婚、添丁或住院",
|
||
source: "dasha_boundary" as const,
|
||
tracks: ["vimshottari", "narayana"] as const,
|
||
tracks_agree: true,
|
||
unique_minute_claim: false as const,
|
||
user_meaning: "时间范围锁定 2013 年 3 月前后。",
|
||
role: "distinguish" as const,
|
||
phase: "candidate_discriminator" as const,
|
||
information_gain: 1.2,
|
||
semantic_key: "family.2013.03.dasha_boundary",
|
||
candidate_split_hash: "family.2013.03",
|
||
candidate_ids: ["05:00", "05:07", "05:12"],
|
||
expected_outcomes: [
|
||
{ answer_class: "yes" as const, supports: ["05:00"], conflicts: ["05:07", "05:12"] },
|
||
{ answer_class: "weak_yes" as const, supports: ["05:07"], conflicts: ["05:00", "05:12"] },
|
||
{ answer_class: "no" as const, supports: ["05:12"], conflicts: ["05:00", "05:07"] },
|
||
{ answer_class: "unsure" as const, supports: [], conflicts: [] },
|
||
],
|
||
style_options: [
|
||
{ label: "明确发生且时间吻合", answer_class: "yes" },
|
||
{ label: "发生过但程度较弱", answer_class: "weak_yes" },
|
||
{ label: "明确没有发生", answer_class: "no" },
|
||
{ label: "这段记不清楚", answer_class: "unsure" },
|
||
],
|
||
} satisfies DiscriminatingEventProbe;
|
||
const dossier = {
|
||
evidence,
|
||
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
|
||
latestResult: {
|
||
resultId: "result-unseparated",
|
||
candidates: [],
|
||
decisionReceipt: {
|
||
discriminating_event_probes: [familyProbe],
|
||
inference_state: {
|
||
algorithm_version: "rectification-inference-v1",
|
||
candidate_set_id: "05:00-05:12:05:00,05:07,05:12",
|
||
revision: 1,
|
||
phase: "discrimination",
|
||
result_status: "discriminating",
|
||
range_start: "05:00",
|
||
range_end: "05:12",
|
||
candidates: inferenceCandidates,
|
||
events: [
|
||
{ id: "edu-2016", year: 2016, usage: "training", domain: "education", precision: "year" },
|
||
{ id: "career-exit", year: 2020, usage: "training", domain: "career", precision: "month" },
|
||
{ id: "career-entry", year: 2020, usage: "holdout", domain: "career", precision: "month" },
|
||
{ id: "rel-start", year: 2024, usage: "training", domain: "relationship", precision: "month" },
|
||
{ id: "rel-end", year: 2024, usage: "training", domain: "relationship", precision: "day" },
|
||
],
|
||
probes: [{
|
||
id: "contrast:varga.d24.05:00|05:07|05:12",
|
||
year: 0,
|
||
domain: "education",
|
||
source: "varga_contrast",
|
||
question: "引擎给出的区分机会绑定 D24。",
|
||
semantic_key: "varga.d24.05:00|05:07|05:12",
|
||
candidate_ids: ["05:00", "05:07", "05:12"],
|
||
information_gain: 2.5,
|
||
expected_outcomes: d24Outcomes,
|
||
candidate_split_hash: "05:00-05:12:varga.d24",
|
||
}],
|
||
answered_probes: [],
|
||
rounds: [],
|
||
entropy: 1.5,
|
||
representative_time: "05:00",
|
||
credible_range: ["05:00", "05:12"],
|
||
},
|
||
window_scan: {
|
||
scanned: true,
|
||
d24_lagna_count: 3,
|
||
d24_candidates_differ: true,
|
||
transitions: [
|
||
{ layer: "d24", at: "05:07", from_sign: "巨蟹座", to_sign: "狮子座" },
|
||
{ layer: "d24", at: "05:12", from_sign: "狮子座", to_sign: "处女座" },
|
||
],
|
||
},
|
||
},
|
||
},
|
||
case: { acceptedTime: null },
|
||
};
|
||
const decision = decideFromDossier(dossier);
|
||
assert.equal(decision.nextAction, "ask_candidate_discriminator");
|
||
assert.equal(decision.sessionOutcome, "discriminate_candidates");
|
||
const packet = contrastPacketFromDossier(dossier);
|
||
const plan = buildMethodFollowupPlan({
|
||
evidence,
|
||
eventProbes: [familyProbe],
|
||
contrastPacket: packet,
|
||
sessionOutcome: decision.sessionOutcome,
|
||
});
|
||
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
|
||
assert.equal(plan.next_followup?.semantic_key, "family.2013.03.dasha_boundary");
|
||
assert.ok(plan.next_followup?.choice_frame);
|
||
assert.match(plan.next_followup?.choice_frame?.period ?? "", /2013 年 3 月前后/);
|
||
assert.doesNotMatch(plan.next_followup?.choice_frame?.prompt ?? "", /2016 年前后/);
|
||
assert.equal(conversationalSessionOutcome({
|
||
selectionAllowed: false,
|
||
proposeAllowed: false,
|
||
confirmationAllowed: false,
|
||
nextFollowup: plan.next_followup,
|
||
methods: plan.methods,
|
||
discriminatorProbe: selectDiscriminatorProbe(packet),
|
||
candidateScores: [],
|
||
trainingGateOpen: true,
|
||
evidence,
|
||
}), "discriminate_candidates");
|
||
assert.equal(plan.methods.find((item) => item.method_id === "relatives")?.status, "uncovered");
|
||
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
|
||
});
|
||
|
||
test("public candidate cards follow the inference ranking and hide an inconsistent state", () => {
|
||
const decision = decideRectification({
|
||
engineCeiling: ENGINE_OPEN,
|
||
methodCoverageAll: true,
|
||
trainingGateOpen: true,
|
||
candidateScores: SEPARATED,
|
||
holdoutValidation: "passed",
|
||
});
|
||
const base = {
|
||
candidates: [
|
||
{ candidateId: "c-0507", time: "05:07", rank: 1, relativeSupport: 13, tiedMinuteCount: 1 },
|
||
{ candidateId: "c-0500", time: "05:00", rank: 2, relativeSupport: 12, tiedMinuteCount: 1 },
|
||
{ candidateId: "c-0515", time: "05:15", rank: 3, relativeSupport: 10, tiedMinuteCount: 1 },
|
||
],
|
||
representativeTime: "05:07",
|
||
decisionReceipt: {
|
||
inference_state: {
|
||
algorithm_version: "rectification-inference-v1",
|
||
candidate_set_id: "05:00-05:15:05:00,05:07,05:15",
|
||
revision: 2,
|
||
phase: "discrimination",
|
||
result_status: "discriminating",
|
||
range_start: "05:00",
|
||
range_end: "05:15",
|
||
candidates: [
|
||
{ id: "05:07", time: "05:07", cluster_range: ["05:07", "05:07"], prior_score: 13, posterior_score: 15, probability: 0.4, status: "active", rank: 2, strong_conflict_count: 0 },
|
||
{ id: "05:00", time: "05:00", cluster_range: ["05:00", "05:00"], prior_score: 12, posterior_score: 20, probability: 0.6, status: "active", rank: 1, strong_conflict_count: 0 },
|
||
{ id: "05:15", time: "05:15", cluster_range: ["05:15", "05:15"], prior_score: 10, posterior_score: 30, probability: 0, status: "eliminated", rank: 3, strong_conflict_count: 1 },
|
||
],
|
||
events: [], probes: [], answered_probes: [], rounds: [], entropy: 0.67,
|
||
representative_time: "05:00",
|
||
credible_range: ["05:00", "05:07"],
|
||
},
|
||
},
|
||
};
|
||
const missingInference = overlayPublicDecision({
|
||
...base,
|
||
decisionReceipt: {},
|
||
}, decision);
|
||
assert.deepEqual(missingInference.candidates, []);
|
||
assert.equal(missingInference.representativeTime, null);
|
||
assert.equal(missingInference.selectionAllowed, false);
|
||
assert.equal(missingInference.can_adopt, false);
|
||
|
||
const projected = overlayPublicDecision(base, decision);
|
||
assert.deepEqual(projected.candidates.map((item) => item.time), ["05:00", "05:07"]);
|
||
assert.deepEqual(projected.candidates.map((item) => item.relativeSupport), [20, 15]);
|
||
assert.equal(projected.representativeTime, "05:00");
|
||
assert.deepEqual(projected.credible_range, ["05:00", "05:07"]);
|
||
|
||
const incompleteCandidateStates = [
|
||
base.decisionReceipt.inference_state.candidates.slice(0, 2),
|
||
[
|
||
...base.decisionReceipt.inference_state.candidates.slice(0, 2),
|
||
{ ...base.decisionReceipt.inference_state.candidates[0], status: "eliminated", probability: 0 },
|
||
],
|
||
];
|
||
for (const candidates of incompleteCandidateStates) {
|
||
const incomplete = overlayPublicDecision({
|
||
...base,
|
||
decisionReceipt: {
|
||
inference_state: {
|
||
...base.decisionReceipt.inference_state,
|
||
candidates,
|
||
},
|
||
},
|
||
}, decision);
|
||
assert.deepEqual(incomplete.candidates, []);
|
||
assert.equal(incomplete.representativeTime, null);
|
||
assert.equal(incomplete.selectionAllowed, false);
|
||
assert.equal(incomplete.can_adopt, false);
|
||
}
|
||
|
||
const malformedInferenceStates = [
|
||
{ ...base.decisionReceipt.inference_state, revision: undefined },
|
||
{ ...base.decisionReceipt.inference_state, candidate_set_id: "" },
|
||
{ ...base.decisionReceipt.inference_state, candidate_set_id: "wrong-set" },
|
||
{ ...base.decisionReceipt.inference_state, range_start: undefined },
|
||
{ ...base.decisionReceipt.inference_state, events: [null] },
|
||
{ ...base.decisionReceipt.inference_state, probes: [{ id: "broken" }] },
|
||
{ ...base.decisionReceipt.inference_state, answered_probes: [{ probe_id: "broken" }] },
|
||
{ ...base.decisionReceipt.inference_state, rounds: [{ round: 1 }] },
|
||
{
|
||
...base.decisionReceipt.inference_state,
|
||
candidates: base.decisionReceipt.inference_state.candidates.map((candidate, index) => (
|
||
index === 0 ? { ...candidate, status: undefined } : candidate
|
||
)),
|
||
},
|
||
{
|
||
...base.decisionReceipt.inference_state,
|
||
candidates: base.decisionReceipt.inference_state.candidates.map((candidate, index) => (
|
||
index === 0 ? { ...candidate, posterior_score: undefined } : candidate
|
||
)),
|
||
},
|
||
{
|
||
...base.decisionReceipt.inference_state,
|
||
candidates: base.decisionReceipt.inference_state.candidates.map((candidate, index) => (
|
||
index === 0 ? { ...candidate, cluster_range: undefined } : candidate
|
||
)),
|
||
},
|
||
];
|
||
for (const inferenceState of malformedInferenceStates) {
|
||
const malformed = overlayPublicDecision({
|
||
...base,
|
||
decisionReceipt: { inference_state: inferenceState },
|
||
}, decision);
|
||
assert.deepEqual(malformed.candidates, []);
|
||
assert.equal(malformed.representativeTime, null);
|
||
assert.equal(malformed.selectionAllowed, false);
|
||
assert.equal(malformed.can_adopt, false);
|
||
}
|
||
|
||
const inconsistent = overlayPublicDecision({
|
||
...base,
|
||
decisionReceipt: {
|
||
inference_state: {
|
||
...base.decisionReceipt.inference_state,
|
||
representative_time: "05:15",
|
||
},
|
||
},
|
||
}, decision);
|
||
assert.deepEqual(inconsistent.candidates, []);
|
||
assert.equal(inconsistent.selectionAllowed, false);
|
||
assert.equal(inconsistent.can_adopt, false);
|
||
|
||
for (const credibleRange of [["04:00", "04:10"], ["05:07", "05:00"]] as const) {
|
||
const rangeMismatch = overlayPublicDecision({
|
||
...base,
|
||
decisionReceipt: {
|
||
inference_state: {
|
||
...base.decisionReceipt.inference_state,
|
||
credible_range: credibleRange,
|
||
},
|
||
},
|
||
}, decision);
|
||
assert.deepEqual(rangeMismatch.candidates, []);
|
||
assert.equal(rangeMismatch.representativeTime, null);
|
||
assert.equal(rangeMismatch.credible_range, null);
|
||
assert.equal(rangeMismatch.selectionAllowed, false);
|
||
assert.equal(rangeMismatch.can_adopt, false);
|
||
}
|
||
});
|
||
|
||
test("turn decision and safe case projection share session_outcome and selection_allowed", () => {
|
||
const base = parseV9CaseDossier(dossierFixture());
|
||
assert.ok(base);
|
||
const dossier = parseV9CaseDossier(dossierFixture({
|
||
latestResult: candidateSnapshotFixture({
|
||
selectionAllowed: true,
|
||
representativeTime: "05:02",
|
||
evidenceLedgerFingerprint: evidenceLedgerFingerprint(base.evidence),
|
||
decisionReceipt: {
|
||
inference_state: {
|
||
algorithm_version: "rectification-inference-v1",
|
||
candidate_set_id: "04:55-05:02:04:55,05:02",
|
||
revision: 0,
|
||
phase: "discrimination",
|
||
result_status: "discriminating",
|
||
range_start: "04:55",
|
||
range_end: "05:02",
|
||
candidates: [
|
||
{ id: "05:02", time: "05:02", cluster_range: ["05:02", "05:02"], prior_score: 58, posterior_score: 58, probability: 0.58, status: "active", rank: 1, strong_conflict_count: 0 },
|
||
{ id: "04:55", time: "04:55", cluster_range: ["04:55", "04:55"], prior_score: 42, posterior_score: 42, probability: 0.42, status: "active", rank: 2, strong_conflict_count: 0 },
|
||
],
|
||
events: [],
|
||
probes: [],
|
||
answered_probes: [],
|
||
rounds: [],
|
||
entropy: 0.98,
|
||
representative_time: "05:02",
|
||
credible_range: ["04:55", "05:02"],
|
||
},
|
||
},
|
||
}),
|
||
}));
|
||
const compute = parseV9ComputeProjection(computeFixture({
|
||
baselineProfileFingerprint: "c".repeat(64),
|
||
}));
|
||
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");
|
||
const choice = readSource("../src/lib/rectification-agentic/v9/answer-choice.ts");
|
||
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, /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/);
|
||
assert.doesNotMatch(choice, /sessionOutcomeFromGate/);
|
||
assert.doesNotMatch(refresh, /sessionOutcomeFromGate/);
|
||
assert.doesNotMatch(tools, /sessionOutcomeFromGate/);
|
||
assert.doesNotMatch(interview, /selectionAllowed: dossier\.latestResult/);
|
||
assert.doesNotMatch(refresh, /selection_allowed: dossier\.latestResult\?\.selectionAllowed/);
|
||
assert.doesNotMatch(tools, /selection_allowed: decision\?\.selectionAllowed \?\? latest/);
|
||
assert.doesNotMatch(tools, /propose_allowed: decision\?\.proposeAllowed \?\? proposeAllowed/);
|
||
assert.match(tools, /evidence: parsed\.evidence/);
|
||
assert.match(followup, /evidence: input\.evidence/);
|
||
assert.match(adapter, /trainingGateOpen: trainingGate\.open/);
|
||
assert.match(adapter, /blockingMethodsCovered/);
|
||
});
|