fix(rectification): score varga-style groups at equal weight

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-29 00:15:21 +08:00
parent adfc75af8c
commit e41eaace8d
8 changed files with 222 additions and 5 deletions
@@ -0,0 +1,171 @@
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,
} 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<typeof buildCandidateContrastPacket>): 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("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);
});