Files
Jyotisha/frontend/src/lib/rectification-agentic/core/apply-probe-outcome.ts
T
Jesse_ChenandCursor 29750d3835
Independent Staging Quality Gate / validate (push) Failing after 9m53s
Independent Staging Quality Gate / publish (push) Has been skipped
fix(web): persist C/D rectification answers without waiting for rescore
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>
2026-08-23 22:41:01 +08:00

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,
};
}