Files
Jyotisha/frontend/src/lib/birth-profile-timezone.ts
T
jesse-ux b85c4a686a
Independent Staging Quality Gate / validate (push) Successful in 13m27s
Independent Staging Quality Gate / publish (push) Failing after 1h0m1s
fix(rectification): anchor candidate windows to civil dates across midnight
Carry explicit local date intervals instead of inferring the day from clock
order. Cluster width, delivery, adoption, and reports keep the actual civil
date; adopted date is stored separately from the reported birth_date.

Algorithm identity is scoring-9 / spec-v5. Scoring weights, confirmation
thresholds, and Skill version are unchanged. Isolated Linux final-3 gates
passed; four pre-existing Python failures remain. This is not a production
release.
2026-09-21 02:55:00 +08:00

120 lines
4.7 KiB
TypeScript

type RecordValue = Record<string, unknown>;
export class BirthProfileTimezoneError extends Error {
constructor() {
super("Unable to resolve historical birth timezone offset");
this.name = "BirthProfileTimezoneError";
}
}
function record(value: unknown): RecordValue | null {
return value !== null && typeof value === "object" && !Array.isArray(value)
? value as RecordValue
: null;
}
function valueFor(profile: RecordValue, snakeKey: string, camelKey: string): unknown {
return profile[snakeKey] ?? profile[camelKey];
}
function text(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function calendarDate(value: unknown): string | null {
if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString().slice(0, 10);
return text(value);
}
export function finiteBirthNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
export function durableBirthCoordinate(value: unknown): number | null {
const coordinate = finiteBirthNumber(value);
if (coordinate === null) return null;
const rounded = Math.round(coordinate * 1_000_000) / 1_000_000;
return Object.is(rounded, -0) ? 0 : rounded;
}
export function birthProfileReferenceTime(value: unknown, preferredTime?: string | null): string {
const preferred = text(preferredTime)?.slice(0, 5);
if (preferred && /^([01]\d|2[0-3]):[0-5]\d$/.test(preferred)) return preferred;
const profile = record(value);
const reported = text(profile ? valueFor(profile, "reported_birth_time", "reportedTime") : null)?.slice(0, 5);
if (reported && /^([01]\d|2[0-3]):[0-5]\d$/.test(reported)) return reported;
const direct = text(profile ? valueFor(profile, "birth_time", "time") : null)?.slice(0, 5);
if (direct && /^([01]\d|2[0-3]):[0-5]\d$/.test(direct)) return direct;
const period = text(profile ? valueFor(profile, "birth_time_period", "birthTimePeriod") : null);
const windowStart = text(profile ? valueFor(profile, "declared_window_start", "declaredWindowStart") : null)?.slice(0, 5);
if (windowStart && /^([01]\d|2[0-3]):[0-5]\d$/.test(windowStart)) return windowStart;
return {
early_morning: "06:00",
morning: "10:00",
afternoon: "15:00",
evening: "20:30",
late_night: "23:30",
}[period ?? ""] ?? "12:00";
}
type ResolveBirthTimezoneOptions = Readonly<{
fetchImpl?: typeof fetch;
apiBase?: string;
preferredTime?: string | null;
useActiveDate?: boolean;
}>;
export async function resolveMissingBirthTimezoneOffset(
value: unknown,
options: ResolveBirthTimezoneOptions = {},
): Promise<unknown> {
const profile = record(value);
if (!profile) return value;
const declaredDate = calendarDate(valueFor(profile, "birth_date", "date"));
if (options.useActiveDate && ["accepted", "confirmed"].includes(String(profile.birth_time_status))) {
const adoptedOffset = finiteBirthNumber(profile.active_birth_timezone_offset);
if (adoptedOffset !== null) {
// Reuse the candidate calculation specification, not a different DST interpretation.
return { ...profile, timezone_offset: adoptedOffset, timezoneOffset: adoptedOffset };
}
if (profile.active_birth_date != null) throw new BirthProfileTimezoneError();
}
const existingOffset = finiteBirthNumber(valueFor(profile, "timezone_offset", "timezoneOffset"));
if (existingOffset !== null) return value;
const latitude = finiteBirthNumber(profile.latitude);
const longitude = finiteBirthNumber(profile.longitude);
const birthDate = declaredDate;
const timezoneId = text(valueFor(profile, "timezone_id", "timezoneId"));
if (latitude === null || longitude === null || !birthDate || !timezoneId) return value;
const fetchImpl = options.fetchImpl ?? fetch;
const apiBase = options.apiBase ?? process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200";
let response: Response;
try {
response = await fetchImpl(`${apiBase}/api/location/timezone`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
latitude,
longitude,
birthDate,
birthTime: birthProfileReferenceTime(profile, options.preferredTime),
}),
cache: "no-store",
});
} catch {
throw new BirthProfileTimezoneError();
}
if (!response.ok) throw new BirthProfileTimezoneError();
const payload = record(await response.json().catch(() => null));
const timezoneOffset = finiteBirthNumber(payload?.timezoneOffset);
if (payload?.available !== true || timezoneOffset === null) {
throw new BirthProfileTimezoneError();
}
return {
...profile,
timezone_offset: timezoneOffset,
timezoneOffset,
};
}