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
@@ -3413,6 +3413,30 @@ def test_varga_full_endpoint_returns_extended_standard_divisions() -> None:
|
||||
assert result['result']['D81_Navamsa-Navamsa']['planets']['Moon']['house'] in range(1, 13)
|
||||
|
||||
|
||||
def test_varga_full_standard_divisions_carry_the_vimsopaka_dignity_level() -> None:
|
||||
"""BUG-1202: the chart page shows this level; it is the Vimsopaka function's."""
|
||||
import jyotish_engine
|
||||
from varga import calc_all_vargas
|
||||
|
||||
handler = _handler()
|
||||
planets = _sample_planets()
|
||||
result = handler._compute_varga_full({
|
||||
'planets': planets,
|
||||
'ascendant': {'lon': 92.0},
|
||||
'divisions': ['D9', 'D10'],
|
||||
})
|
||||
d9 = result['result']['D9_Navamsa']['planets']
|
||||
assert {name: row['dignity_level'] for name, row in d9.items()} == {
|
||||
name: jyotish_engine.varga_dignity_level(name, d9) for name in d9
|
||||
}
|
||||
assert all(row['dignity_level'] in jyotish_engine.DIGNITY_LABELS for row in d9.values())
|
||||
# The Vimsopaka path reads the same positions from varga.calc_all_vargas.
|
||||
vimsopaka_d9 = calc_all_vargas({name: row['lon'] for name, row in planets.items()}, 92.0, None)['D9_Navamsa']
|
||||
for name, row in d9.items():
|
||||
assert vimsopaka_d9[name]['sign'] == row['sign'], name
|
||||
assert jyotish_engine.varga_dignity_level(name, vimsopaka_d9) == row['dignity_level'], name
|
||||
|
||||
|
||||
def test_varga_full_endpoint_supports_custom_and_composite_modes() -> None:
|
||||
handler = _handler()
|
||||
custom = handler._compute_varga_full({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""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`.
|
||||
`frontend/src/lib/vedic-chart-tables.json` is read by the chart page: varga
|
||||
meanings (`divisional_charts_extended.VargaType`) and the dignity wording
|
||||
(`jyotish_engine.DIGNITY_LABELS`). The dignity level itself comes from
|
||||
`/api/varga_full` (`varga_dignity_level`, BUG-1202); the page never derives it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,33 +27,11 @@ TABLES = json.loads((ROOT / "frontend/src/lib/vedic-chart-tables.json").read_tex
|
||||
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_dignity_labels_equal_engine_status_wording() -> None:
|
||||
# BUG-1202: divisional rows carry the engine's level and the page labels it
|
||||
# with the D1 status wording; the three-level page table is retired.
|
||||
assert TABLES["dignityLabels"] == jyotish_engine.DIGNITY_LABELS
|
||||
assert {"signLords", "exaltation", "debilitation", "nodeOwnSign"}.isdisjoint(TABLES)
|
||||
|
||||
|
||||
def test_varga_meanings_equal_engine_varga_types() -> None:
|
||||
@@ -131,3 +109,17 @@ def test_d9_projected_ascendant_matches_native_summary(d9_projection: tuple[dict
|
||||
assert projected["ascendant"]["sign"] == native["lagna"]
|
||||
assert projected["ascendant"]["natalSign"] == golden["chart"]["ascendant"]["sign"]
|
||||
assert projected["ascendant"]["vargottama"] is native["lagna_vargottama"]
|
||||
|
||||
|
||||
def test_d9_projected_dignity_is_the_engine_level(d9_projection: tuple[dict, dict]) -> None:
|
||||
golden, projected = d9_projection
|
||||
d9_planets = golden["varga_full"]["result"]["D9_Navamsa"]["planets"]
|
||||
levels = set()
|
||||
for row in projected["rows"]:
|
||||
expected = jyotish_engine.varga_dignity_level(row["name"], d9_planets)
|
||||
assert expected is not None
|
||||
assert row["dignity"] == expected == d9_planets[row["name"]]["dignity_level"]
|
||||
levels.add(expected)
|
||||
# The golden must show that friendship levels reach the page, not only
|
||||
# exalted / own / debilitated.
|
||||
assert {"OWN_SIGN", "DEBILITATED", "NEUTRAL", "ENEMY", "FRIEND"} <= levels
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Blank report and chart columns (TASK-report-chart-blank-columns-20261003).
|
||||
|
||||
BUG-1199 Kranti column with no producer; BUG-1200 Ashtakavarga house table
|
||||
labelled the with-Lagna total as SAV (split in the reader: ashtakavarga.py is
|
||||
in the frozen rectification identity and stays byte-identical); BUG-1201 ascendant without nakshatra;
|
||||
BUG-1203 the divisional-chart caveat moved to the chart section. Every chart
|
||||
here is fictional.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.ashtakavarga import BAV_TOTALS, EXPECTED_SAV_TOTAL, SIGNS
|
||||
from scripts.jyotish_engine import NAKSHATRA_LIST, _nakshatra_fields, compute_chart_data
|
||||
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
|
||||
from tests.test_report_english_edition import CASES, _packet
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
CAVEAT_ZH = "分盘对出生时间敏感,原始计算供核对,不单独增加结论的确定性。"
|
||||
|
||||
|
||||
def _independent_nakshatra(lon: float) -> tuple[str, int, str]:
|
||||
minutes = (lon % 360) * 60
|
||||
index = int(minutes // 800) # 13°20′ = 800′
|
||||
pada = int((minutes % 800) // 200) + 1 # 3°20′ = 200′
|
||||
name, lord, _ = NAKSHATRA_LIST[index]
|
||||
return name, pada, lord
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lon,expected", [
|
||||
(0.0, ("Ashwini", 1, "Ketu")),
|
||||
(13.33, ("Ashwini", 4, "Ketu")),
|
||||
(13.34, ("Bharani", 1, "Venus")),
|
||||
(134.53, ("Purva Phalguni", 1, "Venus")),
|
||||
(149.3994, ("Uttara Phalguni", 1, "Sun")),
|
||||
(359.99, ("Revati", 4, "Mercury")),
|
||||
(360.0, ("Ashwini", 1, "Ketu")),
|
||||
])
|
||||
def test_nakshatra_fields_at_boundaries(lon, expected) -> None:
|
||||
fields = _nakshatra_fields(lon)
|
||||
assert (fields["nakshatra"], fields["nakshatra_pada"], fields["nakshatra_lord"]) == expected
|
||||
|
||||
|
||||
def test_chart_ascendant_carries_its_nakshatra_and_planets_are_unchanged() -> None:
|
||||
golden = json.loads((ROOT / "frontend/tests/fixtures/chart-view-golden.json").read_text(encoding="utf-8"))
|
||||
chart, *_ = compute_chart_data(1990, 6, 15, 12, 0, 39.9042, 116.4074, 8.0, ayanamsa_name="raman")
|
||||
asc = chart["ascendant"]
|
||||
assert (asc["nakshatra"], asc["nakshatra_pada"], asc["nakshatra_lord"]) == _independent_nakshatra(asc["lon"])
|
||||
assert {k: asc[k] for k in ("nakshatra", "nakshatra_pada", "nakshatra_lord")} == {
|
||||
k: golden["chart"]["ascendant"][k] for k in ("nakshatra", "nakshatra_pada", "nakshatra_lord")
|
||||
}
|
||||
for name, planet in chart["planets"].items():
|
||||
stored = golden["chart"]["planets"][name]
|
||||
assert (planet["nakshatra"], planet["nakshatra_pada"], planet["nakshatra_lord"]) == (
|
||||
stored["nakshatra"], stored["nakshatra_pada"], stored["nakshatra_lord"]), name
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def editions() -> tuple[str, str, dict]:
|
||||
packet = _packet(CASES["day"])
|
||||
zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main")
|
||||
english = copy.deepcopy(packet)
|
||||
english["report_language"] = "en"
|
||||
return zh, _pl9_export_markdown_for_edition(english, "reader_main"), packet
|
||||
|
||||
|
||||
def _table_after(markdown: str, heading: str) -> list[list[str]]:
|
||||
lines = markdown.splitlines()
|
||||
start = next(i for i, line in enumerate(lines) if line.startswith("### ") and heading in line)
|
||||
rows = []
|
||||
for line in lines[start + 1:]:
|
||||
if line.startswith("### ") or (rows and not line.startswith("|")):
|
||||
break
|
||||
if line.startswith("|") and not set(line) <= set("|- "):
|
||||
rows.append([cell.strip() for cell in line.strip("|").split("|")])
|
||||
return rows
|
||||
|
||||
|
||||
def test_declination_table_has_no_kranti_column(editions) -> None:
|
||||
for markdown, heading in ((editions[0], "Declination / Speed"), (editions[1], "Declination / Speed")):
|
||||
assert "Kranti" not in markdown
|
||||
rows = _table_after(markdown, heading)
|
||||
assert rows[0] == ["Planet", "Degree", "Declination", "Speed"]
|
||||
assert len(rows) == 8 and all(len(row) == 4 and "-" not in row for row in rows)
|
||||
|
||||
|
||||
def test_ashtakavarga_house_table_shows_three_real_columns(editions) -> None:
|
||||
zh, en, packet = editions
|
||||
expected_sav = packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]["sav"]["scores"]
|
||||
for markdown, heading, header, total in (
|
||||
(zh, "Ashtakavarga 完整宫位分数", ["House", "Sign", "SAV", "上升 BAV", "SAV + 上升"], "合计"),
|
||||
(en, "Ashtakavarga Full House Scores", ["House", "Sign", "SAV", "Lagna BAV", "SAV + Lagna"], "Total"),
|
||||
):
|
||||
rows = _table_after(markdown, heading)
|
||||
assert rows[0] == header
|
||||
body, totals = rows[1:13], rows[13]
|
||||
for row in body:
|
||||
sav, lagna, full = (int(cell) for cell in row[2:])
|
||||
assert sav + lagna == full
|
||||
assert totals == [total, "-", str(EXPECTED_SAV_TOTAL), str(BAV_TOTALS["Lagna"]), str(EXPECTED_SAV_TOTAL + BAV_TOTALS["Lagna"])]
|
||||
# SAV equals the per-sign SAV table, Lagna BAV the Lagna bindus, and the last
|
||||
# column the engine's `house_scores_full` total that rectification reads.
|
||||
ashtakavarga = packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]
|
||||
full = {row["sign"]: row["sav_score"] for row in ashtakavarga["house_scores_full"].values()}
|
||||
for row in _table_after(en, "Ashtakavarga Full House Scores")[1:13]:
|
||||
assert int(row[2]) == expected_sav[row[1]]
|
||||
assert int(row[3]) == ashtakavarga["bav"]["Lagna"]["bindus"][SIGNS.index(row[1])]
|
||||
assert int(row[4]) == full[row[1]]
|
||||
|
||||
|
||||
def test_ashtakavarga_house_table_never_labels_the_total_as_sav(editions) -> None:
|
||||
packet = copy.deepcopy(editions[2])
|
||||
del packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]["bav"]["Lagna"]
|
||||
rows = _table_after(_pl9_export_markdown_for_edition(packet, "reader_main"), "Ashtakavarga 完整宫位分数")
|
||||
assert len(rows) == 13, "no total row without the split"
|
||||
for row in rows[1:]:
|
||||
assert row[2:4] == ["-", "-"]
|
||||
assert row[4].isdigit()
|
||||
|
||||
|
||||
def test_chart_section_carries_the_divisional_caveat(editions) -> None:
|
||||
zh, en, _ = editions
|
||||
assert re.search(r"### 本命与分盘北印度图盘\n\n" + re.escape(CAVEAT_ZH) + r"\n", zh)
|
||||
assert re.search(r"### Natal and Divisional Charts \(North Indian\)\n\nDivisional charts are sensitive to birth time\.", en)
|
||||
Reference in New Issue
Block a user