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:
Jesse_Chen
2026-09-28 15:21:30 +08:00
co-authored by Claude Opus 5.5
parent af27709de4
commit ab6ee94e95
9 changed files with 503 additions and 25 deletions
@@ -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>
+35
View File
@@ -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>;
+62
View File
@@ -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;
}
+35
View File
@@ -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;
}
+61
View File
@@ -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"
}
}