import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import { decideRectification } from "../src/lib/rectification-agentic/core/rectification-decision.ts"; import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts"; import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts"; import { buildMethodFollowupPlan } from "../src/lib/rectification-agentic/v9/method-followup.ts"; import { parseRectificationCandidateResult, workingRectificationHouseTable, } from "../src/lib/rectification-candidate-result.ts"; import { decideFromDossier } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts"; import { latestResultToolProjection } from "../src/mastra/rectification-v9-tools.ts"; import { isRenderableChoiceOpenQuestion } from "../src/lib/rectification-agentic/v9/server-focus.ts"; const 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 TIED = [ { time: "04:47", score: 12 }, { time: "04:51", score: 12 }, { time: "04:53", score: 11 }, { time: "04:59", score: 11 }, { time: "05:00", score: 11 }, { time: "05:07", score: 11 }, { time: "05:12", score: 11 }, { time: "05:14", score: 8 }, { time: "05:15", score: 7 }, ]; function dated( id: string, domain: string, eventKind: string, occurredFrom: string, extra: { occurredTo?: string | null; datePrecision?: "year" | "month" | "day" | "range" | "unknown"; status?: "confirmed" | "draft"; } = {}, ) { return { id, status: extra.status ?? ("confirmed" as const), domain, datePrecision: extra.datePrecision ?? ("month" as const), occurredFrom, occurredTo: extra.occurredTo ?? null, eventKind, }; } /** 19-row ledger from the stalled case: 15 scoreable, 4 domains; occupation never recorded. */ const CASE_EVIDENCE = [ dated("e-edu-start", "education", "education_start", "2016-09-01"), dated("e-edu-complete", "education", "education_completion", "2020-06-01"), dated("e-edu-interrupt", "education", "education_interruption", "2021-01-01", { occurredTo: "2023-07-01", datePrecision: "range", }), dated("e-edu-change", "education", "education_change", "2023-01-01", { datePrecision: "year" }), dated("e-career-entry-2020", "career", "career_entry", "2020-04-01"), dated("e-career-exit-2020", "career", "career_exit", "2020-10-01"), dated("e-career-entry-2024", "career", "career_entry", "2024-04-07", { datePrecision: "day" }), dated("e-career-exit-2026", "career", "career_exit", "2026-08-11", { datePrecision: "day" }), dated("e-career-change-2022", "career", "career_change", "2022-12-01"), dated("e-career-change-2024", "career", "career_change", "2024-01-01", { datePrecision: "year" }), dated("e-career-range", "career", "career_change", "2020-04-01", { occurredTo: "2020-10-01", datePrecision: "range", }), dated("e-career-draft-a", "career", "career_change", "", { datePrecision: "unknown", status: "draft", occurredTo: null, }), dated("e-career-draft-b", "career", "career_entry", "", { datePrecision: "unknown", status: "draft", occurredTo: null, }), dated("e-rel-2024-05", "relationship", "relationship_change", "2024-05-01"), dated("e-rel-start", "relationship", "relationship_start", "2024-06-01"), dated("e-rel-end", "relationship", "relationship_end", "2024-08-08", { datePrecision: "day" }), dated("e-reloc", "relocation", "home_change", "2022-10-01"), dated("e-family", "family", "family_event", "2016-05-01"), ].map((item) => ({ ...item, occurredFrom: item.occurredFrom || null, })); const OCCUPATION_FOCUS = { intent: "collect_method_evidence" as const, targetDomain: "occupation", targetKind: "occupation_note", questionId: "collect:occupation:collect_method_evidence", }; function yearlessProbe(layer: string, gain: number, domain: string) { return { probeId: `contrast:varga.${layer}.04:47/05:00`, candidateSetVersion: "04:47-05:15", question: `当前几个候选在 ${layer} 上还分得开。`, expectedOutcomes: [ { outcomeId: "yes" as const, supportsCandidateIds: ["04:47"], conflictsCandidateIds: ["05:00"] }, { outcomeId: "no" as const, supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["04:47"] }, ], candidateSplitHash: `varga.${layer}.04:47/05:00`, informationGain: gain, sourceFeatures: [{ technique: layer.toUpperCase(), calculationResultId: null }], domain, year: null as number | null, semanticKey: `varga.${layer}.04:47/05:00`, choiceKind: "existence" as const, styleOptions: STYLE_OPTIONS.map((item) => ({ label: item.label, answerClass: item.answer_class, })), }; } const YEARLESS_PACKET = { candidateSetVersion: "04:47-05:15", vargaDifferences: [] as const, probes: [ yearlessProbe("d24", 2.5, "education"), yearlessProbe("d7", 1.22, "family"), yearlessProbe("d4", 0.99, "relocation"), yearlessProbe("d5", 0.5, "education"), ], }; function occupationPlan( extra: Partial[0]> = {}, ) { return buildMethodFollowupPlan({ evidence: CASE_EVIDENCE, contrastPacket: YEARLESS_PACKET, candidatesSeparated: false, ...extra, }); } function twelveHouses(sign: string, occupant?: string) { return Array.from({ length: 12 }, (_, index) => ({ house: index + 1, sign, occupants: index === 0 && occupant ? [occupant] : [], })); } const CANDIDATE_IDS = [ "88888888-8888-4888-8888-888888888881", "88888888-8888-4888-8888-888888888882", ] as const; test("skill version stays 10.0.13 for the occupation-coverage exit fix", () => { assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.13"); }); test("nineteen-row ledger opens the training gate with four scoreable domains", () => { const gate = trainingScoreableGate(CASE_EVIDENCE); assert.equal(gate.open, true); assert.ok(gate.trainingCount >= 3); assert.ok(gate.trainingDomainCount >= 2); assert.ok(CASE_EVIDENCE.length >= 15); }); test("career answers under an occupation collect focus do not cover occupation before ledger norm", () => { const plan = occupationPlan(); assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered"); assert.equal( CASE_EVIDENCE.some((item) => item.domain === "occupation" && item.eventKind === "occupation_note"), false, ); }); test("answering occupation collect remaps a career-domain job description onto occupation_note", async () => { const { applyOccupationCollectLedgerNorm } = await import( "../src/lib/rectification-agentic/v9/evidence-model.ts" ); const remapped = applyOccupationCollectLedgerNorm(OCCUPATION_FOCUS, [{ domain: "career", eventKind: "career_entry" as const, datePrecision: "unknown" as const, occurredFrom: null, occurredTo: null, summary: "长期做有机合成", quote: "有机合成", subject: "self" as const, }]); assert.equal(remapped[0]?.domain, "occupation"); assert.equal(remapped[0]?.eventKind, "occupation_note"); const plan = occupationPlan({ evidence: [ ...CASE_EVIDENCE, { status: "confirmed" as const, domain: remapped[0]!.domain, datePrecision: remapped[0]!.datePrecision, occurredFrom: remapped[0]!.occurredFrom, occurredTo: remapped[0]!.occurredTo, eventKind: remapped[0]!.eventKind, summary: remapped[0]!.summary, }, ], contrastPacket: { candidateSetVersion: "04:47-05:15", vargaDifferences: [], probes: [] }, }); assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered"); assert.notEqual(plan.next_followup?.method_id, "occupation"); assert.notEqual(plan.next_followup?.ask_theme, "occupation"); }); test("occupation coverage fallback uses a closed occupation collect focus plus career evidence", () => { const uncovered = occupationPlan({ contrastPacket: { candidateSetVersion: "04:47-05:15", vargaDifferences: [], probes: [] }, }); assert.equal(uncovered.methods.find((item) => item.method_id === "occupation")?.status, "uncovered"); const covered = occupationPlan({ contrastPacket: { candidateSetVersion: "04:47-05:15", vargaDifferences: [], probes: [] }, closedCollectFocuses: [{ questionId: OCCUPATION_FOCUS.questionId, targetDomain: "occupation", intent: "collect_method_evidence", status: "resolved", }], }); assert.equal(covered.methods.find((item) => item.method_id === "occupation")?.status, "covered"); assert.notEqual(covered.next_followup?.method_id, "occupation"); }); test("career evidence alone still does not cover occupation", () => { const plan = occupationPlan({ contrastPacket: { candidateSetVersion: "04:47-05:15", vargaDifferences: [], probes: [] }, }); assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered"); assert.equal(plan.next_followup?.method_id, "occupation"); }); test("training gate open does not mint yearless existence or quality varga cards", () => { const plan = occupationPlan(); assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered"); assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /^varga\.d(24|7|4|5)\./); assert.equal(plan.dropped_probes.some((item) => item.reason === "yearless_ungrounded_contrast"), true); assert.equal(plan.next_followup?.method_id, "occupation"); assert.equal(plan.next_followup?.intent, "collect_method_evidence"); assert.equal(plan.next_followup?.choice_frame, null); }); test("training gate open still asks a signed yearless D10 style card", () => { const d10 = { probeId: "contrast:varga.d10.巨蟹座/狮子座", candidateSetVersion: "04:47-05:15", question: "平时做事,你更接近下面哪一种?", expectedOutcomes: [ { outcomeId: "yes" as const, supportsCandidateIds: ["04:47"], conflictsCandidateIds: ["05:00"] }, { outcomeId: "weak_yes" as const, supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["04:47"] }, ], candidateSplitHash: "varga.d10.巨蟹座/狮子座", informationGain: 1.4, sourceFeatures: [{ technique: "D10", calculationResultId: null }], domain: "career", year: null as number | null, semanticKey: "varga.d10.巨蟹座/狮子座", choiceKind: "varga_style" as const, styleOptions: [ { label: "做事以照顾人为主,在意团队里的感受", answerClass: "yes" as const, sign: "巨蟹座" }, { label: "习惯带头,也不排斥站到台前", answerClass: "weak_yes" as const, sign: "狮子座" }, ], }; const plan = occupationPlan({ contrastPacket: { ...YEARLESS_PACKET, probes: [...YEARLESS_PACKET.probes, d10], }, }); assert.equal(plan.next_followup?.intent, "distinguish_candidates"); assert.ok(plan.next_followup?.choice_frame); assert.equal(plan.next_followup?.semantic_key, d10.semanticKey); assert.equal(plan.next_followup?.choice_kind, "varga_style"); }); test("training gate closed still withholds yearless varga cards", () => { const short = [ dated("e-edu", "education", "education_start", "2016-09-01"), dated("e-career", "career", "career_entry", "2020-04-01"), ]; assert.equal(trainingScoreableGate(short).open, false); const plan = buildMethodFollowupPlan({ evidence: short, contrastPacket: YEARLESS_PACKET, candidatesSeparated: false, }); assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /^varga\.d(24|7|4|5)\./); assert.notEqual(plan.next_followup?.intent, "distinguish_candidates"); }); test("training complete with no renderable distinguish card allows offering a range without adopt", () => { const blocked = decideRectification({ methodCoverageAll: false, trainingGateOpen: true, candidateScores: TIED, discriminatorProbe: null, engineAcceptAllowed: true, engineProposeAllowed: true, }); assert.equal(blocked.canOfferRange, true); assert.equal(blocked.canAdopt, false); assert.equal(blocked.canConfirmExactMinute, false); assert.equal(blocked.nextAction, "offer_provisional_range"); assert.equal(blocked.sessionOutcome, "provisional_range"); const stillClosed = decideRectification({ methodCoverageAll: false, trainingGateOpen: false, candidateScores: TIED, discriminatorProbe: null, engineAcceptAllowed: true, engineProposeAllowed: true, }); assert.equal(stillClosed.canOfferRange, false); assert.equal(stillClosed.canAdopt, false); assert.equal(stillClosed.nextAction, "ask_fact_collection"); }); test("decideFromDossier offers a range when training is complete and the ask layer has no card", () => { const decision = decideFromDossier({ evidence: CASE_EVIDENCE, conversationSummary: { activeFocus: null, declinedSkippedTopics: [] }, latestResult: { resultId: "55555555-5555-4555-8555-555555555555", selectionAllowed: true, confirmationAllowed: false, candidates: TIED.map((item, index) => ({ candidateId: `88888888-8888-4888-8888-88888888888${index}`, time: item.time, rank: index + 1, relativeSupport: item.score, })), representativeTime: "04:47", decisionReceipt: { accept_allowed: true, propose_allowed: true, selection_allowed: true, }, }, case: { acceptedTime: null }, }); assert.equal(decision.canOfferRange, true); assert.equal(decision.canAdopt, false); assert.equal(decision.canConfirmExactMinute, false); }); test("agent route narrates a numeric range exit and persists a collect when no renderable distinguish card remains", () => { const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8"); assert.match(route, /nonConvergingRangeNarration/); assert.doesNotMatch(route, /可以先按当前区间看盘,也可以再补一件记得时间的经历/); const fastPath = route.slice( route.indexOf('if (action === "message")'), route.indexOf("const requestTime"), ); assert.match(fastPath, /persistNextInterviewIfIdle|isNonConvergingRangeOffer|canOfferRange/); assert.doesNotMatch(fastPath, /USER_STOP_PATTERN|parseChoiceKeyFromUserMessage/); }); test("public house_table and natal_recast follow the representative minute", () => { const matched = parseRectificationCandidateResult({ resultId: "11111111-1111-4111-8111-111111111111", candidates: [ { candidateId: CANDIDATE_IDS[0], rank: 1, time: "04:47", relativeSupport: 12, tiedMinuteCount: 1 }, { candidateId: CANDIDATE_IDS[1], rank: 2, time: "05:00", relativeSupport: 11, tiedMinuteCount: 1 }, ], overallConfidence: "low", selectionAllowed: true, confirmationAllowed: false, representativeTime: "04:47", selectedTime: null, decisionReceipt: { house_table: { time: "05:00", lagna: "双子座", houses: twelveHouses("双子座", "水星") }, house_tables_by_time: { "04:47": { time: "04:47", lagna: "金牛座", houses: twelveHouses("金牛座", "太阳") }, "05:00": { time: "05:00", lagna: "双子座", houses: twelveHouses("双子座", "水星") }, }, natal_recast: { time: "05:00", lagna: "双子座", user_meaning: "本命宫位已按 05:00 重算(上升 双子座)。下面是本轮实际执行的技法,不能当作唯一分钟确认。", unique_minute_claim: false, confirmation_allowed: false, }, }, }); assert.equal(matched?.representativeTime, "04:47"); assert.equal(matched?.houseTable?.time, "04:47"); assert.equal(matched?.houseTable?.lagna, "金牛座"); assert.equal(matched?.natalRecast?.time, "04:47"); assert.equal(workingRectificationHouseTable(matched!)?.time, "04:47"); }); test("a mismatched receipt house_table is not emitted with another representative minute", () => { const mismatched = parseRectificationCandidateResult({ resultId: "11111111-1111-4111-8111-111111111111", candidates: [ { candidateId: CANDIDATE_IDS[0], rank: 1, time: "04:47", relativeSupport: 12, tiedMinuteCount: 1 }, { candidateId: CANDIDATE_IDS[1], rank: 2, time: "05:00", relativeSupport: 11, tiedMinuteCount: 1 }, ], overallConfidence: "low", selectionAllowed: true, confirmationAllowed: false, representativeTime: "04:47", selectedTime: null, decisionReceipt: { house_table: { time: "05:00", lagna: "双子座", houses: twelveHouses("双子座", "水星") }, natal_recast: { time: "05:00", lagna: "双子座", user_meaning: "本命宫位已按 05:00 重算(上升 双子座)。下面是本轮实际执行的技法,不能当作唯一分钟确认。", unique_minute_claim: false, confirmation_allowed: false, }, }, }); assert.equal(mismatched?.representativeTime, "04:47"); assert.notEqual(mismatched?.houseTable?.time, "05:00"); assert.equal(mismatched?.houseTable, null); assert.notEqual(mismatched?.natalRecast?.time, "05:00"); }); test("tool projection does not pair a 05:00 house table with a 04:47 representative time", () => { const decision = decideRectification({ methodCoverageAll: true, trainingGateOpen: true, candidateScores: TIED, discriminatorProbe: null, }); const projection = latestResultToolProjection({ resultId: "55555555-5555-4555-8555-555555555555", candidates: TIED.map((item, index) => ({ candidateId: `88888888-8888-4888-8888-88888888888${index}`, time: item.time, rank: index + 1, relativeSupport: item.score, tiedMinuteCount: 1, })), selectionAllowed: true, confirmationAllowed: false, representativeTime: "04:47", selectedTime: null, selectionKind: null, algorithmVersion: "test", decisionReceipt: { house_table: { time: "05:00", lagna: "双子座", houses: twelveHouses("双子座", "水星") }, natal_recast: { time: "05:00", lagna: "双子座", user_meaning: "本命宫位已按 05:00 重算(上升 双子座)。", unique_minute_claim: false, confirmation_allowed: false, }, }, }, decision); assert.notEqual(projection.house_table && (projection.house_table as { time?: string }).time, "05:00"); if (projection.representative_time === "04:47" && projection.house_table) { assert.equal((projection.house_table as { time?: string }).time, "04:47"); } }); test("collect focus is still not a renderable choice card", () => { assert.equal(isRenderableChoiceOpenQuestion({ question_id: OCCUPATION_FOCUS.questionId, prompt: "你长期做什么工作?", status: "already_open", kind: "collect_spoken", intent: "collect_method_evidence", domain: "occupation", focus_id: "11111111-1111-4111-8111-111111111111", probe_id: null, }), false); });