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
@@ -0,0 +1,115 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import test from "node:test";
|
||||
|
||||
import { ChartVedicTab } from "../src/components/chart-page/chart-vedic-tab.tsx";
|
||||
import { assembleChartView } from "../src/lib/chart-view-load.ts";
|
||||
import { chartViewOkSchema, type ChartViewOk } from "../src/lib/chart-view-contract.ts";
|
||||
import { CHART_VIEW_COPY } from "../src/lib/chart-view-labels.ts";
|
||||
|
||||
Object.assign(globalThis, { React });
|
||||
|
||||
const golden = JSON.parse(readFileSync(new URL("./fixtures/chart-view-golden.json", import.meta.url), "utf8"));
|
||||
// Real `/api/bhava_chalit` response to the body chart-view-load sends (see its sourceCommand).
|
||||
const bhavaGolden = JSON.parse(readFileSync(new URL("./fixtures/chart-view-bhava-golden.json", import.meta.url), "utf8"));
|
||||
|
||||
const profile = {
|
||||
name: "示例",
|
||||
date: "1990-06-15",
|
||||
time: "12:00",
|
||||
placeLabel: "北京",
|
||||
latitude: 39.9042,
|
||||
longitude: 116.4074,
|
||||
timezoneOffset: 8,
|
||||
timezoneId: "Asia/Shanghai",
|
||||
ayanamsa: "raman",
|
||||
birthTimeStatus: "confirmed",
|
||||
};
|
||||
|
||||
async function assemble(bhavaPayload: Record<string, unknown> = bhavaGolden.bhava_chalit) {
|
||||
const calls: Array<{ path: string; body: Record<string, unknown> }> = [];
|
||||
const result = await assembleChartView({
|
||||
userId: "user-1",
|
||||
profile,
|
||||
asOf: "2026-09-15",
|
||||
layers: ["bhava"],
|
||||
postEngine: async (path, body) => {
|
||||
calls.push({ path, body });
|
||||
if (path === "/api/chart") return { status: "ok", payload: golden.chart };
|
||||
if (path === "/api/bhava_chalit") return { status: "ok", payload: bhavaPayload };
|
||||
return { status: "http_error", path, elapsedMs: 1, httpStatus: 500 };
|
||||
},
|
||||
});
|
||||
assert.equal(result.body.status, "ok");
|
||||
return { view: chartViewOkSchema.parse(result.body), calls };
|
||||
}
|
||||
|
||||
function render(view: ChartViewOk, props: Record<string, unknown> = {}) {
|
||||
return renderToStaticMarkup(React.createElement(ChartVedicTab, { view, vargaId: "BHAVA", onSelectVarga: () => {}, ...props }));
|
||||
}
|
||||
|
||||
test("the bhava layer asks for Sripati with the birth moment the MC needs (BUG-1077)", async () => {
|
||||
const { calls } = await assemble();
|
||||
const call = calls.find((item) => item.path === "/api/bhava_chalit");
|
||||
assert.ok(call);
|
||||
assert.equal(call.body.house_system, "sripati", "the western wheel's house_system P must not leak in");
|
||||
assert.equal(call.body.mode, "compare");
|
||||
for (const key of ["year", "month", "day", "hour", "minute", "lat", "lon", "tz", "ayanamsa"]) assert.ok(call.body[key] !== undefined, key);
|
||||
assert.equal(call.body.mc_lon, undefined, "the page never sends a guessed MC");
|
||||
assert.ok(call.body.planets && call.body.ascendant);
|
||||
});
|
||||
|
||||
test("Sripati rows, cusps and the drawn houses follow the engine's Bhava houses", async () => {
|
||||
const { view } = await assemble();
|
||||
assert.equal(view.bhava.status, "ok");
|
||||
if (view.bhava.status !== "ok") throw new Error("expected bhava ok");
|
||||
const engine = bhavaGolden.bhava_chalit.result;
|
||||
assert.equal(view.bhava.rows.length, 9);
|
||||
for (const row of view.bhava.rows) {
|
||||
assert.equal(row.bhavaHouse, engine.bhava_chart[row.name]);
|
||||
assert.equal(row.rashiHouse, engine.rashi_chart[row.name].house);
|
||||
}
|
||||
const shifted = view.bhava.rows.filter((row) => row.shifted).map((row) => row.name).sort();
|
||||
assert.deepEqual(shifted, engine.shifts.map((item: { planet: string }) => item.planet).sort());
|
||||
assert.ok(shifted.length > 0, "this chart has at least one body that changes house");
|
||||
assert.equal(view.bhava.cusps.length, 12);
|
||||
// Not the old equal-house chart: the boundary spans differ.
|
||||
const starts = view.bhava.cusps.map((cusp) => cusp.sandhiDegreeInSign);
|
||||
assert.ok(new Set(starts.map((value) => value.toFixed(2))).size > 1);
|
||||
for (const row of view.bhava.rows) {
|
||||
const house = view.bhava.chart.houses[row.bhavaHouse - 1]!;
|
||||
assert.ok(house.occupantGlyphs?.some((glyph) => glyph.key === row.name.toLowerCase()), `${row.name} drawn in Bhava house ${row.bhavaHouse}`);
|
||||
}
|
||||
|
||||
const markup = render(view);
|
||||
assert.ok(markup.includes(CHART_VIEW_COPY.bhavaNote));
|
||||
assert.match(markup, /<caption>Bhava · Sripati<\/caption>/);
|
||||
assert.match(markup, /<caption>宫头与宫界<\/caption>/);
|
||||
assert.equal((markup.match(/class="is-shifted"/g) ?? []).length, shifted.length);
|
||||
assert.equal((markup.match(/<td class="chart-page-planet-mid">是<\/td>/g) ?? []).length, shifted.length);
|
||||
});
|
||||
|
||||
test("an equal-house placeholder MC or a house-system fallback is never drawn", async () => {
|
||||
const response = bhavaGolden.bhava_chalit;
|
||||
const asc = response.result.ascendant_lon as number;
|
||||
const placeholder = structuredClone(response);
|
||||
placeholder.result.mc_lon = (asc + 270) % 360;
|
||||
const fallback = structuredClone(response);
|
||||
fallback.selected_house_system = "sripati";
|
||||
fallback.fallback_reason = "placidus requires swisseph plus valid birth date";
|
||||
for (const payload of [placeholder, fallback]) {
|
||||
const { view } = await assemble(payload);
|
||||
assert.equal(view.bhava.status, "unavailable");
|
||||
const markup = render(view, { failure: "unavailable" });
|
||||
assert.doesNotMatch(markup, /宫头与宫界|data-skeleton/);
|
||||
}
|
||||
});
|
||||
|
||||
test("the bhava tab waits with the chart skeleton and shows the layer failure copy", async () => {
|
||||
const { view } = await assemble();
|
||||
const bare = { ...view, bhava: { status: "unavailable" as const, reason: "not_requested", copy: CHART_VIEW_COPY.unavailable } };
|
||||
assert.match(render(bare, { pending: true }), /data-skeleton="true"/);
|
||||
assert.ok(render(bare, { pending: false, failure: "engine_busy" }).includes(CHART_VIEW_COPY.busy));
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import { peekChartPage, pinChartSnapshotIdentity, prefetchSecondaryPage, resetSe
|
||||
import { createClientLifecycleHarness } from "./react-client-lifecycle-test-support.ts";
|
||||
|
||||
const golden = JSON.parse(readFileSync(new URL("./fixtures/chart-view-golden.json", import.meta.url), "utf8"));
|
||||
const bhavaGolden = JSON.parse(readFileSync(new URL("./fixtures/chart-view-bhava-golden.json", import.meta.url), "utf8"));
|
||||
const transitGolden = JSON.parse(readFileSync(new URL("./fixtures/ephemeris-chart-transit-2026-09-15.golden.json", import.meta.url), "utf8"));
|
||||
const profile = { name: "Synthetic", date: "1990-06-15", time: "12:00", placeLabel: "Synthetic", latitude: 39.9042, longitude: 116.4074, timezoneOffset: 8, timezoneId: "Asia/Shanghai", ayanamsa: "raman", birthTimeStatus: "confirmed" };
|
||||
async function packet(layer?: ChartViewLayer, failure?: EngineCallResult) {
|
||||
@@ -20,7 +21,7 @@ async function packet(layer?: ChartViewLayer, failure?: EngineCallResult) {
|
||||
return assembleChartView({ userId: "synthetic", accountId: "synthetic", profileFingerprint: "fixture-v1", profile, asOf: "2026-09-15", layers: layer ? [layer] : [], postEngine: async (path, _body, cacheScope) => {
|
||||
if (path === "/api/chart" && !cacheScope) return { status: "ok", payload: golden.chart };
|
||||
if (failure) return failure;
|
||||
const payload = path === "/api/varga_full" ? golden.varga_full : path === "/api/dasha/chara" ? golden.chara : path === "/api/chart" ? transitGolden : golden.western;
|
||||
const payload = path === "/api/varga_full" ? golden.varga_full : path === "/api/dasha/chara" ? golden.chara : path === "/api/chart" ? transitGolden : path === "/api/bhava_chalit" ? bhavaGolden.bhava_chalit : golden.western;
|
||||
return { status: "ok", payload };
|
||||
} });
|
||||
}
|
||||
@@ -84,9 +85,9 @@ for (const cached of [false, true]) test(`chart 401 ${cached ? "cache" : "respon
|
||||
assert.equal(state().view?.status, "unauthenticated"); assert.deepEqual(assigned, ["/login"]);
|
||||
}));
|
||||
|
||||
// 原值: ["varga", "chara", "western", "qizheng"];新值: 去掉 qizheng、加 transit
|
||||
// 原因: 七政懒加载层已删(TASK-chart-types D1),行运层新增(T4)
|
||||
for (const layer of ["varga", "chara", "western", "transit"] as const) test(`real assemble to fetch to hook preserves ${layer} engine 429`, async () => harness(async (h, state) => {
|
||||
// 原值: ["varga", "chara", "western", "qizheng"];新值: 去掉 qizheng、加 bhava 与 transit
|
||||
// 原因: 七政懒加载层已删(TASK-chart-types D1),行运层(T4)与 Bhava 层(T5)新增
|
||||
for (const layer of ["varga", "chara", "western", "bhava", "transit"] as const) test(`real assemble to fetch to hook preserves ${layer} engine 429`, async () => harness(async (h, state) => {
|
||||
const initial = await packet();
|
||||
const failed = await packet(layer, { status: "busy", path: "/api/synthetic", elapsedMs: 1, httpStatus: 429 });
|
||||
globalThis.fetch = async (url) => response(String(url).includes("layers=") ? failed : initial);
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
{
|
||||
"sourceCommand": "python3: jyotish_api_server.JyotishAPIHandler._compute_bhava_chalit with the chart-view-load body (birth payload + chart-view-golden chart.planets/ascendant/houses, house_system=sripati, mode=compare) after BUG-1077 (MC from bhava_chalit_mc.sidereal_mc_from_birth). Fictional 1990-06-15 12:00 Beijing, Raman.",
|
||||
"bhava_chalit": {
|
||||
"success": true,
|
||||
"endpoint": "bhava_chalit",
|
||||
"mode": "compare",
|
||||
"requested_house_system": "sripati",
|
||||
"selected_house_system": "sripati",
|
||||
"available_house_systems": [
|
||||
"equal",
|
||||
"koch",
|
||||
"placidus",
|
||||
"porphyry",
|
||||
"sripati",
|
||||
"whole_sign"
|
||||
],
|
||||
"fallback_reason": "",
|
||||
"calculation_note": "Bhava Chalit uses Sripati (Vedic standard, Porphyry variant).",
|
||||
"result": {
|
||||
"house_system": "sripati",
|
||||
"house_system_desc": "Sripati (Vedic standard, Porphyry variant)",
|
||||
"ascendant_lon": 149.3994,
|
||||
"mc_lon": 58.152,
|
||||
"rashi_chart": {
|
||||
"Sun": {
|
||||
"sign": "Gemini",
|
||||
"house": 11,
|
||||
"degree_in_sign": 1.5338
|
||||
},
|
||||
"Moon": {
|
||||
"sign": "Aquarius",
|
||||
"house": 7,
|
||||
"degree_in_sign": 18.6462
|
||||
},
|
||||
"Mars": {
|
||||
"sign": "Pisces",
|
||||
"house": 8,
|
||||
"degree_in_sign": 18.5242
|
||||
},
|
||||
"Mercury": {
|
||||
"sign": "Taurus",
|
||||
"house": 10,
|
||||
"degree_in_sign": 12.8439
|
||||
},
|
||||
"Jupiter": {
|
||||
"sign": "Gemini",
|
||||
"house": 11,
|
||||
"degree_in_sign": 23.5398
|
||||
},
|
||||
"Venus": {
|
||||
"sign": "Aries",
|
||||
"house": 9,
|
||||
"degree_in_sign": 26.1082
|
||||
},
|
||||
"Saturn": {
|
||||
"sign": "Capricorn",
|
||||
"house": 6,
|
||||
"degree_in_sign": 1.774
|
||||
},
|
||||
"Rahu": {
|
||||
"sign": "Capricorn",
|
||||
"house": 6,
|
||||
"degree_in_sign": 17.4381
|
||||
},
|
||||
"Ketu": {
|
||||
"sign": "Cancer",
|
||||
"house": 12,
|
||||
"degree_in_sign": 17.4381
|
||||
}
|
||||
},
|
||||
"bhava_chart": {
|
||||
"Sun": 10,
|
||||
"Moon": 7,
|
||||
"Mars": 8,
|
||||
"Mercury": 9,
|
||||
"Jupiter": 11,
|
||||
"Venus": 9,
|
||||
"Saturn": 5,
|
||||
"Rahu": 6,
|
||||
"Ketu": 12
|
||||
},
|
||||
"shifts": [
|
||||
{
|
||||
"planet": "Sun",
|
||||
"sign": "Gemini",
|
||||
"degree_in_sign": 1.5338,
|
||||
"rashi_house": 11,
|
||||
"bhava_house": 10,
|
||||
"shift_direction": "backward",
|
||||
"shift_magnitude": 1,
|
||||
"note": "Sun 从第11宫偏移到第10宫 (backward)"
|
||||
},
|
||||
{
|
||||
"planet": "Mercury",
|
||||
"sign": "Taurus",
|
||||
"degree_in_sign": 12.8439,
|
||||
"rashi_house": 10,
|
||||
"bhava_house": 9,
|
||||
"shift_direction": "backward",
|
||||
"shift_magnitude": 1,
|
||||
"note": "Mercury 从第10宫偏移到第9宫 (backward)"
|
||||
},
|
||||
{
|
||||
"planet": "Saturn",
|
||||
"sign": "Capricorn",
|
||||
"degree_in_sign": 1.774,
|
||||
"rashi_house": 6,
|
||||
"bhava_house": 5,
|
||||
"shift_direction": "backward",
|
||||
"shift_magnitude": 1,
|
||||
"note": "Saturn 从第6宫偏移到第5宫 (backward)"
|
||||
}
|
||||
],
|
||||
"shifted_count": 3,
|
||||
"boundaries": {
|
||||
"house_system": "sripati",
|
||||
"house_system_desc": "Sripati (Vedic standard, Porphyry variant)",
|
||||
"ascendant_lon": 149.3994,
|
||||
"mc_lon": 58.152,
|
||||
"cusps": [
|
||||
149.3994,
|
||||
178.9836,
|
||||
208.5678,
|
||||
238.152,
|
||||
268.5678,
|
||||
298.9836,
|
||||
329.3994,
|
||||
358.9836,
|
||||
28.5678,
|
||||
58.152,
|
||||
88.5678,
|
||||
118.9836
|
||||
],
|
||||
"sandhis": [
|
||||
134.1915,
|
||||
164.1915,
|
||||
193.7757,
|
||||
223.3599,
|
||||
253.3599,
|
||||
283.7757,
|
||||
314.1915,
|
||||
344.1915,
|
||||
13.7757,
|
||||
43.3599,
|
||||
73.3599,
|
||||
103.7757
|
||||
],
|
||||
"houses": [
|
||||
{
|
||||
"house": 1,
|
||||
"cusp_lon": 149.3994,
|
||||
"cusp_sign": "Leo",
|
||||
"cusp_sign_cn": "狮子座",
|
||||
"cusp_degree_in_sign": 29.3994,
|
||||
"sandhi_start_lon": 134.1915,
|
||||
"sandhi_end_lon": 164.1915,
|
||||
"span_degrees": 30.0
|
||||
},
|
||||
{
|
||||
"house": 2,
|
||||
"cusp_lon": 178.9836,
|
||||
"cusp_sign": "Virgo",
|
||||
"cusp_sign_cn": "处女座",
|
||||
"cusp_degree_in_sign": 28.9836,
|
||||
"sandhi_start_lon": 164.1915,
|
||||
"sandhi_end_lon": 193.7757,
|
||||
"span_degrees": 29.5842
|
||||
},
|
||||
{
|
||||
"house": 3,
|
||||
"cusp_lon": 208.5678,
|
||||
"cusp_sign": "Libra",
|
||||
"cusp_sign_cn": "天秤座",
|
||||
"cusp_degree_in_sign": 28.5678,
|
||||
"sandhi_start_lon": 193.7757,
|
||||
"sandhi_end_lon": 223.3599,
|
||||
"span_degrees": 29.5842
|
||||
},
|
||||
{
|
||||
"house": 4,
|
||||
"cusp_lon": 238.152,
|
||||
"cusp_sign": "Scorpio",
|
||||
"cusp_sign_cn": "天蝎座",
|
||||
"cusp_degree_in_sign": 28.152,
|
||||
"sandhi_start_lon": 223.3599,
|
||||
"sandhi_end_lon": 253.3599,
|
||||
"span_degrees": 30.0
|
||||
},
|
||||
{
|
||||
"house": 5,
|
||||
"cusp_lon": 268.5678,
|
||||
"cusp_sign": "Sagittarius",
|
||||
"cusp_sign_cn": "射手座",
|
||||
"cusp_degree_in_sign": 28.5678,
|
||||
"sandhi_start_lon": 253.3599,
|
||||
"sandhi_end_lon": 283.7757,
|
||||
"span_degrees": 30.4158
|
||||
},
|
||||
{
|
||||
"house": 6,
|
||||
"cusp_lon": 298.9836,
|
||||
"cusp_sign": "Capricorn",
|
||||
"cusp_sign_cn": "摩羯座",
|
||||
"cusp_degree_in_sign": 28.9836,
|
||||
"sandhi_start_lon": 283.7757,
|
||||
"sandhi_end_lon": 314.1915,
|
||||
"span_degrees": 30.4158
|
||||
},
|
||||
{
|
||||
"house": 7,
|
||||
"cusp_lon": 329.3994,
|
||||
"cusp_sign": "Aquarius",
|
||||
"cusp_sign_cn": "水瓶座",
|
||||
"cusp_degree_in_sign": 29.3994,
|
||||
"sandhi_start_lon": 314.1915,
|
||||
"sandhi_end_lon": 344.1915,
|
||||
"span_degrees": 30.0
|
||||
},
|
||||
{
|
||||
"house": 8,
|
||||
"cusp_lon": 358.9836,
|
||||
"cusp_sign": "Pisces",
|
||||
"cusp_sign_cn": "双鱼座",
|
||||
"cusp_degree_in_sign": 28.9836,
|
||||
"sandhi_start_lon": 344.1915,
|
||||
"sandhi_end_lon": 13.7757,
|
||||
"span_degrees": 29.5842
|
||||
},
|
||||
{
|
||||
"house": 9,
|
||||
"cusp_lon": 28.5678,
|
||||
"cusp_sign": "Aries",
|
||||
"cusp_sign_cn": "白羊座",
|
||||
"cusp_degree_in_sign": 28.5678,
|
||||
"sandhi_start_lon": 13.7757,
|
||||
"sandhi_end_lon": 43.3599,
|
||||
"span_degrees": 29.5842
|
||||
},
|
||||
{
|
||||
"house": 10,
|
||||
"cusp_lon": 58.152,
|
||||
"cusp_sign": "Taurus",
|
||||
"cusp_sign_cn": "金牛座",
|
||||
"cusp_degree_in_sign": 28.152,
|
||||
"sandhi_start_lon": 43.3599,
|
||||
"sandhi_end_lon": 73.3599,
|
||||
"span_degrees": 30.0
|
||||
},
|
||||
{
|
||||
"house": 11,
|
||||
"cusp_lon": 88.5678,
|
||||
"cusp_sign": "Gemini",
|
||||
"cusp_sign_cn": "双子座",
|
||||
"cusp_degree_in_sign": 28.5678,
|
||||
"sandhi_start_lon": 73.3599,
|
||||
"sandhi_end_lon": 103.7757,
|
||||
"span_degrees": 30.4158
|
||||
},
|
||||
{
|
||||
"house": 12,
|
||||
"cusp_lon": 118.9836,
|
||||
"cusp_sign": "Cancer",
|
||||
"cusp_sign_cn": "巨蟹座",
|
||||
"cusp_degree_in_sign": 28.9836,
|
||||
"sandhi_start_lon": 103.7757,
|
||||
"sandhi_end_lon": 134.1915,
|
||||
"span_degrees": 30.4158
|
||||
}
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"total_planets": 9,
|
||||
"shifted_count": 3,
|
||||
"shifted_names": [
|
||||
"Sun",
|
||||
"Mercury",
|
||||
"Saturn"
|
||||
]
|
||||
},
|
||||
"requested_house_system": "sripati",
|
||||
"selected_house_system": "sripati",
|
||||
"available_house_systems": [
|
||||
"equal",
|
||||
"koch",
|
||||
"placidus",
|
||||
"porphyry",
|
||||
"sripati",
|
||||
"whole_sign"
|
||||
],
|
||||
"fallback_reason": "",
|
||||
"calculation_note": "Bhava Chalit uses Sripati (Vedic standard, Porphyry variant)."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user