317 lines
13 KiB
TypeScript
317 lines
13 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
decideRectification,
|
|
publicDecisionFields,
|
|
} from "../src/lib/rectification-agentic/core/rectification-decision.ts";
|
|
import { decideNextAction } from "../src/lib/rectification-agentic/core/decide-next-action.ts";
|
|
import { contrastPacketFromDossier, overlayPublicDecision } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
|
import { conversationalSessionOutcome } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
|
|
|
const SEPARATED = [
|
|
{ time: "04:48", score: 58 },
|
|
{ time: "04:49", score: 42 },
|
|
];
|
|
|
|
function readSource(relative: string) {
|
|
return readFileSync(new URL(relative, import.meta.url), "utf8");
|
|
}
|
|
|
|
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);
|
|
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"],
|
|
]);
|
|
});
|
|
|
|
test("public candidate cards follow the inference ranking and hide an inconsistent state", () => {
|
|
const decision = decideRectification({
|
|
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 [["05:00", "05:00"], ["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("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 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(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/);
|
|
});
|