import assert from "node:assert/strict"; import test from "node:test"; import { applyProbeOutcome } from "../src/lib/rectification-agentic/core/apply-probe-outcome.ts"; import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts"; import { buildCandidateContrastPacket, conflictProbesFromContrast, inspectDiscriminatorProbes, } from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts"; import { asInferenceState } from "../src/lib/rectification-agentic/core/compose-receipt.ts"; import { decisionStateFingerprint } from "../src/lib/rectification-agentic/core/decision-fingerprint.ts"; import type { AnswerClass, ConflictProbe } from "../src/lib/rectification-agentic/core/types.ts"; function twoGroupStylePacket() { return buildCandidateContrastPacket({ candidateSetVersion: "05:00-05:04", candidateTimes: ["05:00", "05:04"], transitions: [ { layer: "d9", at: "05:04", from_sign: "巨蟹座", to_sign: "狮子座" }, ], }); } function threeGroupStylePacket() { return buildCandidateContrastPacket({ candidateSetVersion: "05:00-05:04", candidateTimes: ["05:00", "05:03", "05:04"], transitions: [ { layer: "d10", at: "05:03", from_sign: "巨蟹座", to_sign: "狮子座" }, { layer: "d10", at: "05:04", from_sign: "狮子座", to_sign: "处女座" }, ], }); } function styleProbeFrom(packet: ReturnType): ConflictProbe { const probe = conflictProbesFromContrast(packet).find((item) => item.source === "varga_contrast"); assert.ok(probe); return probe; } function absSupportDelta(probe: ConflictProbe, answer: AnswerClass, time: string): number { const scores = Object.fromEntries(probe.candidate_ids.map((id) => [id, 10])); const applied = applyProbeOutcome(scores, probe, answer); return Math.abs(applied.deltas[time] ?? 0); } test("two-group varga_style A and B move their groups by the same absolute delta", () => { const probe = styleProbeFrom(twoGroupStylePacket()); assert.equal(probe.choice_kind, "varga_style"); const yesGroup = probe.expected_outcomes.find((row) => row.answer_class === "yes")?.supports[0]; const weakGroup = probe.expected_outcomes.find((row) => row.answer_class === "weak_yes")?.supports[0]; assert.ok(yesGroup); assert.ok(weakGroup); assert.notEqual(yesGroup, weakGroup); const yesDelta = absSupportDelta(probe, "yes", yesGroup); const weakDelta = absSupportDelta(probe, "weak_yes", weakGroup); assert.equal(yesDelta, 2); assert.equal(weakDelta, 2); assert.equal(yesDelta, weakDelta); const unsure = applyProbeOutcome( Object.fromEntries(probe.candidate_ids.map((id) => [id, 10])), probe, "unsure", ); assert.ok(Object.values(unsure.deltas).every((value) => value === 0)); }); test("three-group varga_style A/B/C each carry full peer weight", () => { const probe = styleProbeFrom(threeGroupStylePacket()); assert.equal(probe.choice_kind, "varga_style"); const scored = (["yes", "weak_yes", "no"] as const).map((answer) => { const support = probe.expected_outcomes.find((row) => row.answer_class === answer)?.supports[0]; assert.ok(support, answer); return absSupportDelta(probe, answer, support); }); assert.deepEqual(scored, [2, 2, 2]); }); test("old ConflictProbe receipts without choice_kind keep half-weight weak_yes", () => { const produced = styleProbeFrom(twoGroupStylePacket()); const legacy: ConflictProbe = { id: produced.id, semantic_key: produced.semantic_key, candidate_split_hash: produced.candidate_split_hash, domain: produced.domain, year: produced.year, question: produced.question, candidate_ids: produced.candidate_ids, expected_outcomes: produced.expected_outcomes, information_gain: produced.information_gain, source: produced.source, }; assert.equal(legacy.choice_kind, undefined); const state = buildInferenceState({ range_start: "05:00", range_end: "05:04", candidates: [ { id: "05:00", time: "05:00", relative_support: 34 }, { id: "05:04", time: "05:04", relative_support: 33 }, ], events: [{ id: "e-rel", domain: "relationship", year: 2024, precision: "year" }], probes: [legacy], }); const loaded = asInferenceState(JSON.parse(JSON.stringify(state))); assert.ok(loaded); const loadedProbe = loaded.probes.find((item) => item.id === produced.id); assert.ok(loadedProbe); assert.equal(loadedProbe.choice_kind, undefined); const weakGroup = loadedProbe.expected_outcomes.find((row) => row.answer_class === "weak_yes")?.supports[0]; const yesGroup = loadedProbe.expected_outcomes.find((row) => row.answer_class === "yes")?.supports[0]; assert.ok(weakGroup); assert.ok(yesGroup); assert.equal(absSupportDelta(loadedProbe, "weak_yes", weakGroup), 1); assert.equal(absSupportDelta(loadedProbe, "yes", yesGroup), 2); assert.equal(loaded.candidate_set_id, state.candidate_set_id); assert.equal(loaded.revision, state.revision); }); test("varga_style B-option weak_yes counts as strong conflict; existence weak_yes does not", () => { const style = styleProbeFrom(twoGroupStylePacket()); assert.equal(style.choice_kind, "varga_style"); const styleConflicted = style.expected_outcomes.find((row) => row.answer_class === "weak_yes")?.conflicts[0]; assert.ok(styleConflicted); let styleScores = Object.fromEntries(style.candidate_ids.map((id) => [id, 10])); let styleCounts: Record = {}; let eliminated = new Set(); for (let round = 0; round < 3; round += 1) { const applied = applyProbeOutcome(styleScores, style, "weak_yes", { strongConflictCounts: styleCounts, eliminatedIds: eliminated, }); styleScores = { ...applied.scores }; styleCounts = { ...applied.strong_conflict_counts }; eliminated = new Set(applied.eliminated_ids); } assert.equal(styleCounts[styleConflicted], 3); assert.equal(eliminated.has(styleConflicted), true); const existencePacket = buildCandidateContrastPacket({ candidateSetVersion: "05:00-05:04", candidateTimes: ["05:00", "05:04"], transitions: [{ layer: "d9", at: "05:04" }], }); const existence = conflictProbesFromContrast(existencePacket).find((item) => item.choice_kind === "existence"); assert.ok(existence); const existenceConflicted = existence.expected_outcomes.find((row) => row.answer_class === "weak_yes")?.conflicts[0]; assert.ok(existenceConflicted); let existenceScores = Object.fromEntries(existence.candidate_ids.map((id) => [id, 10])); let existenceCounts: Record = {}; for (let round = 0; round < 3; round += 1) { const applied = applyProbeOutcome(existenceScores, existence, "weak_yes", { strongConflictCounts: existenceCounts, }); existenceScores = { ...applied.scores }; existenceCounts = { ...applied.strong_conflict_counts }; assert.equal(applied.eliminated_ids.length, 0); } assert.equal(existenceCounts[existenceConflicted], 0); }); test("varga_style contrast writeback keeps style_options for the next round", () => { const probe = styleProbeFrom(twoGroupStylePacket()); assert.equal(probe.choice_kind, "varga_style"); assert.ok((probe.style_options?.length ?? 0) >= 2); assert.ok(probe.style_options?.every((item) => item.label.trim().length > 0)); }); test("engine varga.d9/d10 without style_options scores with the render effective kind", () => { for (const semanticKey of ["varga.d9", "varga.d10"] as const) { const packet = { candidateSetVersion: "05:00-05:04", vargaDifferences: [], probes: [{ probeId: `contrast:${semanticKey}.engine-no-signs`, candidateSetVersion: "05:00-05:04", question: "那几年相处更接近哪一种?", expectedOutcomes: [ { outcomeId: "supports_05:00", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:04"] }, { outcomeId: "supports_05:04", supportsCandidateIds: ["05:04"], conflictsCandidateIds: ["05:00"] }, ], candidateSplitHash: `05:00-05:04:${semanticKey}.engine-no-signs`, informationGain: 1.2, sourceFeatures: [{ technique: semanticKey.slice(6).toUpperCase(), calculationResultId: null }], domain: "relationship", year: null, semanticKey, choiceKind: "varga_style" as const, }], }; const scored = conflictProbesFromContrast(packet); const inspected = inspectDiscriminatorProbes(packet); assert.equal(scored.length, 1, semanticKey); assert.equal(inspected.selected, null, semanticKey); assert.equal(inspected.dropped.some((item) => ( item.semantic_key === semanticKey && item.reason === "yearless_ungrounded_contrast" )), true, semanticKey); assert.equal(scored[0]?.choice_kind, "existence", semanticKey); } }); test("replaying a varga_style probe keeps candidate_set_id and a monotonic revision", () => { const probe = styleProbeFrom(twoGroupStylePacket()); const before = buildInferenceState({ range_start: "05:00", range_end: "05:04", candidates: [ { id: "05:00", time: "05:00", relative_support: 34 }, { id: "05:04", time: "05:04", relative_support: 33 }, ], events: [{ id: "e-rel", domain: "relationship", year: 2024, precision: "year" }], probes: [probe], }); const after = buildInferenceState({ range_start: before.range_start, range_end: before.range_end, candidates: before.candidates.map((item) => ({ id: item.id, time: item.time, relative_support: item.prior_score, })), events: before.events, probes: before.probes, previous: before, answered_probes: [{ probe_id: probe.id, semantic_key: probe.semantic_key, candidate_split_hash: probe.candidate_split_hash, answer_class: "weak_yes", classified_from: "choice", }], }); assert.equal(after.candidate_set_id, before.candidate_set_id); assert.ok(after.revision >= before.revision); const beforeFp = decisionStateFingerprint({ caseId: "case-style-weight", evidenceLedgerFingerprint: "fp-a", candidateSetId: before.candidate_set_id, inferenceRevision: before.revision, answeredProbeIds: before.answered_probes.map((item) => item.probe_id), scoringPolicyVersion: "policy-v2", }); const afterFp = decisionStateFingerprint({ caseId: "case-style-weight", evidenceLedgerFingerprint: "fp-a", candidateSetId: after.candidate_set_id, inferenceRevision: after.revision, answeredProbeIds: after.answered_probes.map((item) => item.probe_id), scoringPolicyVersion: "policy-v2", }); assert.notEqual(afterFp, beforeFp); });