import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; 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 { 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({ methodCoverageAll: false, trainingGateOpen: true, candidateScores: TIED, discriminatorProbe: CONTRAST_PROBE, }).sessionOutcome, "discriminate_candidates"); assert.equal(decideRectification({ 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/D24 but prefer unmentioned D4", () => { 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\.d4/); assert.equal(probe.domain, "relocation"); assert.ok(probe.informationGain > 0); assert.ok(probe.expectedOutcomes.length >= 2); const mapped = new Set(probe.expectedOutcomes.flatMap((row) => [ ...row.supportsCandidateIds, ...row.conflictsCandidateIds, ])); assert.ok(mapped.size >= 2); }); 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 beats window D10 signs", () => { 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.ok(probe); assert.match(probe.semanticKey, /varga\.d24/); assert.equal(probe.domain, "education"); assert.deepEqual(probe.expectedOutcomes[0]?.supportsCandidateIds, ["05:00"]); assert.ok(probe.expectedOutcomes[0]?.conflictsCandidateIds.includes("05:06")); assert.ok(probe.expectedOutcomes.every((row) => ( row.supportsCandidateIds.every((id) => id.includes(":")) ))); }); 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({ 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({ 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({ 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({ methodCoverageAll: true, trainingGateOpen: true, candidateScores: [{ time: "05:00", score: 80 }], holdoutValidation: "unavailable", }); assert.equal(decision.sessionOutcome, "adopt_representative"); assert.equal(decision.canConfirmExactMinute, false); assert.doesNotMatch( buildSkillVerificationReport({ representativeTime: "05:00", widthMinutes: 0, candidates: [{ time: "05:00", rank: 1, relativeSupport: 80 }], separation, }), /基本并列/, ); }); test("34/33/33 is a tie, not a recommended winner", () => { const separation = evaluateCandidateSeparation(TIED); assert.equal(separation.status, "not_separated"); 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"/); });