fix(rectification): score varga-style groups at equal weight
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user