Files
Jyotisha/frontend/src/lib/personal-report-longform-birth.ts
T
Jesse_ChenandCursor cfcd369d4f feat(report): render longform Markdown as the report and close gaps2 holes
New reports skip the writer, persist pl9 Markdown as the body, and settle zero-token usage on the catalog model. Planned longform sections now emit blocked rows instead of vanishing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-06 21:46:16 +08:00

92 lines
3.4 KiB
TypeScript

import {
globalBirthProfileFromAccountRow,
} from "./server-owned-birth-profile";
export function utcToday(now = new Date()): string {
return now.toISOString().slice(0, 10);
}
function accountText(row: unknown, key: string): string | undefined {
if (!row || typeof row !== "object" || Array.isArray(row)) return undefined;
const value = (row as Record<string, unknown>)[key];
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function clockToMinutes(value: string): number | null {
const match = /^(\d{2}):(\d{2})$/.exec(value);
if (!match) return null;
return Number.parseInt(match[1], 10) * 60 + Number.parseInt(match[2], 10);
}
export function provenanceUncertaintyMinutes(
row: unknown,
candidateRange: { start_time: string; end_time: string } | null,
): number | undefined {
if (candidateRange) {
const start = clockToMinutes(candidateRange.start_time);
const end = clockToMinutes(candidateRange.end_time);
if (start !== null && end !== null) return Math.max(0, end - start);
}
if (!row || typeof row !== "object" || Array.isArray(row)) return undefined;
const record = row as Record<string, unknown>;
const before = typeof record.uncertainty_before_minutes === "number"
? record.uncertainty_before_minutes
: Number(record.uncertainty_before_minutes);
const after = typeof record.uncertainty_after_minutes === "number"
? record.uncertainty_after_minutes
: Number(record.uncertainty_after_minutes);
const hasBefore = Number.isFinite(before);
const hasAfter = Number.isFinite(after);
if (!hasBefore && !hasAfter) return undefined;
return Math.max(0, (hasBefore ? before : 0) + (hasAfter ? after : 0));
}
export function buildLongformBirthPayload(
row: unknown,
extras: Readonly<{
today: string;
targetYear: number;
candidateRange: { start_time: string; end_time: string } | null;
birthTimeAccuracy: "confirmed" | "provisional";
}>,
): Record<string, unknown> | null {
const profile = globalBirthProfileFromAccountRow(row);
const dateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(profile.date ?? "");
const timeMatch = /^(\d{2}):(\d{2})$/.exec(profile.time ?? "");
if (
!dateMatch
|| !timeMatch
|| profile.latitude === null
|| profile.longitude === null
|| profile.timezoneOffset === null
) {
return null;
}
const year = Number.parseInt(dateMatch[1], 10);
return {
year,
month: Number.parseInt(dateMatch[2], 10),
day: Number.parseInt(dateMatch[3], 10),
hour: Number.parseInt(timeMatch[1], 10),
minute: Number.parseInt(timeMatch[2], 10),
lat: profile.latitude,
lon: profile.longitude,
tz: profile.timezoneOffset,
ayanamsa: profile.ayanamsa,
format: "markdown",
packs: ["full"],
today: extras.today,
target_year: extras.targetYear,
age: extras.targetYear - year,
birth_time_accuracy: extras.birthTimeAccuracy,
...(extras.candidateRange ? { candidate_range: extras.candidateRange } : {}),
birthplace_label: accountText(row, "birth_place_label") ?? undefined,
coordinate_source: "user_reported",
coordinate_precision: "unverified_user_coordinates",
time_source: accountText(row, "birth_time_source") ?? accountText(row, "birth_time_status") ?? undefined,
uncertainty_minutes: provenanceUncertaintyMinutes(row, extras.candidateRange),
};
}