import { loadAdoptedChartTiers } from "./rectification-adopted-chart-tiers.ts"; import type { TargetChart } from "./rectification-agentic/core/segment-summary.ts"; export const READ_REPORT_CANDIDATE_RANGE_RPC = "read_report_candidate_range"; export const ADOPTED_CREDIBLE_RANGE_SOURCE = "inference_credible_range"; export type ReportCandidateClockRange = Readonly<{ startTime: string; endTime: string; candidate_intervals?: readonly Readonly<{ start_at: string; end_at: string }>[]; /** Charts a segment-v1 adoption could not determine (BUG-1138/1139); absent otherwise. */ unreliableVargas?: readonly TargetChart[]; }>; export type AdoptedCredibleRangePayload = Readonly<{ start_time: string; end_time: string; representative_time: string; width_minutes: number; source: typeof ADOPTED_CREDIBLE_RANGE_SOURCE; }>; export type ReportCandidateRangeRpcClient = Readonly<{ rpc: ( fn: string, args: Record, ) => PromiseLike<{ data: unknown; error: unknown }>; }>; export type ReportCandidateRangeWarn = (payload: Readonly<{ event: "report_candidate_range_unavailable"; reason: string; }>) => void; const candidateClockPattern = /^((?:[01]\d|2[0-3]):[0-5]\d)(?::00(?:\.0+)?)?$/; export function reportCandidateRangeErrorCode(error: unknown): string { if (error && typeof error === "object" && "code" in error) { const code = error.code; if (typeof code === "string" && code.trim().length > 0) return code.trim(); } if (error instanceof Error && error.name.trim().length > 0) return error.name; return "unknown"; } export function parseReportCandidateRange(value: unknown): ReportCandidateClockRange | null { const row = value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : null; if (!row) return null; if (row.candidate_intervals !== undefined) { if (!Array.isArray(row.candidate_intervals) || !row.candidate_intervals.length) return null; const intervals: { start_at: string; end_at: string }[] = []; for (const value of row.candidate_intervals) { const interval = value as { start_at?: unknown; end_at?: unknown } | null; if (!interval || typeof interval.start_at !== "string" || typeof interval.end_at !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(interval.start_at) || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(interval.end_at) || !Number.isFinite(Date.parse(interval.start_at + "Z")) || !Number.isFinite(Date.parse(interval.end_at + "Z")) || new Date(interval.start_at + "Z").toISOString().slice(0, 16) !== interval.start_at || new Date(interval.end_at + "Z").toISOString().slice(0, 16) !== interval.end_at || interval.end_at < interval.start_at || (intervals.length > 0 && interval.start_at <= intervals.at(-1)!.end_at)) return null; intervals.push({ start_at: interval.start_at, end_at: interval.end_at }); } return { startTime: intervals[0].start_at.slice(11, 16), endTime: intervals.at(-1)!.end_at.slice(11, 16), candidate_intervals: intervals }; } const startTime = matchClock(row.start_time ?? row.startTime); const endTime = matchClock(row.end_time ?? row.endTime); if (!startTime || !endTime || startTime > endTime) return null; return { startTime, endTime }; } export function adoptedCredibleRangePayload(input: Readonly<{ startTime?: string | null; endTime?: string | null; representativeTime?: string | null; }>): AdoptedCredibleRangePayload | null { const startTime = matchClock(input.startTime); const endTime = matchClock(input.endTime); if (!startTime || !endTime || startTime > endTime) return null; const representativeTime = matchClock(input.representativeTime) ?? startTime; const from = clockMinutes(startTime); const to = clockMinutes(endTime); if (from == null || to == null) return null; const span = to >= from ? to - from : to + 24 * 60 - from; return { start_time: startTime, end_time: endTime, representative_time: representativeTime, width_minutes: span + 1, source: ADOPTED_CREDIBLE_RANGE_SOURCE, }; } export async function loadReportCandidateRange( client: ReportCandidateRangeRpcClient, input: Readonly<{ userId: string; rectificationCaseId?: string | null }>, warn: ReportCandidateRangeWarn = defaultWarn, ): Promise { try { const result = await client.rpc(READ_REPORT_CANDIDATE_RANGE_RPC, { p_user_id: input.userId, p_rectification_case_id: input.rectificationCaseId ?? null, }); if (result.error) { warn({ event: "report_candidate_range_unavailable", reason: reportCandidateRangeErrorCode(result.error), }); return null; } const range = parseReportCandidateRange(result.data); if (!range) return null; const unreliable = await loadUnreliableVargas(client, input.userId); return unreliable.length ? { ...range, unreliableVargas: unreliable } : range; } catch (error) { warn({ event: "report_candidate_range_unavailable", reason: reportCandidateRangeErrorCode(error), }); return null; } } type ProfileProvenanceQuery = Readonly<{ select: (columns: string) => Readonly<{ eq: (column: string, value: string) => Readonly<{ maybeSingle: () => PromiseLike<{ data: unknown; error: unknown }> }>; }>; }>; /** * Report routes and the consultation route share this loader (the consultation * route wires it by name), so the adopted chart tiers ride on the range it * already returns. Only the service client has `from`; a failure here never * drops the range itself. */ async function loadUnreliableVargas(client: ReportCandidateRangeRpcClient, userId: string): Promise { const from = (client as { from?: (table: string) => ProfileProvenanceQuery }).from; if (typeof from !== "function") return []; try { const { data, error } = await from.call(client, "profiles").select("active_birth_provenance").eq("id", userId).maybeSingle(); if (error || !data || typeof data !== "object") return []; const tiers = await loadAdoptedChartTiers(client, { userId, provenance: (data as Record).active_birth_provenance }); return tiers?.unreliable ?? []; } catch { return []; } } function matchClock(value: unknown): string | null { if (typeof value !== "string") return null; return value.trim().match(candidateClockPattern)?.[1] ?? null; } function clockMinutes(value: string): number | null { const match = /^(\d{2}):(\d{2})$/.exec(value); if (!match) return null; return Number(match[1]) * 60 + Number(match[2]); } function defaultWarn(payload: Readonly<{ event: string; reason: string }>): void { console.warn(JSON.stringify(payload)); }