fix(rectification): stop speaking unstampable distinguish stems (BUG-674/675)
Distinguish followups that cannot stamp a probe now go to persistExhaustionCollect instead of repeating the card stem in the assistant body. Choice focuses without asked_turn_id hang on the last assistant turn, and persisted_question with a live choice_card renders the existing card. Representative-time inconsistency is warn-only (BUG-676 investigating).
This commit is contained in:
@@ -2,6 +2,7 @@ import { applyProbeOutcome, outcomeByMinuteForAnswer } from "./apply-probe-outco
|
||||
import type { TransitionSignLookup } from "./sign-from-transitions.ts";
|
||||
import { clusterRangeFor, clusterEquivalentCandidates } from "./cluster-candidates.ts";
|
||||
import { evaluateConvergence, holdoutStillRanksFirst, rankActive } from "./convergence-evaluator.ts";
|
||||
import { warnRepresentativeTimeInconsistency } from "./representative-time-guard.ts";
|
||||
import { rangeFromTimes, unionStillValidRange } from "./credible-range.ts";
|
||||
import { entropyFromScores, normalizeScores } from "./entropy.ts";
|
||||
import { selectHighestGainProbe } from "./select-probe.ts";
|
||||
@@ -197,6 +198,14 @@ export function buildInferenceState(input: {
|
||||
...(transitions ? { transitions } : {}),
|
||||
};
|
||||
const decision = evaluateConvergence({ ...draft, holdout_passed: holdoutPassed });
|
||||
const lastRound = rounds.at(-1) ?? null;
|
||||
warnRepresentativeTimeInconsistency({
|
||||
representativeTime: decision.representative_time,
|
||||
eliminatedIds: lastRound?.eliminated_ids,
|
||||
credibleRange: decision.credible_range ?? draft.credible_range,
|
||||
winnerId: lastRound?.winner_id,
|
||||
scoresAfter: lastRound?.scores_after,
|
||||
});
|
||||
return {
|
||||
...draft,
|
||||
phase: phaseFor(decision.result_status, draft.phase),
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
export type RepresentativeTimeGuardInput = Readonly<{
|
||||
representativeTime: string | null | undefined;
|
||||
eliminatedIds?: readonly string[] | null;
|
||||
credibleRange?: readonly string[] | null;
|
||||
winnerId?: string | null;
|
||||
scoresAfter?: Readonly<Record<string, number>> | null;
|
||||
}>;
|
||||
|
||||
export type RepresentativeTimeGuardResult = Readonly<{
|
||||
ok: boolean;
|
||||
inEliminated: boolean;
|
||||
outsideRange: boolean;
|
||||
}>;
|
||||
|
||||
function clockMinutes(value: string): number | null {
|
||||
const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(value.trim());
|
||||
if (!match) return null;
|
||||
return Number(match[1]) * 60 + Number(match[2]);
|
||||
}
|
||||
|
||||
function outsideCredibleRange(time: string, range: readonly string[]): boolean {
|
||||
if (range.length === 0) return false;
|
||||
if (range.includes(time)) return false;
|
||||
if (range.length < 2) return true;
|
||||
const start = clockMinutes(range[0] ?? "");
|
||||
const end = clockMinutes(range[range.length - 1] ?? "");
|
||||
const point = clockMinutes(time);
|
||||
if (start === null || end === null || point === null) return true;
|
||||
if (start <= end) return point < start || point > end;
|
||||
return point < start && point > end;
|
||||
}
|
||||
|
||||
function scoreExtremes(scores: Readonly<Record<string, number>> | null | undefined): {
|
||||
max?: readonly [string, number];
|
||||
min?: readonly [string, number];
|
||||
} {
|
||||
let max: readonly [string, number] | undefined;
|
||||
let min: readonly [string, number] | undefined;
|
||||
for (const [time, score] of Object.entries(scores ?? {})) {
|
||||
if (!Number.isFinite(score)) continue;
|
||||
if (!max || score > max[1]) max = [time, score];
|
||||
if (!min || score < min[1]) min = [time, score];
|
||||
}
|
||||
return { max, min };
|
||||
}
|
||||
|
||||
export function inspectRepresentativeTime(input: RepresentativeTimeGuardInput): RepresentativeTimeGuardResult {
|
||||
const time = typeof input.representativeTime === "string" ? input.representativeTime.trim() : "";
|
||||
if (!time) {
|
||||
return { ok: true, inEliminated: false, outsideRange: false };
|
||||
}
|
||||
const inEliminated = (input.eliminatedIds ?? []).includes(time);
|
||||
const outsideRange = outsideCredibleRange(time, input.credibleRange ?? []);
|
||||
return {
|
||||
ok: !inEliminated && !outsideRange,
|
||||
inEliminated,
|
||||
outsideRange,
|
||||
};
|
||||
}
|
||||
|
||||
export function warnRepresentativeTimeInconsistency(input: RepresentativeTimeGuardInput): RepresentativeTimeGuardResult {
|
||||
const result = inspectRepresentativeTime(input);
|
||||
if (result.ok) return result;
|
||||
const extremes = scoreExtremes(input.scoresAfter);
|
||||
console.warn(JSON.stringify({
|
||||
event: "rectification_representative_time_inconsistent",
|
||||
winner_id: input.winnerId ?? null,
|
||||
representative_time: input.representativeTime ?? null,
|
||||
credible_range: input.credibleRange ?? null,
|
||||
eliminated_ids: input.eliminatedIds ?? [],
|
||||
scores_after_max: extremes.max ?? null,
|
||||
scores_after_min: extremes.min ?? null,
|
||||
in_eliminated: result.inEliminated,
|
||||
outside_range: result.outsideRange,
|
||||
}));
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user