import assert from "node:assert/strict"; import test from "node:test"; import { candidateSetId } from "../src/lib/rectification-agentic/core/build-state.ts"; import { asInferenceState } from "../src/lib/rectification-agentic/core/compose-receipt.ts"; import { inspectDiscriminatorProbes } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts"; import { INFERENCE_ALGORITHM_VERSION } from "../src/lib/rectification-agentic/core/types.ts"; import type { ConflictProbe } from "../src/lib/rectification-agentic/core/types.ts"; import { decideAfterInferenceChange } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts"; import type { DecisionDossier } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts"; import { buildMethodFollowupPlan, datedLedgerAnchor, existenceProbeAsked, remainingReverseVerifyProbes, sameYearProbeAsked, type MethodFollowupEvidence, } from "../src/lib/rectification-agentic/v9/method-followup.ts"; import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts"; import type { CandidateContrastPacket } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts"; import { askedSemanticKeysForEngine } from "../src/lib/rectification-agentic/v9/inference-adapter.ts"; import { engineRequestBody, toEngineEvents } from "../src/lib/rectification-agentic/v9/engine-client.ts"; import { EXISTENCE_STYLE_OPTIONS } from "../src/lib/rectification-agentic/v9/probe-question-contract.ts"; function existenceProbe( domain: DiscriminatingEventProbe["domain"], year: number, extra: { month?: number; source?: DiscriminatingEventProbe["source"]; key?: string; gain?: number } = {}, ): DiscriminatingEventProbe { const month = extra.month; const source = extra.source ?? "dasha_boundary"; const key = extra.key ?? (month ? `${domain}.${year}.${String(month).padStart(2, "0")}.${source}` : `${domain}.${year}.${source}`); return { year, year_label: month ? `${year} 年 ${month} 月前后` : `${year} 年前后`, month, domain, event_family: domain === "relocation" ? "搬家或长期住到外地" : "入职、升职或职责明显加重", source, tracks: ["vimshottari", "narayana"], tracks_agree: true, unique_minute_claim: false, user_meaning: `时间范围锁定 ${year} 年。`, role: "distinguish", information_gain: extra.gain ?? 1.1, semantic_key: key, candidate_split_hash: key, candidate_ids: ["05:00", "05:20"], expected_outcomes: [ { answer_class: "yes", supports: ["05:00"], conflicts: ["05:20"] }, { answer_class: "no", supports: ["05:20"], conflicts: ["05:00"] }, ], choice_kind: "existence", }; } function dated( id: string, domain: string, eventKind: string, occurredFrom: string, ): MethodFollowupEvidence { return { id, status: "confirmed", domain, datePrecision: "month", occurredFrom, occurredTo: null, eventKind, }; } const D10_STYLE: CandidateContrastPacket["probes"][number] = { probeId: "contrast:varga.d10.巨蟹座/狮子座", candidateSetVersion: "05:00-05:20", question: "平时做事,你更接近下面哪一种?", expectedOutcomes: [ { outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:20"] }, { outcomeId: "weak_yes", supportsCandidateIds: ["05:20"], conflictsCandidateIds: ["05:00"] }, ], candidateSplitHash: "varga.d10.巨蟹座/狮子座", informationGain: 1.4, sourceFeatures: [{ technique: "D10", calculationResultId: null }], domain: "career", year: null, semanticKey: "varga.d10.巨蟹座/狮子座", choiceKind: "varga_style", styleOptions: [ { label: "做事以照顾人为主,在意团队里的感受", answerClass: "yes", sign: "巨蟹座" }, { label: "习惯带头,也不排斥站到台前", answerClass: "weak_yes", sign: "狮子座" }, ], }; test("existenceProbeAsked treats the same career year and nearby years as already asked", () => { const asked = ["career.2018.05.dasha_boundary"]; assert.equal(existenceProbeAsked(asked, "career", 2018), true); assert.equal(existenceProbeAsked(asked, "career", 2017), true); assert.equal(existenceProbeAsked(asked, "career", 2019), true); assert.equal(existenceProbeAsked(asked, "career", 2020), false); assert.equal(existenceProbeAsked(asked, "finance", 2018), false); }); test("remaining reverse-verify probes drop same-domain nearby years after a month probe", () => { const remaining = remainingReverseVerifyProbes( [ existenceProbe("career", 2018, { month: 5 }), existenceProbe("career", 2018, { source: "dasha_activation" }), existenceProbe("career", 2017, { month: 5 }), existenceProbe("career", 2019, { month: 5 }), existenceProbe("career", 2020, { month: 5 }), ], [], new Set(), new Set(["career.2018.05.dasha_boundary"]), ); const years = remaining.filter((item) => item.domain === "career").map((item) => item.year); assert.equal(years.includes(2017), false); assert.equal(years.includes(2018), false); assert.equal(years.includes(2019), false); assert.equal(years.includes(2020), true); }); test("datedLedgerAnchor names the confirmed same-domain month", () => { const anchor = datedLedgerAnchor([ dated("e-career-month", "career", "career_entry", "2018-07-01"), ], "career"); assert.ok(anchor); assert.equal(anchor?.label, "2018 年 7 月"); assert.equal(datedLedgerAnchor([ dated("e-edu", "education", "education_start", "2016-09-01"), ], "career"), null); }); test("unanchored D10 varga_style cards are dropped; anchored cards mention the ledger month", () => { const baseEvidence = [ dated("e-edu", "education", "education_start", "2016-09-01"), dated("e-edu-2", "education", "education_completion", "2020-06-01"), dated("e-rel", "relationship", "relationship_start", "2024-05-01"), dated("e-fin", "finance", "finance_loss", "2021-01-01"), ]; const packet = { candidateSetVersion: "05:00-05:20", vargaDifferences: [] as const, probes: [D10_STYLE], }; const unanchored = buildMethodFollowupPlan({ evidence: baseEvidence, contrastPacket: packet, candidatesSeparated: false, }); assert.equal( unanchored.dropped_probes.some((item) => item.reason === "unanchored_varga_style"), true, ); assert.notEqual(unanchored.next_followup?.semantic_key, D10_STYLE.semanticKey); const anchored = buildMethodFollowupPlan({ evidence: [...baseEvidence, dated("e-career", "career", "career_entry", "2018-07-01")], contrastPacket: packet, candidatesSeparated: false, }); assert.equal(anchored.next_followup?.semantic_key, D10_STYLE.semanticKey); assert.equal(anchored.next_followup?.choice_kind, "varga_style"); assert.match(anchored.next_followup?.user_prompt_hint ?? "", /2018 年 7 月/); assert.doesNotMatch(anchored.next_followup?.choice_frame?.prompt ?? "", /2018/); }); test("after a D9-style answer the compare request body stays legal", () => { const hash = "04:45-05:15:04:47,04:51,04:53,04:59,05:00,05:06,05:08,05:13,05:15:varga.d9.04:47|04:51/05:00|05:06|04:59|04:53/05:08|05:13|05:15"; assert.equal(hash.length, 128); const receipt = { inference_state: { answered_probes: [{ probe_id: "contrast:varga.d9.相处", semantic_key: "varga.d9.巨蟹座/狮子座", candidate_split_hash: hash, answer_class: "yes", classified_from: "choice", }], }, }; const asked = askedSemanticKeysForEngine(receipt, []); assert.equal(asked.includes(hash), false); assert.ok(asked.every((key) => key.length <= 120 && !key.includes(":varga."))); const body = engineRequestBody({ baselineBirthSnapshot: { birth_date: "1997-08-08", latitude: 36.42, longitude: 114.21, timezone_offset: 8, }, candidateRange: { start_time: "04:45", end_time: "05:15" }, events: toEngineEvents([{ id: "00000000-0000-4000-8000-000000000001", sourceTurnId: "33333333-3333-4333-8333-333333333333", subject: "self", eventKind: "education_start", domain: "education", occurredFrom: "2016-09-01", occurredTo: "2016-09-30", datePrecision: "month", summary: "大学入学", }]), askedProbeKeys: asked, }); const keys = (body.asked_probe_keys as string[] | undefined) ?? []; assert.ok(keys.every((key) => key.length <= 120 && !key.includes(":varga."))); }); const COVERED_FOR_DISCRIMINATE: MethodFollowupEvidence[] = [ dated("e-edu", "education", "education_start", "2016-09-01"), dated("e-rel", "relationship", "relationship_start", "2018-05-01"), dated("e-career", "career", "career_entry", "2020-04-01"), dated("e-fam", "family", "family_event", "2019-03-01"), { id: "e-occ", status: "confirmed", domain: "occupation", datePrecision: "unknown", occurredFrom: null, occurredTo: null, eventKind: "occupation_note", }, ]; const CAREER_2023_05_KEY = "career.2023.05.dasha_boundary"; const CAREER_2023_ACTIVATION_KEY = "career.2023.dasha_activation"; const CAREER_2024_04_KEY = "career.2024.04.dasha_boundary"; const CAREER_2023_05 = existenceProbe("career", 2023, { month: 5, gain: 1.1, key: CAREER_2023_05_KEY }); const CAREER_2023_ACTIVATION = existenceProbe("career", 2023, { source: "dasha_activation", gain: 0.46, key: CAREER_2023_ACTIVATION_KEY, }); const CAREER_2024_04 = existenceProbe("career", 2024, { month: 4, gain: 0.97, key: CAREER_2024_04_KEY }); const RELOCATION_2015_05 = existenceProbe("relocation", 2015, { month: 5, gain: 0.74 }); const D9_STYLE: CandidateContrastPacket["probes"][number] = { ...D10_STYLE, probeId: "contrast:varga.d9.巨蟹座/狮子座", question: "亲密关系里更接近下面哪一种相处方式?", candidateSplitHash: "varga.d9.巨蟹座/狮子座", informationGain: 1.53, sourceFeatures: [{ technique: "D9", calculationResultId: null }], domain: "relationship", semanticKey: "varga.d9.巨蟹座/狮子座", styleOptions: [ { label: "相处里更主动,也更愿意把关系往前推", answerClass: "yes", sign: "巨蟹座" }, { label: "相处里更克制,先把分寸看清楚", answerClass: "weak_yes", sign: "狮子座" }, ], }; const D10_LOW: CandidateContrastPacket["probes"][number] = { ...D10_STYLE, informationGain: 0.4, }; function contrastFromEvent(probe: DiscriminatingEventProbe): CandidateContrastPacket["probes"][number] { const yes = probe.expected_outcomes?.find((row) => row.answer_class === "yes")?.supports ?? ["05:00"]; const no = probe.expected_outcomes?.find((row) => row.answer_class === "no")?.supports ?? ["05:20"]; const key = probe.semantic_key ?? `${probe.domain}.${probe.year}`; return { probeId: `probe:${key}`, candidateSetVersion: "05:00-05:20", question: probe.year_label, expectedOutcomes: [ { outcomeId: "yes", supportsCandidateIds: [...yes], conflictsCandidateIds: [...no] }, { outcomeId: "weak_yes", supportsCandidateIds: [], conflictsCandidateIds: [] }, { outcomeId: "no", supportsCandidateIds: [...no], conflictsCandidateIds: [...yes] }, { outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] }, ], candidateSplitHash: probe.candidate_split_hash ?? key, informationGain: probe.information_gain ?? 0, sourceFeatures: [{ technique: "Vimshottari", calculationResultId: null }], domain: probe.domain, year: probe.year, semanticKey: key, choiceKind: "existence", }; } function accidentPacket(probes: readonly CandidateContrastPacket["probes"][number][]): CandidateContrastPacket { return { candidateSetVersion: "05:00-05:20", vargaDifferences: [], probes, }; } function conflictFromContrast(probe: CandidateContrastPacket["probes"][number]): ConflictProbe { const yes = probe.expectedOutcomes.find((row) => row.outcomeId === "yes")?.supportsCandidateIds ?? ["05:00"]; const no = probe.expectedOutcomes.find((row) => row.outcomeId === "no")?.supportsCandidateIds ?? ["05:20"]; return { id: probe.probeId, semantic_key: probe.semanticKey, candidate_split_hash: probe.candidateSplitHash, domain: probe.domain ?? "career", year: probe.year && probe.year > 0 ? probe.year : 0, question: probe.question, candidate_ids: [...new Set([...yes, ...no])], expected_outcomes: [ { answer_class: "yes", supports: [...yes], conflicts: [...no] }, { answer_class: "weak_yes", supports: [], conflicts: [] }, { answer_class: "no", supports: [...no], conflicts: [...yes] }, { answer_class: "unsure", supports: [], conflicts: [] }, ], information_gain: probe.informationGain, source: probe.choiceKind === "varga_style" ? "varga_contrast" : "dasha_boundary", choice_kind: probe.choiceKind ?? "existence", style_options: probe.styleOptions ? probe.styleOptions.map((item) => ({ label: item.label, answer_class: item.answerClass, ...(item.sign ? { sign: item.sign } : {}), })) : [...EXISTENCE_STYLE_OPTIONS], }; } function answered(probe: { probeId?: string; id?: string; semanticKey?: string; semantic_key?: string; candidateSplitHash?: string; candidate_split_hash?: string }) { return { probe_id: probe.probeId ?? probe.id ?? "", semantic_key: probe.semanticKey ?? probe.semantic_key ?? "", candidate_split_hash: probe.candidateSplitHash ?? probe.candidate_split_hash ?? "", answer_class: "no" as const, classified_from: "choice" as const, }; } function accidentState( remaining: readonly CandidateContrastPacket["probes"][number][], asked: readonly ReturnType[], ) { const times = ["05:00", "05:20"] as const; const probes = remaining.map(conflictFromContrast); const raw = { algorithm_version: INFERENCE_ALGORITHM_VERSION, candidate_set_id: candidateSetId("05:00", "05:20", times), revision: asked.length + 1, phase: "discrimination" as const, result_status: "discriminating" as const, range_start: "05:00", range_end: "05:20", candidates: times.map((time, index) => ({ id: time, time, cluster_range: [time, time] as const, prior_score: time === "05:00" ? 20 : 10, posterior_score: time === "05:00" ? 20 : 10, probability: time === "05:00" ? 0.67 : 0.33, status: "active" as const, rank: index + 1, strong_conflict_count: 0, })), events: [ { id: "e-edu", domain: "education", year: 2016, precision: "month" as const, usage: "training" as const }, { id: "e-rel", domain: "relationship", year: 2018, precision: "month" as const, usage: "training" as const }, { id: "e-career", domain: "career", year: 2020, precision: "month" as const, usage: "training" as const }, { id: "e-fam", domain: "family", year: 2019, precision: "month" as const, usage: "holdout" as const }, ], probes: [ ...asked.map((item) => remaining.find((probe) => probe.semanticKey === item.semantic_key)).filter(Boolean).map((probe) => conflictFromContrast(probe as CandidateContrastPacket["probes"][number])), ...probes, ], answered_probes: asked, rounds: [], last_inference_round: null, entropy: 1.2, representative_time: "05:00", credible_range: ["05:00", "05:20"] as const, holdout_passed: null, }; const loaded = asInferenceState(raw); assert.ok(loaded, "accident inference fixture must pass asInferenceState"); return loaded; } function accidentDossier( remaining: readonly CandidateContrastPacket["probes"][number][], asked: readonly ReturnType[], ): DecisionDossier { const state = accidentState(remaining, asked); return { evidence: COVERED_FOR_DISCRIMINATE, conversationSummary: { activeFocus: null, declinedSkippedTopics: [] }, latestResult: { resultId: "55555555-5555-4555-8555-555555555555", selectionAllowed: true, confirmationAllowed: false, evidenceLedgerFingerprint: "fp-same-year", candidates: [ { candidateId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1", time: "05:00", rank: 1, relativeSupport: 20 }, { candidateId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa2", time: "05:20", rank: 2, relativeSupport: 10 }, ], representativeTime: "05:00", decisionReceipt: { accept_allowed: true, acceptance_allowed: true, propose_allowed: true, selection_allowed: true, confirmation_allowed: false, inference_state: state, }, }, case: { acceptedTime: null, status: "active" }, }; } test("sameYearProbeAsked is exact domain+year; nearby years stay on existenceProbeAsked", () => { const asked = ["career.2023.05.dasha_boundary"]; assert.equal(sameYearProbeAsked(asked, "career", 2023), true); assert.equal(sameYearProbeAsked(asked, "career", 2024), false); assert.equal(sameYearProbeAsked(asked, "relocation", 2023), false); assert.equal(sameYearProbeAsked(asked, "career", 0), false); assert.equal(existenceProbeAsked(asked, "career", 2024), true); }); test("after a May 2023 career probe, the same-year activation card is dropped not ranked", () => { const packet = accidentPacket([ contrastFromEvent(CAREER_2023_05), contrastFromEvent(CAREER_2023_ACTIVATION), ]); const inspected = inspectDiscriminatorProbes(packet, { askedKeys: [CAREER_2023_05_KEY], }); assert.notEqual(inspected.selected?.semanticKey, CAREER_2023_ACTIVATION.semantic_key); assert.equal( inspected.dropped.some((item) => ( item.semantic_key === CAREER_2023_ACTIVATION.semantic_key && item.reason === "same_year_asked" )), true, ); const decided = decideAfterInferenceChange({ dossier: accidentDossier( [contrastFromEvent(CAREER_2023_05), contrastFromEvent(CAREER_2023_ACTIVATION)], [answered(contrastFromEvent(CAREER_2023_05))], ), state: accidentState( [contrastFromEvent(CAREER_2023_ACTIVATION)], [answered(contrastFromEvent(CAREER_2023_05))], ), userStopped: false, birthDate: "1997-08-08", }); assert.notEqual(decided.probe?.semanticKey, CAREER_2023_ACTIVATION.semantic_key); assert.equal( decided.droppedProbes.some((item) => ( item.semantic_key === CAREER_2023_ACTIVATION.semantic_key && item.reason === "same_year_asked" )), true, ); }); test("adjacent-year career probes stay eligible after a 2023 answer", () => { const packet = accidentPacket([ contrastFromEvent(CAREER_2023_ACTIVATION), contrastFromEvent(CAREER_2024_04), ]); const inspected = inspectDiscriminatorProbes(packet, { askedKeys: [CAREER_2023_05_KEY], }); assert.equal(inspected.selected?.semanticKey, CAREER_2024_04.semantic_key); assert.equal( inspected.dropped.some((item) => ( item.semantic_key === CAREER_2024_04.semantic_key && item.reason === "same_year_asked" )), false, ); const decided = decideAfterInferenceChange({ dossier: accidentDossier( [contrastFromEvent(CAREER_2023_ACTIVATION), contrastFromEvent(CAREER_2024_04)], [answered(contrastFromEvent(CAREER_2023_05))], ), state: accidentState( [contrastFromEvent(CAREER_2023_ACTIVATION), contrastFromEvent(CAREER_2024_04)], [answered(contrastFromEvent(CAREER_2023_05))], ), userStopped: false, birthDate: "1997-08-08", }); assert.equal(decided.probe?.semanticKey, CAREER_2024_04.semantic_key); }); test("buildMethodFollowupPlan drops the same-year activation after the May card", () => { const plan = buildMethodFollowupPlan({ evidence: COVERED_FOR_DISCRIMINATE, contrastPacket: accidentPacket([ contrastFromEvent(CAREER_2023_05), contrastFromEvent(CAREER_2023_ACTIVATION), contrastFromEvent(CAREER_2024_04), ]), askedProbeKeys: [CAREER_2023_05_KEY], candidatesSeparated: false, birthDate: "1997-08-08", }); assert.notEqual(plan.next_followup?.semantic_key, CAREER_2023_ACTIVATION.semantic_key); assert.equal( plan.dropped_probes.some((item) => ( item.semantic_key === CAREER_2023_ACTIVATION.semantic_key && item.reason === "same_year_asked" )), true, ); assert.equal(plan.next_followup?.semantic_key, CAREER_2024_04.semantic_key); }); test("accident replay: fifth card is not the 2023 activation after four answers", () => { const pool = [ D9_STYLE, contrastFromEvent(CAREER_2023_05), contrastFromEvent(CAREER_2024_04), contrastFromEvent(RELOCATION_2015_05), contrastFromEvent(CAREER_2023_ACTIVATION), D10_LOW, ]; const asked = [ answered(D9_STYLE), answered(contrastFromEvent(CAREER_2023_05)), answered(contrastFromEvent(CAREER_2024_04)), answered(contrastFromEvent(RELOCATION_2015_05)), ]; const remaining = [ contrastFromEvent(CAREER_2023_ACTIVATION), D10_LOW, ]; const decided = decideAfterInferenceChange({ dossier: accidentDossier(pool, asked), state: accidentState(remaining, asked), userStopped: false, birthDate: "1997-08-08", }); assert.notEqual(decided.probe?.semanticKey, CAREER_2023_ACTIVATION.semantic_key); assert.equal( decided.droppedProbes.some((item) => ( item.semantic_key === CAREER_2023_ACTIVATION.semantic_key && item.reason === "same_year_asked" )), true, ); assert.ok( decided.probe?.semanticKey === D10_LOW.semanticKey || decided.nextAction === "ready_to_adopt" || decided.sessionOutcome === "adopt_representative", ); });