fix(report): draw North Indian charts from fences after skipHtml dropped SVG
Longform report pages kept the D1/D9/Moon and varga headings but skipHtml stripped the engine's inline SVG. Emit a jyotish-chart JSON fence beside each SVG and render a diamond chart on the reader without relaxing HTML sanitization. BUG-607. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
"""Structured chart fences next to each engine SVG in longform Markdown."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user