fix(report): draw charts in the full-data body and clean Chinese residue

New reports draw one north-Indian chart for each ready divisional in the body, so the trailing duplicate section drops. Chinese full-data prose no longer glues English onto Han or leaves listed, background, or native. The birth table starts with the ascendant. The D9 same-sign column says yes or no.

Self-test: tsc exit 0; npm run lint exit 0 (0 errors, 125 existing warnings); three frontend test files 39 pass, 0 fail, 0 cancelled; pytest test_report_chart_blank_columns.py 16 passed; seven related report files 88 passed, exit 0; privacy markers 63 passed.

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-13 14:02:26 +08:00
co-authored by Grok (agent loop)
parent 05f8800531
commit a295afa40f
14 changed files with 3466 additions and 10 deletions
+188 -3
View File
@@ -111,7 +111,11 @@ def _pl9_report_language(packet: dict) -> str:
def _strip_pl9_user_engineering_markers(markdown: str) -> str:
"""Remove internal audit/profile vocabulary from user-facing report text."""
text = markdown
try:
from scripts.pl9_language_terms import translate_zh_glued_phrases
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
from pl9_language_terms import translate_zh_glued_phrases
text = translate_zh_glued_phrases(markdown)
replacements = {
"parameter_sensitive / unverified": "参考",
"parameter_sensitive": "参考",
@@ -481,6 +485,149 @@ def _render_pl9_user_markdown(packet: dict) -> str:
return cleaned
# Drawable charts in the parity "north Indian charts" section. Bhava (Sripati)
# has no chart id (its numbers stay in the Sripati table). Navamsha is not
# listed again: it is the same id as D9. English titles live in
# pl9_reader_english_terms, keyed by these Chinese labels.
_PARITY_CHARTS = (
("D1", "D1 本命盘", "d1"),
("MOON", "月亮盘", "moon"),
("D2", "D2 财富分盘", ("D2_Hora", "D2")),
("D3", "D3 手足分盘", ("D3_Drekkana", "D3")),
("D4", "D4 居所分盘", ("D4_Turyamsa", "D4_Chaturthamsa", "D4")),
("D5", "D5 声望分盘", ("D5_Panchamsa", "D5")),
("D6", "D6 压力分盘", ("D6_Shashthamsa", "D6")),
("D7", "D7 子女分盘", ("D7_Saptamsa", "D7")),
("D8", "D8 变化分盘", ("D8_Ashtamsa", "D8")),
("D9", "D9 九分盘", ("D9_Navamsa", "D9")),
("D10", "D10 事业分盘", ("D10_Dasamsa", "D10")),
("D11", "D11 收益分盘", ("D11_Rudramsa", "D11")),
("D12", "D12 父母分盘", ("D12_Dwadashamsa", "D12_Dvadasamsa", "D12")),
("D16", "D16 享受分盘", ("D16_Shodasamsa", "D16")),
("D20", "D20 灵修分盘", ("D20_Vimsamsa", "D20")),
("D24", "D24 学习分盘", ("D24_Siddhamsa", "D24_Chaturvimsamsa", "D24")),
("D27", "D27 内在分盘", ("D27_Bhamsa", "D27")),
("D30", "D30 困难分盘", ("D30_Trimsamsa", "D30")),
("D40", "D40 母系分盘", ("D40_Khavedamsa", "D40")),
("D45", "D45 父系分盘", ("D45_Akshavedamsa", "D45")),
("D60", "D60 深层分盘", ("D60_Shashtyamsa", "D60_Shashtiamsa", "D60")),
)
def _parity_chart_title(zh: str, language: str) -> str:
if language != "en":
return zh
try:
from scripts.pl9_reader_english_terms import PHRASES_EN
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
from pl9_reader_english_terms import PHRASES_EN
return dict(PHRASES_EN).get(zh, zh)
def _parity_chart_source(kind, core_chart, worksheets, divisional, varga_full):
if kind == "d1":
planets = core_chart.get("planets") if isinstance(core_chart.get("planets"), dict) else {}
ascendant = core_chart.get("ascendant") if isinstance(core_chart.get("ascendant"), dict) else {}
return planets, ascendant
if kind == "moon":
d1_sheet = worksheets.get("d1_rasi_bhava") if isinstance(worksheets.get("d1_rasi_bhava"), dict) else {}
moon = d1_sheet.get("moon_chart") if isinstance(d1_sheet.get("moon_chart"), dict) else {}
if not moon and isinstance(divisional.get("moon_chart"), dict):
moon = divisional["moon_chart"]
raw = moon.get("raw") if isinstance(moon.get("raw"), dict) else {}
if not raw and isinstance(moon.get("data"), dict):
raw = moon["data"]
if not raw:
raw = moon
if isinstance(raw.get("raw"), dict):
raw = raw["raw"]
ascendant = {}
for key in ("Ascendant", "ascendant", "Asc"):
if isinstance(raw.get(key), dict):
ascendant = raw[key]
break
planets = raw.get("planets") if isinstance(raw.get("planets"), dict) else {}
return planets, ascendant
chart = {}
for key in kind:
candidate = varga_full.get(key) if isinstance(varga_full, dict) else None
if isinstance(candidate, dict) and candidate:
chart = candidate
break
ascendant = {}
for key in ("Ascendant", "ascendant", "Asc"):
if isinstance(chart.get(key), dict):
ascendant = chart[key]
break
return chart, ascendant
def _append_parity_chart_fences(lines, *, language, core_chart, worksheets, divisional, varga_full) -> None:
"""One north-Indian fence per chart that has a sign to draw. No empty heading."""
try:
from scripts.report_chart_block import PLANET_ORDER, build_chart_block
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
from report_chart_block import PLANET_ORDER, build_chart_block
natal = core_chart.get("planets") if isinstance(core_chart.get("planets"), dict) else {}
retrograde = {
name: bool(row.get("retrograde")) if isinstance(row, dict) else False
for name in PLANET_ORDER
for row in [natal.get(name)]
}
seen: set[str] = set()
for chart_id, title_zh, kind in _PARITY_CHARTS:
if chart_id in seen:
continue
positions, ascendant = _parity_chart_source(kind, core_chart, worksheets, divisional, varga_full)
title = _parity_chart_title(title_zh, language)
try:
block = build_chart_block(chart_id, title, positions, ascendant, retrograde)
except (TypeError, ValueError):
continue
seen.add(chart_id)
lines.extend([f"##### {title}", "", block, ""])
def _ascendant_report_fields(asc: dict) -> dict:
"""Longitude, nakshatra and lords for the report's ascendant row. No new dignity."""
longitude = None
for key in ("lon", "longitude", "degree_raw"):
value = asc.get(key)
if isinstance(value, (int, float)):
longitude = float(value) % 360.0
break
if longitude is None:
sign = asc.get("sign")
degree = asc.get("degree_in_sign")
if not isinstance(degree, (int, float)):
degree = asc.get("degree")
signs = globals().get("SIGNS") or []
if sign in signs and isinstance(degree, (int, float)):
longitude = signs.index(sign) * 30.0 + float(degree)
fields = {}
if longitude is not None:
try:
from scripts.jyotish_engine import _nakshatra_fields
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
from jyotish_engine import _nakshatra_fields
fields = _nakshatra_fields(longitude)
degree = asc.get("degree_in_sign")
if not isinstance(degree, (int, float)):
degree = asc.get("degree")
pada = fields.get("nakshatra_pada") if fields else None
if pada is None:
pada = asc.get("nakshatra_pada") or asc.get("pada")
return {
"sign": asc.get("sign"),
"degree": degree,
"longitude": longitude,
"nakshatra": fields.get("nakshatra") or asc.get("nakshatra"),
"pada": pada,
"nakshatra_lord": fields.get("nakshatra_lord") or asc.get("nakshatra_lord"),
"sign_lord": asc.get("lord") or asc.get("rasi_lord"),
}
def render_pl9_parity_markdown(packet: dict) -> str:
"""Render only PL9 page-family material admitted to the parity main volume."""
_bind_engine_symbols()
@@ -544,6 +691,18 @@ def render_pl9_parity_markdown(packet: dict) -> str:
elif isinstance(value, (list, tuple)):
value = ';'.join(str(item) for item in value if item not in (None, '', [], {}))
text = str(value if value not in (None, '') else '-').replace('\n', ' ').strip()
if _lang != 'en':
# Whole status tokens. Longer phrases are handled in the glossary
# before these short words, so a half-translated fragment is not
# cleaned up by deleting letters here.
text = {
'active': '当前运行',
'background': '背景',
'listed': '已列出',
'native': '本命',
'Custom condition met': '满足自定义条件',
'custom condition satisfied': '满足自定义条件',
}.get(text, text)
return text.replace('|', '/')
def _brief(items, limit=2) -> str:
@@ -668,6 +827,10 @@ def render_pl9_parity_markdown(packet: dict) -> str:
def _public_status_note(value) -> str:
zh_mapping = {
'partial_verified': '已生成',
'listed': '已列出',
'active': '当前运行',
'background': '背景',
'native': '本命',
'parameter_sensitive': '参数敏感',
'blocked': '需补资料',
'verified': '已核验',
@@ -856,6 +1019,14 @@ def render_pl9_parity_markdown(packet: dict) -> str:
'| Planet | R/C | Sign | Degree | Longitude | House | Nakshatra | Pada | Speed |',
'|--------|-----|------|--------|-----------|-------|-----------|------|-------|',
])
ascendant = _dict(core_chart.get('ascendant'))
if ascendant:
info = _ascendant_report_fields(ascendant)
asc_label = 'Ascendant' if _lang == 'en' else '上升'
lines.append(
f"| {_cell(asc_label)} | - | {_cell(_token(info['sign']))} | {_cell(_degree(info['degree']))} | "
f"{_cell(_degree(info['longitude']))} | 1 | {_cell(info['nakshatra'])} | {_cell(info['pada'])} | - |"
)
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'):
row = _dict(planets.get(planet))
if not row:
@@ -875,6 +1046,14 @@ def render_pl9_parity_markdown(packet: dict) -> str:
'| 行星 | 星座主 | 星宿主 | 分主 | 分分主 | 尊贵状态 | 力量比 |',
'|------|--------|--------|------|--------|----------|--------|',
])
if ascendant:
info = _ascendant_report_fields(ascendant)
asc_label = 'Ascendant' if _lang == 'en' else '上升'
dignity = 'not applicable' if _lang == 'en' else '不适用'
lines.append(
f"| {_cell(asc_label)} | {_cell(_token(info['sign_lord']))} | {_cell(_token(info['nakshatra_lord']))} | "
f"- | - | {_cell(dignity)} | - |"
)
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'):
row = _dict(planets.get(planet))
if not row:
@@ -1078,8 +1257,14 @@ def render_pl9_parity_markdown(packet: dict) -> str:
ready_natal_specs = [spec for spec in natal_specs if spec[2]]
if ready_natal_specs:
lines.extend(['### p6-p17 本命与分盘北印度图盘', '', '分盘对出生时间敏感,原始计算供核对,不单独增加结论的确定性。', ''])
for page, label, _ready in ready_natal_specs:
lines.extend([f'##### p{page} · {label}', ''])
_append_parity_chart_fences(
lines,
language=_lang,
core_chart=core_chart,
worksheets=worksheets,
divisional=divisional,
varga_full=varga_full,
)
# The original PL9 charts are visual, but their sign placements are
# still part of the visible evidence. Keep the chart drawing and add
# a compact numeric transcription so an AI reader can recover the