Files
Jyotisha/frontend/src/lib/chart-view-contract.ts
T
jesse-uxandClaude Code 64aa0ec20c
Independent Staging Quality Gate / publish (push) Canceled after 0s
Independent Staging Quality Gate / validate (push) Canceled after 1m27s
feat(chart): polish chart presentation and symbol explanations
Hide engine branding, gate supported divisional columns, align house cells, and add accessible symbol help. Reduce desktop chart width and stabilize the waiting slot.

Validated with 78 browser checks and 87 focused frontend tests. Full suite retains 29 identical baseline loader failures; no new failures.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-29 19:03:28 +08:00

352 lines
12 KiB
TypeScript

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<typeof chartViewFailureStatusSchema>;
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<typeof chartViewOkSchema>;
export type ChartViewMessage = z.infer<typeof chartViewMessageSchema>;
export type ChartViewResponse = z.infer<typeof chartViewResponseSchema>;
export type ChartViewUnavailableLayer = z.infer<typeof chartViewUnavailableLayerSchema>;
export type ChartViewWesternOk = z.infer<typeof chartViewWesternOkSchema>;
export type ChartViewTransitOk = z.infer<typeof chartViewTransitOkSchema>;
export type ChartViewBhavaOk = z.infer<typeof chartViewBhavaOkSchema>;
export type ChartViewMoonChart = z.infer<typeof chartViewMoonChartSchema>;
export type ChartViewVarga = z.infer<typeof chartViewVargaSchema>;
export type ChartViewVargaRow = z.infer<typeof chartViewVargaRowSchema>;
export type ChartViewVargaAscendantRow = z.infer<typeof chartViewVargaAscendantRowSchema>;
export type ChartViewPlanetRow = z.infer<typeof chartViewPlanetRowSchema>;
export type NorthIndianChart = z.infer<typeof northIndianChartSchema>;
export type ChartViewAscendantRow = z.infer<typeof chartViewAscendantRowSchema>;
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"];