import assert from "node:assert/strict"; import test from "node:test"; import { applyProbeOutcome } from "../src/lib/rectification-agentic/core/apply-probe-outcome.ts"; import { applyAnswerToState, buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts"; import { asInferenceState } from "../src/lib/rectification-agentic/core/compose-receipt.ts"; import { PROBE_WEIGHT, SCORE_DELTA, STRONG_CONFLICT_ELIMINATION_COUNT, type ConflictProbe, } from "../src/lib/rectification-agentic/core/types.ts"; import type { CandidateDiscriminatorProbe } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts"; import { buildMethodFollowupPlan } from "../src/lib/rectification-agentic/v9/method-followup.ts"; import { buildSkillVerificationPacket } from "../src/lib/rectification-agentic/v9/skill-verification-report.ts"; import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts"; import { RECTIFICATION_SKILL_VERSION } from "../src/lib/rectification-agentic/v9/case-status.ts"; const TIMES = ["05:00", "05:10"] as const; function dated( id: string, domain: string, eventKind: string, occurredFrom: string, ) { return { id, status: "confirmed" as const, domain, datePrecision: "month" as const, occurredFrom, occurredTo: null, eventKind, }; } const LEDGER = [ dated("e-edu", "education", "education_start", "2016-09-01"), dated("e-rel", "relationship", "relationship_start", "2024-05-01"), dated("e-family", "family", "family_event", "2016-05-01"), dated("e-reloc", "relocation", "home_change", "2022-10-01"), ]; const D9_STYLE: CandidateDiscriminatorProbe = { probeId: "contrast:varga.d9.巨蟹座/狮子座", candidateSetVersion: "05:00-05:10", question: "亲密关系里更接近下面哪一种相处方式?", expectedOutcomes: [ { outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:10"] }, { outcomeId: "weak_yes", supportsCandidateIds: ["05:10"], conflictsCandidateIds: ["05:00"] }, ], candidateSplitHash: "varga.d9.巨蟹座/狮子座", informationGain: 1.4, sourceFeatures: [{ technique: "D9", calculationResultId: null }], domain: "relationship", year: null, semanticKey: "varga.d9.巨蟹座/狮子座", choiceKind: "varga_style", styleOptions: [ { label: "相处里更在意照顾对方的感受", answerClass: "yes", sign: "巨蟹座" }, { label: "习惯带头,也不排斥站到台前", answerClass: "weak_yes", sign: "狮子座" }, ], }; const CAREER_2023_05: DiscriminatingEventProbe = { year: 2023, year_label: "2023 年 5 月前后", month: 5, domain: "career", event_family: "入职或职责变化", source: "dasha_boundary", tracks: ["vimshottari", "narayana"], tracks_agree: true, unique_minute_claim: false, user_meaning: "2023 年 5 月前后有没有入职或换工作?", role: "distinguish", phase: "candidate_discriminator", information_gain: 0.9, semantic_key: "career.2023.05.dasha_boundary", candidate_split_hash: "career.2023.05", candidate_ids: [...TIMES], expected_outcomes: [ { answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] }, { answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] }, { answer_class: "unsure", supports: [], conflicts: [] }, ], style_options: [ { label: "明确发生且时间吻合", answer_class: "yes" }, { label: "发生过但程度较弱", answer_class: "weak_yes" }, { label: "明确没有发生", answer_class: "no" }, { label: "这段记不清楚", answer_class: "unsure" }, ], choice_kind: "existence", }; function personalityPlan(extra: Partial[0]> = {}) { return buildMethodFollowupPlan({ evidence: LEDGER, sessionOutcome: "discriminate_candidates", candidatesSeparated: false, topCandidateTimes: [...TIMES], contrastPacket: { candidateSetVersion: "05:00-05:10", vargaDifferences: [], probes: [D9_STYLE], }, ...extra, }); } function clockProbe(input: { id: string; semanticKey: string; domain: string; year: number; source: string; choiceKind?: ConflictProbe["choice_kind"]; yesSupports: readonly string[]; yesConflicts: readonly string[]; }): ConflictProbe { return { id: input.id, semantic_key: input.semanticKey, candidate_split_hash: `${input.semanticKey}:${input.yesSupports.join(",")}`, domain: input.domain, year: input.year, question: input.semanticKey, candidate_ids: [...input.yesSupports, ...input.yesConflicts], expected_outcomes: [ { answer_class: "yes", supports: input.yesSupports, conflicts: input.yesConflicts }, { answer_class: "weak_yes", supports: input.yesConflicts, conflicts: input.yesSupports }, { answer_class: "no", supports: [], conflicts: [] }, { answer_class: "unsure", supports: [], conflicts: [] }, ], information_gain: 0.4, source: input.source, ...(input.choiceKind ? { choice_kind: input.choiceKind } : {}), }; } const D9_PROBE = clockProbe({ id: "probe:varga.d9", semanticKey: "varga.d9.巨蟹座/狮子座", domain: "relationship", year: 0, source: "varga_contrast", choiceKind: "varga_style", yesSupports: ["05:00"], yesConflicts: ["05:10"], }); test("SCORE_DELTA stays ±2/±1 and yearless weight is half", () => { assert.deepEqual(SCORE_DELTA, { support: 2, weak_support: 1, neutral: 0, weak_conflict: -1, conflict: -2, }); assert.equal(PROBE_WEIGHT.dated, 1); assert.equal(PROBE_WEIGHT.yearless, 0.5); assert.equal(STRONG_CONFLICT_ELIMINATION_COUNT, 3); // 原值: "10.0.24" // 新值: "10.0.25" // 原因: BUG-661/662/663 定向补事改逐条点选,Skill 升版 assert.equal(RECTIFICATION_SKILL_VERSION, "10.0.25"); }); test("D9 answer B moves scores by ±1 and does not count toward elimination", () => { const applied = applyProbeOutcome( { "05:00": 20, "05:10": 20 }, D9_PROBE, "weak_yes", ); assert.equal(applied.kind, "tie_break"); assert.equal(applied.deltas["05:10"], 1); assert.equal(applied.deltas["05:00"], -1); assert.equal(applied.strong_conflict_counts["05:00"], 0); assert.equal(applied.strong_conflict_counts["05:10"], 0); assert.deepEqual(applied.eliminated_ids, []); }); test("three yearless conflicts never eliminate; three dated conflicts still do", () => { const yearless = [1, 2, 3].map((index) => clockProbe({ id: `probe:varga.d9.${index}`, semanticKey: `varga.d9.style.${index}`, domain: "relationship", year: 0, source: "varga_contrast", choiceKind: "varga_style", yesSupports: ["05:00"], yesConflicts: ["05:10"], })); let scores: Record = { "05:00": 20, "05:10": 20 }; let counts: Record = { "05:00": 0, "05:10": 0 }; let eliminated = new Set(); for (const probe of yearless) { const applied = applyProbeOutcome(scores, probe, "yes", { eliminatedIds: eliminated, strongConflictCounts: counts, }); scores = { ...applied.scores }; counts = { ...applied.strong_conflict_counts }; eliminated = new Set(applied.eliminated_ids); } assert.equal(counts["05:10"], 0); assert.equal(eliminated.has("05:10"), false); const dated = [1, 2, 3].map((index) => clockProbe({ id: `probe:career.${2019 + index}`, semanticKey: `career.${2019 + index}.dasha_boundary`, domain: "career", year: 2019 + index, source: "dasha_boundary", yesSupports: ["05:00"], yesConflicts: ["05:10"], })); for (const probe of dated) { const applied = applyProbeOutcome(scores, probe, "yes", { eliminatedIds: eliminated, strongConflictCounts: counts, }); scores = { ...applied.scores }; counts = { ...applied.strong_conflict_counts }; eliminated = new Set(applied.eliminated_ids); assert.equal(applied.kind, "informative"); assert.equal(applied.deltas["05:00"], 2); assert.equal(applied.deltas["05:10"], -2); } assert.ok(counts["05:10"] >= STRONG_CONFLICT_ELIMINATION_COUNT); assert.equal(eliminated.has("05:10"), true); }); test("nakshatra_boundary is yearless: ±1 and no conflict count", () => { const probe = clockProbe({ id: "probe:nakshatra", semanticKey: "nakshatra.boundary.a/b", domain: "other", year: 0, source: "nakshatra_boundary", choiceKind: "varga_style", yesSupports: ["05:00"], yesConflicts: ["05:10"], }); const applied = applyProbeOutcome({ "05:00": 20, "05:10": 20 }, probe, "yes"); assert.equal(applied.kind, "tie_break"); assert.equal(applied.deltas["05:00"], 1); assert.equal(applied.deltas["05:10"], -1); assert.equal(applied.strong_conflict_counts["05:10"], 0); }); test("an askable 2023.05 career probe keeps D9 deferred", () => { const plan = personalityPlan({ eventProbes: [CAREER_2023_05] }); assert.equal(plan.next_followup?.semantic_key, CAREER_2023_05.semantic_key); assert.notEqual(plan.next_followup?.choice_kind, "varga_style"); assert.equal( plan.dropped_probes.some((item) => ( item.semantic_key === D9_STYLE.semanticKey && item.reason === "yearless_deferred" )), true, JSON.stringify(plan.dropped_probes), ); }); test("dated distinguish empty and two unseparated candidates defers D9", () => { const plan = personalityPlan({ eventProbes: [] }); // 原值: next_followup 是 D9 varga_style,dropped 不含 yearless_deferred // 新值: D9 记 yearless_deferred,next 不再是性格卡 // 原因: BUG-651 带年月池空即交付,性格题不得挡在结果前面 assert.notEqual(plan.next_followup?.semantic_key, D9_STYLE.semanticKey); assert.notEqual(plan.next_followup?.choice_kind, "varga_style"); assert.equal( plan.dropped_probes.some((item) => ( item.semantic_key === D9_STYLE.semanticKey && item.reason === "yearless_deferred" )), true, JSON.stringify(plan.dropped_probes), ); }); test("a sole remaining candidate defers D9 as yearless_deferred", () => { const plan = personalityPlan({ eventProbes: [], topCandidateTimes: ["05:00"], }); assert.notEqual(plan.next_followup?.semantic_key, D9_STYLE.semanticKey); assert.equal( plan.dropped_probes.some((item) => ( item.semantic_key === D9_STYLE.semanticKey && item.reason === "yearless_deferred" )), true, JSON.stringify(plan.dropped_probes), ); }); test("already-separated candidates do not ask D9", () => { const plan = personalityPlan({ eventProbes: [], candidatesSeparated: true, }); assert.notEqual(plan.next_followup?.semantic_key, D9_STYLE.semanticKey); assert.notEqual(plan.next_followup?.choice_kind, "varga_style"); }); test("asInferenceState round-trips a varga_style tie_break round", () => { const before = buildInferenceState({ range_start: "05:00", range_end: "05:10", candidates: [ { id: "05:00", time: "05:00", relative_support: 20 }, { id: "05:10", time: "05:10", relative_support: 18 }, ], events: [{ id: "e-rel", domain: "relationship", year: 2024, precision: "year" }], probes: [D9_PROBE], }); const after = applyAnswerToState(before, D9_PROBE.id, "weak_yes"); assert.equal(after.rounds.at(-1)?.kind, "tie_break"); const loaded = asInferenceState(JSON.parse(JSON.stringify(after))); assert.ok(loaded); assert.equal(loaded.rounds.at(-1)?.kind, "tie_break"); assert.equal(loaded.answered_probes.length, 1); }); test("verification report marks D9/D10 and nakshatra as reference", () => { const packet = buildSkillVerificationPacket({ representativeTime: "05:00", widthMinutes: 10, candidates: [ { time: "05:00", rank: 1, relativeSupport: 20 }, { time: "05:10", rank: 2, relativeSupport: 18 }, ], }); assert.match(packet.markdown, /\| D9 \/ D10 类型对照 \| reference \| 性格自评,只作排序参考,不参与淘汰 \|/); assert.match(packet.markdown, /\| 月宿边界 \| reference \| 性格自评,只作排序参考,不参与淘汰 \|/); });