import { z } from "zod"; import { CHART_BIRTH_FAILURE_REASONS } from "./chart-birth-truth.ts"; import { PLANET_GLYPH_KEYS } from "./planet-glyphs.ts"; const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/); export const chartViewUnavailableLayerSchema = z.object({ status: z.literal("unavailable"), reason: z.string().min(1), copy: z.string().min(1), }); export const northIndianChartSchema = z.object({ houses: z.array(z.object({ houseNumber: z.number().int().min(1).max(12), sign: z.string().min(1), occupants: z.array(z.string()), // The chart draws symbols, so it needs the body and its direction rather // than the formatted label. Optional because the schema also has to accept // house tables that only ever carried display text. occupantGlyphs: z.array(z.object({ key: z.enum(PLANET_GLYPH_KEYS), retrograde: z.boolean(), degree: z.number().gte(0).lt(30), })).optional(), })).length(12), planets: z.array(z.object({ name: z.string().min(1), retrograde: z.boolean(), })).optional(), }); export const chartViewPlanetRowSchema = z.object({ name: z.string().min(1), label: z.string().min(1), sign: z.string().min(1), signLabel: z.string().min(1), degreeInSign: z.number(), house: z.number().int().min(1).max(12), retrograde: z.boolean(), status: z.string().min(1), nakshatra: z.string().min(1), pada: z.number().int().min(1).max(4), nakshatraLord: z.string().min(1), d9Sign: z.string().nullable(), d9SignLabel: z.string().nullable(), symbol: z.string().nullable(), }); /** * The ascendant is a chart point, not a body: the engine returns its sign and * degree but no nakshatra, so those three fields are nullable rather than * filled in with a plausible value. */ export const chartViewAscendantRowSchema = z.object({ label: z.string().min(1), abbr: z.string().min(1), sign: z.string().min(1), signLabel: z.string().min(1), degreeInSign: z.number(), house: z.literal(1), retrograde: z.literal(false), nakshatra: z.string().min(1).nullable(), pada: z.number().int().min(1).max(4).nullable(), nakshatraLord: z.string().min(1).nullable(), }); /** * One body in a divisional chart's own table (BUG-1076). Sign, in-varga degree * and house are the engine's divisional values; `natalSign` is the D1 sign it * came from; retrograde is the natal direction because a varga does not change * motion. Nakshatra and pada are deliberately absent: they belong to the D1 * longitude and would be wrong data in a divisional table. */ export const chartViewVargaRowSchema = z.object({ name: z.string().min(1), label: z.string().min(1), sign: z.string().min(1), signLabel: z.string().min(1), degreeInSign: z.number(), house: z.number().int().min(1).max(12), natalSign: z.string().min(1), natalSignLabel: z.string().min(1), dignity: z.enum(["exalted", "own", "debilitated"]).nullable(), vargottama: z.boolean(), retrograde: z.boolean(), }); export const chartViewVargaAscendantRowSchema = z.object({ sign: z.string().min(1), signLabel: z.string().min(1), degreeInSign: z.number(), natalSign: z.string().min(1), natalSignLabel: z.string().min(1), vargottama: z.boolean(), }); export const chartViewVargaSchema = z.object({ id: z.string().regex(/^D\d{1,3}$/), title: z.string().min(1), meaning: z.string().min(1), division: z.number().int().positive(), chart: northIndianChartSchema, // D2 and up only. D1 keeps the natal eight-column table (`vedic.planets`). ascendant: chartViewVargaAscendantRowSchema.optional(), rows: z.array(chartViewVargaRowSchema).optional(), // Older module-cache snapshots have no declaration: consumers must hide the // corresponding column unless its server-projected capability is exactly true. supportsDignity: z.boolean().optional(), supportsVargottama: z.boolean().optional(), }); /** * The Moon chart (Chandra lagna): the D1 counted from the Moon's sign. The * rows are the natal rows with only the house recounted; the natal ascendant * sits in `ascendantHouse`. */ export const chartViewMoonChartSchema = z.object({ moonSign: z.string().min(1), moonSignLabel: z.string().min(1), ascendantHouse: z.number().int().min(1).max(12), chart: northIndianChartSchema, planets: z.array(chartViewPlanetRowSchema).min(1), }); /** * Today's sky placed in the natal houses (natal ascendant = house 1). Same * ayanamsa and node mode as the natal chart; `date` / `time` / `timezoneLabel` * say which moment was computed. */ export const chartViewTransitOkSchema = z.object({ status: z.literal("ok"), date: isoDate, time: z.string().regex(/^\d{2}:\d{2}$/), timezoneLabel: z.string().min(1), chart: northIndianChartSchema, rows: z.array(z.object({ name: z.string().min(1), label: z.string().min(1), sign: z.string().min(1), signLabel: z.string().min(1), degreeInSign: z.number(), retrograde: z.boolean(), natalHouse: z.number().int().min(1).max(12), natalPlanetHouse: z.number().int().min(1).max(12).nullable(), })).min(1), }); /** * Sripati Bhava Chalit: the natal bodies re-housed by the unequal house * boundaries (sandhi) built from the birth ascendant and the sidereal MC. */ export const chartViewBhavaOkSchema = z.object({ status: z.literal("ok"), houseSystem: z.literal("Sripati"), chart: northIndianChartSchema, rows: z.array(z.object({ name: z.string().min(1), label: z.string().min(1), sign: z.string().min(1), signLabel: z.string().min(1), degreeInSign: z.number(), rashiHouse: z.number().int().min(1).max(12), bhavaHouse: z.number().int().min(1).max(12), shifted: z.boolean(), })).min(1), cusps: z.array(z.object({ house: z.number().int().min(1).max(12), sign: z.string().min(1), signLabel: z.string().min(1), degreeInSign: z.number(), sandhiSign: z.string().min(1), sandhiSignLabel: z.string().min(1), sandhiDegreeInSign: z.number(), })).length(12), }); export const chartViewPeriodSchema = z.object({ lord: z.string().min(1), label: z.string().min(1), start: isoDate, end: isoDate, current: z.boolean(), derived: z.boolean(), }); export const chartViewMahadashaSchema = chartViewPeriodSchema.extend({ sign: z.string().nullable(), antardashas: z.array(chartViewPeriodSchema), // Present only for a Chara mahadasha whose engine duration is exactly 0. // It stays on the list, occupies no timeline, and has no antardashas. zeroYear: z.literal(true).optional(), }); export const chartViewDashaTrackSchema = z.object({ id: z.enum(["vimshottari", "chara"]), title: z.string().min(1), method: z.string().min(1), currentLabel: z.string().min(1), antardashaNote: z.string().nullable(), periods: z.array(chartViewMahadashaSchema), }); export const westernPointSchema = z.object({ id: z.string().min(1), label: z.string().min(1), longitude: z.number(), sign: z.string().min(1), signLabel: z.string().min(1), degreeInSign: z.number(), house: z.number().int().min(1).max(12).nullable(), retrograde: z.boolean(), }); export const chartViewWesternOkSchema = z.object({ status: z.literal("ok"), zodiac: z.literal("tropical"), houseSystem: z.string().min(1), boundary: z.string().min(1), ayanamsaGapNote: z.string().min(1), ascendantLongitude: z.number(), planets: z.array(westernPointSchema), houses: z.array(z.object({ house: z.number().int().min(1).max(12), longitude: z.number(), sign: z.string().min(1), signLabel: z.string().min(1), degreeInSign: z.number(), })).length(12), aspects: z.array(z.object({ left: z.string().min(1), right: z.string().min(1), aspect: z.string().min(1), aspectLabel: z.string().min(1), orb: z.number(), })), elements: z.array(z.object({ id: z.string().min(1), label: z.string().min(1), count: z.number().int().nonnegative(), })), modes: z.array(z.object({ id: z.string().min(1), label: z.string().min(1), count: z.number().int().nonnegative(), })), }); export const chartViewProfileSchema = z.object({ name: z.string().min(1), date: isoDate, time: z.string().regex(/^\d{2}:\d{2}/), placeLabel: z.string().min(1), latitude: z.number(), longitude: z.number(), timezoneOffset: z.number().finite().optional(), timezoneLabel: z.string().min(1), ayanamsa: z.string().min(1), ayanamsaDisplay: z.string().min(1), nodeMode: z.enum(["mean", "true"]), nodeModeLabel: z.string().min(1), birthTimeStatus: z.string().min(1), birthTimeStatusLabel: z.string().min(1), moonNakshatra: z.string().min(1), moonPada: z.number().int().min(1).max(4), ascendantSign: z.string().min(1), ascendantSignLabel: z.string().min(1), currentDashaLabel: z.string().min(1), }); export const chartViewFailureStatusSchema = z.enum([ ...CHART_BIRTH_FAILURE_REASONS, "client_timeout", "unavailable", "unauthenticated", "rate_limited", ]); export type ChartViewFailureStatus = z.infer; export const chartViewOkSchema = z.object({ layerFailures: z.object({ varga: chartViewFailureStatusSchema.optional(), chara: chartViewFailureStatusSchema.optional(), western: chartViewFailureStatusSchema.optional(), bhava: chartViewFailureStatusSchema.optional(), transit: chartViewFailureStatusSchema.optional(), }).optional(), status: z.literal("ok"), billed: z.literal(false), profile: chartViewProfileSchema, vedic: z.object({ coordinateSystem: z.literal("sidereal"), boundary: z.string().min(1), vargas: z.array(chartViewVargaSchema).min(1), ascendant: chartViewAscendantRowSchema.nullable(), planets: z.array(chartViewPlanetRowSchema).min(1), moon: chartViewMoonChartSchema.nullable(), }), dasha: z.object({ // The day the current flags were computed against. The dasha tab uses it // for the "现在" card and the timeline cursor. It is not an engine field. asOf: isoDate, vimshottari: chartViewDashaTrackSchema, chara: chartViewDashaTrackSchema, dualTrackNote: z.string().min(1), }), western: z.union([chartViewWesternOkSchema, chartViewUnavailableLayerSchema]), bhava: z.union([chartViewBhavaOkSchema, chartViewUnavailableLayerSchema]), transit: z.union([chartViewTransitOkSchema, chartViewUnavailableLayerSchema]), accountId: z.string().min(1).optional(), profileFingerprint: z.string().min(1).optional(), subjectId: z.string().min(1).optional(), }); const chartViewIdentitySchema = { accountId: z.string().min(1).optional(), profileFingerprint: z.string().min(1).optional(), subjectId: z.string().min(1).optional(), }; export const chartViewMessageSchema = z.object({ status: z.enum(["unauthenticated", "birth_profile_incomplete", "chart_unavailable", "rate_limited", "client_timeout"]), billed: z.literal(false), message: z.string().min(1), reason: z.enum(CHART_BIRTH_FAILURE_REASONS).optional(), ...chartViewIdentitySchema, }); export const chartViewResponseSchema = z.union([chartViewOkSchema, chartViewMessageSchema]); export type ChartViewOk = z.infer; export type ChartViewMessage = z.infer; export type ChartViewResponse = z.infer; export type ChartViewUnavailableLayer = z.infer; export type ChartViewWesternOk = z.infer; export type ChartViewTransitOk = z.infer; export type ChartViewBhavaOk = z.infer; export type ChartViewMoonChart = z.infer; export type ChartViewVarga = z.infer; export type ChartViewVargaRow = z.infer; export type ChartViewVargaAscendantRow = z.infer; export type ChartViewPlanetRow = z.infer; export type NorthIndianChart = z.infer; export type ChartViewAscendantRow = z.infer; export const CHART_VIEW_TABS = [ { id: "vedic", label: "星盘" }, { id: "basics", label: "基础信息" }, { id: "dasha", label: "大运" }, { id: "western", label: "西洋盘" }, ] as const; export type ChartViewTabId = (typeof CHART_VIEW_TABS)[number]["id"];