fix(report,chart): blank columns — drop Kranti, split Ashtakavarga house SAV, ascendant nakshatra, engine varga dignity, no duplicate charts (BUG-1199..1203)
- Report p33 table: no Kranti column (no producer; materials.kranti stays blocked). - Report p53 table: the "SAV" column was the with-Lagna total (386). Split in the reader into SAV / Lagna BAV / SAV + Lagna with a totals row; ashtakavarga.py is untouched (frozen rectification identity, ERR-115). - /api/chart ascendant carries nakshatra / pada / lord (_nakshatra_fields shared with the nine bodies); chart cache schema 4 -> 5. - /api/varga_full bodies carry dignity_level from varga_dignity_level, the function the Vimsopaka path now shares (A/B identical); the chart page shows the D1 status wording and no longer derives a three-level dignity itself. - Report page draws each chart once: the trailing block skips charts the body already fenced (same rule as the export); the birth-time caveat moves under the body's chart heading. 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
0e856f9cb6
commit
ae18952e51
@@ -78,7 +78,8 @@ function PlanetRow({ row }: { row: ChartViewPlanetRow }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The ascendant is a chart point: the engine returns no nakshatra for it.
|
||||
* The ascendant is a chart point: nakshatra, lord and pada come from the
|
||||
* engine (BUG-1201); it has no dignity, so the status cell stays empty.
|
||||
* `house` is 1 except on the Moon chart, which counts from the Moon.
|
||||
*/
|
||||
function AscendantRow({ row, house = row.house }: { row: ChartViewAscendantRow; house?: number }) {
|
||||
|
||||
@@ -566,7 +566,7 @@ export function PersonalReportPage({ reportId, initialLanguage = "zh" }: { repor
|
||||
<div className="personal-report-reader-body" lang={shown === "en" ? "en" : "zh-CN"}>
|
||||
{/* Siblings remount per edition under distinct keys; a shared key left the old edition on screen (BUG-1126). */}
|
||||
<PersonalReportMarkdownView key={`markdown-${shown}`} language={shown} markdown={markdown} drawnThrough={drawnThrough} />
|
||||
{state.calculationCharts && <ReportCalculationCharts charts={state.calculationCharts} language={shown} />}
|
||||
{state.calculationCharts && <ReportCalculationCharts charts={state.calculationCharts} markdown={markdown} language={shown} />}
|
||||
{factTables && <ReportFactTables key={`facts-${shown}`} language={shown} tables={factTables} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { memo, useMemo } from "react";
|
||||
import type { ChartV1, ReportDocumentV2 } from "@/lib/personal-report-contract";
|
||||
import { fencedReportChartIds } from "@/lib/report-chart-block";
|
||||
import { hasRealChartData, VedicChartSvg } from "./vedic-chart-svg";
|
||||
import type { ReportLanguage } from "@/lib/report-language";
|
||||
|
||||
const COPY = {
|
||||
zh: { heading: "分盘核对", note: "原始计算供核对,分盘对出生时间敏感,不单独增加结论的确定性。" },
|
||||
en: { heading: "Divisional charts for checking", note: "Raw calculations for checking. Divisional charts are sensitive to birth time and do not by themselves make a conclusion more certain." },
|
||||
zh: { heading: "分盘核对" },
|
||||
en: { heading: "Divisional charts for checking" },
|
||||
} as const;
|
||||
|
||||
export const ReportCalculationCharts = memo(function ReportCalculationCharts({ charts, language = "zh" }: { charts: ReportDocumentV2["charts"]; language?: ReportLanguage }) {
|
||||
/**
|
||||
* Structured charts the report body did not already draw (BUG-1203): the same
|
||||
* rule as the export. A current report draws every chart in its body, so this
|
||||
* renders nothing; an older report without chart fences still gets its charts.
|
||||
* The birth-time caveat lives under the body's chart heading.
|
||||
*/
|
||||
export const ReportCalculationCharts = memo(function ReportCalculationCharts({ charts, markdown, language = "zh" }: {
|
||||
charts: ReportDocumentV2["charts"];
|
||||
/** The body shown on the page, whose chart fences are already drawn. */
|
||||
markdown: string;
|
||||
language?: ReportLanguage;
|
||||
}) {
|
||||
const copy = COPY[language];
|
||||
const actual = charts.filter(chart => hasRealChartData(chart as ChartV1));
|
||||
const drawn = useMemo(() => fencedReportChartIds(markdown), [markdown]);
|
||||
const actual = charts.filter(chart => hasRealChartData(chart as ChartV1) && !drawn.has(chart.id));
|
||||
if (!actual.length) return null;
|
||||
return <section className="personal-report-section" aria-label={copy.heading}>
|
||||
<h2>{copy.heading}</h2>
|
||||
<p>{copy.note}</p>
|
||||
<div className="personal-report-chart-grid">
|
||||
{actual.map(chart => <article className="personal-report-chart-card" key={chart.id}>
|
||||
<h3>{language === "en" ? `${chart.id} chart` : chart.title}</h3>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
|
||||
import { CHART_BIRTH_FAILURE_REASONS } from "./chart-birth-truth.ts";
|
||||
import { PLANET_GLYPH_KEYS } from "./planet-glyphs.ts";
|
||||
import { VARGA_DIGNITY_LEVELS } from "./chart-view-varga.ts";
|
||||
|
||||
const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
||||
|
||||
@@ -82,7 +83,7 @@ export const chartViewVargaRowSchema = z.object({
|
||||
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(),
|
||||
dignity: z.enum(VARGA_DIGNITY_LEVELS).nullable(),
|
||||
vargottama: z.boolean(),
|
||||
retrograde: z.boolean(),
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
signZh,
|
||||
} from "./chart-view-labels.ts";
|
||||
import { traditionalSymbol } from "./chart-view-symbols.ts";
|
||||
import { vargaDignity } from "./chart-view-varga.ts";
|
||||
import { vargaDignityLevel } from "./chart-view-varga.ts";
|
||||
import { transitsFromEngine } from "./ephemeris-view.ts";
|
||||
import { TRANSIT_LOCAL_TIME } from "./transit-chart-body.ts";
|
||||
import {
|
||||
@@ -209,9 +209,8 @@ function houseFromSigns(sign: string, ascSign: string): number {
|
||||
|
||||
/**
|
||||
* 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`).
|
||||
* degree, house (counted from the divisional ascendant) and dignity level,
|
||||
* plus the D1 sign each body came from. Nothing is recomputed here.
|
||||
*/
|
||||
function vargaRows(
|
||||
ascSign: string,
|
||||
@@ -236,7 +235,7 @@ function vargaRows(
|
||||
house: engineHouse !== null && engineHouse >= 1 && engineHouse <= 12 ? Math.trunc(engineHouse) : houseFromSigns(sign, ascSign),
|
||||
natalSign,
|
||||
natalSignLabel: signZh(natalSign),
|
||||
dignity: vargaDignity(name, sign),
|
||||
dignity: vargaDignityLevel(planet.dignity_level),
|
||||
vargottama: sign === natalSign,
|
||||
retrograde: natalRetro.get(name) ?? false,
|
||||
});
|
||||
@@ -323,9 +322,10 @@ function buildVargas(
|
||||
chart: mapped.chart,
|
||||
...(table ? {
|
||||
...table,
|
||||
// A null dignity or false same-sign result is still a valid result.
|
||||
// Availability follows validated source rows, never positive hits.
|
||||
supportsDignity: table.rows.length > 0,
|
||||
// The engine gives every body a level; a row without one is an
|
||||
// older cached payload, and then the column is hidden (BUG-1202).
|
||||
supportsDignity: table.rows.length > 0 && table.rows.every((row) => row.dignity !== null),
|
||||
supportsVargottama: division === 9 && (table.rows.length > 0 || table.ascendant !== undefined),
|
||||
} : {}),
|
||||
});
|
||||
@@ -383,9 +383,9 @@ function planetRows(chart: Record<string, unknown>, vargaPacket: Record<string,
|
||||
}
|
||||
|
||||
/**
|
||||
* The ascendant as a planet-table row. Sign and degree come straight from the
|
||||
* engine's `ascendant`; nakshatra, pada and their lord stay null because the
|
||||
* engine does not return them for a chart point and they must not be guessed.
|
||||
* The ascendant as a planet-table row, straight from the engine's `ascendant`
|
||||
* (nakshatra, pada and lord since BUG-1201). An older cached chart without
|
||||
* them leaves the three cells empty; they are never derived here.
|
||||
*/
|
||||
function ascendantRow(chart: Record<string, unknown>) {
|
||||
const ascendant = record(chart.ascendant);
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
import tables from "./vedic-chart-tables.json";
|
||||
|
||||
export type VargaDignity = "exalted" | "own" | "debilitated";
|
||||
/**
|
||||
* The engine's dignity levels (`jyotish_engine._get_dignity_level`). A
|
||||
* divisional row carries the level `/api/varga_full` computed with
|
||||
* `varga_dignity_level`, the Vimsopaka function; the page never recomputes it.
|
||||
*/
|
||||
export const VARGA_DIGNITY_LEVELS = [
|
||||
"EXALTED",
|
||||
"MOOLATRIKONA",
|
||||
"OWN_SIGN",
|
||||
"GREAT_FRIEND",
|
||||
"FRIEND",
|
||||
"NEUTRAL",
|
||||
"ENEMY",
|
||||
"GREAT_ENEMY",
|
||||
"DEBILITATED",
|
||||
"NEECHA_BHANGA",
|
||||
] as const;
|
||||
|
||||
export type VargaDignity = (typeof VARGA_DIGNITY_LEVELS)[number];
|
||||
|
||||
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;
|
||||
}
|
||||
/** Same wording as the D1 "状态" column (`jyotish_engine.DIGNITY_LABELS`). */
|
||||
export const VARGA_DIGNITY_LABEL: Readonly<Record<VargaDignity, string>> = tables.dignityLabels;
|
||||
|
||||
export const VARGA_DIGNITY_LABEL: Record<VargaDignity, string> = {
|
||||
exalted: "入旺",
|
||||
own: "自宫",
|
||||
debilitated: "落陷",
|
||||
};
|
||||
export function vargaDignityLevel(value: unknown): VargaDignity | null {
|
||||
return typeof value === "string" && (VARGA_DIGNITY_LEVELS as readonly string[]).includes(value)
|
||||
? value as VargaDignity
|
||||
: null;
|
||||
}
|
||||
|
||||
/** The engine's own one-line use of a D-number chart (`VargaType.meaning`). */
|
||||
export function vargaMeaning(division: number): string | null {
|
||||
|
||||
@@ -203,6 +203,19 @@ export function toNorthIndianChart(block: ReportChartBlock, language: ReportLang
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ids of the valid chart fences a report body draws. The reader page and the
|
||||
* export both skip a structured chart whose id is already here (BUG-1203).
|
||||
*/
|
||||
export function fencedReportChartIds(markdown: string): Set<string> {
|
||||
const ids = new Set<string>();
|
||||
for (const match of markdown.matchAll(/```jyotish-chart\r?\n([\s\S]*?)```/g)) {
|
||||
const block = parseReportChartBlock(match[1]);
|
||||
if (block) ids.add(block.id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
export function stripReportChartBlocks(markdown: string): string {
|
||||
const stripped = markdown.replace(FENCE_RE, "\n\n");
|
||||
return stripped.replace(/\n{3,}/g, "\n\n");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReportDocumentV2 } from "./personal-report-contract";
|
||||
import { reportChartExportSvg } from "./report-chart-export";
|
||||
import { buildLongformOutline, personalReportMarkdownFilename } from "./personal-report-longform-outline";
|
||||
import { chartAriaLabel, parseReportChartBlock, stripReportChartBlocks, toNorthIndianChart, type ReportChartBlock } from "./report-chart-block";
|
||||
import { chartAriaLabel, fencedReportChartIds, parseReportChartBlock, stripReportChartBlocks, toNorthIndianChart, type ReportChartBlock } from "./report-chart-block";
|
||||
import type { ReportFactTable } from "./report-fact-table-schema";
|
||||
import { factCellText, factStatusLabel } from "./report-fact-table-display";
|
||||
import type { ReportLanguage } from "./report-language";
|
||||
@@ -71,12 +71,11 @@ export function buildReportExportBlocks({ markdown, charts = [], factTables = []
|
||||
for (const section of outline.sections) {
|
||||
add(`body:${section.id}`, "body", section.title, stripReportChartBlocks(section.sourceMarkdown));
|
||||
}
|
||||
const chartIds = new Set<string>();
|
||||
const chartIds = fencedReportChartIds(projected);
|
||||
for (const match of projected.matchAll(/```jyotish-chart\r?\n([\s\S]*?)```/g)) {
|
||||
const block = parseReportChartBlock(match[1]);
|
||||
if (!block) continue;
|
||||
const occurrence = blocks.filter(item => item.group === "chart" && item.id.startsWith(`chart:${block.id}:`)).length + 1;
|
||||
chartIds.add(block.id);
|
||||
// Only the immediately preceding SVG belongs to this occurrence (D9 may repeat).
|
||||
// The public projection has already rejected unsafe SVG; never recover raw HTML.
|
||||
const preceding = projected.slice(0, match.index);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"_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.",
|
||||
"_comment": "Engine tables the chart page reads. vargaMeaning = divisional_charts_extended.VargaType meaning; dignityLabels = jyotish_engine.DIGNITY_LABELS (the D1 status wording). Pinned by tests/test_chart_page_vedic_tables_contract.py.",
|
||||
"vargaMeaning": {
|
||||
"1": "本命盘",
|
||||
"2": "财富",
|
||||
@@ -22,40 +22,16 @@
|
||||
"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"
|
||||
"dignityLabels": {
|
||||
"EXALTED": "入旺(Exalted)",
|
||||
"MOOLATRIKONA": "本垣(Moolatrikona)",
|
||||
"OWN_SIGN": "入庙(Own Sign)",
|
||||
"GREAT_FRIEND": "极友(Great Friend)",
|
||||
"FRIEND": "入友(Friendly Sign)",
|
||||
"NEUTRAL": "中性(Neutral)",
|
||||
"ENEMY": "入敌(Enemy Sign)",
|
||||
"GREAT_ENEMY": "极敌(Great Enemy)",
|
||||
"DEBILITATED": "落陷(Debilitated)",
|
||||
"NEECHA_BHANGA": "落陷取消(Neecha Bhanga)"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user