fix(rectification): unify discriminator question contract and rank by information gain
Python and TypeScript now share a four-option probe contract, persist Focus before asking, and pick the highest-value renderable probe instead of preferring low-gain career events over D24. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,11 @@
|
||||
|
||||
import type { AnswerClass, ConflictProbe } from "./types.ts";
|
||||
import { d9StyleLabel, d10StyleLabel } from "../v9/varga-type-tables.ts";
|
||||
import {
|
||||
completeStyleOptions,
|
||||
isRenderableProbe,
|
||||
rankDiscriminatorScore,
|
||||
} from "../v9/probe-question-contract.ts";
|
||||
|
||||
export type ContrastChoiceKind = "existence" | "varga_style" | "event_quality";
|
||||
|
||||
@@ -342,15 +347,134 @@ function vargaDifferencesForPacket(input: {
|
||||
|
||||
export function selectDiscriminatorProbe(
|
||||
packet: CandidateContrastPacket | null | undefined,
|
||||
options?: { askedKeys?: readonly string[]; topCandidateTimes?: readonly string[] },
|
||||
): CandidateDiscriminatorProbe | null {
|
||||
const ranked = (packet?.probes ?? []).filter((probe) => {
|
||||
const ids = new Set(probe.expectedOutcomes.flatMap((row) => [
|
||||
const asked = new Set(options?.askedKeys ?? []);
|
||||
const ranked = (packet?.probes ?? []).flatMap((probe) => {
|
||||
const completed = withCompletedContrastOptions(probe);
|
||||
if (!completed) return [];
|
||||
const ids = [...new Set(completed.expectedOutcomes.flatMap((row) => [
|
||||
...row.supportsCandidateIds,
|
||||
...row.conflictsCandidateIds,
|
||||
]));
|
||||
return probe.expectedOutcomes.length >= 2 && probe.informationGain > 0 && ids.size >= 2;
|
||||
]))];
|
||||
if (!isRenderableProbe({
|
||||
informationGain: completed.informationGain,
|
||||
candidateIds: ids,
|
||||
expectedOutcomeCount: completed.expectedOutcomes.length,
|
||||
choiceKind: completed.choiceKind,
|
||||
styleOptions: completed.styleOptions,
|
||||
})) return [];
|
||||
const askedAlready = asked.has(completed.semanticKey)
|
||||
|| asked.has(completed.candidateSplitHash)
|
||||
|| asked.has(completed.probeId);
|
||||
return [{
|
||||
probe: completed,
|
||||
score: rankDiscriminatorScore({
|
||||
informationGain: completed.informationGain,
|
||||
asked: askedAlready,
|
||||
candidateIds: ids,
|
||||
topCandidateTimes: options?.topCandidateTimes,
|
||||
}),
|
||||
}];
|
||||
}).sort((left, right) => right.score - left.score || right.probe.informationGain - left.probe.informationGain);
|
||||
return ranked[0]?.probe ?? null;
|
||||
}
|
||||
|
||||
function withCompletedContrastOptions(
|
||||
probe: CandidateDiscriminatorProbe,
|
||||
): CandidateDiscriminatorProbe | null {
|
||||
const mapped = probe.styleOptions?.map((item) => ({
|
||||
label: item.label,
|
||||
answer_class: item.answerClass,
|
||||
...(item.sign ? { sign: item.sign } : {}),
|
||||
})) ?? [];
|
||||
const incoming = mapped.length >= 2 ? mapped : [...mapped, ...inferredVargaStyleIncoming(probe)];
|
||||
const choiceKind = effectiveContrastChoiceKind({
|
||||
...probe,
|
||||
styleOptions: incoming.map((item) => ({
|
||||
label: item.label,
|
||||
answerClass: item.answer_class as AnswerClass,
|
||||
...(item.sign ? { sign: item.sign } : {}),
|
||||
})),
|
||||
});
|
||||
return ranked[0] ?? null;
|
||||
const styleOptions = completeStyleOptions({
|
||||
choiceKind,
|
||||
styleOptions: incoming,
|
||||
});
|
||||
if (!styleOptions) return null;
|
||||
const outcomes = withUnsureOutcome(probe.expectedOutcomes);
|
||||
return {
|
||||
...probe,
|
||||
choiceKind,
|
||||
expectedOutcomes: outcomes,
|
||||
styleOptions: styleOptions.map((item) => ({
|
||||
label: item.label,
|
||||
answerClass: item.answer_class,
|
||||
...(item.sign ? { sign: item.sign } : {}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function inferredVargaStyleIncoming(
|
||||
probe: CandidateDiscriminatorProbe,
|
||||
): Array<{ label: string; answer_class: AnswerClass; sign?: string }> {
|
||||
const parsed = signsFromVargaProbe(probe);
|
||||
if (!parsed) return [];
|
||||
const labelFor = parsed.layer === "d9" ? d9StyleLabel : d10StyleLabel;
|
||||
const classes = ["yes", "weak_yes", "no"] as const;
|
||||
return parsed.signs.slice(0, 3).flatMap((sign, index) => {
|
||||
const label = labelFor(sign);
|
||||
const answerClass = classes[index];
|
||||
if (!label || !answerClass) return [];
|
||||
return [{ label, answer_class: answerClass, sign }];
|
||||
});
|
||||
}
|
||||
|
||||
function signsFromVargaProbe(
|
||||
probe: CandidateDiscriminatorProbe,
|
||||
): { layer: "d9" | "d10"; signs: string[] } | null {
|
||||
const match = probe.semanticKey.match(/^varga\.(d9|d10)\.(.+)$/);
|
||||
const layer = match?.[1] === "d9" || match?.[1] === "d10" ? match[1] : null;
|
||||
const fromKey = match?.[2]
|
||||
?.split(/[|/]/)
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item && !/^\d{1,2}:\d{2}$/.test(item))
|
||||
?? [];
|
||||
const fromOutcomes = probe.expectedOutcomes.flatMap((row) => {
|
||||
const token = row.outcomeId.replace(/^supports_/, "").trim();
|
||||
return token && !/^\d{1,2}:\d{2}$/.test(token) ? [token] : [];
|
||||
});
|
||||
const signs = (fromKey.length >= 2 ? fromKey : fromOutcomes).slice(0, 3);
|
||||
if (!layer || signs.length < 2) return null;
|
||||
return { layer, signs };
|
||||
}
|
||||
|
||||
function effectiveContrastChoiceKind(probe: CandidateDiscriminatorProbe): ContrastChoiceKind {
|
||||
const key = probe.semanticKey;
|
||||
if (probe.choiceKind === "varga_style" && (probe.styleOptions?.length ?? 0) < 2) {
|
||||
if (key.startsWith("varga.d24") || key.startsWith("varga.d5")) return "event_quality";
|
||||
if (key.startsWith("varga.d9") || key.startsWith("varga.d10")) return "varga_style";
|
||||
return "existence";
|
||||
}
|
||||
if (probe.choiceKind === "varga_style" || probe.choiceKind === "event_quality" || probe.choiceKind === "existence") {
|
||||
return probe.choiceKind;
|
||||
}
|
||||
if (key.startsWith("varga.d24") || key.startsWith("varga.d5")) return "event_quality";
|
||||
if (key.startsWith("varga.d9") || key.startsWith("varga.d10")) return "varga_style";
|
||||
return "existence";
|
||||
}
|
||||
|
||||
function withUnsureOutcome(
|
||||
outcomes: readonly ContrastExpectedOutcome[],
|
||||
): readonly ContrastExpectedOutcome[] {
|
||||
const rows = [...outcomes];
|
||||
if (!rows.some((row) => row.outcomeId === "unsure")) {
|
||||
rows.push({ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] });
|
||||
}
|
||||
if (!rows.some((row) => row.outcomeId === "no") && rows.some((row) => row.outcomeId === "weak_yes")) {
|
||||
rows.push({ outcomeId: "no", supportsCandidateIds: [], conflictsCandidateIds: [] });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function conflictProbesFromContrast(
|
||||
@@ -513,15 +637,22 @@ function remainingStyleOptions(
|
||||
split: RemainingVargaSplit,
|
||||
kind: ContrastChoiceKind,
|
||||
): readonly ContrastStyleOption[] | undefined {
|
||||
if (kind !== "varga_style") return undefined;
|
||||
const classes = ["yes", "weak_yes", "no"] as const;
|
||||
const options = split.groups.slice(0, 3).flatMap((group, index) => {
|
||||
const sign = split.signs[index];
|
||||
if (!sign) return [];
|
||||
const label = split.layer === "d10" ? d10StyleLabel(sign) : d9StyleLabel(sign);
|
||||
return [{ label, answerClass: classes[index] ?? "unsure", sign }];
|
||||
});
|
||||
return options.length >= 2 ? uniquifyStyleLabels(options) : undefined;
|
||||
const incoming = kind === "varga_style"
|
||||
? split.groups.slice(0, 3).flatMap((group, index) => {
|
||||
const sign = split.signs[index];
|
||||
if (!sign) return [];
|
||||
const label = split.layer === "d10" ? d10StyleLabel(sign) : d9StyleLabel(sign);
|
||||
const classes = ["yes", "weak_yes", "no"] as const;
|
||||
return [{ label, answer_class: classes[index] ?? "unsure", sign }];
|
||||
})
|
||||
: [];
|
||||
const completed = completeStyleOptions({ choiceKind: kind, styleOptions: incoming });
|
||||
if (!completed) return undefined;
|
||||
return uniquifyStyleLabels(completed.map((item) => ({
|
||||
label: item.label,
|
||||
answerClass: item.answer_class,
|
||||
...(item.sign ? { sign: item.sign } : {}),
|
||||
})));
|
||||
}
|
||||
|
||||
function uniquifyStyleLabels(
|
||||
@@ -558,24 +689,31 @@ function remainingOutcomes(
|
||||
allMinutes: readonly string[],
|
||||
kind: ContrastChoiceKind,
|
||||
): ContrastExpectedOutcome[] {
|
||||
let rows: ContrastExpectedOutcome[];
|
||||
if (kind === "varga_style" && groups.length === 2) {
|
||||
return [
|
||||
rows = [
|
||||
{ outcomeId: "yes", supportsCandidateIds: groups[0], conflictsCandidateIds: groups[1] },
|
||||
{ outcomeId: "weak_yes", supportsCandidateIds: groups[1], conflictsCandidateIds: groups[0] },
|
||||
{ outcomeId: "no", supportsCandidateIds: [], conflictsCandidateIds: [] },
|
||||
{ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] },
|
||||
];
|
||||
}
|
||||
if (groups.length === 2) {
|
||||
return [
|
||||
} else if (groups.length === 2) {
|
||||
rows = [
|
||||
{ outcomeId: "yes", supportsCandidateIds: groups[0], conflictsCandidateIds: groups[1] },
|
||||
{ outcomeId: "weak_yes", supportsCandidateIds: groups[0], conflictsCandidateIds: groups[1] },
|
||||
{ outcomeId: "no", supportsCandidateIds: groups[1], conflictsCandidateIds: groups[0] },
|
||||
{ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] },
|
||||
];
|
||||
} else {
|
||||
const classes = ["yes", "weak_yes", "no"] as const;
|
||||
rows = groups.slice(0, 3).map((group, index) => ({
|
||||
outcomeId: classes[index] ?? `group_${index}`,
|
||||
supportsCandidateIds: group,
|
||||
conflictsCandidateIds: allMinutes.filter((time) => !group.includes(time)),
|
||||
}));
|
||||
if (!rows.some((row) => row.outcomeId === "unsure")) {
|
||||
rows.push({ outcomeId: "unsure", supportsCandidateIds: [], conflictsCandidateIds: [] });
|
||||
}
|
||||
}
|
||||
const classes = ["yes", "weak_yes", "no"] as const;
|
||||
return groups.slice(0, 3).map((group, index) => ({
|
||||
outcomeId: classes[index] ?? `group_${index}`,
|
||||
supportsCandidateIds: group,
|
||||
conflictsCandidateIds: allMinutes.filter((time) => !group.includes(time)),
|
||||
}));
|
||||
return rows;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user