Consultation was passing the birth mahadasha into event-class splits, so marriage hits stayed empty and timing still said 缔结. Read live MD/AD from dasha_sub_periods, derive activation copy from the split, and isolate fence failures so one bad varga cannot 500 the whole report. Co-authored-by: Cursor <cursoragent@cursor.com>
210 lines
6.5 KiB
Python
210 lines
6.5 KiB
Python
"""Structured chart fences next to each engine SVG in longform Markdown."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import json
|
|
from functools import lru_cache
|
|
from types import SimpleNamespace
|
|
|
|
from report_chart_block import SIGNS, extract_chart_blocks
|
|
from scripts.jyotish_engine import (
|
|
SIGNS as ENGINE_SIGNS,
|
|
_compute_chart_from_args,
|
|
build_professional_report_reference_packet,
|
|
cmd_varga_full,
|
|
render_pl9_markdown,
|
|
)
|
|
|
|
PLACE = "FictionalHarborTownship"
|
|
PLANET_ORDER = (
|
|
"Sun",
|
|
"Moon",
|
|
"Mars",
|
|
"Mercury",
|
|
"Jupiter",
|
|
"Venus",
|
|
"Saturn",
|
|
"Rahu",
|
|
"Ketu",
|
|
)
|
|
|
|
|
|
def _fictional_args() -> SimpleNamespace:
|
|
return SimpleNamespace(
|
|
year=1990,
|
|
month=6,
|
|
day=15,
|
|
hour=12,
|
|
minute=0,
|
|
second=0,
|
|
lat=39.9042,
|
|
lon=116.4074,
|
|
tz=8.0,
|
|
ayanamsa="raman",
|
|
node_mode="mean",
|
|
divisions=None,
|
|
custom=None,
|
|
composite=None,
|
|
variant=None,
|
|
today="2026-09-09",
|
|
target_year=2026,
|
|
age=36,
|
|
visual_chart_observations=None,
|
|
startrack_language_bridge=False,
|
|
birth_time_accuracy="confirmed",
|
|
position_mode="legacy",
|
|
pack=[],
|
|
packs=None,
|
|
)
|
|
|
|
|
|
def _moon_chart(planets: dict) -> dict:
|
|
longitudes: dict[str, float] = {}
|
|
for name, row in planets.items():
|
|
if not isinstance(row, dict):
|
|
continue
|
|
value = row.get("degree_raw", row.get("degree"))
|
|
if isinstance(value, (int, float)):
|
|
longitudes[name] = float(value)
|
|
moon_lon = longitudes["Moon"] % 360
|
|
moon_sign_idx = int(moon_lon // 30) % 12
|
|
return {
|
|
"status": "computed",
|
|
"raw": {
|
|
"Ascendant": {
|
|
"sign": ENGINE_SIGNS[moon_sign_idx],
|
|
"longitude": round(moon_lon, 6),
|
|
"reference": "Moon",
|
|
},
|
|
"planets": {
|
|
name: {
|
|
"longitude": round(lon % 360, 6),
|
|
"sign": ENGINE_SIGNS[int(lon % 360 // 30) % 12],
|
|
"house_from_moon": ((int(lon % 360 // 30) - moon_sign_idx) % 12) + 1,
|
|
}
|
|
for name, lon in longitudes.items()
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _golden() -> dict:
|
|
args = _fictional_args()
|
|
chart, _asc_idx, _jd, _ayanamsa = _compute_chart_from_args(args)
|
|
assert isinstance(chart, dict)
|
|
varga_full = cmd_varga_full(args)
|
|
full_reading = {
|
|
"version": "4.4.0-full-reading",
|
|
"birth_info": {
|
|
"date": "1990-06-15",
|
|
"time": "12:00:00",
|
|
"hour": 12,
|
|
"minute": 0,
|
|
"second": 0,
|
|
"lat": args.lat,
|
|
"lon": args.lon,
|
|
"tz": "UTC+8.0",
|
|
"place": PLACE,
|
|
},
|
|
"ascendant": chart.get("ascendant"),
|
|
"planets": chart.get("planets"),
|
|
"houses": chart.get("houses"),
|
|
"chart": chart,
|
|
"modules": {
|
|
"varga_full": varga_full,
|
|
"moon_chart": _moon_chart(chart.get("planets") or {}),
|
|
},
|
|
"errors": [],
|
|
"warnings": [],
|
|
}
|
|
packet = build_professional_report_reference_packet(full_reading, args)
|
|
markdown = render_pl9_markdown(packet)
|
|
return {"packet": packet, "markdown": markdown}
|
|
|
|
|
|
def _svg_after_heading(markdown: str, heading: str) -> str:
|
|
rest = markdown[markdown.index(heading):]
|
|
start = rest.index("<svg")
|
|
end = rest.index("</svg>") + len("</svg>")
|
|
return rest[start:end]
|
|
|
|
|
|
def test_fence_count_matches_svg_and_is_twenty_two() -> None:
|
|
markdown = _golden()["markdown"]
|
|
assert markdown.count("<svg") == 22
|
|
assert markdown.count("```jyotish-chart") == 22
|
|
assert len(extract_chart_blocks(markdown)) == 22
|
|
|
|
|
|
def test_each_block_is_parseable_and_in_range() -> None:
|
|
blocks = extract_chart_blocks(_golden()["markdown"])
|
|
assert len(blocks) == 22
|
|
for block in blocks:
|
|
assert block["version"] == 1
|
|
assert block["layout"] == "north"
|
|
assert block["id"] == "MOON" or block["id"].startswith("D")
|
|
assert block["ascendant"]["sign"] in SIGNS
|
|
assert 0 <= block["ascendant"]["degree"] < 30
|
|
assert len(block["planets"]) <= 9
|
|
for planet in block["planets"]:
|
|
assert planet["name"] in PLANET_ORDER
|
|
assert planet["sign"] in SIGNS
|
|
assert 0 <= planet["degree"] < 30
|
|
assert isinstance(planet["retrograde"], bool)
|
|
|
|
|
|
def test_d1_ascendant_matches_core_chart() -> None:
|
|
golden = _golden()
|
|
core = golden["packet"]["core_chart"]
|
|
d1 = next(block for block in extract_chart_blocks(golden["markdown"]) if block["id"] == "D1")
|
|
assert d1["ascendant"]["sign"] == core["ascendant"]["sign"]
|
|
natal = core["planets"]
|
|
for planet in d1["planets"]:
|
|
row = natal.get(planet["name"]) or {}
|
|
assert planet["retrograde"] is bool(row.get("retrograde"))
|
|
|
|
|
|
def test_every_block_uses_natal_retrograde() -> None:
|
|
golden = _golden()
|
|
natal = golden["packet"]["core_chart"]["planets"]
|
|
for block in extract_chart_blocks(golden["markdown"]):
|
|
for planet in block["planets"]:
|
|
row = natal.get(planet["name"]) or {}
|
|
assert planet["retrograde"] is bool(row.get("retrograde"))
|
|
|
|
|
|
def test_chart_blocks_omit_birthplace_label() -> None:
|
|
for block in extract_chart_blocks(_golden()["markdown"]):
|
|
dumped = json.dumps(block, ensure_ascii=False)
|
|
assert PLACE not in dumped
|
|
assert PLACE not in block["title"]
|
|
|
|
|
|
def test_d9_svg_center_label_follows_title() -> None:
|
|
markdown = _golden()["markdown"]
|
|
d9_svg = _svg_after_heading(markdown, "#### D9 — Navamsa(婚盘)")
|
|
assert "(D9)" in d9_svg
|
|
assert "(D1)" not in d9_svg
|
|
d1_svg = _svg_after_heading(markdown, "#### D1 — Rashi Chart(本命盘)")
|
|
assert "(D1)" in d1_svg
|
|
assert "Rasi Chart" in d1_svg
|
|
|
|
|
|
def test_chart_block_suite_is_on_the_quick_quality_gate() -> None:
|
|
from scripts.run_quality_gate import CORE_PYTEST_TARGETS
|
|
|
|
assert "tests/test_report_chart_block.py" in CORE_PYTEST_TARGETS
|
|
|
|
|
|
def test_chinese_ascendant_sign_keeps_svg_and_drops_fence() -> None:
|
|
packet = copy.deepcopy(_golden()["packet"])
|
|
packet["core_chart"]["ascendant"]["sign"] = "白羊"
|
|
markdown = render_pl9_markdown(packet)
|
|
start = markdown.index("#### D1 — Rashi Chart(本命盘)")
|
|
end = markdown.index("#### D9 — Navamsa(婚盘)")
|
|
section = markdown[start:end]
|
|
assert "<svg" in section
|
|
assert "```jyotish-chart" not in section
|