报告创建请求、校正入口和同名类型改从 contracts 推导;同义的对象判断与 sha256 改走共享函数。调用处用 import 别名保留原名,使 diff 低于 1500 行。行为、哈希和状态字符串不变。 Co-Authored-By: Grok (agent loop)
272 lines
8.9 KiB
TypeScript
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;
|
|
}
|