Files
Jyotisha/frontend/tests/rectification-decide-next-action.test.ts
T
Jesse_Chen 9f011194f2
Independent Staging Quality Gate / validate (push) Successful in 13m0s
Independent Staging Quality Gate / publish (push) Successful in 10m58s
fix(rectification): centralize delivery authority
2026-09-01 01:13:04 +08:00

582 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { OPEN_ENGINE_CAPABILITY_CEILING } from "./rectification-v9-test-support.ts";
import {
askedEventProbeKeysFromLedgerEvidence,
buildCandidateContrastPacket,
mentionedVargaKeysFromLedgerEvidence,
selectDiscriminatorProbe,
} from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
import { evaluateCandidateSeparation } from "../src/lib/rectification-agentic/core/candidate-separation.ts";
import { decideNextAction } from "../src/lib/rectification-agentic/core/decide-next-action.ts";
import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts";
import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
import {
classifySnapshotStaleReason,
scoreableSnapshotIsCurrent,
snapshotIsCurrent,
storedSnapshotIsCurrent,
type CandidateSnapshotSource,
} from "../src/lib/rectification-agentic/core/snapshot-source.ts";
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 },
{ id: "c2", time: "05:02", score: 33 },
];
const SEPARATED = [
{ id: "c0", time: "05:00", score: 62 },
{ id: "c1", time: "05:01", score: 22 },
{ id: "c2", time: "05:02", score: 16 },
];
const CONTRAST_PROBE = selectDiscriminatorProbe(buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:02:05:00,05:01,05:02",
calculationResultId: "11111111-1111-4111-8111-111111111111",
engineProbes: [{
semantic_key: "career.2018.dasha_activation",
candidate_split_hash: "career:2018:05:00|05:01",
domain: "career",
year: 2018,
user_meaning: "2018 年前后是否职责明显加重?",
information_gain: 0.21,
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:01", "05:02"] },
{ answer_class: "no", supports: ["05:01"], conflicts: ["05:00"] },
],
}],
vargaDifferences: [{ layer: "d10", signs: ["巨蟹", "狮子", "处女"] }],
}));
function source(overrides: Partial<CandidateSnapshotSource> = {}): CandidateSnapshotSource {
return {
birthProfileFingerprint: "birth-a",
scoreableEvidenceFingerprint: "score-a",
inferenceRevision: 3,
candidateSetVersion: "set-a",
scoringPolicyVersion: "policy-v2",
...overrides,
};
}
test("does not adopt when method coverage is complete but candidates remain tied", () => {
const next = decideNextAction({
methodCoverageAll: true,
proposeAllowed: true,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
});
assert.equal(next.type, "ask_candidate_discriminator");
assert.equal(next.separation.status, "not_separated");
assert.ok((next.probe?.expectedOutcomes.length ?? 0) >= 2);
});
test("tied candidates prefer a discriminator probe with at least two predicted outcomes", () => {
const next = decideNextAction({
methodCoverageAll: true,
proposeAllowed: true,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
});
assert.equal(next.type, "ask_candidate_discriminator");
assert.ok((next.probe?.expectedOutcomes.length ?? 0) >= 2);
});
test("holdout that has not passed cannot enter ready_to_adopt", () => {
const next = decideNextAction({
methodCoverageAll: true,
proposeAllowed: true,
candidateScores: SEPARATED,
holdoutValidation: "not_started",
});
assert.equal(next.type, "ask_holdout_validation");
assert.notEqual(next.type, "ready_to_adopt");
});
test("separated candidates with holdout passed are ready to adopt", () => {
const next = decideNextAction({
methodCoverageAll: true,
proposeAllowed: true,
candidateScores: SEPARATED,
holdoutValidation: "passed",
});
assert.equal(next.type, "ready_to_adopt");
});
test("missing method coverage stays in fact collection even if scores look separated", () => {
const next = decideNextAction({
methodCoverageAll: false,
proposeAllowed: true,
candidateScores: SEPARATED,
discriminatorProbe: CONTRAST_PROBE,
});
assert.equal(next.type, "ask_fact_collection");
});
test("unseparated scores still discriminate when training is open and a probe exists", () => {
const next = decideNextAction({
methodCoverageAll: false,
trainingGateOpen: true,
proposeAllowed: false,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
});
assert.equal(next.type, "ask_candidate_discriminator");
assert.equal(decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: true,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
}).sessionOutcome, "discriminate_candidates");
assert.equal(decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
trainingGateOpen: true,
candidateScores: [],
discriminatorProbe: CONTRAST_PROBE,
}).sessionOutcome, "discriminate_candidates");
});
test("dated evidence suppresses an ordinary probe for the same domain and year", () => {
const askedKeys = askedEventProbeKeysFromLedgerEvidence([{
status: "confirmed",
domain: "career",
occurredFrom: "2018-01-01",
}]);
const packet = buildCandidateContrastPacket({
candidateSetVersion: "set-a",
engineProbes: [{
semantic_key: "career.2018.dasha_activation",
candidate_split_hash: "career:2018:split",
domain: "career",
year: 2018,
user_meaning: "2018 年前后是否有职业变化?",
information_gain: 0.4,
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:07"] },
{ answer_class: "no", supports: ["05:07"], conflicts: ["05:00"] },
],
}],
askedKeys,
});
assert.equal(selectDiscriminatorProbe(packet), null);
});
test("ordinary education evidence does not suppress a structured D24 probe", () => {
const askedKeys = askedEventProbeKeysFromLedgerEvidence([{
status: "confirmed",
domain: "education",
occurredFrom: "2016-09-01",
}]);
const packet = buildCandidateContrastPacket({
candidateSetVersion: "set-a",
engineProbes: [{
semantic_key: "varga.d24",
candidate_split_hash: "d24:split",
domain: "education",
year: 2016,
choice_kind: "varga_style",
user_meaning: "这段学习经历更接近哪种过程?",
information_gain: 0.8,
expected_outcomes: [
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:07"] },
{ answer_class: "no", supports: ["05:07"], conflicts: ["05:00"] },
],
}],
askedKeys,
});
assert.equal(selectDiscriminatorProbe(packet)?.semanticKey, "varga.d24");
});
test("whole-window D9/D10 signs do not synthesize a discriminator without remaining-minute splits", () => {
const packet = buildCandidateContrastPacket({
candidateSetVersion: "set-a",
calculationResultId: "22222222-2222-4222-8222-222222222222",
vargaDifferences: [
{ layer: "d9", signs: ["天秤", "天蝎", "射手"] },
{ layer: "d10", signs: ["巨蟹", "狮子", "处女"] },
],
});
assert.equal(selectDiscriminatorProbe(packet), null);
});
test("exam quality and occupation notes keep remaining D10 and drop yearless D4/D24 from selection", () => {
const evidence = [
{ domain: "education", eventKind: "education_interruption", summary: "高考失利复读", status: "confirmed" },
{ domain: "occupation", eventKind: "occupation_note", summary: "互联网程序员 / 前端", status: "confirmed" },
];
const mentionedKeys = mentionedVargaKeysFromLedgerEvidence(evidence);
const packet = buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:04",
calculationResultId: "22222222-2222-4222-8222-222222222222",
candidateTimes: ["05:00", "05:03", "05:04"],
transitions: [
{ layer: "d4", at: "05:03", from_sign: "金牛座", to_sign: "双子座" },
{ layer: "d4", at: "05:04", from_sign: "双子座", to_sign: "巨蟹座" },
{ layer: "d10", at: "05:03", from_sign: "巨蟹座", to_sign: "狮子座" },
{ layer: "d10", at: "05:04", from_sign: "狮子座", to_sign: "处女座" },
{ layer: "d24", at: "05:03", from_sign: "白羊座", to_sign: "金牛座" },
{ layer: "d24", at: "05:04", from_sign: "金牛座", to_sign: "双子座" },
],
mentionedKeys,
});
assert.ok(packet.probes.some((item) => item.semanticKey.startsWith("varga.d10.")));
assert.ok(packet.probes.some((item) => item.semanticKey.startsWith("varga.d24.")));
const probe = selectDiscriminatorProbe(packet, { mentionedKeys });
assert.ok(probe);
assert.match(probe.semanticKey, /varga\.d10/);
assert.equal(probe.domain, "career");
assert.equal(probe.choiceKind, "varga_style");
});
test("encoded D24/D10/D4 remaining splits leave no discriminator", () => {
const packet = buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:04",
candidateTimes: ["05:00", "05:03", "05:04"],
transitions: [
{ layer: "d4", at: "05:00" },
{ layer: "d4", at: "05:03" },
{ layer: "d10", at: "05:00" },
{ layer: "d24", at: "05:00" },
],
askedKeys: ["varga.d4", "varga.d10", "varga.d24", "varga.d5"],
});
assert.equal(selectDiscriminatorProbe(packet), null);
});
test("remaining-candidate D24 split is not a selectable discriminator", () => {
const packet = buildCandidateContrastPacket({
candidateSetVersion: "05:00-05:07",
calculationResultId: "22222222-2222-4222-8222-222222222222",
vargaDifferences: [
{ layer: "d10", signs: ["巨蟹座", "狮子座", "处女座"] },
],
candidateTimes: ["05:00", "05:06", "05:07"],
transitions: [
{ layer: "d10", at: "05:00" },
{ layer: "d10", at: "05:15" },
{ layer: "d24", at: "05:00" },
{ layer: "d24", at: "05:06" },
],
askedKeys: ["education.2016", "varga.d10"],
});
const probe = selectDiscriminatorProbe(packet);
assert.equal(probe, null);
});
test("user stop completes with a credible range instead of trapping on exact-minute", () => {
const next = decideNextAction({
methodCoverageAll: true,
proposeAllowed: true,
selectionAllowed: true,
userStopped: true,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
});
assert.equal(next.type, "complete_with_range");
});
test("user stop skips remaining collection and holdout when candidates already exist", () => {
const incomplete = decideNextAction({
methodCoverageAll: false,
proposeAllowed: true,
selectionAllowed: true,
userStopped: true,
candidateScores: TIED,
});
assert.equal(incomplete.type, "complete_with_range");
const separated = decideNextAction({
methodCoverageAll: false,
proposeAllowed: true,
selectionAllowed: true,
userStopped: true,
candidateScores: [
{ time: "04:48", score: 58 },
{ time: "04:49", score: 42 },
],
holdoutValidation: "not_started",
});
assert.equal(separated.type, "complete_with_range");
const stopped = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: false,
userStopped: true,
candidateScores: [
{ time: "04:48", score: 58 },
{ time: "04:49", score: 42 },
],
holdoutValidation: "not_started",
});
assert.equal(stopped.sessionOutcome, "provisional_range_user_stopped");
assert.equal(stopped.completionStatus, "provisional_range_user_stopped");
assert.equal(stopped.validated, false);
});
test("discrimination waits for three training events after reserving holdout", () => {
const two = trainingScoreableGate([
{ id: "e1", status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
{ id: "e2", status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2020-04-01", occurredTo: null },
]);
const three = trainingScoreableGate([
{ id: "e1", status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
{ id: "e2", status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2020-04-01", occurredTo: null },
{ id: "e3", status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2024-08-01", occurredTo: null },
]);
const four = trainingScoreableGate([
{ id: "e1", status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null },
{ id: "e2", status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2020-04-01", occurredTo: null },
{ id: "e3", status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2020-10-01", occurredTo: null },
{ id: "e4", status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2024-08-01", occurredTo: null },
]);
assert.equal(two.open, false);
assert.equal(two.holdoutCount, 1);
assert.equal(three.open, false);
assert.equal(three.holdoutCount, 1);
assert.equal(three.trainingCount, 2);
assert.equal(four.open, true);
assert.equal(four.trainingCount, 3);
assert.equal(four.holdoutCount, 1);
assert.equal(decideNextAction({
methodCoverageAll: true,
trainingGateOpen: two.open,
proposeAllowed: false,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
}).type, "ask_fact_collection");
assert.equal(decideNextAction({
methodCoverageAll: true,
trainingGateOpen: three.open,
proposeAllowed: false,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
}).type, "ask_fact_collection");
assert.equal(decideNextAction({
methodCoverageAll: true,
trainingGateOpen: four.open,
proposeAllowed: false,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
}).type, "ask_candidate_discriminator");
});
test("holdout passed is a validated range, not a user-stop close", () => {
const validated = decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
candidateScores: [
{ time: "04:48", score: 58 },
{ time: "04:49", score: 42 },
],
holdoutValidation: "passed",
});
assert.equal(validated.sessionOutcome, "validated_range");
assert.equal(validated.completionStatus, "validated_range");
assert.equal(validated.validated, true);
assert.equal(validated.canConfirmExactMinute, false);
});
test("tied candidates with no remaining discriminator offer a provisional range", () => {
const next = decideNextAction({
methodCoverageAll: true,
proposeAllowed: true,
candidateScores: TIED,
});
assert.equal(next.type, "offer_provisional_range");
});
test("non-scoreable occupation note does not change the scoreable evidence fingerprint", () => {
const dated = [{
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1",
status: "confirmed" as const,
eventKind: "business_start",
domain: "career",
occurredFrom: "2026-07-19",
occurredTo: null,
datePrecision: "day" as const,
summary: "注册公司",
}];
const withNote = [...dated, {
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2",
status: "confirmed" as const,
eventKind: "occupation_note",
domain: "occupation",
occurredFrom: null,
occurredTo: null,
datePrecision: "unknown" as const,
summary: "长期一直是程序员",
}];
const before = evidenceLedgerFingerprint(dated as never);
const after = evidenceLedgerFingerprint(withNote as never);
assert.equal(after, before);
const snapshot = source({ scoreableEvidenceFingerprint: before });
const current = source({ scoreableEvidenceFingerprint: after, inferenceRevision: snapshot.inferenceRevision });
assert.equal(snapshotIsCurrent(snapshot, current), true);
assert.equal(scoreableSnapshotIsCurrent(snapshot, current), true);
});
test("scoreable occupation evidence requires a new inference revision", () => {
const before = source();
const afterScoreable = source({
scoreableEvidenceFingerprint: "score-b",
inferenceRevision: before.inferenceRevision + 1,
});
assert.equal(classifySnapshotStaleReason(before, afterScoreable), "scoreable_evidence_changed");
assert.equal(afterScoreable.inferenceRevision, before.inferenceRevision + 1);
});
test("candidate cards must be created from the current inference and scoreable revisions", () => {
const current = source({ inferenceRevision: 4 });
const card = source({ inferenceRevision: 3 });
assert.equal(classifySnapshotStaleReason(card, current), "inference_revision_changed");
const matching = source({ inferenceRevision: 4 });
assert.equal(snapshotIsCurrent(matching, current), true);
});
test("stale reasons distinguish birth profile from scoreable evidence", () => {
const current = source();
assert.equal(
classifySnapshotStaleReason(source({ birthProfileFingerprint: "birth-b" }), current),
"birth_profile_changed",
);
assert.equal(
classifySnapshotStaleReason(source({ scoreableEvidenceFingerprint: "score-b" }), current),
"scoreable_evidence_changed",
);
assert.equal(
classifySnapshotStaleReason(source({ candidateSetVersion: "set-b" }), current),
"candidate_set_superseded",
);
});
test("missing scoreable fingerprint is stale, not current", () => {
const current = source();
assert.equal(scoreableSnapshotIsCurrent(source({ scoreableEvidenceFingerprint: "" }), current), false);
assert.equal(scoreableSnapshotIsCurrent(current, source({ scoreableEvidenceFingerprint: "" })), false);
assert.equal(
classifySnapshotStaleReason(source({ scoreableEvidenceFingerprint: "" }), current),
"fingerprint_missing",
);
assert.equal(storedSnapshotIsCurrent(null, current), true);
assert.equal(storedSnapshotIsCurrent(source({ scoreableEvidenceFingerprint: "" }), current), false);
assert.equal(storedSnapshotIsCurrent(current, current), true);
});
test("stale snapshot keeps discrimination instead of returning to collection", () => {
const next = decideNextAction({
methodCoverageAll: true,
trainingGateOpen: true,
snapshotCurrent: false,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
});
assert.equal(next.type, "ask_candidate_discriminator");
assert.equal(decideRectification({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
snapshotCurrent: false,
candidateScores: TIED,
discriminatorProbe: CONTRAST_PROBE,
}).sessionOutcome, "discriminate_candidates");
assert.equal(decideNextAction({
methodCoverageAll: false,
trainingGateOpen: false,
snapshotCurrent: false,
candidateScores: TIED,
}).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({
engineCeiling: OPEN_ENGINE_CAPABILITY_CEILING,
methodCoverageAll: true,
trainingGateOpen: true,
candidateScores: [{ time: "05:00", score: 80 }],
holdoutValidation: "unavailable",
});
// 原值是 adopt_representativeholdout unavailable 且用户未停止只能提供 review-only range。
assert.equal(decision.sessionOutcome, "provisional_range");
assert.equal(decision.canAdopt, false);
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");
assert.equal(separation.sufficient, false);
assert.deepEqual(separation.credibleRange, ["05:00", "05:01", "05:02"]);
assert.equal(separation.representativeTime, "05:00");
});
test("final report does not claim executed techniques without calculationResultId", () => {
const report = buildSkillVerificationReport({
representativeTime: "05:00",
widthMinutes: 3,
candidates: [
{ time: "05:00", rank: 1, relativeSupport: 34 },
{ time: "05:01", rank: 2, relativeSupport: 33 },
{ time: "05:02", rank: 3, relativeSupport: 33 },
],
separation: evaluateCandidateSeparation(TIED),
techniqueAuditTable: [
{ technique: "D9", status: "executed", note: "no id" },
{ technique: "D10", status: "executed", note: "has id", calculation_result_id: "33333333-3333-4333-8333-333333333333" },
],
eventFitRate: {
matched: 8,
total: 9,
percent: 89,
label: "8/9",
user_meaning: "这段时间窗对已收事件有一定解释力",
unique_minute_claim: false,
},
});
assert.match(report, /基本并列/);
assert.match(report, /事件拟合程度,不是候选区分程度/);
assert.match(report, /\| D9 \| input_covered \|/);
assert.match(report, /\| D10 \| executed \|/);
assert.doesNotMatch(report, /当前推荐/);
});
test("core barrel re-exports decideNextAction without duplicating session helpers", () => {
const index = readFileSync(new URL("../src/lib/rectification-agentic/core/index.ts", import.meta.url), "utf8");
assert.match(
index,
/export \{\s*decideNextAction,\s*type DecideNextActionInput,\s*type RectificationNextAction,\s*\} from "\.\/decide-next-action\.ts"/,
);
assert.doesNotMatch(index, /export \* from "\.\/decide-next-action\.ts"/);
assert.match(index, /export \* from "\.\/rectification-decision\.ts"/);
});
test("MethodFollowup unions include holdout validation kinds used by next_followup", () => {
const source = readFileSync(new URL("../src/lib/rectification-agentic/v9/method-followup.ts", import.meta.url), "utf8");
const methodId = source.match(/export type MethodFollowup = Readonly<\{[\s\S]*?method_id: ([^;]+);/)?.[1] ?? "";
const askTheme = source.match(/export type MethodFollowup = Readonly<\{[\s\S]*?ask_theme: ([^;]+);/)?.[1] ?? "";
assert.match(methodId, /"holdout_validation"/);
assert.match(askTheme, /"holdout"/);
assert.match(source, /ask_theme: "holdout"/);
assert.match(source, /method_id: "holdout_validation"/);
});