Files
Jyotisha/frontend/src/lib/rectification-agentic/core/split-holdout.ts
T
Jesse_Chen 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

59 lines
2.3 KiB
TypeScript

import type { EventUsage, InferenceEvent } from "./types.ts";
export type DatedEventInput = Readonly<{
id: string;
domain: string;
year: number | null;
precision: InferenceEvent["precision"];
}>;
/**
* Domain-stratified holdout: keep at least one dated event out of training
* so the winner is not certified by the same fact that selected it.
*/
export function splitHoldoutEvents(events: readonly DatedEventInput[]): InferenceEvent[] {
const dated = events.filter((item) => item.year !== null && item.precision !== "unknown");
if (dated.length < 4) {
return events.map((item) => ({ ...item, usage: "training" as const }));
}
const byDomain = new Map<string, DatedEventInput[]>();
for (const item of dated) {
const rows = byDomain.get(item.domain) ?? [];
rows.push(item);
byDomain.set(item.domain, rows);
}
const holdoutId = pickHoldoutId(byDomain, dated);
return events.map((item) => ({
...item,
usage: usageFor(item, holdoutId, dated),
}));
}
function usageFor(item: DatedEventInput, holdoutId: string | null, dated: readonly DatedEventInput[]): EventUsage {
if (item.year === null || item.precision === "unknown") return "unused";
if (item.id === holdoutId) return "holdout";
return dated.some((row) => row.id === item.id) ? "training" : "unused";
}
function pickHoldoutId(
byDomain: Map<string, DatedEventInput[]>,
dated: readonly DatedEventInput[],
): string | null {
const monthOrBetter = dated.filter((item) => item.precision === "day" || item.precision === "month");
const singletonDomain = [...byDomain.entries()].find(([, rows]) => rows.length === 1);
if (singletonDomain && dated.length - 1 >= 3) {
const preferred = monthOrBetter.find((item) => item.domain === singletonDomain[0]);
return (preferred ?? singletonDomain[1][0])?.id ?? null;
}
const ranked = [...monthOrBetter, ...dated];
return ranked[ranked.length - 1]?.id ?? null;
}
export function trainingEventIds(events: readonly InferenceEvent[]): ReadonlySet<string> {
return new Set(events.filter((item) => item.usage === "training").map((item) => item.id));
}
export function holdoutEventIds(events: readonly InferenceEvent[]): ReadonlySet<string> {
return new Set(events.filter((item) => item.usage === "holdout").map((item) => item.id));
}