import assert from "node:assert/strict"; import test from "node:test"; import { buildCandidateContrastPacket, 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 { classifySnapshotStaleReason, scoreableSnapshotIsCurrent, snapshotIsCurrent, 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("D9/D10 sign differences synthesize a contrast probe when engine probes are empty", () => { const packet = buildCandidateContrastPacket({ candidateSetVersion: "set-a", calculationResultId: "22222222-2222-4222-8222-222222222222", vargaDifferences: [ { layer: "d9", signs: ["天秤", "天蝎", "射手"] }, { layer: "d10", signs: ["巨蟹", "狮子", "处女"] }, ], }); const probe = selectDiscriminatorProbe(packet); assert.ok(probe); assert.ok(probe.expectedOutcomes.length >= 2); assert.equal(probe.sourceFeatures[0]?.calculationResultId, "22222222-2222-4222-8222-222222222222"); assert.match(probe.question, /职业前事|事业盘/); }); 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("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, /当前推荐/); });