报告创建请求、校正入口和同名类型改从 contracts 推导;同义的对象判断与 sha256 改走共享函数。调用处用 import 别名保留原名,使 diff 低于 1500 行。行为、哈希和状态字符串不变。 Co-Authored-By: Grok (agent loop)
138 lines
6.5 KiB
TypeScript
138 lines
6.5 KiB
TypeScript
import { parseSegmentSummary, TARGET_CHARTS, type SegmentSummary, type TargetChart } from "./rectification-agentic/core/segment-summary.ts";
|
|
import { asRecord as record } from "@/lib/guards";
|
|
|
|
/**
|
|
* 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; windowOffset: number | null }>;
|
|
|
|
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;
|
|
|
|
/** 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 : "";
|
|
const offset = row.window_offset_minutes;
|
|
const windowOffset = typeof offset === "number" && Number.isInteger(offset) && offset >= 0 ? offset : null;
|
|
return uuidPattern.test(caseId) && uuidPattern.test(resultId) ? { caseId, resultId, windowOffset } : 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)));
|
|
}
|
|
|
|
/**
|
|
* A chart is reliable for the saved minute only when its tier is not unreliable and the
|
|
* saved minute sits in that chart's head segment (or the chart never changes in the window).
|
|
* D7 adopts the head-intersection midpoint and falls back to the D1 head when the heads do not
|
|
* intersect, and a later result may move a head after adoption, so the tier alone can claim a
|
|
* sign the saved minute does not have. Missing offset is treated as unknown: not reliable.
|
|
*/
|
|
export function unreliableChartsForSavedMinute(summary: SegmentSummary, windowOffset: number | null): TargetChart[] {
|
|
return TARGET_CHARTS.filter((chart) => {
|
|
const row = summary.charts.find((item) => item.chart === chart);
|
|
if (!row) return false;
|
|
if (UNRELIABLE_CHART_TIERS.has(row.tier)) return true;
|
|
if (row.full_window_unique) return false;
|
|
if (windowOffset === null) return true;
|
|
const segment = row.segments.find((item) => item.start <= windowOffset && windowOffset <= item.end);
|
|
return !segment || segment.index !== row.top_segment;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reads the Case's latest result (the adopted one, or a later one after more answers) and
|
|
* judges each chart against the saved minute, not the result's own head: see
|
|
* unreliableChartsForSavedMinute. 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: unreliableChartsForSavedMinute(summary, provenance.windowOffset),
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|