Files
Jyotisha/frontend/src/lib/rectification-agentic/core/representative-time-guard.ts
T
jesse-ux 69ffa07d6b
Independent Staging Quality Gate / validate (push) Failing after 20s
Independent Staging Quality Gate / publish (push) Skipped
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).
2026-09-14 03:42:48 +08:00

78 lines
2.8 KiB
TypeScript

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