import { resolveAyanamsa } from "./ayanamsa.ts"; import { birthCalendarDate } from "./effective-birth-date.ts"; import { globalBirthProfileFromAccountRow } from "./server-owned-birth-profile.ts"; import { recordOrEmpty as record } from "@/lib/guards"; export const CHART_BIRTH_FAILURE_REASONS = [ "profile_query_error", "profile_incomplete", "adopted_calculation_incomplete", "timezone_resolver_failure", "engine_busy", "engine_timeout", "engine_bad_payload", "engine_http_error", "response_schema_failure", ] as const; export type ChartBirthFailureReason = (typeof CHART_BIRTH_FAILURE_REASONS)[number]; export type ChartBirthAdoption = "adopted" | "legacy" | "declared" | "incomplete"; export type ServerOwnedChartBirth = { status: string | null; date: string | null; time: string | null; timezoneOffset: number | null; timezoneId: string | null; provenance: unknown; reportedTime: string | null; activeTime: string | null; activeDate: string | null; declaredDate: string | null; fingerprint: string; chartable: boolean; failure: ChartBirthFailureReason | null; adoption: ChartBirthAdoption; }; const ADOPTION_CLEAR_KEYS = [ "birth_date", "reported_birth_time", "birth_time_source", "birth_time_period", "declared_window_start", "declared_window_end", "uncertainty_before_minutes", "uncertainty_after_minutes", "latitude", "longitude", "timezone_id", "timezone_offset", ] as const; function text(value: unknown): string | null { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function clock(value: unknown): string | null { const raw = text(value); if (!raw) return null; return raw.slice(0, 5); } function finite(value: unknown): number | null { if (typeof value === "number" && Number.isFinite(value)) return Object.is(value, -0) ? 0 : value; if (typeof value === "string" && value.trim()) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } return null; } function canonical(value: unknown, depth = 0): string { if (depth > 8 || value == null) return ""; if (typeof value === "number") return Number.isFinite(value) ? String(Object.is(value, -0) ? 0 : value) : ""; if (typeof value === "string") return value; if (typeof value === "boolean") return value ? "1" : "0"; if (Array.isArray(value)) return `[${value.map((item) => canonical(item, depth + 1)).join(",")}]`; if (typeof value === "object") { const entries = Object.keys(value as Record).sort(); return `{${entries.map((key) => `${key}:${canonical((value as Record)[key], depth + 1)}`).join(",")}}`; } return ""; } function fingerprintOf(parts: Record): string { const textValue = canonical(parts); let out = ""; for (const seed of [0x811c9dc5, 0x01000193, 0x9e3779b9, 0x85ebca6b]) { let hash = seed >>> 0; for (let index = 0; index < textValue.length; index += 1) { hash ^= textValue.charCodeAt(index); hash = Math.imul(hash, 0x01000193); } out += (hash >>> 0).toString(16).padStart(8, "0"); } return out; } function comparable(key: string, value: unknown): unknown { if (key === "birth_date" || key === "active_birth_date") return birthCalendarDate(value) ?? null; if (key.endsWith("_time") || key.endsWith("_start") || key.endsWith("_end")) return clock(value); if (key === "latitude" || key === "longitude" || key === "timezone_offset" || key.endsWith("_minutes")) { return finite(value); } if (key === "active_birth_provenance") return canonical(value); return value ?? null; } function isDistinct(key: string, left: unknown, right: unknown): boolean { return comparable(key, left) !== comparable(key, right); } function emptyBirth(failure: ChartBirthFailureReason): ServerOwnedChartBirth { return { status: null, date: null, time: null, timezoneOffset: null, timezoneId: null, provenance: null, reportedTime: null, activeTime: null, activeDate: null, declaredDate: null, fingerprint: fingerprintOf({ failure }), chartable: false, failure, adoption: "incomplete", }; } export function unresolvedChartBirth(failure: ChartBirthFailureReason): ServerOwnedChartBirth { return emptyBirth(failure); } /** * Mirrors `public.guard_adopted_birth_date`. Date, offset, and provenance * clear together. This never clears `active_birth_time`. */ export function applyAdoptedBirthDateGuard>( previous: T | null, next: T, ): T { const status = next.birth_time_status; const statusUnusable = status == null || (status !== "accepted" && status !== "confirmed"); const activeTimeMissing = next.active_birth_time == null || next.active_birth_time === ""; const declarationChanged = previous != null && ADOPTION_CLEAR_KEYS.some((key) => ( isDistinct(key, next[key], previous[key]) )); const activeTimeChangedWithoutProvenance = previous != null && isDistinct("active_birth_time", next.active_birth_time, previous.active_birth_time) && !isDistinct("active_birth_provenance", next.active_birth_provenance, previous.active_birth_provenance); if (statusUnusable || activeTimeMissing || declarationChanged || activeTimeChangedWithoutProvenance) { return { ...next, active_birth_date: null, active_birth_timezone_offset: null, active_birth_provenance: null, }; } return next; } export function resolveServerOwnedChartBirth(row: unknown): ServerOwnedChartBirth { const source = record(row); const status = text(source.birth_time_status); const declaredDate = birthCalendarDate(source.birth_date) ?? null; const activeDate = birthCalendarDate(source.active_birth_date) ?? null; const reportedTime = clock(source.reported_birth_time); const activeTime = clock(source.active_birth_time) ?? clock(source.birth_time); const timezoneId = text(source.timezone_id); const provenance = source.active_birth_provenance ?? null; const name = text(source.name); const latitude = finite(source.latitude); const longitude = finite(source.longitude); const ayanamsa = resolveAyanamsa(source); let profile: ReturnType | null = null; let failure: ChartBirthFailureReason | null = null; try { profile = globalBirthProfileFromAccountRow(row); } catch (error) { if (error instanceof Error && error.message === "adopted_birth_calculation_incomplete") { failure = "adopted_calculation_incomplete"; } else { throw error; } } const usable = status === "accepted" || status === "confirmed"; const date = failure ? null : profile?.date ?? null; const time = failure ? null : profile?.time ?? null; // An incomplete adopted tuple must not fall back to the declared offset. const timezoneOffset = failure ? null : typeof profile?.timezoneOffset === "number" && Number.isFinite(profile.timezoneOffset) ? profile.timezoneOffset : null; let adoption: ChartBirthAdoption = "declared"; if (failure === "adopted_calculation_incomplete") adoption = "incomplete"; else if (usable && activeDate) adoption = "adopted"; else if (usable) adoption = "legacy"; if (!failure && (date == null || time == null || timezoneOffset == null || latitude == null || longitude == null)) { failure = "profile_incomplete"; } return { status, date, time, timezoneOffset, timezoneId, provenance, reportedTime, activeTime, activeDate, declaredDate, fingerprint: fingerprintOf({ name, status, date, time, timezoneOffset, timezoneId, provenance, reportedTime, activeTime, activeDate, declaredDate, latitude, longitude, ayanamsa, failure, }), chartable: failure == null, failure, adoption, }; } export function chartBirthAfterAccountWrite(input: { current: unknown; written: unknown; returned: unknown; }): ServerOwnedChartBirth { const current = input.current == null ? null : record(input.current); const written = record(input.written); const returned = input.returned == null ? null : record(input.returned); const merged = { ...(current ?? {}), ...written, ...(returned ?? {}), }; const databaseAlreadyJudgedAdoption = returned != null && ( "active_birth_date" in returned || "active_birth_timezone_offset" in returned || "active_birth_provenance" in returned ); const row = databaseAlreadyJudgedAdoption ? merged : applyAdoptedBirthDateGuard(current, merged); return resolveServerOwnedChartBirth(row); } export function readChartBirthFingerprint(value: unknown): string | null { if (!value || typeof value !== "object" || !("chartBirth" in value)) return null; const chartBirth = (value as { chartBirth?: { fingerprint?: unknown } }).chartBirth; return chartBirth && typeof chartBirth.fingerprint === "string" && chartBirth.fingerprint ? chartBirth.fingerprint : null; }