/** * Opening scan for the nearest chart change. Failures keep the original * two-sentence opening. Question order stays on its own switch. */ import { resolveAyanamsa } from "../ayanamsa.ts"; import { requestCandidateIntervals, type DatedCandidateRange } from "./core/candidate-window.ts"; import { CHART_LAYERS, NEAREST_BOUNDARY_PROBE_THRESHOLD_MINUTES, nearestBoundaryProbesEnabled, topicsForLayers, } from "./nearest-boundary.ts"; import type { OpeningBoundaryFacts } from "./user-copy.ts"; const BOUNDARY_CHARTS = ["D1", "D2", "D4", "D5", "D7", "D9", "D10", "D11", "D12", "D24", "D30"] as const; const LAYERS = new Set(CHART_LAYERS); export function openingFactsFromBoundaryPayload( payload: unknown, askFirst: boolean, ): OpeningBoundaryFacts & { at?: string | null } { if (!payload || typeof payload !== "object") return { kind: "unavailable" }; const row = payload as { found?: unknown; reason?: unknown; distance_minutes?: unknown; layers?: unknown }; if (row.found !== true) return row.reason === "no_transition" ? { kind: "stable" } : { kind: "unavailable" }; const distance = row.distance_minutes; if (typeof distance !== "number" || !Number.isInteger(distance) || distance < 0 || distance > 720) { return { kind: "unavailable" }; } const rawLayers = Array.isArray(row.layers) ? row.layers : []; const layers = rawLayers.flatMap((item) => { const layer = item && typeof item === "object" ? (item as { layer?: unknown }).layer : null; return typeof layer === "string" && LAYERS.has(layer) ? [layer] : []; }); const d1Changed = rawLayers.some((item) => { const layer = item && typeof item === "object" ? (item as { layer?: unknown }).layer : null; return layer === "d1"; }); const topics = topicsForLayers(layers); const at = (payload as { at?: unknown }).at; return { kind: "near", distanceMinutes: distance, topics, d1Changed, ...(typeof at === "string" ? { at: at.slice(0, 5) } : {}), askFirst: askFirst && topics.length > 0 && distance <= NEAREST_BOUNDARY_PROBE_THRESHOLD_MINUTES, }; } function preferBoundary( left: OpeningBoundaryFacts & { at?: string | null }, right: OpeningBoundaryFacts & { at?: string | null }, ): OpeningBoundaryFacts & { at?: string | null } { if (left.kind !== "near") return right.kind === "near" ? right : left; if (right.kind !== "near") return left; const distance = (left.distanceMinutes ?? 9999) - (right.distanceMinutes ?? 9999); if (distance !== 0) return distance < 0 ? left : right; return (left.at ?? "99:99") <= (right.at ?? "99:99") ? left : right; } export async function fetchOpeningBoundary(input: { snapshot: Readonly>; range: DatedCandidateRange; recordedTime: string; }): Promise { const recorded = input.recordedTime.trim().slice(0, 5); if (!/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(recorded)) return { kind: "unavailable" }; const latitude = input.snapshot.latitude; const longitude = input.snapshot.longitude; const timezone = input.snapshot.timezone_offset; if (typeof latitude !== "number" || typeof longitude !== "number" || typeof timezone !== "number") { return { kind: "unavailable" }; } const intervals = requestCandidateIntervals(input.snapshot, input.range); const askFirst = nearestBoundaryProbesEnabled(); let best: OpeningBoundaryFacts = { kind: "unavailable" }; for (const interval of intervals) { const response = await fetch(`${process.env.JYOTISH_API_BASE?.trim() || "http://127.0.0.1:5200"}/api/rectification/v5/varga-segments`, { method: "POST", headers: { "Content-Type": "application/json" }, signal: AbortSignal.timeout(60000), body: JSON.stringify({ start_at: interval.start_at, end_at: interval.end_at, latitude, longitude, timezone_offset: timezone, target_charts: ["D1"], boundary_charts: BOUNDARY_CHARTS, recorded_time: recorded, ayanamsa: resolveAyanamsa(input.snapshot), }), }); if (!response.ok) return { kind: "unavailable" }; const data = await response.json() as { nearest_boundary?: unknown }; best = preferBoundary(best, openingFactsFromBoundaryPayload(data.nearest_boundary, askFirst)); } return best; } export async function resolveOpeningBoundaryFacts(input: { birthTimeSource?: string | null; reportedBirthTime?: string | null; candidateRange?: { start_time?: string | null; end_time?: string | null } | null; loadCompute: () => Promise<{ baselineBirthSnapshot: Readonly>; candidateRange: DatedCandidateRange }>; }): Promise { if (input.birthTimeSource === "period_only") return { kind: "period_only" }; const reported = input.reportedBirthTime?.trim() ?? ""; const range = input.candidateRange; if (!input.birthTimeSource || !reported || !range?.start_time || !range.end_time) return { kind: "unavailable" }; try { const compute = await input.loadCompute(); return await fetchOpeningBoundary({ snapshot: compute.baselineBirthSnapshot, range: compute.candidateRange, recordedTime: reported, }); } catch { return { kind: "unavailable" }; } }