Add bounded chart loading, per-layer retry states, and shared SVG skeletons. Add report block downloads with SVG, localized metadata, and inline deletion. Record verification and retain CRLF export, full-build, and controlled-device acceptance blockers. User authorized staging delivery with these gaps documented. Co-Authored-By: Claude Code <noreply@anthropic.com>
258 lines
8.3 KiB
TypeScript
258 lines
8.3 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(),
|
|
});
|
|
|
|
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,
|
|
});
|
|
|
|
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),
|
|
});
|
|
|
|
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 chartViewQizhengOkSchema = z.object({
|
|
status: z.literal("ok"),
|
|
coordinateSystem: z.string().min(1),
|
|
ketuMode: z.string().min(1),
|
|
boundary: z.string().min(1),
|
|
palaces: z.array(z.object({
|
|
branch: z.string().min(1),
|
|
lifePalace: z.string().min(1),
|
|
mansions: z.array(z.string()),
|
|
occupants: z.array(z.string()),
|
|
})),
|
|
bodies: z.array(z.object({
|
|
name: z.string().min(1),
|
|
mansion: z.string().min(1),
|
|
mansionDegree: z.number(),
|
|
palace: z.string().min(1),
|
|
dignity: z.string().min(1),
|
|
dignityClosed: z.boolean(),
|
|
})),
|
|
});
|
|
|
|
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),
|
|
engineName: 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(),
|
|
qizheng: 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),
|
|
}),
|
|
dasha: z.object({
|
|
vimshottari: chartViewDashaTrackSchema,
|
|
chara: chartViewDashaTrackSchema,
|
|
dualTrackNote: z.string().min(1),
|
|
}),
|
|
western: z.union([chartViewWesternOkSchema, chartViewUnavailableLayerSchema]),
|
|
qizheng: z.union([chartViewQizhengOkSchema, chartViewUnavailableLayerSchema]),
|
|
accountId: z.string().min(1).optional(),
|
|
profileFingerprint: z.string().min(1).optional(),
|
|
});
|
|
|
|
const chartViewIdentitySchema = {
|
|
accountId: z.string().min(1).optional(),
|
|
profileFingerprint: 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 ChartViewQizhengOk = z.infer<typeof chartViewQizhengOkSchema>;
|
|
export type ChartViewVarga = z.infer<typeof chartViewVargaSchema>;
|
|
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: "西洋盘" },
|
|
{ id: "qizheng", label: "七政四余" },
|
|
] as const;
|
|
|
|
export type ChartViewTabId = (typeof CHART_VIEW_TABS)[number]["id"];
|