fix(chart): Sripati Bhava from the real sidereal MC; Bhava chart and tables (T5, BUG-1077)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3f6bcb081a
commit
c35e0195e1
@@ -143,6 +143,35 @@ export const chartViewTransitOkSchema = z.object({
|
||||
})).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),
|
||||
@@ -248,6 +277,7 @@ export const chartViewOkSchema = z.object({
|
||||
varga: chartViewFailureStatusSchema.optional(),
|
||||
chara: chartViewFailureStatusSchema.optional(),
|
||||
western: chartViewFailureStatusSchema.optional(),
|
||||
bhava: chartViewFailureStatusSchema.optional(),
|
||||
transit: chartViewFailureStatusSchema.optional(),
|
||||
}).optional(),
|
||||
status: z.literal("ok"),
|
||||
@@ -270,6 +300,7 @@ export const chartViewOkSchema = z.object({
|
||||
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(),
|
||||
@@ -298,6 +329,7 @@ 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>;
|
||||
|
||||
@@ -2,7 +2,7 @@ export const CHART_VIEW_ENGINE_TIMEOUT_MS = 10_000;
|
||||
export const CHART_VIEW_ENGINE_CACHE_TTL_MS = 5 * 60 * 1_000;
|
||||
const ENGINE_CACHE_MAX_ENTRIES = 200;
|
||||
|
||||
export const CHART_VIEW_LAYERS = ["varga", "chara", "western", "transit"] as const;
|
||||
export const CHART_VIEW_LAYERS = ["varga", "chara", "western", "bhava", "transit"] as const;
|
||||
export type ChartViewLayer = (typeof CHART_VIEW_LAYERS)[number];
|
||||
|
||||
export type EngineCallResult =
|
||||
|
||||
@@ -191,6 +191,7 @@ export const CHART_VIEW_COPY = {
|
||||
chartTypeSheetTitle: "星盘类型",
|
||||
chartTypeMenu: "全部盘型",
|
||||
chartTypeSheetClose: "关闭星盘类型",
|
||||
bhavaNote: "宫制 Sripati:用出生时刻的上升和天顶把四个象限各分三份,行星按宫界重新归宫。星座度数和本命盘相同。",
|
||||
transitNote: "以本命上升为第 1 宫,放的是这一天中午的行星位置。岁差和交点与本命相同。",
|
||||
precisionHint: "这张盘每格不到一度,出生时间差两三分钟就可能换一格。先做生时校正再看,会更稳。",
|
||||
unauthenticated: "请先登录后再看星盘。",
|
||||
|
||||
@@ -167,7 +167,7 @@ export async function assembleChartView(input: ChartViewLoadInput): Promise<{
|
||||
return enginePayload(result);
|
||||
};
|
||||
const transitDate = input.asOf.slice(0, 10);
|
||||
const [varga, chara, western, transit] = await Promise.all([
|
||||
const [varga, chara, western, bhava, transit] = await Promise.all([
|
||||
layers.has("varga")
|
||||
? callEngine(input.postEngine, "/api/varga_full", {
|
||||
...followUp,
|
||||
@@ -184,6 +184,16 @@ export async function assembleChartView(input: ChartViewLoadInput): Promise<{
|
||||
layers.has("western")
|
||||
? callEngine(input.postEngine, "/api/western", payload).then(layerPayload("western"))
|
||||
: Promise.resolve(null),
|
||||
// Sripati needs the birth moment for the sidereal MC (BUG-1077); followUp
|
||||
// carries it. `house_system: "P"` in the birth payload is the western
|
||||
// wheel's and must not reach this call.
|
||||
layers.has("bhava")
|
||||
? callEngine(input.postEngine, "/api/bhava_chalit", {
|
||||
...followUp,
|
||||
house_system: "sripati",
|
||||
mode: "compare",
|
||||
}).then(layerPayload("bhava"))
|
||||
: Promise.resolve(null),
|
||||
layers.has("transit")
|
||||
? callEngine(
|
||||
input.postEngine,
|
||||
@@ -206,6 +216,7 @@ export async function assembleChartView(input: ChartViewLoadInput): Promise<{
|
||||
varga,
|
||||
chara,
|
||||
western,
|
||||
bhava,
|
||||
transit,
|
||||
profile,
|
||||
asOf: input.asOf,
|
||||
|
||||
@@ -28,6 +28,7 @@ import { transitsFromEngine } from "./ephemeris-view.ts";
|
||||
import { TRANSIT_LOCAL_TIME } from "./transit-chart-body.ts";
|
||||
import {
|
||||
chartViewOkSchema,
|
||||
type ChartViewBhavaOk,
|
||||
type ChartViewMoonChart,
|
||||
type ChartViewOk,
|
||||
type ChartViewTransitOk,
|
||||
@@ -57,6 +58,7 @@ export type ChartViewEngineBundle = {
|
||||
varga: Record<string, unknown> | null;
|
||||
chara: Record<string, unknown> | null;
|
||||
western: Record<string, unknown> | null;
|
||||
bhava?: Record<string, unknown> | null;
|
||||
transit?: Record<string, unknown> | null;
|
||||
profile: ChartViewProfileInput;
|
||||
asOf: string;
|
||||
@@ -481,6 +483,92 @@ function mapTransit(
|
||||
};
|
||||
}
|
||||
|
||||
function unavailableBhava(reason: string): ChartViewUnavailableLayer {
|
||||
return { status: "unavailable", reason, copy: CHART_VIEW_COPY.unavailable };
|
||||
}
|
||||
|
||||
function signAt(lon: number): { sign: (typeof SIGN_ORDER)[number]; degree: number } {
|
||||
const normalized = ((lon % 360) + 360) % 360;
|
||||
return { sign: SIGN_ORDER[Math.floor(normalized / 30) % 12]!, degree: normalized % 30 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Sripati Bhava Chalit from `/api/bhava_chalit` (mode `compare`). Refuses to
|
||||
* draw anything the engine did not compute as Sripati from a real MC: a
|
||||
* fallback, a missing boundary table, or the old `asc + 270` placeholder MC
|
||||
* (BUG-1077) all come back unavailable instead of an equal-house chart.
|
||||
*/
|
||||
function mapBhava(raw: Record<string, unknown> | null | undefined, chart: Record<string, unknown>): ChartViewBhavaOk | ChartViewUnavailableLayer {
|
||||
if (!raw) return unavailableBhava("not_requested");
|
||||
const result = record(raw.result);
|
||||
if (!result) return unavailableBhava("payload_incomplete");
|
||||
if (text(raw.selected_house_system) !== "sripati" || text(raw.fallback_reason)) return unavailableBhava("house_system_fallback");
|
||||
const asc = finite(result.ascendant_lon);
|
||||
const mc = finite(result.mc_lon);
|
||||
if (asc === null || mc === null) return unavailableBhava("mc_missing");
|
||||
const placeholder = (asc + 270) % 360;
|
||||
if (Math.abs(mc - placeholder) < 1e-6) return unavailableBhava("mc_placeholder");
|
||||
const natalAsc = text(record(chart.ascendant)?.sign);
|
||||
if (!natalAsc || !isSignName(natalAsc)) return unavailableBhava("natal_ascendant_missing");
|
||||
const rashi = asPlanetMap(result.rashi_chart);
|
||||
const bhavaHouses = record(result.bhava_chart) ?? {};
|
||||
const rows: ChartViewBhavaOk["rows"] = [];
|
||||
for (const name of PLANET_ORDER) {
|
||||
const planet = rashi[name];
|
||||
const sign = text(planet?.sign);
|
||||
const rashiHouse = finite(planet?.house);
|
||||
const bhavaHouse = finite(bhavaHouses[name]);
|
||||
if (!planet || !sign || !isSignName(sign) || rashiHouse === null || bhavaHouse === null) continue;
|
||||
rows.push({
|
||||
name,
|
||||
label: planetZh(name),
|
||||
sign,
|
||||
signLabel: signZh(sign),
|
||||
degreeInSign: Math.min(29.9999, Math.max(0, finite(planet.degree_in_sign) ?? 0)),
|
||||
rashiHouse: Math.trunc(rashiHouse),
|
||||
bhavaHouse: Math.trunc(bhavaHouse),
|
||||
shifted: Math.trunc(bhavaHouse) !== Math.trunc(rashiHouse),
|
||||
});
|
||||
}
|
||||
const houses = Array.isArray(record(result.boundaries)?.houses) ? record(result.boundaries)!.houses as unknown[] : [];
|
||||
const cusps: ChartViewBhavaOk["cusps"] = houses.flatMap((entry) => {
|
||||
const house = record(entry);
|
||||
const number = finite(house?.house);
|
||||
const cusp = finite(house?.cusp_lon);
|
||||
const sandhi = finite(house?.sandhi_start_lon);
|
||||
if (!house || number === null || cusp === null || sandhi === null) return [];
|
||||
const at = signAt(cusp);
|
||||
const start = signAt(sandhi);
|
||||
return [{
|
||||
house: Math.trunc(number),
|
||||
sign: at.sign,
|
||||
signLabel: signZh(at.sign),
|
||||
degreeInSign: at.degree,
|
||||
sandhiSign: start.sign,
|
||||
sandhiSignLabel: signZh(start.sign),
|
||||
sandhiDegreeInSign: start.degree,
|
||||
}];
|
||||
});
|
||||
if (rows.length === 0 || cusps.length !== 12) return unavailableBhava("payload_incomplete");
|
||||
// Draw each body in its Bhava house: the North Indian block places a body by
|
||||
// sign, so hand it the sign that occupies that house from the natal lagna.
|
||||
const ascIndex = SIGN_ORDER.indexOf(natalAsc);
|
||||
const block: ReportChartBlock = {
|
||||
version: 1,
|
||||
id: "D1",
|
||||
title: "Bhava",
|
||||
layout: "north",
|
||||
ascendant: { sign: natalAsc, degree: Math.min(29.999, Math.max(0, planetDegreeInSign(record(chart.ascendant) ?? {}))) },
|
||||
planets: rows.map((row) => ({
|
||||
name: row.name as ReportChartBlock["planets"][number]["name"],
|
||||
sign: SIGN_ORDER[(ascIndex + row.bhavaHouse - 1) % 12]!,
|
||||
degree: Math.min(29.999, row.degreeInSign),
|
||||
retrograde: Boolean(asPlanetMap(chart.planets)[row.name]?.retrograde),
|
||||
})),
|
||||
};
|
||||
return { status: "ok", houseSystem: "Sripati", chart: toNorthIndianChart(block), rows, cusps };
|
||||
}
|
||||
|
||||
function vimshottariTrack(chart: Record<string, unknown>, asOf: string) {
|
||||
const dasha = record(chart.dasha) ?? {};
|
||||
const rawPeriods = Array.isArray(dasha.periods) ? dasha.periods : [];
|
||||
@@ -699,7 +787,7 @@ function mapWestern(raw: Record<string, unknown> | null, ayanamsaDisplay: string
|
||||
}
|
||||
|
||||
export function buildChartView(bundle: ChartViewEngineBundle): ChartViewOk {
|
||||
const { chart, varga, chara, western, transit, profile, asOf } = bundle;
|
||||
const { chart, varga, chara, western, bhava, transit, profile, asOf } = bundle;
|
||||
const planets = asPlanetMap(chart.planets);
|
||||
const moon = planets.Moon ?? {};
|
||||
const ascendant = record(chart.ascendant);
|
||||
@@ -753,6 +841,7 @@ export function buildChartView(bundle: ChartViewEngineBundle): ChartViewOk {
|
||||
dualTrackNote: DASHA_DUAL_TRACK_NOTE,
|
||||
},
|
||||
western: mapWestern(western, ayanamsaDisplay),
|
||||
bhava: mapBhava(bhava, chart),
|
||||
transit: mapTransit(transit, chart, profile, asOf),
|
||||
};
|
||||
return chartViewOkSchema.parse(view);
|
||||
|
||||
@@ -214,6 +214,7 @@ async function loadChartSnapshot(started: number, signal: AbortSignal): Promise<
|
||||
...(prior.vedic.vargas.length > 1 ? { vedic: prior.vedic } : {}),
|
||||
dasha: { ...snapshot.view.dasha, ...(prior.dasha.chara.periods.length ? { chara: prior.dasha.chara } : {}) },
|
||||
...(prior.western.status === "ok" ? { western: prior.western } : {}),
|
||||
...(prior.bhava.status === "ok" ? { bhava: prior.bhava } : {}),
|
||||
...(prior.transit.status === "ok" ? { transit: prior.transit } : {}),
|
||||
} };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user