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:
Jesse_Chen
2026-09-09 15:11:00 +08:00
co-authored by Cursor
parent d5972ef44a
commit 2fdcb14fd6
23 changed files with 2628 additions and 124 deletions
+197
View File
@@ -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