Files
Jyotisha/frontend/src/lib/chart-birth-truth.ts
T
jesse-uxandGrok (agent loop) d2296c2654 把前后端共用的请求契约和小工具收成一份,避免再各抄一份。
报告创建请求、校正入口和同名类型改从 contracts 推导;同义的对象判断与 sha256 改走共享函数。调用处用 import 别名保留原名,使 diff 低于 1500 行。行为、哈希和状态字符串不变。

Co-Authored-By: Grok (agent loop)
2026-10-15 22:22:44 +08:00

272 lines
8.9 KiB
TypeScript

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<string, unknown>).sort();
return `{${entries.map((key) => `${key}:${canonical((value as Record<string, unknown>)[key], depth + 1)}`).join(",")}}`;
}
return "";
}
function fingerprintOf(parts: Record<string, unknown>): 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<T extends Record<string, unknown>>(
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<typeof globalBirthProfileFromAccountRow> | 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;
}