import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { buildMethodFollowupPlan, buildNextUserAction, conversationalSessionOutcome, isOfferBlockingFollowup } from "../src/lib/rectification-agentic/v9/method-followup.ts"; import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts"; import { askedKeysFromLedgerEvidence, buildCandidateContrastPacket, } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts"; import { internalObservationsFromWindowScan, parseWindowScan, } from "../src/lib/rectification-agentic/v9/varga-observations.ts"; import { WINDOW_SCAN_DISPLAY_LAYER_ORDER } from "../src/lib/rectification-agentic/v9/refinement-packet.ts"; import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts"; import { readVedastroMinuteSensitiveStatus } from "../src/lib/rectification-agentic/v9/confirmation-gate.ts"; import { authoritativeCandidateProjection } from "../src/lib/rectification-agentic/v9/inference-adapter.ts"; import { createRectificationV9Tools, latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts"; import { PUBLIC_RECTIFICATION_TOOLS } from "../src/lib/rectification-agentic/v9/public-receipt.ts"; import { candidateRangeFingerprint, evidenceLedgerFingerprint, parseV9CaseDossier, RectificationToolServiceError, } from "../src/lib/rectification-agentic/v9/tool-service.ts"; import { resolveExactSkillPackage, } from "../src/lib/skill-package-registry.ts"; import { CASE_ID, CANDIDATE_ID, FOCUS_ID, RESULT_ID, SECOND_CANDIDATE_ID, TURN_ID, USER_ID, candidateSnapshotFixture, computeFixture, dossierFixture, fakeAccounting, receiptHandlers, } from "./rectification-v9-test-support.ts"; const THIRD_CANDIDATE_ID = "88888888-8888-4888-8888-888888888883"; const EDUCATION_ID = "44444444-4444-4444-8444-444444444441"; const RELATIONSHIP_ID = "44444444-4444-4444-8444-444444444442"; const FAMILY_ID = "44444444-4444-4444-8444-444444444443"; const CAREER_ID = "44444444-4444-4444-8444-444444444444"; const UNIQUE_MINUTE_COPY = /±5 分钟确定性/; const DYNAMIC_STYLE_OPTIONS = [ { label: "明确发生且时间吻合", answer_class: "yes" as const }, { label: "发生过但程度较弱", answer_class: "weak_yes" as const }, { label: "明确没有发生", answer_class: "no" as const }, { label: "不记得这段经历", answer_class: "unsure" as const }, ]; const CLASSIC_COVERAGE = [ { status: "confirmed", domain: "education", datePrecision: "year" as const, occurredFrom: "2016-01-01", occurredTo: null }, { status: "confirmed", domain: "relationship", datePrecision: "year" as const, occurredFrom: "2018-01-01", occurredTo: null }, { status: "confirmed", domain: "career", datePrecision: "year" as const, occurredFrom: "2019-01-01", occurredTo: null }, { status: "confirmed", domain: "family", datePrecision: "year" as const, occurredFrom: "2020-01-01", occurredTo: null }, { status: "confirmed", domain: "appearance", datePrecision: "unknown" as const, occurredFrom: null, occurredTo: null }, { status: "confirmed", domain: "marks", datePrecision: "unknown" as const, occurredFrom: null, occurredTo: null }, { status: "confirmed", domain: "occupation", datePrecision: "unknown" as const, occurredFrom: null, occurredTo: null }, { status: "confirmed", domain: "horary", datePrecision: "day" as const, occurredFrom: "2024-01-01", occurredTo: null }, ]; function datedEvidence( domain: string, year: string, extra: { eventKind?: string | null; summary?: string | null; datePrecision?: "year" | "month" | "day"; } = {}, ) { return { status: "confirmed" as const, domain, datePrecision: extra.datePrecision ?? ("year" as const), occurredFrom: `${year}-01-01`, occurredTo: null, ...(extra.eventKind !== undefined ? { eventKind: extra.eventKind } : {}), ...(extra.summary !== undefined ? { summary: extra.summary } : {}), }; } const CAREER_CONFLICT_PROBE = { year: 2018, year_label: "2018 年前后", domain: "career" as const, event_family: "入职、升职或职责明显加重", source: "dasha_activation" as const, tracks: ["vimshottari", "narayana"] as const, tracks_agree: true, unique_minute_claim: false as const, user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。", role: "distinguish" as const, phase: "candidate_discriminator" as const, information_gain: 0.21, semantic_key: "career.2018.dasha_activation", candidate_set_version: "set-test", candidate_split_hash: "set-test:career:2018", 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"] }, ], style_options: DYNAMIC_STYLE_OPTIONS, }; const EDUCATION_QUALITY_PROBE = { year: 2016, year_label: "2016 年前后", domain: "education" as const, event_family: "高考或重要考试发挥明显失常、压力很大", source: "known_event_quality" as const, tracks: ["vimshottari", "narayana"] as const, tracks_agree: true, unique_minute_claim: false as const, user_meaning: "年份锁定 2016 年前后。已有高考或考试经历。请写成一句自然语言,问那次是否发挥失常或压力特别大。", role: "clarify" as const, phase: "event_clarification" as const, choice_kind: "event_quality" as const, information_gain: 0, semantic_key: "education.2016", style_options: DYNAMIC_STYLE_OPTIONS, }; const CAREER_QUALITY_PROBE = { year: 2020, year_label: "2020 年前后", domain: "career" as const, event_family: "入职、升职或职责明显加重", source: "known_event_quality" as const, tracks: ["vimshottari", "narayana"] as const, tracks_agree: true, unique_minute_claim: false as const, user_meaning: "年份锁定 2020 年前后。已有相关经历。请写成一句自然语言,问入职、升职或职责明显加重有没有发生过。不得改年份。", role: "clarify" as const, phase: "event_clarification" as const, choice_kind: "event_quality" as const, information_gain: 0, semantic_key: "career.2020", style_options: DYNAMIC_STYLE_OPTIONS, }; const ENGINE_SCORE = { success: true, endpoint: "rectification_v5_score", result_id: "e4fbf2e0-85dc-5b42-a5a3-34e5dd4b7e62", algorithm_version: "rectification-event-contract-v2", event_contract_version: "rectification-event-contract-v2", decision_policy_version: "rectification-candidate-policy-v2", execution_ledger_version: "rectification-execution-ledger-v2", candidate_decisions: [ { candidate_id: CANDIDATE_ID, time: "04:50", rank: 1, relative_support: 57, tied_minute_count: 1 }, { candidate_id: SECOND_CANDIDATE_ID, time: "04:51", rank: 2, relative_support: 25, tied_minute_count: 2 }, { candidate_id: THIRD_CANDIDATE_ID, time: "04:52", rank: 3, relative_support: 18, tied_minute_count: 2 }, ], decision_receipt: { receipt_version: "candidate-decision-receipt-v2", contract_version: "v2", event_contract_version: "rectification-event-contract-v2", policy_version: "rectification-candidate-policy-v2", decision_policy_version: "rectification-candidate-policy-v2", display_allowed: true, selection_allowed: true, acceptance_allowed: true, confirmation_allowed: false, accept_allowed: true, confirm_allowed: false, representative_candidate_id: CANDIDATE_ID, representative_time: "04:50", overall_confidence: "high", margin_percent: 42.5, }, execution_ledger: [ { ledger_version: "rectification-execution-ledger-v2", stage: "technique_layer", method: "d1-rashi", status: "executed", source: "python-engine" }, { ledger_version: "rectification-execution-ledger-v2", stage: "technique_layer", method: "d9-navamsa", status: "executed", source: "python-engine" }, ], diagnostics: { window_scan: { scanned: true, confirmation_allowed: false, unique_minute_claim: false, d9_lagna_count: 2, d10_lagna_count: 1, d9_candidates_differ: true, d10_candidates_differ: false, d9_sign_names: ["白羊座", "天蝎"], }, }, }; const educationEvidence = { id: EDUCATION_ID, source_turn_id: TURN_ID, subject: "self", event_kind: "education_milestone", domain: "education", occurred_from: "2016-06-01", occurred_to: null, date_precision: "month", summary: "2016年6月一次学业节点", status: "confirmed", supersedes_evidence_id: null, created_at: "2026-08-12T10:00:06.000Z", }; const relationshipEvidence = { id: RELATIONSHIP_ID, source_turn_id: TURN_ID, subject: "self", event_kind: "relationship_start", domain: "relationship", occurred_from: "2018-01-01", occurred_to: null, date_precision: "year", summary: "2018年一段感情开始", status: "confirmed", supersedes_evidence_id: null, created_at: "2026-08-12T10:00:07.000Z", }; const familyEvidence = { id: FAMILY_ID, source_turn_id: TURN_ID, subject: "family", event_kind: "family_event", domain: "family", occurred_from: "2020-01-01", occurred_to: null, date_precision: "year", summary: "2020年家人相关变化", status: "confirmed", supersedes_evidence_id: null, created_at: "2026-08-12T10:00:08.000Z", }; const careerEvidence = { id: CAREER_ID, source_turn_id: TURN_ID, subject: "self", event_kind: "career_entry", domain: "career", occurred_from: "2019-07-01", occurred_to: null, date_precision: "year", summary: "2019年开始工作", status: "confirmed", supersedes_evidence_id: null, created_at: "2026-08-12T10:00:09.000Z", }; function stubEngine(response: unknown) { const previous = globalThis.fetch; globalThis.fetch = (async () => ({ ok: true, status: 200, json: async () => response, })) as unknown as typeof fetch; return () => { globalThis.fetch = previous; }; } test("eight-method routing asks relationship after dated education, not relocation", () => { const plan = buildMethodFollowupPlan({ evidence: [{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-06-01", occurredTo: null, }], }); assert.equal(plan.next_followup?.method_id, "d9_relationship"); assert.equal(plan.next_followup?.domain, "relationship"); assert.equal(plan.stop_domain_rotation, true); assert.deepEqual([...plan.not_in_rotation], ["relocation"]); assert.equal(plan.methods.find((item) => item.method_id === "appearance")?.status, "skipped_by_policy"); }); test("dasha conflict probe does not jump ahead of method rotation before acceptance event quality", () => { const plan = buildMethodFollowupPlan({ evidence: [datedEvidence("education", "2016")], eventProbes: [CAREER_CONFLICT_PROBE], }); assert.equal(plan.next_followup?.source, "method_coverage"); assert.equal(plan.next_followup?.method_id, "d9_relationship"); assert.equal(plan.next_followup?.choice_frame, null); assert.notEqual(plan.next_followup?.source, "event_probe"); }); test("known exam quality does not create a scoring card after one recorded event", () => { const plan = buildMethodFollowupPlan({ evidence: [datedEvidence("education", "2016")], eventProbes: [EDUCATION_QUALITY_PROBE, CAREER_CONFLICT_PROBE], }); assert.equal(plan.next_followup?.source, "method_coverage"); assert.equal(plan.next_followup?.method_id, "d9_relationship"); assert.equal(plan.next_followup?.choice_frame, null); assert.notEqual(plan.next_followup?.choice_kind, "event_quality"); }); test("career known-event quality does not jump the adoption gate", () => { const plan = buildMethodFollowupPlan({ evidence: [ datedEvidence("career", "2020", { eventKind: "career_entry", datePrecision: "month" }), datedEvidence("career", "2020", { eventKind: "career_exit", datePrecision: "month" }), ], eventProbes: [CAREER_QUALITY_PROBE, CAREER_CONFLICT_PROBE], }); assert.equal(plan.next_followup?.source, "method_coverage"); assert.equal(plan.next_followup?.method_id, "d9_relationship"); assert.equal(plan.next_followup?.choice_frame, null); assert.notEqual(plan.next_followup?.source, "event_probe"); assert.notEqual(plan.next_followup?.choice_kind, "event_quality"); }); test("encoded exam quality does not stamp another card and keeps method rotation", () => { const plan = buildMethodFollowupPlan({ evidence: [datedEvidence("education", "2016", { summary: "2016年高考发挥异常" })], eventProbes: [EDUCATION_QUALITY_PROBE, CAREER_CONFLICT_PROBE], }); assert.equal(plan.next_followup?.source, "method_coverage"); assert.equal(plan.next_followup?.method_id, "d9_relationship"); assert.equal(plan.next_followup?.choice_frame, null); }); test("dasha conflict probe jumps after four scoreable events leave three training domains", () => { const plan = buildMethodFollowupPlan({ evidence: [ datedEvidence("education", "2016"), datedEvidence("education", "2020"), datedEvidence("relationship", "2018"), datedEvidence("family", "2023"), ], eventProbes: [CAREER_CONFLICT_PROBE], }); assert.equal(plan.next_followup?.source, "event_probe"); assert.equal(plan.next_followup?.intent, "distinguish_candidates"); assert.equal(plan.next_followup?.domain, "career"); assert.equal(plan.next_followup?.choice_frame?.scoring, true); assert.equal(plan.next_followup?.choice_frame?.period, "2018 年前后"); assert.deepEqual(plan.next_followup?.candidate_ids, ["05:00", "05:20"]); assert.ok((plan.next_followup?.information_gain ?? 0) > 0); assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true); assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, }), "collect_evidence"); }); test("three scoreable events in one domain still rotate methods instead of reverse-inferring", () => { const plan = buildMethodFollowupPlan({ evidence: [ datedEvidence("education", "2012"), datedEvidence("education", "2016"), datedEvidence("education", "2020"), ], eventProbes: [CAREER_CONFLICT_PROBE], }); assert.equal(plan.next_followup?.source, "method_coverage"); assert.equal(plan.next_followup?.method_id, "d9_relationship"); assert.equal(plan.next_followup?.choice_frame, null); }); test("occupation_note does not count toward reverse-inference event quality", () => { const plan = buildMethodFollowupPlan({ evidence: [ datedEvidence("education", "2016"), datedEvidence("education", "2020"), { status: "confirmed", domain: "occupation", datePrecision: "unknown", occurredFrom: null, occurredTo: null, eventKind: "occupation_note", }, ], eventProbes: [CAREER_CONFLICT_PROBE], }); assert.equal(plan.next_followup?.source, "method_coverage"); assert.equal(plan.next_followup?.method_id, "d9_relationship"); assert.equal(plan.next_followup?.choice_frame, null); }); test("age-band probe does not jump ahead of uncovered relationship", () => { const plan = buildMethodFollowupPlan({ evidence: [{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null, }], eventProbes: [{ year: 2018, year_label: "2018 年前后", domain: "relocation", event_family: "搬家、离乡或长期异地", source: "age_band", tracks: ["vimshottari", "narayana"], tracks_agree: false, unique_minute_claim: false, user_meaning: "年份锁定 2018 年前后。", role: "reverse_verify", }], }); assert.equal(plan.next_followup?.method_id, "d9_relationship"); assert.equal(plan.next_followup?.source, "method_coverage"); assert.equal(plan.next_followup?.choice_frame, null); }); test("user-stop action records stated events when the ledger is empty", () => { const action = buildNextUserAction({ scorableCount: 0, evidenceCount: 0, hasLatestResult: false, selectionAllowed: false, sessionOutcome: "collect_evidence", nextFollowup: null, workingTime: "12:00", }); assert.equal(action.id, "record_stated_events"); assert.equal(action.on_user_stop.id, "record_stated_events"); assert.match(action.on_user_stop.user_meaning, /batch/); }); test("user-stop action offers a range when candidates already exist", () => { const plan = buildMethodFollowupPlan({ evidence: [{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-06-01", occurredTo: null, }], }); const withCandidates = buildNextUserAction({ scorableCount: 1, evidenceCount: 1, hasLatestResult: true, selectionAllowed: false, sessionOutcome: "collect_evidence", nextFollowup: plan.next_followup, workingTime: "12:00", }); assert.equal(withCandidates.id, "ask_method_followup"); assert.equal(withCandidates.on_user_stop.id, "offer_provisional_range"); const withoutCandidates = buildNextUserAction({ scorableCount: 0, evidenceCount: 1, hasLatestResult: false, selectionAllowed: false, sessionOutcome: "collect_evidence", nextFollowup: plan.next_followup, workingTime: "12:00", }); assert.equal(withoutCandidates.id, "ask_method_followup"); assert.equal(withoutCandidates.on_user_stop.id, "explain_current_window"); assert.match(withoutCandidates.on_user_stop.user_meaning, /12:00/); assert.match(withoutCandidates.on_user_stop.user_meaning, /不要只说会话会保留/); assert.equal(plan.methods.find((item) => item.method_id === "marks")?.status, "skipped_by_policy"); assert.equal(plan.methods.find((item) => item.method_id === "horary")?.status, "uncovered"); assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered"); assert.equal(plan.next_followup?.method_id, "d9_relationship"); assert.match(plan.next_followup?.user_prompt_hint ?? "", /自然语言/); assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/); assert.equal(plan.next_followup?.choice_frame, null); assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY); }); test("selectionAllowed with remaining method follow-up keeps collecting and offers a range on stop", () => { const plan = buildMethodFollowupPlan({ evidence: [{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-06-01", occurredTo: null, }], }); const action = buildNextUserAction({ scorableCount: 3, evidenceCount: 3, hasLatestResult: true, selectionAllowed: true, sessionOutcome: "collect_evidence", nextFollowup: plan.next_followup, workingTime: "05:07", }); assert.equal(plan.next_followup?.method_id, "d9_relationship"); assert.equal(action.id, "ask_method_followup"); assert.equal(action.on_user_stop.id, "offer_provisional_range"); }); test("adopt_representative defers method follow-up instead of asking this turn", () => { const plan = buildMethodFollowupPlan({ evidence: [{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-06-01", occurredTo: null, }], sessionOutcome: "adopt_representative", }); assert.equal(plan.next_followup, null); assert.equal(plan.deferred_followup?.method_id, "d9_relationship"); assert.equal(plan.session_outcome, "adopt_representative"); }); test("adopt_representative ignores leftover distinguish focus", () => { const plan = buildMethodFollowupPlan({ evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"), sessionOutcome: "adopt_representative", activeFocus: { intent: "distinguish_candidates", targetDomain: "relocation", targetKind: "home_change", }, }); assert.equal(plan.next_followup, null); assert.equal(plan.session_outcome, "adopt_representative"); }); test("declined relationship skips to career and leaves horary uncovered", () => { const plan = buildMethodFollowupPlan({ evidence: [{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null, }], declinedTopics: [{ target_domain: "relationship", status: "declined" }], }); assert.equal(plan.next_followup?.method_id, "d10_career"); assert.equal(plan.next_followup?.domain, "career"); assert.deepEqual([...plan.do_not_poll], []); assert.equal(plan.methods.find((item) => item.method_id === "horary")?.status, "uncovered"); }); test("D9 differ keeps sign names for the type-table report and still forbids unique-minute claims", () => { const scan = parseWindowScan({ scanned: true, confirmation_allowed: true, unique_minute_claim: true, d9_lagna_count: 2, d10_lagna_count: 1, d9_candidates_differ: true, d10_candidates_differ: false, d9_sign_names: ["白羊座", "天蝎"], type_table: "热情冲动", transitions: [{ layer: "d9", at: "05:14", user_meaning: "白羊座在 05:14 换成天蝎", }], }); assert.ok(scan); assert.equal(scan.confirmation_allowed, false); assert.equal(scan.unique_minute_claim, false); assert.equal(scan.d9_candidates_differ, true); assert.deepEqual(scan.d9_sign_names, ["白羊座", "天蝎"]); assert.deepEqual(scan.transitions, [{ layer: "d9", at: "05:14", user_meaning: "D9 在 05:14 发生变化", }]); const observations = internalObservationsFromWindowScan(scan); assert.deepEqual(observations, [ { layer: "d9", candidates_differ: true, ask_theme: "relationship_style" }, { layer: "d10", candidates_differ: false, ask_theme: null }, { layer: "d4", candidates_differ: false, ask_theme: null }, { layer: "d5", candidates_differ: false, ask_theme: null }, { layer: "d7", candidates_differ: false, ask_theme: null }, { layer: "d12", candidates_differ: false, ask_theme: null }, { layer: "d11", candidates_differ: false, ask_theme: null }, { layer: "d30", candidates_differ: false, ask_theme: null }, ]); assert.doesNotMatch(JSON.stringify({ scan, observations }), UNIQUE_MINUTE_COPY); const plan = buildMethodFollowupPlan({ evidence: CLASSIC_COVERAGE, observations, }); assert.equal(plan.next_followup, null); assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY); }); test("D24-only window change folds into education follow-up without a second layer", () => { const scan = parseWindowScan({ scanned: true, d9_lagna_count: 1, d10_lagna_count: 1, d5_lagna_count: 1, d24_lagna_count: 2, d5_candidates_differ: false, d24_candidates_differ: true, transitions: [ { layer: "d24", at: "05:14" }, { layer: "pada", at: "05:14" }, ], }); assert.ok(scan); assert.equal(scan.d24_candidates_differ, true); assert.equal(scan.d5_candidates_differ, false); assert.deepEqual(scan.transitions, [ { layer: "d24", at: "05:14", user_meaning: "D24 在 05:14 发生变化" }, { layer: "pada", at: "05:14", user_meaning: "Nakshatra pada 在 05:14 发生变化" }, ]); const observations = internalObservationsFromWindowScan(scan); assert.equal(observations.find((item) => item.layer === "d5")?.candidates_differ, true); assert.equal(observations.find((item) => item.layer === "d5")?.ask_theme, "education_style"); assert.deepEqual( observations.map((item) => item.layer), ["d9", "d10", "d4", "d5", "d7", "d12", "d11", "d30"], ); const plan = buildMethodFollowupPlan({ evidence: CLASSIC_COVERAGE, observations, }); assert.equal(plan.next_followup?.method_id, "d5_education"); assert.equal(plan.next_followup?.ask_theme, "education_style"); }); test("D11-only window change folds into finance follow-up without delaying adopt", () => { const scan = parseWindowScan({ scanned: true, d9_lagna_count: 1, d10_lagna_count: 1, d2_lagna_count: 1, d11_lagna_count: 2, d30_lagna_count: 2, d2_candidates_differ: false, d11_candidates_differ: true, d30_candidates_differ: true, transitions: [ { layer: "d11", at: "05:14" }, { layer: "d30", at: "05:14" }, { layer: "bhava", at: "05:14" }, { layer: "pranapada", at: "05:14" }, ], }); assert.ok(scan); assert.equal(scan.d11_candidates_differ, true); assert.equal(scan.d2_candidates_differ, false); assert.deepEqual(scan.transitions, [ { layer: "d11", at: "05:14", user_meaning: "D11 在 05:14 发生变化" }, { layer: "d30", at: "05:14", user_meaning: "D30 在 05:14 发生变化" }, { layer: "bhava", at: "05:14", user_meaning: "Bhava Lagna 在 05:14 发生变化" }, { layer: "pranapada", at: "05:14", user_meaning: "Pranapada Lagna 在 05:14 发生变化" }, ]); const observations = internalObservationsFromWindowScan(scan); assert.equal(observations.find((item) => item.layer === "d11")?.candidates_differ, true); assert.equal(observations.find((item) => item.layer === "d11")?.ask_theme, "finance_change"); assert.equal(observations.find((item) => item.layer === "d30")?.ask_theme, "health_pressure"); assert.deepEqual( observations.map((item) => item.layer), ["d9", "d10", "d4", "d5", "d7", "d12", "d11", "d30"], ); const plan = buildMethodFollowupPlan({ evidence: [ ...CLASSIC_COVERAGE, { status: "confirmed", domain: "finance", datePrecision: "year", occurredFrom: "2021-01-01", occurredTo: null }, ], observations, }); assert.equal(plan.next_followup?.method_id, "d2_finance"); assert.equal(plan.next_followup?.source, "varga_observation"); }); test("window scan displays KP sub-lord changes without opening confirmation", () => { assert.deepEqual( ["kp1", "kp4", "kp7", "kp10"].every((layer) => WINDOW_SCAN_DISPLAY_LAYER_ORDER.includes(layer as typeof WINDOW_SCAN_DISPLAY_LAYER_ORDER[number])), true, ); const scan = parseWindowScan({ scanned: true, confirmation_allowed: true, unique_minute_claim: true, d9_lagna_count: 1, d10_lagna_count: 1, transitions: [ { layer: "kp1", at: "05:14" }, { layer: "kp10", at: "05:15" }, ], }); assert.ok(scan); assert.equal(scan.confirmation_allowed, false); assert.equal(scan.unique_minute_claim, false); assert.deepEqual(scan.transitions, [ { layer: "kp1", at: "05:14", user_meaning: "KP 1宫子主 在 05:14 发生变化" }, { layer: "kp10", at: "05:15", user_meaning: "KP 10宫子主 在 05:15 发生变化" }, ]); }); test("read-case follows method plan and keeps D9/D10 type tables when SQL missing categories rotate", async () => { const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture({ evidence: [educationEvidence], conversationSummary: { confirmed_evidence_summary: [], pending_revisions: [], active_focus: null, declined_skipped_topics: [], candidate_divergence_summary: null, missing_evidence_categories: ["relocation", "health", "finance"], last_result_policy: null, summary_version: 1, updated_at: "2026-08-12T10:00:06.000Z", }, latestResult: { ...candidateSnapshotFixture({ confirmationAllowed: true, representativeTime: "04:45", candidates: [ { candidate_id: CANDIDATE_ID, rank: 1, time: "04:45", relative_support: 40, tied_minute_count: 25 }, { candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:46", relative_support: 35, tied_minute_count: 25 }, { candidate_id: THIRD_CANDIDATE_ID, rank: 3, time: "04:47", relative_support: 25, tied_minute_count: 25 }, ], }), selection_allowed: true, confirmation_allowed: true, decision_receipt: { receipt_version: "candidate-decision-receipt-v2", policy_version: "rectification-candidate-policy-v2", selection_allowed: true, acceptance_allowed: true, confirmation_allowed: false, representative_candidate_id: CANDIDATE_ID, overall_confidence: "medium", window_scan: { scanned: true, confirmation_allowed: false, unique_minute_claim: false, d9_lagna_count: 2, d10_lagna_count: 1, d9_candidates_differ: true, d10_candidates_differ: false, d9_sign_names: ["白羊", "天蝎"], }, }, }, }), get_agentic_rectification_case_compute: () => computeFixture(), }); const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, accounting: accounting.client as never, }); const projection = await (tools["rectification-read-case"] as unknown as { execute(input: unknown): Promise<{ conversation_summary: { missing_evidence_categories: string[] }; method_followup_plan: { next_followup: { method_id: string; domain: string | null } | null; deferred_followup: { method_id: string; domain: string | null } | null; session_outcome: string; }; internal_observations: Array<{ layer: string; ask_theme: string | null }>; latest_result: { confirmation_allowed: boolean; indistinguishable_width_minutes: number; window_scan: { d9_candidates_differ: boolean } | null; session_outcome: { kind: string }; }; }>; }).execute({ caseId: CASE_ID, projection: "full_diagnostics" }); assert.deepEqual(projection.conversation_summary.missing_evidence_categories, ["relocation", "health", "finance"]); assert.equal(projection.method_followup_plan.next_followup?.method_id, "d9_relationship"); assert.equal(projection.method_followup_plan.next_followup?.domain, "relationship"); assert.equal(projection.method_followup_plan.deferred_followup, null); assert.equal(projection.method_followup_plan.session_outcome, "collect_evidence"); assert.equal( (projection as { next_user_action?: { id?: string; on_user_stop?: { id?: string } } }).next_user_action?.id, "ask_method_followup", ); assert.equal( (projection as { next_user_action?: { on_user_stop?: { id?: string } } }).next_user_action?.on_user_stop?.id, "offer_provisional_range", ); assert.equal(projection.latest_result.session_outcome.kind, "collect_evidence"); assert.equal(projection.internal_observations.find((item) => item.layer === "d9")?.ask_theme, "relationship_style"); assert.equal(projection.latest_result.confirmation_allowed, false); assert.ok(projection.latest_result.indistinguishable_width_minutes >= 25); assert.equal(projection.latest_result.window_scan?.d9_candidates_differ, true); assert.doesNotMatch(JSON.stringify(projection), UNIQUE_MINUTE_COPY); assert.doesNotMatch(JSON.stringify(projection), /A\/B\/C\/D/); assert.match(JSON.stringify(projection.method_followup_plan.next_followup), /自然语言/); }); test("accepted batch evidence triggers server rescore without offering adoption", async () => { const restore = stubEngine(ENGINE_SCORE); try { const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture({ evidence: [educationEvidence], latestResult: null, }), get_agentic_rectification_case_compute: () => computeFixture(), record_agentic_rectification_evidence_batch: () => ({ items: [{ index: 0, outcome: "accepted", evidence_id: EDUCATION_ID, status: "confirmed", idempotent: false, clarification_fields: [], error_code: null, }], accepted_count: 1, needs_clarification_count: 0, rejected_count: 0, focus_id: null, }), persist_agentic_rectification_candidate_v2: () => ({ ...candidateSnapshotFixture(), cached: false, }), }); const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, userMessage: "2016年6月高考结束", accounting: accounting.client as never, }); const result = await (tools["rectification-record-evidence-batch"] as unknown as { execute(input: unknown): Promise<{ accepted_count: number; rescore: { status: string; executed_methods: string[]; error_code: string | null }; }>; }).execute({ caseId: CASE_ID, items: [{ quote: "2016年6月高考结束", proposedKind: "education_milestone", subject: "self", domain: "education", datePrecision: "month", occurredFrom: "2016-06", summary: "2016年6月一次学业节点", }], }); assert.equal(result.accepted_count, 1); assert.equal(result.rescore.status, "completed"); assert.ok(result.rescore.executed_methods.includes("d1-rashi")); const persistCall = accounting.calls.find((call) => call.fn === "persist_agentic_rectification_candidate_v2"); assert.ok(persistCall); const receipt = persistCall.args.p_decision_receipt as { window_scan?: { d9_candidates_differ?: boolean; d9_sign_names?: unknown } }; assert.equal(receipt.window_scan?.d9_candidates_differ, true); assert.deepEqual((receipt.window_scan as { d9_sign_names?: string[] } | undefined)?.d9_sign_names, ["白羊座", "天蝎"]); const completedReceipt = accounting.calls.find((call) => call.fn === "insert_agentic_rectification_tool_receipt" && call.args.p_tool_name === "rectification-record-evidence-batch" && call.args.p_status === "completed" ); assert.ok(completedReceipt); assert.ok((completedReceipt.args.p_executed_methods as string[]).includes("d1-rashi")); assert.equal( accounting.calls.some((call) => call.fn === "transition_agentic_rectification_case_status" && call.args.p_to_status === "candidate_ready" ), false, ); assert.doesNotMatch(JSON.stringify(result), UNIQUE_MINUTE_COPY); } finally { restore(); } }); test("cached candidates retry a failed VedAstro validation without recomputing ranking", async () => { const validationResponse = { status: "passed", can_confirm_exact_minute: true, event_validation: { search_events_primary_supports_local_winner: true, }, minute_sensitive_validation: { status: "passed", }, }; const restore = stubEngine(validationResponse); try { const compute = computeFixture(); const evidence = [educationEvidence]; const evidenceFingerprint = evidenceLedgerFingerprint([{ id: educationEvidence.id, sourceTurnId: educationEvidence.source_turn_id, subject: educationEvidence.subject, eventKind: educationEvidence.event_kind, domain: educationEvidence.domain, occurredFrom: educationEvidence.occurred_from, occurredTo: educationEvidence.occurred_to, datePrecision: educationEvidence.date_precision, summary: educationEvidence.summary, status: educationEvidence.status, supersedesEvidenceId: educationEvidence.supersedes_evidence_id, createdAt: educationEvidence.created_at, }]); const rangeFingerprint = candidateRangeFingerprint( compute.candidate_range, compute.baseline_profile_fingerprint, ); const cached = { ...candidateSnapshotFixture({ representativeTime: "05:02", evidenceLedgerFingerprint: evidenceFingerprint, decisionReceipt: { inference_state: { algorithm_version: "rectification-inference-v1", candidate_set_id: "04:55-05:02:04:55,05:02", revision: 1, phase: "discrimination", result_status: "credible_range", range_start: "04:55", range_end: "05:02", candidates: [ { id: CANDIDATE_ID, time: "05:02", cluster_range: ["05:02", "05:02"], prior_score: 58, posterior_score: 58, probability: 0.58, status: "active", rank: 1, strong_conflict_count: 0 }, { id: SECOND_CANDIDATE_ID, time: "04:55", cluster_range: ["04:55", "04:55"], prior_score: 42, posterior_score: 42, probability: 0.42, status: "active", rank: 2, strong_conflict_count: 0 }, ], events: [], probes: [], answered_probes: [], rounds: [], entropy: 0.98, representative_time: "05:02", credible_range: ["04:55", "05:02"], }, gates: { exact_confirmation: { external_validation_status: "failed", vedastro_event_validation: { status: "failed", search_events_primary_supports_local_winner: false, can_confirm_exact_minute: false, failure: { code: "timeout" }, }, }, }, }, }), candidate_range_fingerprint: rangeFingerprint, }; const refreshedReceipt = { ...cached.decision_receipt, gates: { exact_confirmation: { external_validation_status: "passed", vedastro_event_validation: { status: "passed", search_events_primary_supports_local_winner: true, can_confirm_exact_minute: true, failure: null, }, }, }, }; const parsedCached = parseV9CaseDossier(dossierFixture({ evidence, latestResult: cached })); assert.equal(parsedCached?.latestResult?.selectionAllowed, true); assert.equal(parsedCached?.latestResult?.evidenceLedgerFingerprint, evidenceFingerprint); assert.equal(parsedCached?.latestResult?.candidateRangeFingerprint, rangeFingerprint); assert.equal(readVedastroMinuteSensitiveStatus(parsedCached?.latestResult?.decisionReceipt), "failed"); assert.equal(authoritativeCandidateProjection(parsedCached!.latestResult!).candidates.length, 2); const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture({ evidence, latestResult: cached, }), get_agentic_rectification_case_compute: () => compute, refresh_agentic_rectification_vedastro_validation: () => ({ result_id: RESULT_ID, decision_receipt: refreshedReceipt, }), set_agentic_rectification_conversation_focus: (_fn, args) => ({ focus: { id: FOCUS_ID, case_id: CASE_ID, question_id: args.p_question_id, intent: args.p_intent, target_evidence_id: args.p_target_evidence_id, target_domain: args.p_target_domain, target_kind: args.p_target_kind, expected_answer_schema: args.p_expected_answer_schema, status: "active", asked_at: "2026-08-27T00:00:00.000Z", resolved_at: null, }, idempotent: false, }), }); const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, userMessage: "继续比较", accounting: accounting.client as never, }); const result = await (tools["rectification-compare-candidates"] as unknown as { execute(input: unknown): Promise<{ cached: boolean; candidates: Array<{ time: string; relativeSupport: number }>; confirmation_gate: { blockers: Array<{ id: string; status: string }> }; }>; }).execute({ caseId: CASE_ID }); const refreshCall = accounting.calls.find((call) => call.fn === "refresh_agentic_rectification_vedastro_validation" ); assert.ok(refreshCall, JSON.stringify(accounting.calls.map((call) => call.fn))); assert.deepEqual(refreshCall.args.p_validation, { status: "passed", search_events_primary_supports_local_winner: true, can_confirm_exact_minute: true, failure: null, }); assert.equal(refreshCall.args.p_minute_sensitive_status, "passed"); assert.equal( accounting.calls.some((call) => call.fn === "persist_agentic_rectification_candidate_v2"), false, ); assert.equal(result.cached, true); assert.deepEqual(result.candidates.map((item) => [item.time, item.relativeSupport]), [ ["05:02", 58], ["04:55", 42], ]); assert.ok(result.confirmation_gate, JSON.stringify(result)); assert.equal( result.confirmation_gate.blockers.find((gate) => gate.id === "vedastro_minute_sensitive")?.status, "passed", ); } finally { restore(); } }); test("evidence batch returns the persisted choice prompt as open_question", async () => { const restore = stubEngine({ ...ENGINE_SCORE, decision_receipt: { ...ENGINE_SCORE.decision_receipt, discriminating_event_probes: [{ year: 2023, year_label: "2023 年前后", domain: "career", event_family: "入职、升职或职责明显加重", source: "dasha_activation", tracks: ["vimshottari", "narayana"], tracks_agree: false, unique_minute_claim: false, user_meaning: "年份锁定 2023 年前后。事件家族:入职、升职或职责明显加重。", role: "distinguish", phase: "candidate_discriminator", information_gain: 1.09, semantic_key: "career.2023.dasha_activation", candidate_set_version: "set-test", candidate_split_hash: "set-test:career:2023", candidate_ids: ["04:50", "05:20"], expected_outcomes: [ { answer_class: "yes", supports: ["04:50"], conflicts: ["05:20"] }, { answer_class: "no", supports: ["05:20"], conflicts: ["04:50"] }, ], style_options: DYNAMIC_STYLE_OPTIONS, choice_kind: "existence", }], }, }); try { const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture({ evidence: [educationEvidence, relationshipEvidence, familyEvidence, careerEvidence], evidenceCount: 4, latestResult: null, }), get_agentic_rectification_case_compute: () => computeFixture(), record_agentic_rectification_evidence_batch: () => ({ items: [{ index: 0, outcome: "accepted", evidence_id: EDUCATION_ID, status: "confirmed", idempotent: false, clarification_fields: [], error_code: null, }], accepted_count: 1, needs_clarification_count: 0, rejected_count: 0, focus_id: null, }), persist_agentic_rectification_candidate_v2: (_fn, args) => ({ ...candidateSnapshotFixture({ decisionReceipt: args.p_decision_receipt as Record, }), cached: false, }), set_agentic_rectification_conversation_focus: (_fn, args) => ({ focus: { id: FOCUS_ID, case_id: CASE_ID, question_id: args.p_question_id, intent: args.p_intent, target_evidence_id: args.p_target_evidence_id, target_domain: args.p_target_domain, target_kind: args.p_target_kind, expected_answer_schema: args.p_expected_answer_schema, status: "active", asked_at: "2026-08-25T14:47:09.000Z", resolved_at: null, }, idempotent: false, }), }); const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, userMessage: "2016年6月高考结束", accounting: accounting.client as never, }); const result = await (tools["rectification-record-evidence-batch"] as unknown as { execute(input: unknown): Promise<{ accepted_count: number; rescore: { status: string }; open_question: { prompt?: string } | null; }>; }).execute({ caseId: CASE_ID, items: [{ quote: "2016年6月高考结束", proposedKind: "education_milestone", subject: "self", domain: "education", datePrecision: "month", occurredFrom: "2016-06", summary: "2016年6月一次学业节点", }], }); assert.equal(result.accepted_count, 1); assert.equal(result.rescore.status, "completed"); const setFocus = accounting.calls.find((call) => call.fn === "set_agentic_rectification_conversation_focus"); const schema = setFocus?.args.p_expected_answer_schema as { choice?: { prompt?: string } } | undefined; assert.match(schema?.choice?.prompt ?? "", /2023 年前后/); assert.match(result.open_question?.prompt ?? "", /2023 年前后/); assert.match(result.open_question?.prompt ?? "", /入职、升职或职责明显加重/); assert.doesNotMatch(result.open_question?.prompt ?? "", /高考/); } finally { restore(); } }); test("rescore failure does not fail the evidence write", async () => { const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture({ evidence: [educationEvidence], }), get_agentic_rectification_case_compute: () => computeFixture(), record_agentic_rectification_evidence_batch: () => ({ items: [{ index: 0, outcome: "accepted", evidence_id: EDUCATION_ID, status: "confirmed", idempotent: false, clarification_fields: [], error_code: null, }], accepted_count: 1, needs_clarification_count: 0, rejected_count: 0, focus_id: null, }), }); const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, userMessage: "2016年6月高考结束", accounting: accounting.client as never, }); const result = await (tools["rectification-record-evidence-batch"] as unknown as { execute(input: unknown): Promise<{ accepted_count: number; rescore: { status: string; error_code: string | null }; }>; }).execute({ caseId: CASE_ID, items: [{ quote: "2016年6月高考结束", proposedKind: "education_milestone", subject: "self", domain: "education", datePrecision: "month", occurredFrom: "2016-06", summary: "2016年6月一次学业节点", }], }); assert.equal(result.accepted_count, 1); assert.equal(result.rescore.status, "failed"); assert.ok(result.rescore.error_code); }); test("public tool surface stays at 14 and new cases bind 10.0.13", () => { assert.equal(PUBLIC_RECTIFICATION_TOOLS.length, 14); assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.13"); const deprecated = resolveExactSkillPackage( "jyotish-birth-time-rectification", "10.0.2", "8d7aa2d4bea0414e9a89ef908ccbc8c708c98f79f5b78ae4f7dc229b5f7dbb30", ); assert.equal(deprecated.status, "deprecated"); const plateau = latestResultToolProjection({ resultId: RESULT_ID, candidates: [ { candidateId: CANDIDATE_ID, time: "04:45", rank: 1, relativeSupport: 40, tiedMinuteCount: 25 }, { candidateId: SECOND_CANDIDATE_ID, time: "04:46", rank: 2, relativeSupport: 35, tiedMinuteCount: 25 }, { candidateId: THIRD_CANDIDATE_ID, time: "04:47", rank: 3, relativeSupport: 25, tiedMinuteCount: 25 }, ], selectionAllowed: true, confirmationAllowed: true, representativeTime: "04:45", selectedTime: null, selectionKind: null, algorithmVersion: "rectification-v5", }); assert.equal(plateau.confirmation_allowed, false); assert.equal(plateau.unique_minute_claim, false); assert.match(String(plateau.skill_verification_report), /Dasha \+ Gochara/); assert.match(String(plateau.skill_verification_report), /candidate_range_not_birth_time_truth/); assert.doesNotMatch(String(plateau.skill_verification_report), UNIQUE_MINUTE_COPY); assert.equal((plateau.session_outcome as { kind: string }).kind, "collect_evidence"); const skill = readFileSync(new URL("../../skills/jyotish-birth-time-rectification/SKILL.md", import.meta.url), "utf8"); assert.match(skill, /method_followup_plan/); assert.match(skill, /感情 → 事业 → 家人 → 职业 → 占问/); assert.match(skill, /外貌、体质、胎记或疤痕不得追问/); assert.match(skill, /KP 观察不计分、不挡提出门/); assert.match(skill, /唯一领先和宽度≤5只挡确认门/); assert.match(skill, /D9\/D10 类型表是校时方法/); assert.doesNotMatch(skill, /±5 分钟确定性/); assert.doesNotMatch(skill, /KP 政策跳过不挡提出门/); }); test("Mastra hides active candidates when the receipt range excludes one of them", () => { const candidates = [ { candidateId: CANDIDATE_ID, time: "05:00", rank: 1, relativeSupport: 20, tiedMinuteCount: 1 }, { candidateId: SECOND_CANDIDATE_ID, time: "05:07", rank: 2, relativeSupport: 15, tiedMinuteCount: 1 }, ]; const inferenceState = { algorithm_version: "rectification-inference-v1", candidate_set_id: "05:00-05:07:05:00,05:07", revision: 2, phase: "discrimination", result_status: "credible_range", range_start: "05:00", range_end: "05:07", candidates: [ { id: CANDIDATE_ID, time: "05:00", cluster_range: ["05:00", "05:00"], prior_score: 20, posterior_score: 20, probability: 0.57, status: "active", rank: 1, strong_conflict_count: 0 }, { id: SECOND_CANDIDATE_ID, time: "05:07", cluster_range: ["05:07", "05:07"], prior_score: 15, posterior_score: 15, probability: 0.43, status: "active", rank: 2, strong_conflict_count: 0 }, ], events: [], probes: [], answered_probes: [], rounds: [], entropy: 0.98, representative_time: "05:00", credible_range: ["05:00", "05:07"], }; const latest = { resultId: RESULT_ID, candidates, selectionAllowed: true, confirmationAllowed: false, representativeTime: "05:00", selectedTime: null, selectionKind: null, algorithmVersion: "rectification-v5", decisionReceipt: { inference_state: inferenceState }, }; const session = { methods: buildMethodFollowupPlan({ evidence: CLASSIC_COVERAGE }).methods, userStopped: true, candidateScores: candidates.map((item) => ({ time: item.time, score: item.relativeSupport })), holdoutValidation: "passed" as const, snapshotCurrent: true, trainingGateOpen: true, }; const valid = latestResultToolProjection(latest, session); assert.deepEqual((valid.candidates as typeof candidates).map((item) => item.time), ["05:00", "05:07"]); assert.equal(valid.selection_allowed, true); const invalid = latestResultToolProjection({ ...latest, decisionReceipt: { inference_state: { ...inferenceState, credible_range: ["05:00", "05:00"] }, }, }, session); assert.deepEqual(invalid.candidates, []); assert.equal(invalid.representative_time, null); assert.equal(invalid.selection_allowed, false); }); test("family then occupation then horary follow the method plan without appearance or marks", () => { const afterFamily = buildMethodFollowupPlan({ evidence: [ { status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null }, { status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null }, { status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null }, { status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null }, ], }); assert.equal(afterFamily.next_followup?.method_id, "occupation"); assert.equal(afterFamily.next_followup?.domain, "occupation"); assert.equal(afterFamily.methods.find((item) => item.method_id === "appearance")?.status, "skipped_by_policy"); assert.equal(afterFamily.methods.find((item) => item.method_id === "marks")?.status, "skipped_by_policy"); assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: afterFamily.next_followup, methods: afterFamily.methods, }), "collect_evidence"); assert.equal(afterFamily.next_followup?.choice_frame, null); assert.match(afterFamily.next_followup?.user_prompt_hint ?? "", /自然语言/); assert.doesNotMatch(afterFamily.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/); const afterOccupation = buildMethodFollowupPlan({ evidence: [ { status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null }, { status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null }, { status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null }, { status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null }, { status: "confirmed", domain: "occupation", datePrecision: "unknown", occurredFrom: null, occurredTo: null }, ], }); assert.equal(afterOccupation.next_followup?.method_id, "horary"); assert.match(afterOccupation.next_followup?.user_prompt_hint ?? "", /第 10 宫|D10|占问/); assert.match(afterOccupation.next_followup?.user_prompt_hint ?? "", /自然语言/); assert.doesNotMatch(afterOccupation.next_followup?.user_prompt_hint ?? "", /A\/B\/C\/D/); assert.doesNotMatch(JSON.stringify(afterFamily), /外貌|疤痕|胎记/); const afterHorary = buildMethodFollowupPlan({ evidence: [ { status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null }, { status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null }, { status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null }, { status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null }, { status: "confirmed", domain: "occupation", datePrecision: "unknown", occurredFrom: null, occurredTo: null }, { status: "confirmed", domain: "horary", datePrecision: "day", occurredFrom: "2024-01-01", occurredTo: null }, ], }); assert.equal(afterHorary.next_followup, null); }); test("precision stage lagna_frame waits for uncovered career before asking another dated event", () => { const plan = buildMethodFollowupPlan({ evidence: [ { status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null }, { status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null }, ], precisionStage: "lagna_frame", }); assert.equal(plan.next_followup?.method_id, "d10_career"); assert.equal(plan.next_followup?.source, "method_coverage"); assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true); assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY); }); test("lagna_frame after classic coverage does not keep a tie in discrimination without a remaining split", () => { const plan = buildMethodFollowupPlan({ evidence: CLASSIC_COVERAGE, precisionStage: "lagna_frame", }); assert.equal(plan.next_followup?.source, "precision_stage"); assert.equal(plan.next_followup?.ask_theme, "dated_event"); assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true); assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods, { separated: true }), false); assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, discriminatorProbe: null, candidateScores: [ { time: "05:00", score: 34 }, { time: "05:01", score: 33 }, { time: "05:02", score: 33 }, ], }), "provisional_range"); }); test("precision stage d4 asks home change not family, and d5 asks education", () => { const covered = CLASSIC_COVERAGE; const d4 = buildMethodFollowupPlan({ evidence: covered, precisionStage: "d4_refine", }); assert.equal(d4.next_followup?.source, "precision_stage"); assert.equal(d4.next_followup?.method_id, "d4_home"); assert.equal(d4.next_followup?.domain, "relocation"); assert.equal(d4.next_followup?.ask_theme, "home_change"); assert.doesNotMatch(d4.next_followup?.user_prompt_hint ?? "", /家人/); const legacyTheme = buildMethodFollowupPlan({ evidence: [...covered], precisionStage: "theme_refine", }); assert.equal(legacyTheme.next_followup?.ask_theme, "home_change"); const d5 = buildMethodFollowupPlan({ evidence: covered.filter((item) => item.domain !== "education"), precisionStage: "d5_refine", }); assert.equal(d5.next_followup?.method_id, "d5_education"); assert.equal(d5.next_followup?.domain, "education"); assert.doesNotMatch(JSON.stringify(d4), UNIQUE_MINUTE_COPY); }); test("accepted representative time reverse-verifies predicted events then hands off", () => { const careerProbe = { year: 2018, year_label: "2018 年前后", domain: "career" as const, event_family: "入职、升职或职责明显加重", source: "dasha_activation" as const, tracks: ["vimshottari", "narayana"] as const, tracks_agree: true, unique_minute_claim: false as const, user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。", role: "reverse_verify" as const, style_options: DYNAMIC_STYLE_OPTIONS, }; const plan = buildMethodFollowupPlan({ evidence: [{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-06-01", occurredTo: null, }], accepted: true, eventProbes: [ { ...careerProbe, domain: "education", source: "known_event_quality", role: "distinguish", event_family: "高考或重要考试发挥明显失常、压力很大", }, careerProbe, ], oosBlindPrompts: [{ domain: "family", user_meaning: "校时还没用过家人这条线。有没有一件没提过、但记得大概时间的家人变化?", used_for_scoring: false, }], activeFocus: { intent: "collect_method_evidence", targetDomain: "relationship", targetKind: null, }, }); assert.equal(plan.next_followup?.method_id, "reverse_verify"); assert.equal(plan.next_followup?.domain, "career"); assert.equal(plan.next_followup?.choice_frame?.scoring, true); assert.match(plan.next_followup?.choice_frame?.why ?? "", /2018 年前后/); assert.equal(plan.deferred_followup, null); assert.equal(plan.next_followup?.source, "reverse_verify"); const action = buildNextUserAction({ scorableCount: 3, evidenceCount: 3, hasLatestResult: true, selectionAllowed: true, sessionOutcome: "adopt_representative", nextFollowup: plan.next_followup, workingTime: "05:07", accepted: true, }); assert.equal(action.id, "verify_adopted_time"); assert.equal(action.on_user_stop.id, "start_consultation"); assert.doesNotMatch(JSON.stringify({ plan, action }), UNIQUE_MINUTE_COPY); }); test("accepted representative time without remaining probes hands off to consultation", () => { const plan = buildMethodFollowupPlan({ evidence: [{ status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-06-01", occurredTo: null, }], accepted: true, eventProbes: [{ year: 2016, year_label: "2016 年前后", domain: "education", event_family: "高考或重要考试发挥明显失常、压力很大", source: "known_event_quality", tracks: ["vimshottari", "narayana"], tracks_agree: true, unique_minute_claim: false, user_meaning: "年份锁定 2016 年前后。已有高考经历。", role: "distinguish", }], }); assert.equal(plan.next_followup, null); assert.equal(plan.deferred_followup, null); const action = buildNextUserAction({ scorableCount: 3, evidenceCount: 3, hasLatestResult: true, selectionAllowed: true, sessionOutcome: "adopt_representative", nextFollowup: plan.next_followup, workingTime: "05:07", accepted: true, }); assert.equal(action.id, "start_consultation"); assert.match(action.user_meaning, /看盘/); assert.match(action.user_meaning, /改选/); assert.doesNotMatch(JSON.stringify({ plan, action }), UNIQUE_MINUTE_COPY); }); test("confirmed relationship evidence skips generic D9 followups unless a real probe is bound", () => { const evidence = CLASSIC_COVERAGE; const precision = buildMethodFollowupPlan({ evidence, precisionStage: "d9_refine" }); const varga = buildMethodFollowupPlan({ evidence, observations: [{ layer: "d9", candidates_differ: true, ask_theme: "relationship_style" }], }); assert.notEqual(precision.next_followup?.domain, "relationship"); assert.notEqual(varga.next_followup?.domain, "relationship"); const probed = buildMethodFollowupPlan({ evidence, precisionStage: "d9_refine", eventProbes: [{ ...CAREER_CONFLICT_PROBE, year: 2021, year_label: "2021 年前后", domain: "relationship", event_family: "关系状态或相处方式明显变化", semantic_key: "relationship.2021.dasha_activation", candidate_split_hash: "set-test:relationship:2021", }], }); assert.equal(probed.next_followup?.domain, "relationship"); assert.equal(probed.next_followup?.source, "event_probe"); assert.equal(probed.next_followup?.semantic_key, "relationship.2021.dasha_activation"); }); test("d9_refine after relationship still asks uncovered career first", () => { const plan = buildMethodFollowupPlan({ evidence: [ { status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null }, { status: "confirmed", domain: "education", datePrecision: "month", occurredFrom: "2020-06-01", occurredTo: null }, { status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null }, { status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-08-08", occurredTo: null }, ], precisionStage: "d9_refine", }); assert.equal(plan.next_followup?.method_id, "d10_career"); assert.equal(plan.next_followup?.source, "method_coverage"); assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: false, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, }), "collect_evidence"); assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, }), "collect_evidence"); }); test("career evidence does not cover occupation; occupation still blocks until asked", () => { const plan = buildMethodFollowupPlan({ evidence: [ { status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null }, { status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null }, { status: "confirmed", domain: "career", datePrecision: "day", occurredFrom: "2019-09-01", occurredTo: null }, { status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null }, ], }); assert.equal(plan.next_followup?.method_id, "occupation"); assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered"); assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, }), "collect_evidence"); }); test("draft occupation_note without a date covers occupation and does not adopt a tie", () => { const plan = buildMethodFollowupPlan({ evidence: [ { status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null }, { status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null }, { status: "confirmed", domain: "career", datePrecision: "day", occurredFrom: "2019-09-01", occurredTo: null }, { status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null }, { status: "draft", domain: "occupation", datePrecision: "unknown", occurredFrom: null, occurredTo: null, eventKind: "occupation_note", summary: "职业类型轨迹为技术开发", }, ], }); assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered"); assert.notEqual(plan.next_followup?.method_id, "occupation"); assert.notEqual(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, candidateScores: [ { time: "05:00", score: 34 }, { time: "05:01", score: 33 }, { time: "05:02", score: 33 }, ], }), "adopt_representative"); }); test("stale occupation collect focus does not keep interviewing after occupation is covered", () => { const plan = buildMethodFollowupPlan({ evidence: [ { status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null }, { status: "confirmed", domain: "relationship", datePrecision: "day", occurredFrom: "2024-05-01", occurredTo: null }, { status: "confirmed", domain: "career", datePrecision: "day", occurredFrom: "2019-09-01", occurredTo: null }, { status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null }, { status: "draft", domain: "occupation", datePrecision: "unknown", occurredFrom: null, occurredTo: null, eventKind: "occupation_note", summary: "前端工程师", }, ], activeFocus: { intent: "collect_method_evidence", targetDomain: "occupation", targetKind: "occupation_note", }, }); assert.notEqual(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, candidateScores: [ { time: "05:00", score: 34 }, { time: "05:01", score: 33 }, { time: "05:02", score: 33 }, ], }), "adopt_representative"); }); test("D9/D10 contrast after occupation coverage asks a discriminator, not adopt", () => { const plan = buildMethodFollowupPlan({ evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"), contrastPacket: { candidateSetVersion: "05:00-05:02", vargaDifferences: [ { layer: "d9", signs: ["天秤", "天蝎", "射手"] }, { layer: "d10", signs: ["巨蟹", "狮子", "处女"] }, ], probes: [{ probeId: "contrast:varga.d10.巨蟹|狮子|处女", candidateSetVersion: "05:00-05:02", question: "当前几个候选在事业盘上还分得开。请核对一段还没用进评分的职业前事。", expectedOutcomes: [ { outcomeId: "supports_巨蟹", supportsCandidateIds: ["巨蟹"], conflictsCandidateIds: ["狮子", "处女"] }, { outcomeId: "supports_狮子", supportsCandidateIds: ["狮子"], conflictsCandidateIds: ["巨蟹", "处女"] }, { outcomeId: "supports_处女", supportsCandidateIds: ["处女"], conflictsCandidateIds: ["巨蟹", "狮子"] }, ], candidateSplitHash: "varga.d10.巨蟹|狮子|处女", informationGain: 0.12, sourceFeatures: [{ technique: "D10", calculationResultId: RESULT_ID }], domain: "career", year: null, semanticKey: "varga.d10.巨蟹|狮子|处女", }], }, }); assert.equal(plan.next_followup?.intent, "distinguish_candidates"); assert.match(plan.next_followup?.user_prompt_hint ?? "", /职业前事|事业盘/); assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, discriminatorProbe: { probeId: "contrast:varga.d10", candidateSetVersion: "05:00-05:02", question: "核对一段还没用进评分的职业前事", expectedOutcomes: [ { outcomeId: "a", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:01"] }, { outcomeId: "b", supportsCandidateIds: ["05:01"], conflictsCandidateIds: ["05:00"] }, ], candidateSplitHash: "varga.d10", informationGain: 0.12, sourceFeatures: [{ technique: "D10", calculationResultId: RESULT_ID }], domain: "career", year: null, semanticKey: "varga.d10", }, candidateScores: [ { time: "05:00", score: 34 }, { time: "05:01", score: 33 }, { time: "05:02", score: 33 }, ], }), "discriminate_candidates"); }); test("answered duty language skips window D10 and uses remaining D24", () => { const packet = { candidateSetVersion: "05:00-05:07", vargaDifferences: [ { layer: "d10", signs: ["巨蟹座", "狮子座", "处女座"] }, { layer: "d24", signs: ["05:00", "05:06|05:07"] }, ], probes: [{ probeId: "contrast:varga.d24.05:00/05:06|05:07", candidateSetVersion: "05:00-05:07", question: "当前几个候选在学业盘上还分得开。请核对一段还没用进评分的学业前事。", expectedOutcomes: [ { outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:06", "05:07"] }, { outcomeId: "no", supportsCandidateIds: ["05:06", "05:07"], conflictsCandidateIds: ["05:00"] }, ], candidateSplitHash: "varga.d24.05:00/05:06|05:07", informationGain: 0.16, sourceFeatures: [{ technique: "D24", calculationResultId: RESULT_ID }], domain: "education", year: null, semanticKey: "varga.d24.05:00/05:06|05:07", }], }; const evidence = [ ...CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"), { status: "draft" as const, domain: "occupation", datePrecision: "unknown" as const, occurredFrom: null, occurredTo: null, eventKind: "occupation_note", summary: "医疗器械算法,第三个(技术执行)", }, ]; const plan = buildMethodFollowupPlan({ evidence, contrastPacket: packet, }); assert.equal(plan.next_followup?.domain, "education"); assert.equal(plan.next_followup?.semantic_key, "varga.d24.05:00/05:06|05:07"); assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d10/); }); const DUMP_COVERAGE = [ { status: "confirmed" as const, domain: "education", datePrecision: "month" as const, occurredFrom: "2016-09-01", occurredTo: null, eventKind: "education_start", summary: "上大学", }, { status: "confirmed" as const, domain: "education", datePrecision: "month" as const, occurredFrom: "2015-06-01", occurredTo: null, eventKind: "education_interruption", summary: "高考失利复读", }, { status: "confirmed" as const, domain: "relationship", datePrecision: "day" as const, occurredFrom: "2024-05-01", occurredTo: null, eventKind: "relationship_start", summary: "开始一段感情", }, { status: "confirmed" as const, domain: "relationship", datePrecision: "day" as const, occurredFrom: "2024-08-08", occurredTo: null, eventKind: "relationship_end", summary: "感情结束", }, { status: "confirmed" as const, domain: "career", datePrecision: "day" as const, occurredFrom: "2024-04-07", occurredTo: null, eventKind: "career_entry", summary: "入职", }, { status: "confirmed" as const, domain: "family", datePrecision: "month" as const, occurredFrom: "2016-05-01", occurredTo: null, eventKind: "family_event", summary: "家人变化", }, { status: "confirmed" as const, domain: "occupation", datePrecision: "unknown" as const, occurredFrom: null, occurredTo: null, eventKind: "occupation_note", summary: "互联网程序员 / 前端 / Agent 开发", }, ]; const DUMP_TRANSITIONS = [ { layer: "d4", at: "05:00" }, { layer: "d4", at: "05:03" }, { layer: "d10", at: "05:00" }, { layer: "d10", at: "05:03" }, { layer: "d24", at: "05:00" }, { layer: "d24", at: "05:03" }, { layer: "d9", at: "04:52" }, { layer: "d9", at: "05:08" }, { layer: "d5", at: "05:15" }, ]; const DUMP_SCORES = [ { time: "05:00", score: 34 }, { time: "05:03", score: 33 }, { time: "05:04", score: 33 }, ]; test("coverage-complete tie with encoded D24/D10 asks remaining D4, not enrollment or breakup", () => { const packet = buildCandidateContrastPacket({ candidateSetVersion: "05:00-05:04", candidateTimes: DUMP_SCORES.map((item) => item.time), transitions: DUMP_TRANSITIONS, askedKeys: askedKeysFromLedgerEvidence(DUMP_COVERAGE), }); const plan = buildMethodFollowupPlan({ evidence: DUMP_COVERAGE, contrastPacket: packet, askedProbeKeys: askedKeysFromLedgerEvidence(DUMP_COVERAGE), }); assert.equal(plan.next_followup?.domain, "relocation"); assert.equal(plan.next_followup?.kind_hint, "home_change"); assert.match(plan.next_followup?.semantic_key ?? "", /varga\.d4/); assert.doesNotMatch(plan.next_followup?.kind_hint ?? "", /education_start|relationship_end/); assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /大学哪年入学|高考是 \d{4}|哪年毕业/); assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, discriminatorProbe: packet.probes[0] ?? null, candidateScores: DUMP_SCORES, }), "discriminate_candidates"); }); test("coverage-complete tie with no remaining split offers a provisional range", () => { const packet = buildCandidateContrastPacket({ candidateSetVersion: "05:00-05:04", candidateTimes: DUMP_SCORES.map((item) => item.time), transitions: DUMP_TRANSITIONS, askedKeys: askedKeysFromLedgerEvidence([ ...DUMP_COVERAGE, { status: "confirmed", domain: "relocation", eventKind: "home_change", summary: "搬家离乡", }, ]), }); const plan = buildMethodFollowupPlan({ evidence: [ ...DUMP_COVERAGE, { status: "confirmed" as const, domain: "relocation", datePrecision: "year" as const, occurredFrom: "2016-09-01", occurredTo: null, eventKind: "home_change", summary: "搬家离乡", }, ], contrastPacket: packet, sessionOutcome: "provisional_range", }); assert.equal(plan.next_followup, null); assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: { method_id: "d9_relationship", intent: "distinguish_candidates", ask_theme: "relationship_style", domain: "relationship", kind_hint: "relationship_change", user_prompt_hint: "当前候选在关系主题上仍分不开。", must_not_label: false, choice_frame: null, source: "varga_observation", }, methods: plan.methods, discriminatorProbe: null, candidateScores: DUMP_SCORES, }), "provisional_range"); const action = buildNextUserAction({ scorableCount: 6, evidenceCount: 8, hasLatestResult: true, selectionAllowed: true, sessionOutcome: "provisional_range", nextFollowup: null, workingTime: "05:00", }); assert.equal(action.id, "offer_provisional_range"); }); test("structured paused state ends evidence collection without parsing user copy", () => { assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: null, methods: buildMethodFollowupPlan({ evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"), }).methods, userStopped: true, candidateScores: [ { time: "05:00", score: 34 }, { time: "05:06", score: 33 }, { time: "05:07", score: 33 }, ], }), "provisional_range_user_stopped"); }); test("same domain different year still asks a conflict probe", () => { const plan = buildMethodFollowupPlan({ evidence: [ datedEvidence("education", "2016"), datedEvidence("relationship", "2018"), datedEvidence("career", "2015"), datedEvidence("family", "2023"), ], eventProbes: [{ ...CAREER_CONFLICT_PROBE, information_gain: 0.21, semantic_key: "career.2018.dasha_activation", }], }); assert.equal(plan.next_followup?.source, "event_probe"); assert.equal(plan.next_followup?.domain, "career"); }); test("three dated events with one holdout keep collecting instead of discriminating", () => { const evidence = [ datedEvidence("education", "2016"), datedEvidence("career", "2020"), datedEvidence("relationship", "2024"), { status: "confirmed" as const, domain: "occupation", datePrecision: "unknown" as const, occurredFrom: null, occurredTo: null, eventKind: "occupation_note", }, ]; const gate = trainingScoreableGate(evidence); assert.equal(gate.open, false); assert.equal(gate.trainingCount, 2); assert.equal(gate.holdoutCount, 1); const plan = buildMethodFollowupPlan({ evidence, declinedTopics: [{ target_domain: "family", status: "declined" }], precisionStage: "lagna_frame", eventProbes: [CAREER_CONFLICT_PROBE], }); assert.equal(plan.next_followup?.source, "method_coverage"); assert.equal(plan.next_followup?.intent, "collect_method_evidence"); assert.notEqual(plan.next_followup?.source, "event_probe"); assert.notEqual(plan.next_followup, null); assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, evidence, candidateScores: [ { time: "05:00", score: 34 }, { time: "05:01", score: 33 }, { time: "05:02", score: 33 }, ], discriminatorProbe: { probeId: "p-cd", candidateSetVersion: "set-test", question: "2018 年前后事业是否有明显变化?", expectedOutcomes: [ { outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:20"] }, { outcomeId: "no", supportsCandidateIds: ["05:20"], conflictsCandidateIds: ["05:00"] }, ], candidateSplitHash: "split", informationGain: 0.4, sourceFeatures: [{ technique: "dasha_activation", calculationResultId: null }], domain: "career", year: 2018, semanticKey: "career.2018", }, }), "collect_evidence"); }); test("adjacent education year does not re-ask enrollment after a recorded start", () => { const plan = buildMethodFollowupPlan({ evidence: [{ status: "confirmed", domain: "education", eventKind: "education_start", datePrecision: "month", occurredFrom: "2016-09-01", occurredTo: null, summary: "2016年9月上大学", }, { status: "confirmed", domain: "relationship", eventKind: "relationship_end", datePrecision: "day", occurredFrom: "2024-08-08", occurredTo: "2024-08-08", summary: "一段感情结束", }], eventProbes: [{ year: 2015, year_label: "2015 年前后", domain: "education", event_family: "升学、高考、转学或学习环境变化", source: "dasha_activation", tracks: ["vimshottari", "narayana"], tracks_agree: true, unique_minute_claim: false, user_meaning: "年份锁定 2015 年前后。事件家族:升学、高考、转学或学习环境变化。请写成一句自然语言是/否题。不得改年份。", role: "reverse_verify", information_gain: 0.21, semantic_key: "education.2015.dasha_activation", }], }); assert.notEqual(plan.next_followup?.domain, "education"); assert.notEqual(plan.next_followup?.source, "event_probe"); assert.equal(plan.next_followup?.method_id, "d10_career"); assert.match(plan.next_followup?.user_prompt_hint ?? "", /2016 年入学/); assert.match(plan.next_followup?.user_prompt_hint ?? "", /2024 年感情结束/); assert.match(plan.next_followup?.user_prompt_hint ?? "", /不要再问这些事发生在哪一年/); assert.doesNotMatch(plan.next_followup?.user_prompt_hint ?? "", /高考是 2015/); }); test("high information_gain leftover probe still blocks offering after coverage", () => { const plan = buildMethodFollowupPlan({ evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"), eventProbes: [{ year: 2018, year_label: "2018 年前后", domain: "relocation", event_family: "搬家、离乡或长期异地", source: "dasha_activation", tracks: ["vimshottari", "narayana"], tracks_agree: true, unique_minute_claim: false, user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否搬家。", role: "distinguish", phase: "candidate_discriminator", information_gain: 0.21, semantic_key: "relocation.2018.dasha_activation", 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"] }, ], }], }); assert.equal(plan.next_followup?.source, "event_probe"); assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true); assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, }), "discriminate_candidates"); }); test("event_probe still discriminates after coverage when candidates remain tied", () => { const plan = buildMethodFollowupPlan({ evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"), eventProbes: [{ year: 2018, year_label: "2018 年前后", domain: "relocation", event_family: "搬家、离乡或长期异地", source: "dasha_activation", tracks: ["vimshottari", "narayana"], tracks_agree: true, unique_minute_claim: false, user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否搬家。", role: "distinguish", phase: "candidate_discriminator", information_gain: 0.21, semantic_key: "relocation.2018.dasha_activation", 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"] }, ], }], }); assert.equal(plan.next_followup?.source, "event_probe"); assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true); assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, candidateScores: [ { time: "05:00", score: 34 }, { time: "05:01", score: 33 }, { time: "05:02", score: 33 }, ], }), "discriminate_candidates"); }); test("accepted time reverse-verifies an uncovered year in a covered domain", () => { const plan = buildMethodFollowupPlan({ evidence: [{ status: "confirmed", domain: "career", datePrecision: "day", occurredFrom: "2024-04-07", occurredTo: null, }], accepted: true, eventProbes: [{ year: 2018, year_label: "2018 年前后", domain: "career", event_family: "入职、升职或职责明显加重", source: "dasha_activation", tracks: ["vimshottari", "narayana"], tracks_agree: true, unique_minute_claim: false, user_meaning: "年份锁定 2018 年前后。请写成一句自然语言,问是否入职或职责加重。", role: "reverse_verify", style_options: DYNAMIC_STYLE_OPTIONS, }], }); assert.equal(plan.next_followup?.source, "reverse_verify"); assert.equal(plan.next_followup?.domain, "career"); assert.equal(plan.next_followup?.choice_frame?.period, "2018 年前后"); }); test("declining occupation covers the method; declining horary is skipped_by_policy", () => { const plan = buildMethodFollowupPlan({ evidence: [ { status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null }, { status: "confirmed", domain: "relationship", datePrecision: "year", occurredFrom: "2018-01-01", occurredTo: null }, { status: "confirmed", domain: "career", datePrecision: "year", occurredFrom: "2019-01-01", occurredTo: null }, { status: "confirmed", domain: "family", datePrecision: "year", occurredFrom: "2020-01-01", occurredTo: null }, { status: "confirmed", domain: "appearance", datePrecision: "unknown", occurredFrom: null, occurredTo: null }, { status: "confirmed", domain: "marks", datePrecision: "unknown", occurredFrom: null, occurredTo: null }, ], declinedTopics: [ { target_domain: "occupation", status: "declined" }, { target_domain: "horary", status: "declined" }, ], }); assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered"); assert.equal(plan.methods.find((item) => item.method_id === "horary")?.status, "skipped_by_policy"); assert.equal(plan.next_followup, null); assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, candidateScores: [ { time: "05:00", score: 62 }, { time: "05:01", score: 22 }, { time: "05:02", score: 16 }, ], holdoutValidation: "passed", }), "validated_range"); }); test("horary follow-up does not block propose once occupation is covered", () => { const plan = buildMethodFollowupPlan({ evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"), }); assert.equal(plan.next_followup?.method_id, "horary"); assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), false); assert.equal(conversationalSessionOutcome({ selectionAllowed: true, proposeAllowed: true, confirmationAllowed: false, nextFollowup: plan.next_followup, methods: plan.methods, candidateScores: [ { time: "05:00", score: 62 }, { time: "05:01", score: 22 }, { time: "05:02", score: 16 }, ], holdoutValidation: "passed", }), "validated_range"); }); test("offer-candidates refuses while method coverage remains", async () => { const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture({ evidence: [educationEvidence], latestResult: candidateSnapshotFixture({ selectionAllowed: true, representativeTime: "04:48", candidates: [ { candidate_id: CANDIDATE_ID, rank: 1, time: "04:48", relative_support: 58, tied_minute_count: 1 }, { candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:49", relative_support: 42, tied_minute_count: 2 }, ], }), }), }); const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, accounting: accounting.client as never, }); await assert.rejects( () => (tools["rectification-offer-candidates"] as unknown as { execute(input: unknown): Promise; }).execute({ caseId: CASE_ID }), (error: unknown) => error instanceof RectificationToolServiceError && error.code === "offer_not_allowed", ); assert.equal( accounting.calls.some((call) => call.fn === "transition_agentic_rectification_case_status" && call.args.p_status === "candidate_ready" ), false, ); }); test("paused case with selection_allowed may offer the escape hatch", async () => { const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture({ status: "paused", evidence: [educationEvidence], latestResult: candidateSnapshotFixture({ selectionAllowed: true, representativeTime: "04:48", evidenceLedgerFingerprint: null, candidates: [ { candidate_id: CANDIDATE_ID, rank: 1, time: "04:48", relative_support: 58, tied_minute_count: 1 }, { candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:49", relative_support: 42, tied_minute_count: 2 }, ], decisionReceipt: { inference_state: { algorithm_version: "rectification-inference-v1", candidate_set_id: "04:48-04:49:04:48,04:49", revision: 1, phase: "discrimination", result_status: "credible_range", range_start: "04:48", range_end: "04:49", candidates: [ { id: CANDIDATE_ID, time: "04:48", cluster_range: ["04:48", "04:48"], prior_score: 58, posterior_score: 58, probability: 0.58, status: "active", rank: 1, strong_conflict_count: 0 }, { id: SECOND_CANDIDATE_ID, time: "04:49", cluster_range: ["04:49", "04:49"], prior_score: 42, posterior_score: 42, probability: 0.42, status: "active", rank: 2, strong_conflict_count: 0 }, ], events: [], probes: [], answered_probes: [], rounds: [], entropy: 0.98, representative_time: "04:48", credible_range: ["04:48", "04:49"], }, }, }), }), transition_agentic_rectification_case_status: () => ({ case_id: CASE_ID, status: "candidate_ready", idempotent: false, }), }); const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, accounting: accounting.client as never, }); const projection = await (tools["rectification-offer-candidates"] as unknown as { execute(input: unknown): Promise<{ session_outcome: { kind: string } }>; }).execute({ caseId: CASE_ID }); assert.equal(projection.session_outcome.kind, "provisional_range_user_stopped"); assert.equal( accounting.calls.some((call) => call.fn === "transition_agentic_rectification_case_status" && call.args.p_status === "candidate_ready" ), false, ); }); test("paused case resumes only when the Agent explicitly requests it", async () => { let reads = 0; const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture({ status: reads++ === 0 ? "paused" : "collecting_evidence", evidence: [], }), transition_agentic_rectification_case_status: (_fn, args) => ({ case_id: CASE_ID, status: args.p_status, idempotent: false, }), }); const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, userMessage: "继续校正", accounting: accounting.client as never, }); const projection = await (tools["rectification-read-case"] as unknown as { execute(input: unknown): Promise<{ status: string }>; }).execute({ caseId: CASE_ID, resume: true }); assert.equal(projection.status, "collecting_evidence"); assert.equal( accounting.calls.some((call) => call.fn === "transition_agentic_rectification_case_status" && call.args.p_status === "collecting_evidence" ), true, ); }); test("offer-candidates allows a 34/33/33 tie after method coverage when remaining minutes do not split", async () => { const accounting = fakeAccounting({ ...receiptHandlers, get_agentic_rectification_case_dossier: () => dossierFixture({ evidence: [ { ...educationEvidence, domain: "education" }, { id: "44444444-4444-4444-8444-444444444442", source_turn_id: TURN_ID, subject: "self", event_kind: "relationship_start", domain: "relationship", occurred_from: "2018-01-01", occurred_to: null, date_precision: "year", summary: "感情变化", status: "confirmed", supersedes_evidence_id: null, created_at: "2026-08-12T10:00:07.000Z", }, { id: "44444444-4444-4444-8444-444444444443", source_turn_id: TURN_ID, subject: "self", event_kind: "career_entry", domain: "career", occurred_from: "2019-01-01", occurred_to: null, date_precision: "year", summary: "工作变化", status: "confirmed", supersedes_evidence_id: null, created_at: "2026-08-12T10:00:08.000Z", }, { id: "44444444-4444-4444-8444-444444444446", source_turn_id: TURN_ID, subject: "self", event_kind: "family_event", domain: "family", occurred_from: "2020-01-01", occurred_to: null, date_precision: "year", summary: "家人变化", status: "confirmed", supersedes_evidence_id: null, created_at: "2026-08-12T10:00:09.000Z", }, { id: "44444444-4444-4444-8444-444444444445", source_turn_id: TURN_ID, subject: "self", event_kind: "occupation_note", domain: "occupation", occurred_from: null, occurred_to: null, date_precision: "unknown", summary: "长期一直是程序员", status: "confirmed", supersedes_evidence_id: null, created_at: "2026-08-12T10:00:10.000Z", }, ], latestResult: candidateSnapshotFixture({ selectionAllowed: true, representativeTime: "05:00", evidenceLedgerFingerprint: null, decisionReceipt: { propose_allowed: true, window_scan: { scanned: true, d9_lagna_count: 3, d10_lagna_count: 3, d9_candidates_differ: true, d10_candidates_differ: true, d9_sign_names: ["天秤", "天蝎", "射手"], d10_sign_names: ["巨蟹", "狮子", "处女"], }, }, candidates: [ { candidate_id: CANDIDATE_ID, rank: 1, time: "05:00", relative_support: 34, tied_minute_count: 1 }, { candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "05:01", relative_support: 33, tied_minute_count: 1 }, { candidate_id: THIRD_CANDIDATE_ID, rank: 3, time: "05:02", relative_support: 33, tied_minute_count: 1 }, ], }), }), }); const tools = createRectificationV9Tools({ userId: USER_ID, caseId: CASE_ID, turnId: TURN_ID, accounting: accounting.client as never, }); const projection = await (tools["rectification-offer-candidates"] as unknown as { execute(input: unknown): Promise<{ session_outcome: { kind: string } }>; }).execute({ caseId: CASE_ID }); assert.equal(projection.session_outcome.kind, "provisional_range"); assert.equal( accounting.calls.some((call) => call.fn === "transition_agentic_rectification_case_status" && call.args.p_status === "candidate_ready" ), true, ); });