Choice C/D without new evidence never changed the candidate posterior until the next dated-event rescore, and persist-v2 would cache-hit on the same evidence fingerprint. Patch the latest decision_receipt.inference_state in place so the next follow-up sees the asked split immediately. Co-authored-by: Cursor <cursoragent@cursor.com>
63 lines
2.3 KiB
TypeScript
63 lines
2.3 KiB
TypeScript
import { SCORE_DELTA, type AnswerClass, type ConflictProbe, type ProbeOutcome, type ScoreDirection } from "./types.ts";
|
|
|
|
export type ProbeApplyResult = Readonly<{
|
|
scores: Readonly<Record<string, number>>;
|
|
eliminated_ids: readonly string[];
|
|
kind: "informative" | "low_information";
|
|
deltas: Readonly<Record<string, number>>;
|
|
}>;
|
|
|
|
export function outcomeForAnswer(probe: ConflictProbe, answer: AnswerClass): ProbeOutcome | null {
|
|
return probe.expected_outcomes.find((item) => item.answer_class === answer) ?? null;
|
|
}
|
|
|
|
export function directionFor(candidateId: string, outcome: ProbeOutcome): ScoreDirection {
|
|
if (outcome.supports.includes(candidateId)) return outcome.answer_class === "weak_yes" ? "weak_support" : "support";
|
|
if (outcome.conflicts.includes(candidateId)) return outcome.answer_class === "weak_yes" ? "weak_conflict" : "conflict";
|
|
return "neutral";
|
|
}
|
|
|
|
/**
|
|
* Pure reducer: every active candidate is updated from the same probe outcome.
|
|
* A strong conflict eliminates that candidate. Unsure answers are low-information.
|
|
*/
|
|
export function applyProbeOutcome(
|
|
scores: Readonly<Record<string, number>>,
|
|
probe: ConflictProbe,
|
|
answer: AnswerClass,
|
|
options: { eliminatedIds?: ReadonlySet<string>; eliminateBelow?: number } = {},
|
|
): ProbeApplyResult {
|
|
const eliminated = new Set(options.eliminatedIds ?? []);
|
|
const outcome = outcomeForAnswer(probe, answer);
|
|
const deltas: Record<string, number> = {};
|
|
const next: Record<string, number> = {};
|
|
if (!outcome || answer === "unsure") {
|
|
for (const [id, score] of Object.entries(scores)) {
|
|
next[id] = score;
|
|
deltas[id] = 0;
|
|
}
|
|
return { scores: next, eliminated_ids: [...eliminated], kind: "low_information", deltas };
|
|
}
|
|
for (const [id, score] of Object.entries(scores)) {
|
|
if (eliminated.has(id)) {
|
|
next[id] = score;
|
|
deltas[id] = 0;
|
|
continue;
|
|
}
|
|
const direction = directionFor(id, outcome);
|
|
const delta = SCORE_DELTA[direction];
|
|
deltas[id] = delta;
|
|
next[id] = score + delta;
|
|
if (direction === "conflict" || next[id] < (options.eliminateBelow ?? -4)) {
|
|
eliminated.add(id);
|
|
}
|
|
}
|
|
const changed = Object.values(deltas).some((value) => value !== 0);
|
|
return {
|
|
scores: next,
|
|
eliminated_ids: [...eliminated],
|
|
kind: changed ? "informative" : "low_information",
|
|
deltas,
|
|
};
|
|
}
|