fix(chart): divisional tables show that chart's own rows, not D1 (T3, BUG-1076)
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
af27709de4
commit
ab6ee94e95
@@ -6,6 +6,9 @@ import type {
|
||||
ChartViewAscendantRow,
|
||||
ChartViewOk,
|
||||
ChartViewPlanetRow,
|
||||
ChartViewVarga,
|
||||
ChartViewVargaAscendantRow,
|
||||
ChartViewVargaRow,
|
||||
} from "@/lib/chart-view-contract";
|
||||
import { CHART_VIEW_COPY, VARGA_CHIP_ORDER } from "@/lib/chart-view-labels";
|
||||
import {
|
||||
@@ -19,6 +22,7 @@ import {
|
||||
import { formatDegreeMinutes } from "./chart-format";
|
||||
import { ChartParamTable } from "./chart-param-table";
|
||||
import { chartLayerFailureCopy } from "@/lib/chart-view-failure";
|
||||
import { VARGA_DIGNITY_LABEL } from "@/lib/chart-view-varga";
|
||||
import { ChartPageSkeleton } from "./chart-page-skeleton";
|
||||
|
||||
/** The same coloured symbol the chart draws, so a row and a house read as one body. */
|
||||
@@ -86,6 +90,99 @@ function AscendantRow({ row }: { row: ChartViewAscendantRow }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The natal eight-column table. Everything the chart stopped drawing lands
|
||||
* here: the chart carries symbols, this table carries the numbers behind them.
|
||||
*/
|
||||
function NatalTable({ view, caption }: { view: ChartViewOk; caption: string }) {
|
||||
return (
|
||||
<div className="chart-page-planet-table-wrap">
|
||||
<table className="chart-page-planet-table">
|
||||
<caption>{caption}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">行星</th>
|
||||
<th scope="col">黄道经度</th>
|
||||
<th scope="col">速度</th>
|
||||
<th scope="col">宫位</th>
|
||||
<th scope="col">星宿</th>
|
||||
<th scope="col">宿主</th>
|
||||
<th scope="col">pada</th>
|
||||
<th scope="col">状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{view.vedic.ascendant && <AscendantRow row={view.vedic.ascendant} />}
|
||||
{view.vedic.planets.map((planet) => (
|
||||
<PlanetRow key={planet.name} row={planet} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VargaRow({ row }: { row: ChartViewVargaRow }) {
|
||||
const key = planetGlyphKey(row.name);
|
||||
return (
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<PlanetCell name={row.name} label={row.label} abbr={key ? PLANET_ABBR[key] : "—"} />
|
||||
{row.retrograde ? <span className="chart-page-planet-abbr"> 逆</span> : null}
|
||||
</th>
|
||||
<td className="chart-page-planet-num">{row.signLabel} {formatDegreeMinutes(row.degreeInSign)}</td>
|
||||
<td className="chart-page-planet-mid">{row.house}宫</td>
|
||||
<td>{row.natalSignLabel}</td>
|
||||
<td className="chart-page-planet-mid">{row.dignity ? VARGA_DIGNITY_LABEL[row.dignity] : "—"}</td>
|
||||
<td className="chart-page-planet-mid">{row.vargottama ? "同位" : "—"}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function VargaAscendantRow({ row }: { row: ChartViewVargaAscendantRow }) {
|
||||
return (
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<PlanetCell name={null} label="上升" abbr="As" />
|
||||
</th>
|
||||
<td className="chart-page-planet-num">{row.signLabel} {formatDegreeMinutes(row.degreeInSign)}</td>
|
||||
<td className="chart-page-planet-mid">1宫</td>
|
||||
<td>{row.natalSignLabel}</td>
|
||||
<td className="chart-page-planet-mid">—</td>
|
||||
<td className="chart-page-planet-mid">{row.vargottama ? "同位" : "—"}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A divisional chart's own table (BUG-1076). Six columns, not the natal eight:
|
||||
* nakshatra, its lord and pada are properties of the D1 longitude and would be
|
||||
* wrong data here, so they are not shown at all.
|
||||
*/
|
||||
export function VargaTable({ varga }: { varga: ChartViewVarga }) {
|
||||
return (
|
||||
<div className="chart-page-planet-table-wrap">
|
||||
<table className="chart-page-planet-table">
|
||||
<caption>{varga.id} · {varga.meaning}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">行星</th>
|
||||
<th scope="col">分盘星座</th>
|
||||
<th scope="col">宫位</th>
|
||||
<th scope="col">本命星座</th>
|
||||
<th scope="col">庙旺</th>
|
||||
<th scope="col">同位</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{varga.ascendant ? <VargaAscendantRow row={varga.ascendant} /> : null}
|
||||
{(varga.rows ?? []).map((row) => <VargaRow key={row.name} row={row} />)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChartVedicTab({
|
||||
view,
|
||||
vargaId,
|
||||
@@ -138,31 +235,11 @@ export function ChartVedicTab({
|
||||
chart can and cannot be read for. */}
|
||||
<p className="chart-page-boundary">{view.vedic.boundary}</p>
|
||||
</div>
|
||||
{/* Everything the chart stopped drawing lands here: the chart carries
|
||||
symbols, this table carries the numbers behind them. */}
|
||||
<div className="chart-page-planet-table-wrap">
|
||||
<table className="chart-page-planet-table">
|
||||
<caption>{varga.id} · {varga.meaning}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">行星</th>
|
||||
<th scope="col">黄道经度</th>
|
||||
<th scope="col">速度</th>
|
||||
<th scope="col">宫位</th>
|
||||
<th scope="col">星宿</th>
|
||||
<th scope="col">宿主</th>
|
||||
<th scope="col">pada</th>
|
||||
<th scope="col">状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{view.vedic.ascendant && <AscendantRow row={view.vedic.ascendant} />}
|
||||
{view.vedic.planets.map((planet) => (
|
||||
<PlanetRow key={planet.name} row={planet} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{varga.id === "D1" || !varga.rows ? (
|
||||
<NatalTable view={view} caption={`${varga.id} · ${varga.meaning}`} />
|
||||
) : (
|
||||
<VargaTable varga={varga} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -66,12 +66,45 @@ export const chartViewAscendantRowSchema = z.object({
|
||||
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(),
|
||||
});
|
||||
|
||||
export const chartViewPeriodSchema = z.object({
|
||||
@@ -226,6 +259,8 @@ export type ChartViewResponse = z.infer<typeof chartViewResponseSchema>;
|
||||
export type ChartViewUnavailableLayer = z.infer<typeof chartViewUnavailableLayerSchema>;
|
||||
export type ChartViewWesternOk = z.infer<typeof chartViewWesternOkSchema>;
|
||||
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>;
|
||||
|
||||
@@ -22,11 +22,14 @@ import {
|
||||
signZh,
|
||||
} from "./chart-view-labels.ts";
|
||||
import { traditionalSymbol } from "./chart-view-symbols.ts";
|
||||
import { vargaDignity } from "./chart-view-varga.ts";
|
||||
import {
|
||||
chartViewOkSchema,
|
||||
type ChartViewOk,
|
||||
type ChartViewUnavailableLayer,
|
||||
type ChartViewVarga,
|
||||
type ChartViewVargaAscendantRow,
|
||||
type ChartViewVargaRow,
|
||||
type ChartViewWesternOk,
|
||||
type NorthIndianChart,
|
||||
} from "./chart-view-contract.ts";
|
||||
@@ -190,6 +193,58 @@ function vargaKey(division: number, result: Record<string, unknown>): Record<str
|
||||
return exact ? record(exact[1]) : null;
|
||||
}
|
||||
|
||||
type NatalSigns = { planets: ReadonlyMap<string, string>; ascendant: string | null };
|
||||
|
||||
function houseFromSigns(sign: string, ascSign: string): number {
|
||||
return ((SIGN_ORDER.indexOf(sign as (typeof SIGN_ORDER)[number]) - SIGN_ORDER.indexOf(ascSign as (typeof SIGN_ORDER)[number]) + 12) % 12) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* The divisional chart's own rows: the engine's divisional sign, in-varga
|
||||
* degree and house (counted from the divisional ascendant), plus the D1 sign
|
||||
* each body came from. Nothing is recomputed here except the sign-dignity
|
||||
* lookup, whose tables are the engine's (`chart-view-varga.ts`).
|
||||
*/
|
||||
function vargaRows(
|
||||
ascSign: string,
|
||||
varga: Record<string, unknown>,
|
||||
natalRetro: ReadonlyMap<string, boolean>,
|
||||
natal: NatalSigns,
|
||||
): { ascendant: ChartViewVargaAscendantRow | undefined; rows: ChartViewVargaRow[] } {
|
||||
const planets = asPlanetMap(varga.planets);
|
||||
const rows: ChartViewVargaRow[] = [];
|
||||
for (const name of PLANET_ORDER) {
|
||||
const planet = planets[name];
|
||||
const sign = text(planet?.sign);
|
||||
const natalSign = natal.planets.get(name);
|
||||
if (!planet || !sign || !isSignName(sign) || !natalSign) continue;
|
||||
const engineHouse = finite(planet.house);
|
||||
rows.push({
|
||||
name,
|
||||
label: planetZh(name),
|
||||
sign,
|
||||
signLabel: signZh(sign),
|
||||
degreeInSign: Math.min(29.9999, Math.max(0, planetDegreeInSign(planet))),
|
||||
house: engineHouse !== null && engineHouse >= 1 && engineHouse <= 12 ? Math.trunc(engineHouse) : houseFromSigns(sign, ascSign),
|
||||
natalSign,
|
||||
natalSignLabel: signZh(natalSign),
|
||||
dignity: vargaDignity(name, sign),
|
||||
vargottama: sign === natalSign,
|
||||
retrograde: natalRetro.get(name) ?? false,
|
||||
});
|
||||
}
|
||||
const ascendant = record(varga.ascendant);
|
||||
const ascendantRow = natal.ascendant ? {
|
||||
sign: ascSign,
|
||||
signLabel: signZh(ascSign),
|
||||
degreeInSign: Math.min(29.9999, Math.max(0, finite(ascendant?.degree) ?? 0)),
|
||||
natalSign: natal.ascendant,
|
||||
natalSignLabel: signZh(natal.ascendant),
|
||||
vargottama: ascSign === natal.ascendant,
|
||||
} : undefined;
|
||||
return { ascendant: ascendantRow, rows };
|
||||
}
|
||||
|
||||
function vargaToChart(
|
||||
id: string,
|
||||
title: string,
|
||||
@@ -234,9 +289,14 @@ function buildVargas(
|
||||
vargaPacket: Record<string, unknown> | null,
|
||||
): ChartViewVarga[] {
|
||||
const natalRetro = new Map<string, boolean>();
|
||||
const natalPlanetSigns = new Map<string, string>();
|
||||
for (const [name, planet] of Object.entries(asPlanetMap(chart.planets))) {
|
||||
natalRetro.set(name, Boolean(planet.retrograde));
|
||||
const sign = text(planet.sign);
|
||||
if (sign && isSignName(sign)) natalPlanetSigns.set(name, sign);
|
||||
}
|
||||
const natalAsc = text(record(chart.ascendant)?.sign);
|
||||
const natal: NatalSigns = { planets: natalPlanetSigns, ascendant: natalAsc && isSignName(natalAsc) ? natalAsc : null };
|
||||
const result = record(vargaPacket?.result) ?? {};
|
||||
const vargas: ChartViewVarga[] = [];
|
||||
for (const division of VARGA_CHIP_ORDER) {
|
||||
@@ -245,12 +305,14 @@ function buildVargas(
|
||||
if (raw) {
|
||||
const mapped = vargaToChart(id, `${id} ${text(raw.name) ?? ""}`.trim(), raw, natalRetro);
|
||||
if (mapped) {
|
||||
const ascSign = text(record(raw.ascendant)?.sign)!;
|
||||
vargas.push({
|
||||
id,
|
||||
title: id,
|
||||
meaning: mapped.meaning,
|
||||
division: mapped.division,
|
||||
chart: mapped.chart,
|
||||
...(division === 1 ? {} : vargaRows(ascSign, raw, natalRetro, natal)),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import tables from "./vedic-chart-tables.json";
|
||||
|
||||
export type VargaDignity = "exalted" | "own" | "debilitated";
|
||||
|
||||
const EXALTATION: Readonly<Record<string, string>> = tables.exaltation;
|
||||
const DEBILITATION: Readonly<Record<string, string>> = tables.debilitation;
|
||||
const SIGN_LORDS: Readonly<Record<string, string>> = tables.signLords;
|
||||
const NODE_OWN_SIGN: Readonly<Record<string, string>> = tables.nodeOwnSign;
|
||||
const VARGA_MEANING: Readonly<Record<string, string>> = tables.vargaMeaning;
|
||||
|
||||
/**
|
||||
* Sign dignity of a body inside a divisional chart.
|
||||
*
|
||||
* Same precedence as `jyotish_engine._get_dignity_level` (exalted, then own
|
||||
* sign, then debilitated) without the parts that need a D1 degree or the whole
|
||||
* chart: no moolatrikona band, no neecha bhanga, no friendship. The tables are
|
||||
* the engine's; `tests/test_chart_page_vedic_tables_contract.py` pins them.
|
||||
*/
|
||||
export function vargaDignity(planet: string, sign: string): VargaDignity | null {
|
||||
if (EXALTATION[planet] === sign) return "exalted";
|
||||
if (SIGN_LORDS[sign] === planet || NODE_OWN_SIGN[planet] === sign) return "own";
|
||||
if (DEBILITATION[planet] === sign) return "debilitated";
|
||||
return null;
|
||||
}
|
||||
|
||||
export const VARGA_DIGNITY_LABEL: Record<VargaDignity, string> = {
|
||||
exalted: "入旺",
|
||||
own: "自宫",
|
||||
debilitated: "落陷",
|
||||
};
|
||||
|
||||
/** The engine's own one-line use of a D-number chart (`VargaType.meaning`). */
|
||||
export function vargaMeaning(division: number): string | null {
|
||||
return VARGA_MEANING[String(division)] ?? null;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"_comment": "Chart-page reference tables. Mirrors jyotish_engine.SIGN_LORDS / EXALTATION / DEBILITATION, the Rahu-Virgo / Ketu-Pisces own-sign rule in jyotish_engine._get_dignity_level, and divisional_charts_extended.VargaType meanings. tests/test_chart_page_vedic_tables_contract.py fails if either side drifts.",
|
||||
"vargaMeaning": {
|
||||
"1": "本命盘",
|
||||
"2": "财富",
|
||||
"3": "兄弟姐妹",
|
||||
"4": "财产/运气",
|
||||
"5": "名声/权力",
|
||||
"6": "健康/敌人",
|
||||
"7": "子女",
|
||||
"8": "突发事件",
|
||||
"9": "配偶/灵性",
|
||||
"10": "事业",
|
||||
"11": "破坏/转化",
|
||||
"12": "父母",
|
||||
"16": "交通工具/舒适",
|
||||
"20": "灵性修行",
|
||||
"24": "教育/学习",
|
||||
"27": "力量/弱点",
|
||||
"30": "不幸/困难",
|
||||
"40": "吉凶效果",
|
||||
"45": "全面判断",
|
||||
"60": "前世业力"
|
||||
},
|
||||
"signLords": {
|
||||
"Aries": "Mars",
|
||||
"Taurus": "Venus",
|
||||
"Gemini": "Mercury",
|
||||
"Cancer": "Moon",
|
||||
"Leo": "Sun",
|
||||
"Virgo": "Mercury",
|
||||
"Libra": "Venus",
|
||||
"Scorpio": "Mars",
|
||||
"Sagittarius": "Jupiter",
|
||||
"Capricorn": "Saturn",
|
||||
"Aquarius": "Saturn",
|
||||
"Pisces": "Jupiter"
|
||||
},
|
||||
"exaltation": {
|
||||
"Sun": "Aries",
|
||||
"Moon": "Taurus",
|
||||
"Mars": "Capricorn",
|
||||
"Mercury": "Virgo",
|
||||
"Jupiter": "Cancer",
|
||||
"Venus": "Pisces",
|
||||
"Saturn": "Libra"
|
||||
},
|
||||
"debilitation": {
|
||||
"Sun": "Libra",
|
||||
"Moon": "Scorpio",
|
||||
"Mars": "Cancer",
|
||||
"Mercury": "Pisces",
|
||||
"Jupiter": "Capricorn",
|
||||
"Venus": "Virgo",
|
||||
"Saturn": "Aries"
|
||||
},
|
||||
"nodeOwnSign": {
|
||||
"Rahu": "Virgo",
|
||||
"Ketu": "Pisces"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
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 type { ChartViewOk } from "../src/lib/chart-view-contract.ts";
|
||||
|
||||
Object.assign(globalThis, { React });
|
||||
|
||||
// Real engine output (see the fixture's sourceCommand); nothing here is hand-shaped.
|
||||
const golden = JSON.parse(
|
||||
readFileSync(new URL("./fixtures/chart-view-golden.json", import.meta.url), "utf8"),
|
||||
) as {
|
||||
chart: Record<string, unknown>;
|
||||
varga_full: { result: Record<string, { ascendant: Record<string, unknown>; planets: Record<string, Record<string, unknown>> }> };
|
||||
chara: Record<string, unknown>;
|
||||
};
|
||||
|
||||
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 vargaView(): Promise<ChartViewOk> {
|
||||
const result = await assembleChartView({
|
||||
userId: "user-1",
|
||||
profile,
|
||||
asOf: "2026-09-15",
|
||||
layers: ["varga"],
|
||||
postEngine: async (path) => {
|
||||
if (path === "/api/chart") return { status: "ok", payload: golden.chart };
|
||||
if (path === "/api/varga_full") return { status: "ok", payload: golden.varga_full as unknown as Record<string, unknown> };
|
||||
return { status: "http_error", path, elapsedMs: 1, httpStatus: 500 };
|
||||
},
|
||||
});
|
||||
assert.equal(result.body.status, "ok");
|
||||
return result.body as ChartViewOk;
|
||||
}
|
||||
|
||||
function render(view: ChartViewOk, vargaId: string): string {
|
||||
return renderToStaticMarkup(React.createElement(ChartVedicTab, { view, vargaId, onSelectVarga: () => {} }));
|
||||
}
|
||||
|
||||
/** The table row whose header cell names this body. */
|
||||
function row(markup: string, label: string): string {
|
||||
const tbody = markup.match(/<tbody>([\s\S]*?)<\/tbody>/)?.[1] ?? "";
|
||||
const hit = [...tbody.matchAll(/<tr>([\s\S]*?)<\/tr>/g)].map((match) => match[1]!).find((cells) => {
|
||||
const header = cells.match(/<th scope="row">([\s\S]*?)<\/th>/)?.[1] ?? "";
|
||||
return header.includes(label);
|
||||
});
|
||||
assert.ok(hit, `no row for ${label}`);
|
||||
return hit.replace(/<!-- -->/g, "");
|
||||
}
|
||||
|
||||
function degreeMinutes(degree: number): string {
|
||||
const whole = Math.floor(degree);
|
||||
const minutes = Math.floor((degree - whole) * 60);
|
||||
return `${whole}°${String(minutes).padStart(2, "0")}′`;
|
||||
}
|
||||
|
||||
// BUG-1076: the D9 caption changed but every row stayed D1.
|
||||
test("a divisional chart's table carries that chart's signs, degrees and houses", async () => {
|
||||
const view = await vargaView();
|
||||
const d9 = golden.varga_full.result.D9_Navamsa!;
|
||||
const d1Markup = render(view, "D1");
|
||||
const d9Markup = render(view, "D9");
|
||||
|
||||
const sunD9 = row(d9Markup, "太阳");
|
||||
const moonD9 = row(d9Markup, "月亮");
|
||||
assert.match(sunD9, /天秤/, "D9 Sun is in Libra");
|
||||
assert.ok(sunD9.includes(degreeMinutes(Number(d9.planets.Sun!.degree))), "D9 Sun degree is the in-varga degree");
|
||||
assert.match(moonD9, /双鱼/, "D9 Moon is in Pisces");
|
||||
assert.match(moonD9, />4宫</, "D9 Moon house counts from the D9 ascendant");
|
||||
assert.notEqual(sunD9, row(d1Markup, "太阳"));
|
||||
assert.notEqual(moonD9, row(d1Markup, "月亮"));
|
||||
|
||||
const ascD9 = row(d9Markup, "上升");
|
||||
assert.match(ascD9, /射手/, "D9 ascendant is Sagittarius");
|
||||
});
|
||||
|
||||
test("the D1 table is byte-for-byte the table the page drew before BUG-1076", async () => {
|
||||
const view = await vargaView();
|
||||
const markup = render(view, "D1");
|
||||
const table = markup.match(/<div class="chart-page-planet-table-wrap">[\s\S]*?<\/table><\/div>/)?.[0];
|
||||
// Rendered from the baseline commit 5dc977ef with the same golden.
|
||||
const baseline = readFileSync(new URL("./fixtures/chart-d1-table-baseline.html", import.meta.url), "utf8");
|
||||
assert.equal(table, baseline);
|
||||
});
|
||||
|
||||
test("divisional rows carry engine dignity and vargottama, and drop nakshatra columns", async () => {
|
||||
const view = await vargaView();
|
||||
const d9 = view.vedic.vargas.find((item) => item.id === "D9");
|
||||
assert.ok(d9?.rows);
|
||||
const saturn = d9.rows.find((item) => item.name === "Saturn");
|
||||
// D1 Saturn is in Capricorn and D9 Saturn is in Capricorn: own sign, vargottama.
|
||||
assert.equal(saturn?.sign, "Capricorn");
|
||||
assert.equal(saturn?.dignity, "own");
|
||||
assert.equal(saturn?.vargottama, true);
|
||||
const sun = d9.rows.find((item) => item.name === "Sun");
|
||||
assert.equal(sun?.sign, "Libra");
|
||||
assert.equal(sun?.dignity, "debilitated");
|
||||
assert.equal(sun?.vargottama, false);
|
||||
const markup = render(view, "D9");
|
||||
const header = markup.match(/<thead>([\s\S]*?)<\/thead>/)?.[1] ?? "";
|
||||
for (const column of ["行星", "分盘星座", "宫位", "本命星座", "庙旺", "同位"]) assert.match(header, new RegExp(`>${column}<`));
|
||||
assert.doesNotMatch(header, /星宿|宿主|pada/);
|
||||
assert.match(row(markup, "土星"), />自宫<[\s\S]*>同位</);
|
||||
assert.match(row(markup, "太阳"), />落陷</);
|
||||
// Every divisional chart the engine returned has its own rows, D1 has none.
|
||||
for (const varga of view.vedic.vargas) {
|
||||
if (varga.id === "D1") assert.equal(varga.rows, undefined);
|
||||
else assert.equal(varga.rows?.length, 9, varga.id);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { vargaDignity, vargaMeaning } from "../src/lib/chart-view-varga.ts";
|
||||
|
||||
// The tables are pinned to the engine by tests/test_chart_page_vedic_tables_contract.py;
|
||||
// this pins the precedence (exalted, own, debilitated) the Python test mirrors.
|
||||
test("sign dignity follows the engine's precedence and node own signs", () => {
|
||||
assert.equal(vargaDignity("Sun", "Aries"), "exalted");
|
||||
assert.equal(vargaDignity("Sun", "Leo"), "own");
|
||||
assert.equal(vargaDignity("Sun", "Libra"), "debilitated");
|
||||
assert.equal(vargaDignity("Mercury", "Virgo"), "exalted", "exaltation wins over own sign");
|
||||
assert.equal(vargaDignity("Rahu", "Virgo"), "own");
|
||||
assert.equal(vargaDignity("Ketu", "Pisces"), "own");
|
||||
assert.equal(vargaDignity("Rahu", "Taurus"), null, "the engine has no node exaltation");
|
||||
assert.equal(vargaDignity("Moon", "Leo"), null);
|
||||
});
|
||||
|
||||
test("varga meanings come from the engine table including D40, D45 and D60", () => {
|
||||
assert.equal(vargaMeaning(9), "配偶/灵性");
|
||||
assert.equal(vargaMeaning(45), "全面判断");
|
||||
assert.equal(vargaMeaning(60), "前世业力");
|
||||
assert.equal(vargaMeaning(81), null);
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,58 @@
|
||||
"""The chart page's dignity and varga-meaning tables must be the engine's.
|
||||
|
||||
`frontend/src/lib/vedic-chart-tables.json` is read by the chart-page mapper so
|
||||
the divisional tables can show 入旺 / 自宫 / 落陷 without a second hand-written
|
||||
rule set. This test pins it to the constants `jyotish_engine._get_dignity_level`
|
||||
uses and to `divisional_charts_extended.VargaType`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
import divisional_charts_extended # noqa: E402
|
||||
import jyotish_engine # noqa: E402
|
||||
|
||||
TABLES = json.loads((ROOT / "frontend/src/lib/vedic-chart-tables.json").read_text(encoding="utf-8"))
|
||||
PLANETS = ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"]
|
||||
|
||||
|
||||
def _table_dignity(planet: str, sign: str) -> str | None:
|
||||
"""Same precedence as `vargaDignity` in frontend/src/lib/chart-view-varga.ts."""
|
||||
if TABLES["exaltation"].get(planet) == sign:
|
||||
return "EXALTED"
|
||||
if TABLES["signLords"].get(sign) == planet or TABLES["nodeOwnSign"].get(planet) == sign:
|
||||
return "OWN_SIGN"
|
||||
if TABLES["debilitation"].get(planet) == sign:
|
||||
return "DEBILITATED"
|
||||
return None
|
||||
|
||||
|
||||
def test_tables_equal_engine_constants() -> None:
|
||||
assert TABLES["signLords"] == jyotish_engine.SIGN_LORDS
|
||||
assert TABLES["exaltation"] == jyotish_engine.EXALTATION
|
||||
assert TABLES["debilitation"] == jyotish_engine.DEBILITATION
|
||||
|
||||
|
||||
def test_every_planet_sign_pair_matches_engine_dignity_level() -> None:
|
||||
for planet in PLANETS:
|
||||
for sign in jyotish_engine.SIGN_LORDS:
|
||||
# No degree and no chart context: the varga table shows the sign
|
||||
# dignity only (no moolatrikona band, no neecha bhanga, no friendship).
|
||||
engine = jyotish_engine._get_dignity_level(planet, sign)
|
||||
expected = engine if engine in {"EXALTED", "OWN_SIGN", "DEBILITATED"} else None
|
||||
if engine == "NEECHA_BHANGA":
|
||||
expected = "DEBILITATED"
|
||||
assert _table_dignity(planet, sign) == expected, (planet, sign, engine)
|
||||
|
||||
|
||||
def test_varga_meanings_equal_engine_varga_types() -> None:
|
||||
engine = {str(v.division): v.meaning for v in divisional_charts_extended.VargaType if v.division <= 60}
|
||||
assert TABLES["vargaMeaning"] == engine
|
||||
for division in ("40", "45", "60"):
|
||||
assert division in TABLES["vargaMeaning"]
|
||||
Reference in New Issue
Block a user