feat(rectification): read the adopted segment result's unreliable charts for reports and chat (BUG-1138, T1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
44cdd9c9b9
commit
35b89bdf21
@@ -0,0 +1,119 @@
|
||||
import { parseSegmentSummary, TARGET_CHARTS, type SegmentSummary, type TargetChart } from "./rectification-agentic/core/segment-summary.ts";
|
||||
|
||||
/**
|
||||
* Which divisional charts a segment-v1 adoption could not determine (BUG-1138/1139).
|
||||
*
|
||||
* The rectification delivery card already says "D9 cannot be determined" for a
|
||||
* wide window; reports and chat used to read every chart from the saved minute
|
||||
* anyway. This module reads the adopted result's own segment summary so they can
|
||||
* say the same thing. Anything else (older adoptions, unrectified profiles, a
|
||||
* missing or malformed result) returns null and leaves callers unchanged.
|
||||
*/
|
||||
|
||||
export const UNRELIABLE_CHART_TIERS = new Set(["blocked", "indistinct"]);
|
||||
export const CASE_DOSSIER_RPC = "get_agentic_rectification_case_dossier";
|
||||
|
||||
export type SegmentProvenance = Readonly<{ caseId: string; resultId: string }>;
|
||||
|
||||
export type AdoptedChartTiers = Readonly<{
|
||||
source: "segment-v1";
|
||||
/** "adopted": the summary of the adopted result; "latest": a newer result of the same Case. */
|
||||
resultMatch: "adopted" | "latest";
|
||||
unreliable: readonly TargetChart[];
|
||||
}>;
|
||||
|
||||
type RpcResponse = PromiseLike<{ data: unknown; error: unknown }>;
|
||||
export type AdoptedChartTierClient = Readonly<{ rpc: (fn: string, args: Record<string, unknown>) => RpcResponse }>;
|
||||
export type AdoptedChartTierWarn = (payload: Readonly<{ event: "adopted_chart_tiers_unavailable"; reason: string }>) => void;
|
||||
|
||||
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
/** Only the segment-v1 adoption contract carries a result whose summary describes the saved minute. */
|
||||
export function segmentProvenance(value: unknown): SegmentProvenance | null {
|
||||
const row = record(value);
|
||||
if (!row || row.contract !== "segment-v1") return null;
|
||||
const caseId = typeof row.case_id === "string" ? row.case_id : "";
|
||||
const resultId = typeof row.result_id === "string" ? row.result_id : "";
|
||||
return uuidPattern.test(caseId) && uuidPattern.test(resultId) ? { caseId, resultId } : null;
|
||||
}
|
||||
|
||||
export function unreliableChartsFromSummary(summary: SegmentSummary): TargetChart[] {
|
||||
return TARGET_CHARTS.filter((chart) => summary.charts.some((row) => row.chart === chart && UNRELIABLE_CHART_TIERS.has(row.tier)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer the adopted result. A later result of the same Case (answers after
|
||||
* adoption) carries strictly more evidence about the same window, so its tiers
|
||||
* are used instead; a different Case is never read.
|
||||
*/
|
||||
export function chartTiersFromDossier(dossier: unknown, provenance: SegmentProvenance): AdoptedChartTiers | "invalid" {
|
||||
const root = record(dossier);
|
||||
const caseRow = record(root?.case);
|
||||
const latest = record(root?.latest_result);
|
||||
const caseId = typeof caseRow?.case_id === "string" ? caseRow.case_id : typeof caseRow?.id === "string" ? caseRow.id : null;
|
||||
if (!root || !latest || (caseId !== null && caseId !== provenance.caseId)) return "invalid";
|
||||
const resultId = typeof latest.result_id === "string" ? latest.result_id : null;
|
||||
if (!resultId) return "invalid";
|
||||
const receipt = record(latest.decision_receipt);
|
||||
const state = record(receipt?.inference_state);
|
||||
const summary = parseSegmentSummary(state?.segment_summary);
|
||||
if (!summary) return "invalid";
|
||||
return {
|
||||
source: "segment-v1",
|
||||
resultMatch: resultId === provenance.resultId ? "adopted" : "latest",
|
||||
unreliable: unreliableChartsFromSummary(summary),
|
||||
};
|
||||
}
|
||||
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
const CACHE_LIMIT = 500;
|
||||
const cache = new Map<string, { at: number; value: AdoptedChartTiers }>();
|
||||
|
||||
export function clearAdoptedChartTierCache(): void {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
function defaultWarn(payload: Readonly<{ event: string; reason: string }>): void {
|
||||
console.warn(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
function errorCode(error: unknown): string {
|
||||
if (error && typeof error === "object" && "code" in error && typeof error.code === "string" && error.code.trim()) return error.code.trim();
|
||||
if (error instanceof Error && error.name.trim()) return error.name;
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export async function loadAdoptedChartTiers(
|
||||
client: AdoptedChartTierClient,
|
||||
input: Readonly<{ userId: string; provenance: unknown }>,
|
||||
warn: AdoptedChartTierWarn = defaultWarn,
|
||||
now: () => number = Date.now,
|
||||
): Promise<AdoptedChartTiers | null> {
|
||||
const provenance = segmentProvenance(input.provenance);
|
||||
if (!provenance) return null;
|
||||
const key = `${input.userId}|${provenance.caseId}|${provenance.resultId}`;
|
||||
const hit = cache.get(key);
|
||||
if (hit && now() - hit.at < CACHE_TTL_MS) return hit.value;
|
||||
try {
|
||||
const result = await client.rpc(CASE_DOSSIER_RPC, { p_user_id: input.userId, p_case_id: provenance.caseId });
|
||||
if (result.error) {
|
||||
warn({ event: "adopted_chart_tiers_unavailable", reason: errorCode(result.error) });
|
||||
return null;
|
||||
}
|
||||
const tiers = chartTiersFromDossier(result.data, provenance);
|
||||
if (tiers === "invalid") {
|
||||
warn({ event: "adopted_chart_tiers_unavailable", reason: "invalid_segment_summary" });
|
||||
return null;
|
||||
}
|
||||
if (cache.size >= CACHE_LIMIT) cache.delete(cache.keys().next().value!);
|
||||
cache.set(key, { at: now(), value: tiers });
|
||||
return tiers;
|
||||
} catch (error) {
|
||||
warn({ event: "adopted_chart_tiers_unavailable", reason: errorCode(error) });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user