Files
Jyotisha/scripts/pl9_reader_export.py
T
jesse-ux 2a1f5f0fc9 fix(report): use the upstream reader edition for new longform bodies
New reports request reader_main from upstream origin/main 23b9609e.
KP and transit no longer copy an empty vars() dict, solar returns keep
birth_asc_sign_idx, and ordinary projection deletes internal lines whole.
2026-09-25 02:19:32 +08:00

2505 lines
128 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""User-facing PL9 reader export.
Copied from upstream yinduzhanxing ``origin/main`` ``23b9609e330dbee70a4725bdc222b125d9b0e39f``
(reader edition introduced by ``f3b5196fecb23b4c71d60301bf424c697cb0e8cb``).
The task book expected tip ``19195f6e``; fetch showed ``23b9609e``, which contains that commit.
``f3b5196f..23b9609e`` reader closure lives here so ``scripts/jyotish_engine.py`` is not
rewritten in place (concession 1). Algorithm text is unchanged except ``_bind_engine_symbols``:
``render_pl9_parity_markdown`` reads ``SIGNS`` / ``SIGNS_CN`` through ``globals()``, which only
works in the engine module.
Upstream ``_pl9_export_markdown_for_edition`` also serves ``pl9_ai_density``. That branch needs
``_render_pl9_ai_density_raw_data_markdown_en`` and the English density sanitizers, which are
outside the ``reader_main`` closure and are not ported. ``reader_main`` and the reference
default match the upstream branches.
"""
from __future__ import annotations
import re
SIGNS: list[str] = []
SIGNS_CN: dict[str, str] = {}
def _bind_engine_symbols() -> None:
"""Point sign tables at the live engine module without importing it at load time."""
global SIGNS, SIGNS_CN
try:
from scripts import jyotish_engine as engine
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
import jyotish_engine as engine
SIGNS = list(engine.SIGNS)
SIGNS_CN = dict(engine.SIGNS_CN)
def _reference_markdown(packet: dict) -> str:
try:
from scripts import jyotish_engine as engine
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
import jyotish_engine as engine
return engine.render_pl9_markdown(packet)
def _pl9_export_markdown_for_edition(packet: dict, edition: str) -> str:
"""Dispatch reader_main vs the reference renderer.
Upstream ``23b9609e`` ``scripts/jyotish_engine.py`` lines 13099-13145.
``pl9_ai_density`` is not ported; see the module docstring.
"""
_bind_engine_symbols()
normalized = str(edition or "").strip()
if normalized == "reader_main":
return _render_pl9_user_markdown(packet)
if normalized in {"pl9_parity", "pl9_fixed_204"}:
return render_pl9_parity_markdown(packet)
return _reference_markdown(packet)
def _pl9_public_report_title(packet: dict) -> str:
"""Return the user-facing report title without edition or parity labels."""
birth = packet.get('birth_info') if isinstance(packet, dict) else {}
if not isinstance(birth, dict):
birth = {}
name = str(birth.get('name') or birth.get('user_name') or '').strip()
return f"{name}的印度占星星盘报告" if name else "印度占星星盘报告"
def _pl9_english_report_title(packet: dict) -> str:
"""Return the English user-facing report title."""
birth = packet.get('birth_info') if isinstance(packet, dict) else {}
if not isinstance(birth, dict):
birth = {}
name = str(birth.get('name') or birth.get('user_name') or '').strip()
return f"{name}'s Vedic Astrology Chart Report" if name else "Vedic Astrology Chart Report"
def _pl9_report_language(packet: dict) -> str:
language = str((packet or {}).get('report_language') or '').strip().lower()
return language if language in {'zh', 'en'} else 'zh'
def _strip_pl9_user_engineering_markers(markdown: str) -> str:
"""Remove internal audit/profile vocabulary from user-facing report text."""
text = markdown
replacements = {
"parameter_sensitive / unverified": "参考",
"parameter_sensitive": "参考",
"partial_verified": "已列出",
"pyjhora_behavior_only / not_multiengine_parity": "参考",
"pyjhora_behavior_only": "参考",
"restricted_or_unclosed": "参考",
"not_applicable": "未列",
"blocked_and_conflict_fields_cannot_generate_final_predictions": "仅列资料",
"blocked": "未列",
"profile_sensitive": "参考",
"producer": "计算项",
"Producer": "计算项",
"source_path": "来源",
"schema": "结构",
"audit": "核对",
"status": "状态",
"profile": "计算设置",
"Profile": "计算设置",
"Status": "状态",
"private display": "显示口径",
"exact parity": "精确对照",
"PL9-compatible p51-p53 source-bound 口径": "本报告采用的 Ashtakavarga 口径",
"PL9-compatible": "本报告采用",
"source-bound": "已核对",
"type=partial": "type=局部",
"=partial": "=局部",
"PL9 对标主册": "印度占星星盘报告",
"PL9 风格专业排盘导出": "印度占星星盘报告",
"Applicable Benefic Yogas": "可用吉性瑜伽",
"Applicable Mixed Yogas": "可用混合型瑜伽",
"Applicable Malefic Yogas": "可用凶性瑜伽",
"Applicable Benefic / Mixed / Malefic Yogas": "可用瑜伽总表",
"Applicable Yogas": "可用瑜伽",
"Tajika Yoga Presence 对照表": "Tajika 瑜伽呈现对照表",
"Tajika Yoga Presence 表": "Tajika 瑜伽呈现表",
"Tajika Presence": "Tajika 呈现",
"Yoga detected count": "检出的瑜伽数量",
"Context layers": "参考层",
"| Yoga | Category | Strength | Combination | Effects / notes |": "| 瑜伽 | 类别 | 强度 | 组合条件 | 作用说明 |",
"|------|----------|----------|-------------|-----------------|": "|------|------|------|----------|----------|",
"Tajika Strength": "Tajika 力量",
"Strength": "强度",
"annual_tajika_pack": "年度结构",
"kp_monthly_report_packet": "月度重点",
"dispositor": "定位星",
"functional_role_not_returned": "未单独列出",
"未返回": "未列",
}
for raw, public in replacements.items():
text = text.replace(raw, public)
nakshatra_terms = {
"Ashwini": "阿湿毗尼(Ashwini)",
"Bharani": "巴拉尼(Bharani)",
"Krittika": "基利提卡(Krittika)",
"Rohini": "罗希尼(Rohini)",
"Mrigashira": "鹿首(Mrigashira)",
"Ardra": "阿尔德拉(Ardra)",
"Punarvasu": "普那婆苏(Punarvasu)",
"Pushya": "普沙(Pushya)",
"Ashlesha": "阿什列沙(Ashlesha)",
"Magha": "摩伽(Magha)",
"Purva Phalguni": "前破伽(Purva Phalguni)",
"Uttara Phalguni": "后破伽(Uttara Phalguni)",
"Hasta": "哈斯塔(Hasta)",
"Chitra": "吉多罗(Chitra)",
"Swati": "斯瓦蒂(Swati)",
"Vishakha": "毗舍佉(Vishakha)",
"Anuradha": "阿奴罗陀(Anuradha)",
"Jyeshtha": "杰耶什塔(Jyeshtha)",
"Mula": "Mula(根、本源)",
"Purva Ashadha": "前阿沙陀(Purva Ashadha)",
"Uttara Ashadha": "后阿沙陀(Uttara Ashadha)",
"Shravana": "室罗伐那(Shravana)",
"Dhanishta": "陀尼湿陀(Dhanishta)",
"Shatabhisha": "百药宿(Shatabhisha)",
"Purva Bhadrapada": "前跋陀罗(Purva Bhadrapada)",
"Uttara Bhadrapada": "后跋陀罗(Uttara Bhadrapada)",
"Revati": "雷瓦蒂(Revati)",
}
for raw, public in sorted(nakshatra_terms.items(), key=lambda item: len(item[0]), reverse=True):
text = re.sub(rf"(?<![((A-Za-z]){re.escape(raw)}(?![A-Za-z))])", public, text)
glossary_terms = {
"Rahu (North Node)": "北交点/罗睺",
"Ketu (South Node)": "南交点/计都",
"Great Friend": "极友",
"Friendly Sign": "友好星座",
"Own Sign": "入庙",
"Enemy Sign": "敌对星座",
"Debilitated": "落陷",
"Exalted": "入旺",
"Neutral": "中性",
"Paravatamsa 分盘等级": "Paravatamsa",
"Simhasanamsa 分盘等级": "Simhasanamsa",
"Paravatamsa": "Paravatamsa",
"Simhasanamsa": "Simhasanamsa",
"Shadbala": "六维力量",
"Vimsopaka": "二十分力量",
"Dasha": "大运",
"Nakshatra": "星宿",
"conjunction": "合相",
"special": "特殊组合",
"kalatra": "婚恋",
"solar_yoga": "太阳瑜伽",
"lunar_yoga": "月亮瑜伽",
"durbhaga": "不利组合",
"ari": "障碍组合",
"moderate": "中等",
"common": "常见",
"occasional": "偶见",
"raja": "王权/事业",
"dhana": "财富",
"putra": "子女/创造",
"ayur": "寿命/健康",
"auspicious": "吉性",
"nabhasa": "天象格局",
"surya": "太阳类",
"chandra": "月亮类",
"health": "健康",
"travel": "迁移/出行",
"asha": "愿望/收益",
"moksha": "解脱/内在",
"aryama": "贵人/助力",
"Sun": "太阳",
"Moon": "月亮",
"Mars": "火星",
"Mercury": "水星",
"Jupiter": "木星",
"Venus": "金星",
"Saturn": "土星",
"Rahu": "北交点/罗睺",
"Ketu": "南交点/计都",
"Aries": "白羊座",
"Taurus": "金牛座",
"Gemini": "双子座",
"Cancer": "巨蟹座",
"Leo": "狮子座",
"Virgo": "处女座",
"Libra": "天秤座",
"Scorpio": "天蝎座",
"Sagittarius": "射手座",
"Capricorn": "摩羯座",
"Aquarius": "水瓶座",
"Pisces": "双鱼座",
}
for raw, public in sorted(glossary_terms.items(), key=lambda item: len(item[0]), reverse=True):
text = re.sub(rf"(?<![A-Za-z]){re.escape(raw)}(?![A-Za-z])", public, text)
text = re.sub(r"\b[a-zA-Z_]+_profile\b", "计算口径", text)
text = re.sub(r"\b[a-zA-Z_]+_status\b", "状态", text)
text = re.sub(r"\b[a-zA-Z_]+_producer\b", "计算项", text)
text = re.sub(r"\b[a-zA-Z_]+_path\b", "来源", text)
text = re.sub(r"(?m)^(#{2,6}\s*)p\d+(?:-p\d+)?\s*[·.-]?\s*", r"\1", text)
text = re.sub(r"\s*\(PL9 p\d+(?:-p\d+)?[^)]*\)", "", text)
text = re.sub(r"\s*(PL9 p\d+(?:-p\d+)?[^)]*)", "", text)
natural_replacements = {
"这条链用于追踪该落点如何接回本命结构。": "定位星所在宫位会继续说明这颗星的表达方向。",
"星宿主与 Dasha 激活共同细化表达方式,不单独决定结果。": "星宿主会进一步修饰表达方式,并与 Dasha 节奏合看。",
"力量只用于调节该落点的承载度,不能覆盖宫主、星宿与时间层。": "力量信息用于判断表现强弱,并与宫主、星宿和 Dasha 节奏合看。",
"本页先作为本命结构背景保留。": "其主题以本命基础背景为主。",
"本页先保留为本命结构。": "其主题以本命基础背景为主。",
"当前上升口径": "当前上升",
"上升口径": "上升",
"活动 Dasha 主导层": "Dasha 主导层",
"活动 Dasha lord 集合": "Dasha 主导层",
"证据标签:": "说明:",
"年度 大运 表": "年度大运表",
"Annual 大运 阶段表": "年度大运阶段表",
"Annual 大运 结果解释": "年度大运结果解释",
"大运 节奏": "大运节奏",
"大运 主导层": "大运主导层",
"大运 lord": "大运主星",
"当前 大运": "当前大运",
"活动 大运": "活动大运",
"行星、星宿与宫位长页解释": "行星、星宿与宫位解释",
"True 普沙(Pushya)": "True Pushya",
"Excessive sensuality, libidinous nature": "感官欲望较强,关系中需要节制与稳定",
"Sharp intellect, good communication skills, government service": "思维敏捷,沟通能力较好,也可能与行政、制度或公共事务相关",
"Lean/thin body, frail constitution": "体型偏瘦,体质较敏感",
"Mental disturbance, confusion, erratic behavior": "思绪容易扰动,需留意情绪、压力与判断稳定性",
"Speech impediment, stammering, difficulty expressing": "表达受阻,可能表现为说话不顺、迟疑或难以清楚表达",
"Lives by agriculture, helpful, amasses wealth": "适合通过土地、资源、生产性事务或稳定积累获得成果,也有助人倾向",
"Dharidhra:": "贫困瑜伽:",
"Dharidhra Yoga": "贫困瑜伽",
"Lagna lord in a dry sign": "上升主落在干燥星座",
"7 visible planets occupy exactly 4 distinct signs": "七颗可见行星分布在四个星座中",
"Budha-Aditya conjunction": "水日合相",
"Budha-Aditya": "水日",
" Yoga(": "瑜伽(",
" Yoga": "瑜伽",
"{planets}": "行星",
"太阳 and 水星": "太阳与水星",
"金星 and 水星": "金星与水星",
"Lagna": "上升",
"Kendra": "角宫",
"Trikona": "三方宫",
"Trine": "三方宫",
"Navamsa": "九分盘",
"Maraka": "死亡/损耗宫",
"Dusthana": "凶宫",
"| Topic | Practice | Gem / count | Notes |": "| 主题 | 建议 | 宝石/次数 | 说明 |",
"|-------|----------|-------------|-------|": "|------|------|-----------|------|",
"Mangal Dosha": "火星婚姻煞",
"Kaal Sarp Dosha": "时蛇煞",
"Shanti Puja": "平息仪式",
"Kumbha Vivah": "水瓶婚仪式",
"Mangal Chandika Path": "火星/旃迪诵读",
"Red Coral": "红珊瑚",
"in the same house": "同宫",
"本页是基于本仓实际盘面字段生成的原创解释,": "",
"不复制 PL9 的行星落宫文案,也不以单颗行星断定具体事件。": "",
"Raman 核心子规则状态:support_only_candidate。": "",
"该页只落实宫位和宫主的字段绑定,不执行无权重合同的综合判断,不生成关系、健康、死亡或具体事件结论。": "这一宫位需要结合宫主位置、尊贵、相位、分盘和 Dasha 节奏阅读。",
"不能由单一宫主落点替代这些层。": "需要合并阅读。",
"不能覆盖": "需要结合",
"不单独决定结果": "需要合并阅读",
}
for raw, public in natural_replacements.items():
text = text.replace(raw, public)
cleanups = {
"Special 上升s and Points": "特殊上升与派生点",
"Special 上升s": "特殊上升",
"月亮 Chart": "月亮盘",
"上升 角宫di Rashi 大运": "Lagna Kendradi Rashi 大运",
"Ashtakavarga Shodhya /瑜伽 Pinda 原始值": "Ashtakavarga Shodhya / Yoga Pinda 原始值",
"Patyayini / Patyamsha ending-date 表": "Patyayini / Patyamsha 结束日期表",
"Results of the Dhayya of Sadhesati": "Sadhesati Dhayya 结果",
"Results of Laghu Kalyani Dhayya and Kantaka 土星": "Laghu Kalyani Dhayya 与 Kantaka Saturn 结果",
"### / p45-p47 相位矩阵": "### 相位矩阵",
}
for raw, public in cleanups.items():
text = text.replace(raw, public)
text = re.sub(r"Results of (.*?) as the Year Lord", r"年主\1的结果", text)
text = text.replace("Results of Muntha in the Sign of a Graha", "Muntha 落入行星星座的结果")
text = text.replace("Results of Munthesh", "Munthesh 结果")
text = text.replace("Results of Muntha", "Muntha 结果")
text = text.replace("field_状态", "字段状态")
text = text.replace("Dasha", "大运")
text = re.sub(r"(?<=[\u4e00-\u9fff])\s+大运", "大运", text)
text = re.sub(r"大运\s+(?=[\u4e00-\u9fff])", "大运", text)
text = re.sub(r"(太阳|月亮|火星|水星|木星|金星|土星|北交点/罗睺|南交点/计都)\1", r"\1", text)
return text
def _sanitize_pl9_public_markdown(markdown: str) -> str:
"""Hide engineering-only confidence/status tokens from the PL9 reader volume."""
replacements = {
"parameter_sensitive / unverified": "待核对",
"parameter_sensitive": "参考",
"partial_verified": "已列出",
"pyjhora_behavior_only / not_multiengine_parity": "参考",
"restricted_or_unclosed": "待核对",
"not_applicable": "未列",
"blocked": "未列",
"blocked_and_conflict_fields_cannot_generate_final_predictions": "仅列资料",
"profile": "口径",
"Profile": "口径",
}
text = markdown
for raw, public in replacements.items():
text = text.replace(raw, public)
return _strip_pl9_user_engineering_markers(text)
def _render_pl9_user_markdown(packet: dict) -> str:
"""Render the user-facing PL9-style main volume without parity/audit scaffolding."""
text = render_pl9_parity_markdown(packet)
title = _pl9_public_report_title(packet)
replacements = {
"# PL9 对标主册": f"# {title}",
"(PL9 p1-p17)": "",
"(PL9 p18-p29 对标)": "",
"(PL9 p30-p60 对标)": "",
"(PL9 p61-p120 对标)": "",
"(PL9 p121-p125 对标)": "",
"(PL9 p126-p155 对标)": "",
"(PL9 p156-p189 对标)": "",
"(PL9 p190-p204 对标)": "",
"PL9 p": "第",
"PL9": "经典印度占星",
"参数敏感": "需结合命盘整体阅读",
"外部行为参考": "参考规则",
"待补资料": "未单独列出",
"missing_in_local": "未单独列出",
"另参": "未单独列出",
"external_mudda_start_boundary": "参考起点",
"dasha_beginning_dates": "起始日期",
"dasha_ending_dates": "结束日期",
"生成模块": "来源",
"结构": "资料",
"校验": "核对",
"资料路径": "资料来源",
"候选承接面": "重点议题",
"不将单一组合写成确定事件": "用于说明阶段主题",
"{planets}": "相关行星",
"natural_benefic": "自然吉星",
"natural_malefic": "自然凶星",
"functional_benefic": "功能吉星",
"functional_malefic": "功能凶星",
"functional_neutral": "功能中性",
"functional_role_not_returned": "功能属性未单独返回",
"absolute Virupas; precise Sthana/Dig/Kala/Drik; bounded BPHS Chesta": "以 Virupa 为单位,综合 Sthana、Dig、Kala、Drik 与 BPHS Chesta",
"Chesta remains cross-engine method-conflicted; totals inherit that boundary.": "Chesta Bala 的算法口径会影响总分,阅读时以同一口径内比较为主。",
"Great Friend": "大友",
"Friendly Sign": "友好星座",
"Debilitated": "落陷",
"Neutral": "中性",
"Paravatamsa": "Paravatamsa",
"Simhasanamsa": "Simhasanamsa",
"dispositor": "定位星",
";功能性角色为待补充": "",
"功能性角色为待补充。": "",
}
for raw, public in replacements.items():
text = text.replace(raw, public)
text = re.sub(r"^#\s+.*$", f"# {title}", text, count=1, flags=re.MULTILINE)
drop_fragments = (
"本册只编排原版",
"研究性证据",
"审计",
"producer",
"PyJHora",
"source_path",
"schema",
"blocked",
"parameter_sensitive",
"native producer",
"Report Layers",
"Worksheet Index",
"Report Pack Manifest",
"发布状态",
"质量检查",
"Chart Identity",
"不自动进入公开发布",
"对标主册",
)
cleaned_lines: list[str] = []
for raw_line in text.splitlines():
if any(fragment in raw_line for fragment in drop_fragments):
continue
line = re.sub(r"p\d+-p\d+\s*", "", raw_line)
line = re.sub(r"p\d+\s*[·.-]\s*", "", line)
line = re.sub(r"^#+\s*p\d+\s*", lambda match: match.group(0).replace(match.group(0).split()[-1], ""), line)
line = line.replace(" · ", " ")
cleaned_lines.append(line.rstrip())
cleaned = "\n".join(cleaned_lines)
cleaned = cleaned.replace("{planets}", "相关行星")
cleaned = _strip_pl9_user_engineering_markers(cleaned)
cleaned = re.sub(r"^#\s+.*$", f"# {title}", cleaned, count=1, flags=re.MULTILINE)
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned).strip() + "\n"
return cleaned
def render_pl9_parity_markdown(packet: dict) -> str:
"""Render only PL9 page-family material admitted to the parity main volume."""
_bind_engine_symbols()
def _unwrap(raw_value):
if isinstance(raw_value, dict) and 'result' in raw_value and 'response_envelope' in raw_value:
return raw_value.get('result')
return raw_value
def _list(raw_value):
raw_value = _unwrap(raw_value)
if isinstance(raw_value, list):
return raw_value
if isinstance(raw_value, tuple):
return list(raw_value)
if isinstance(raw_value, dict) and isinstance(raw_value.get('result'), list):
return raw_value.get('result')
return []
def _dict(raw_value):
raw_value = _unwrap(raw_value)
return raw_value if isinstance(raw_value, dict) else {}
def _cell(value) -> str:
if isinstance(value, dict):
parts = []
for key, item in value.items():
if key in {
'status',
'状态',
'source_path',
'schema',
'execution_status',
'confidence_status',
'boundary_status',
'review_status',
'authority_status',
'raw_root',
'method',
'note',
'iterations',
'error_deg',
}:
continue
if item in (None, '', [], {}):
continue
if isinstance(item, dict):
nested = []
for nested_key, nested_item in item.items():
if nested_key in {'raw_root', 'method', 'note', 'iterations', 'error_deg', 'status', '状态'}:
continue
if nested_item not in (None, '', [], {}):
nested.append(f"{nested_key}: {nested_item}")
if nested:
parts.append(f"{key}: " + ','.join(nested))
continue
if isinstance(item, (list, tuple)):
item = ','.join(str(entry) for entry in item if entry not in (None, '', [], {}))
parts.append(f"{key}: {item}")
value = ';'.join(parts) if parts else '-'
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()
return text.replace('|', '/')
def _brief(items, limit=2) -> str:
values = [str(item).strip() for item in _list(items) if str(item).strip()]
return ';'.join(values[:limit]) if values else '-'
def _token(value) -> str:
label_map = {
'Sun': '太阳',
'Moon': '月亮',
'Mars': '火星',
'Mercury': '水星',
'Jupiter': '木星',
'Venus': '金星',
'Saturn': '土星',
'Rahu': '北交点',
'Ketu': '南交点',
}
sign_map = globals().get('SIGNS_CN', {})
raw = str(value or '').strip()
return label_map.get(raw) or sign_map.get(raw) or raw or '-'
def _degree(value) -> str:
try:
return f"{float(value):.2f}°"
except (TypeError, ValueError):
return '-'
def _date_cell(value) -> str:
text = str(value if value not in (None, '') else '-').strip()
if 'T' in text and len(text) >= 19:
return text[:19]
return _cell(text)
def _period_start(period: dict) -> str:
return _date_cell(period.get('start') or period.get('start_date') or period.get('start_datetime'))
def _period_end(period: dict) -> str:
return _date_cell(period.get('end') or period.get('end_date') or period.get('end_datetime'))
def _period_years(period: dict) -> str:
return _cell(period.get('years') or period.get('duration_years') or period.get('duration'))
def _producer_label(value) -> str:
text = str(value or '').strip()
if not text:
return '-'
leaf = text.rsplit('/', 1)[-1]
return leaf.replace('.py:', ':')
def _display_confidence(row: dict) -> str:
if row.get('execution_status') == 'blocked':
return 'blocked'
return _cell(row.get('confidence_status'))
def _num(value, digits=2) -> str:
try:
return f"{float(value):.{digits}f}"
except (TypeError, ValueError):
return _cell(value)
def _status_counts(summary: dict) -> str:
counts = summary.get('by_status') or summary.get('closure_status_counts') or {}
if not isinstance(counts, dict) or not counts:
return '-'
return ';'.join(f"{key}={value}" for key, value in counts.items())
def _public_status_note(value) -> str:
mapping = {
'partial_verified': '已生成',
'parameter_sensitive': '参数敏感',
'blocked': '需补资料',
'verified': '已核验',
'match': '一致',
'mismatch': '需校对',
'not_applicable': '不适用',
'pyjhora_behavior_only': '外部行为参考',
'pyjhora_behavior_only / not_multiengine_parity': '外部行为参考',
'blocked_external_callable': '外部回放待校对',
'superseded_by_annual_tajika_pack.lifetime_annual_chart_pages': '由生命周期图盘页承接',
}
text = str(value or '').strip()
return mapping.get(text, text or '-')
def _public_report_text(value) -> str:
text = str(value or '').strip()
replacements = {
'parameter_sensitive': '参考',
'blocked': '未列',
'observed_relationships': '已形成',
'observed_empty': '未形成',
'external_replay_pending': '待补充',
'Evidence label': '证据口径',
'证据标签': '证据口径',
}
text = text.replace('blocked_external_callable', '外部回放待校对')
text = text.replace('pyjhora_behavior_only / not_multiengine_parity', '外部行为参考')
text = text.replace('pyjhora_behavior_only', '外部行为参考')
for raw, public in replacements.items():
text = text.replace(raw, public)
return text
def _relation_summary(value) -> str:
if not isinstance(value, dict):
return '-'
chunks = []
for key, raw_items in value.items():
items = _list(raw_items)
if items:
chunks.append(f"{key}: {_brief(items, 4)}")
elif raw_items not in (None, '', [], {}):
chunks.append(f"{key}: {raw_items}")
return ';'.join(chunks) if chunks else '-'
def _lord_chain(raw_value) -> str:
data = _dict(raw_value)
parts = []
for key, label in (
('rasi_lord', 'RL'),
('nakshatra_lord', 'NL'),
('sub_lord', 'SL'),
('sub_sub_lord', 'SSL'),
):
if data.get(key):
parts.append(f"{label}={_token(data.get(key))}")
return ';'.join(parts) if parts else '-'
worksheets = packet.get('worksheets') if isinstance(packet.get('worksheets'), dict) else {}
birth = packet.get('birth_info') if isinstance(packet.get('birth_info'), dict) else {}
advanced = worksheets.get('advanced_systems') if isinstance(worksheets.get('advanced_systems'), dict) else {}
timing = worksheets.get('timing_and_predictive_systems') if isinstance(worksheets.get('timing_and_predictive_systems'), dict) else {}
lines = [
'# PL9 对标主册',
'',
'## 出生资料与图盘(PL9 p1-p17)',
'',
f"- 姓名:{birth.get('name') or birth.get('user_name') or '-'}",
f"- 出生:{birth.get('date') or '-'} {birth.get('time') or ''}",
f"- 计算口径:{birth.get('ayanamsa_name') or birth.get('ayanamsa') or '-'} / {birth.get('node_mode') or '-'}",
'',
]
core_chart = packet.get('core_chart') if isinstance(packet.get('core_chart'), dict) else {}
divisional = worksheets.get('divisional_and_special_charts') if isinstance(worksheets.get('divisional_and_special_charts'), dict) else {}
raw_divisional = packet.get('divisional_and_special_charts') if isinstance(packet.get('divisional_and_special_charts'), dict) else {}
for key in ('moon_chart', 'bhava_chalit'):
if key not in divisional and isinstance(raw_divisional.get(key), dict):
divisional[key] = raw_divisional[key]
varga_full = divisional.get('varga_full') if isinstance(divisional.get('varga_full'), dict) else {}
birth_sheet = worksheets.get('birth_and_parameters') if isinstance(worksheets.get('birth_and_parameters'), dict) else {}
birth_details = birth_sheet.get('birth_info') if isinstance(birth_sheet.get('birth_info'), dict) else birth
panchanga_pack = birth_sheet.get('panchanga') if isinstance(birth_sheet.get('panchanga'), dict) else {}
panchanga = panchanga_pack.get('raw') if isinstance(panchanga_pack.get('raw'), dict) else panchanga_pack
if birth_details:
lines.extend([
'### p3-p5 Birth Particulars / Hindu Calendar',
'',
'| Field | Value |',
'|-------|-------|',
])
for key, label in (
('date', '出生日期'),
('time', '出生时间'),
('day_of_birth', '出生星期'),
('place', '出生地点'),
('city', '城市'),
('state', '省州'),
('country', '国家'),
('lat', '纬度'),
('lon', '经度'),
('tz', '时区'),
('timezone', '时区'),
('ayanamsa_display', '岁差'),
('ayanamsa_name', '岁差名称'),
('ayanamsa', '岁差值'),
('node_mode', '交点模式'),
('house_system', '宫位系统'),
):
value = birth_details.get(key)
if value not in (None, ''):
lines.append(f"| {_cell(label)} | {_cell(value)} |")
if panchanga:
lines.extend([
'',
'| Panchanga | Value |',
'|-----------|-------|',
])
for key, value in panchanga.items():
if isinstance(value, (dict, list, tuple)) or value in (None, ''):
continue
lines.append(f"| {_cell(key)} | {_cell(value)} |")
lines.append('')
source_visible = packet.get('pl9_source_visible_tables') if isinstance(packet.get('pl9_source_visible_tables'), dict) else {}
birth_extra = _list(source_visible.get('birth_particulars_extra'))
if birth_extra:
lines.extend([
'#### p3 Birth Particulars 原版补充字段',
'',
'| Field | Value |',
'|-------|-------|',
])
for row in birth_extra:
if isinstance(row, dict):
lines.append(f"| {_cell(row.get('field'))} | {_cell(row.get('value'))} |")
lines.append('')
avakhada_rows = _list(source_visible.get('avakhada_chakra'))
if avakhada_rows:
lines.extend([
'#### p3 Avakhada Chakra',
'',
'| Field | Value |',
'|-------|-------|',
])
for row in avakhada_rows:
if isinstance(row, dict):
lines.append(f"| {_cell(row.get('field'))} | {_cell(row.get('value'))} |")
lines.append('')
d1_sheet = worksheets.get('d1_rasi_bhava') if isinstance(worksheets.get('d1_rasi_bhava'), dict) else {}
planets = core_chart.get('planets') if isinstance(core_chart.get('planets'), dict) else {}
if not planets and isinstance(d1_sheet.get('planets'), dict):
planets = d1_sheet.get('planets')
if planets:
lines.extend([
'### p6 Birth Chart 行星原始位置',
'',
'| Planet | R/C | Sign | Degree | Longitude | House | Nakshatra | Pada | Speed |',
'|--------|-----|------|--------|-----------|-------|-----------|------|-------|',
])
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'):
row = _dict(planets.get(planet))
if not row:
continue
longitude = row.get('longitude') or row.get('longitude_deg') or row.get('lon')
sign_degree = row.get('degree_in_sign') or row.get('sign_degree')
lines.append(
f"| {_cell(_token(planet))} | {_cell(row.get('r_c') or row.get('retrograde') and 'R' or 'C')} | "
f"{_cell(_token(row.get('sign')))} | {_cell(_degree(sign_degree))} | {_cell(_degree(longitude))} | "
f"{_cell(row.get('house'))} | {_cell(row.get('nakshatra'))} | {_cell(row.get('pada') or row.get('nakshatra_pada'))} | "
f"{_cell(_num(row.get('speed'), 6))} |"
)
lines.append('')
lines.extend([
'#### p6 Birth Chart 星主与状态字段',
'',
'| Planet | RL | NL | SL | SS | Status | SB |',
'|--------|----|----|----|----|--------|----|',
])
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'):
row = _dict(planets.get(planet))
if not row:
continue
lines.append(
f"| {_cell(_token(planet))} | {_cell(_token(row.get('rasi_lord')))} | "
f"{_cell(_token(row.get('nakshatra_lord')))} | {_cell(_token(row.get('sub_lord')))} | "
f"{_cell(_token(row.get('sub_sub_lord')))} | {_cell(row.get('status'))} | {_cell(_num(row.get('sb'), 2))} |"
)
lines.append('')
moon_chart = d1_sheet.get('moon_chart') if isinstance(d1_sheet.get('moon_chart'), dict) else {}
bhava = d1_sheet.get('bhava_chalit') if isinstance(d1_sheet.get('bhava_chalit'), dict) else {}
sudarshana = divisional.get('sudarshana') if isinstance(divisional.get('sudarshana'), dict) else {}
upagrahas = divisional.get('upagrahas') if isinstance(divisional.get('upagrahas'), dict) else {}
special_lagnas = divisional.get('special_lagnas') if isinstance(divisional.get('special_lagnas'), dict) else {}
if moon_chart or bhava:
lines.extend(['### Chandra Chart / Bhava Spashta - Sripati System', ''])
moon_raw = moon_chart.get('raw') if isinstance(moon_chart.get('raw'), dict) else moon_chart
moon_planets = moon_raw.get('planets') if isinstance(moon_raw.get('planets'), dict) else {}
if moon_planets:
signs = globals().get('SIGNS', [])
moon_asc_sign = _dict(moon_raw.get('Ascendant')).get('sign')
moon_asc_index = signs.index(moon_asc_sign) if moon_asc_sign in signs else None
lines.extend([
'| Moon Chart | Sign | Degree | House |',
'|------------|------|--------|-------|',
])
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'):
row = _dict(moon_planets.get(planet))
if row:
sign = row.get('sign')
moon_house = row.get('house')
if moon_house in (None, '') and moon_asc_index is not None and sign in signs:
moon_house = ((signs.index(sign) - moon_asc_index) % 12) + 1
lines.append(
f"| {_cell(_token(planet))} | {_cell(_token(sign))} | "
f"{_cell(_degree(row.get('degree_in_sign') or row.get('longitude')))} | {_cell(moon_house)} |"
)
lines.append('')
bhava_raw = bhava.get('raw') if isinstance(bhava.get('raw'), dict) else bhava
boundaries = _dict(bhava_raw.get('boundaries'))
houses = _list(boundaries.get('houses'))
if houses:
lines.extend([
'| Bhava Number | Bhava Arambha / House beginning | Bhava Madhya / Middle of House | Bhava Antya / House ending |',
'|--------------|----------------------------------|-------------------------------|-----------------------------|',
])
for index, row in enumerate(houses[:12], start=1):
if isinstance(row, dict):
lines.append(
f"| {index} | {_cell(_degree(row.get('sandhi_start_lon')))} | "
f"{_cell(_token(row.get('cusp_sign')))} {_cell(_degree(row.get('cusp_degree_in_sign')))} | "
f"{_cell(_degree(row.get('sandhi_end_lon')))} |"
)
lines.append('')
if sudarshana:
raw = sudarshana.get('raw') if isinstance(sudarshana.get('raw'), dict) else sudarshana
rows = [
(key, value) for key, value in raw.items()
if key not in {'method', 'version', 'schema', 'schema_version', 'status'}
and not isinstance(value, (dict, list, tuple))
and value not in (None, '')
]
if rows:
lines.extend(['### Sudarshan Chakra 资料', '', '| Field | Value |', '|-------|-------|'])
for key, value in rows:
lines.append(f"| {_cell(key)} | {_cell(value)} |")
lines.append('')
upagraha_raw = upagrahas.get('raw') if isinstance(upagrahas.get('raw'), dict) else {}
if upagraha_raw:
lines.extend([
'### Upagraha / Sub-Planets 点位',
'',
'| Point | Sign | Degree | Longitude |',
'|-------|------|--------|-----------|',
])
signs = globals().get('SIGNS', [])
for name, row in upagraha_raw.items():
if not isinstance(row, dict):
continue
longitude = row.get('longitude')
sign = row.get('sign')
if not sign and isinstance(row.get('sign_idx'), int) and signs:
sign = signs[int(row.get('sign_idx')) % 12]
lines.append(
f"| {_cell(name)} | {_cell(_token(sign))} | {_cell(_degree(row.get('degree_in_sign')))} | {_cell(_degree(longitude))} |"
)
lines.append('')
source_gulikaadi = _list(source_visible.get('gulikaadi_group'))
source_dhoomadi = _list(source_visible.get('dhoomadi_group'))
if source_gulikaadi:
lines.extend([
'#### p17 Gulikaadi Group',
'',
'| Upagraha | Lord | Duration | Beginning | Ending |',
'|----------|------|----------|-----------|--------|',
])
for row in source_gulikaadi:
if isinstance(row, dict):
lines.append(
f"| {_cell(row.get('upagraha'))} | {_cell(row.get('lord'))} | {_cell(row.get('duration'))} | "
f"{_cell(row.get('beginning'))} | {_cell(row.get('ending'))} |"
)
lines.append('')
if source_dhoomadi:
lines.extend([
'#### p17 Dhoomadi Group',
'',
'| Upagraha | Lord | Position |',
'|----------|------|----------|',
])
for row in source_dhoomadi:
if isinstance(row, dict):
lines.append(f"| {_cell(row.get('upagraha'))} | {_cell(row.get('lord'))} | {_cell(row.get('position'))} |")
lines.append('')
if special_lagnas:
lines.extend([
'### Special Lagnas and Points',
'',
'| Point | Sign | Degree | House |',
'|-------|------|--------|-------|',
])
for name, row in special_lagnas.items():
if not isinstance(row, dict):
continue
lines.append(
f"| {_cell(row.get('full_name') or row.get('label') or name)} | {_cell(_token(row.get('sign')))} | "
f"{_cell(_degree(row.get('sign_degree') or row.get('degree_in_sign')))} | {_cell(row.get('house'))} |"
)
lines.append('')
source_special_points = _list(source_visible.get('source_special_points'))
if source_special_points:
lines.extend([
'#### p17 Special Lagnas and Points 原版补充字段',
'',
'| Point | Value |',
'|-------|-------|',
])
for row in source_special_points:
if isinstance(row, dict):
lines.append(f"| {_cell(row.get('point'))} | {_cell(row.get('value'))} |")
lines.append('')
sensitive_points = divisional.get('sensitive_points') if isinstance(divisional.get('sensitive_points'), dict) else {}
if sensitive_points:
lines.extend([
'#### 敏感点与特殊落点',
'',
'| Point | Sign | Degree | Longitude | Lord |',
'|-------|------|--------|-----------|------|',
])
sensitive_rows = []
for name, row in sensitive_points.items():
if name == 'sarpa_drekkana' and isinstance(row, dict):
for planet, placement in row.items():
if isinstance(placement, dict):
sensitive_rows.append((f'{name} / {planet}', {
**placement,
'lord': placement.get('lord') or 'Not supplied by source',
}))
else:
sensitive_rows.append((name, row))
for name, row in sensitive_rows:
if not isinstance(row, dict) or not row:
continue
lines.append(
f"| {_cell(name)} | {_cell(_token(row.get('sign')))} | "
f"{_cell(_degree(row.get('degree_in_sign')))} | {_cell(_degree(row.get('longitude')))} | "
f"{_cell(_token(row.get('lord')))} |"
)
lines.append('')
natal_specs = [
(6, 'D1 Rashi', bool(core_chart.get('planets'))),
(7, 'Moon Chart', isinstance(divisional.get('moon_chart'), dict)),
(7, 'Navamsha', isinstance(varga_full.get('D9_Navamsa') or varga_full.get('D9'), dict)),
(7, 'Bhava (Sripati)', isinstance(divisional.get('bhava_chalit'), dict)),
(9, 'D5 Panchamsa', isinstance(varga_full.get('D5_Panchamsa') or varga_full.get('D5'), dict)),
(10, 'D6 Shashthamsa', isinstance(varga_full.get('D6_Shashthamsa') or varga_full.get('D6'), dict)),
(11, 'D9 Navamsa', isinstance(varga_full.get('D9_Navamsa') or varga_full.get('D9'), dict)),
(12, 'D10 Dasamsa', isinstance(varga_full.get('D10_Dasamsa') or varga_full.get('D10'), dict)),
(13, 'D2 Hora', isinstance(varga_full.get('D2_Hora') or varga_full.get('D2'), dict)),
(14, 'D12 Dwadashamsa', isinstance(varga_full.get('D12_Dwadashamsa') or varga_full.get('D12'), dict)),
(15, 'D16 Shodasamsa', isinstance(varga_full.get('D16_Shodasamsa') or varga_full.get('D16'), dict)),
(16, 'D20 Vimsamsa', isinstance(varga_full.get('D20_Vimsamsa') or varga_full.get('D20'), dict)),
(17, 'D24 Chaturvimsamsa', isinstance(varga_full.get('D24_Chaturvimsamsa') or varga_full.get('D24'), dict)),
(18, 'D27 Bhamsa', isinstance(varga_full.get('D27_Bhamsa') or varga_full.get('D27'), dict)),
(19, 'D30 Trimsamsa', isinstance(varga_full.get('D30_Trimsamsa') or varga_full.get('D30'), dict)),
(20, 'D40 Khavedamsa', isinstance(varga_full.get('D40_Khavedamsa') or varga_full.get('D40'), dict)),
(21, 'D45 Akshavedamsa', isinstance(varga_full.get('D45_Akshavedamsa') or varga_full.get('D45'), dict)),
(22, 'D60 Shashtyamsa', isinstance(varga_full.get('D60_Shashtyamsa') or varga_full.get('D60'), dict)),
(23, 'D7 Saptamsa', isinstance(varga_full.get('D7_Saptamsa') or varga_full.get('D7'), dict)),
(24, 'D11 Rudramsa', isinstance(varga_full.get('D11_Rudramsa') or varga_full.get('D11'), dict)),
]
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}', ''])
# 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
# same placements without relying on image recognition alone.
chart_numeric_specs = [
('D1_Rashi', 'D1 Rashi'),
('D2_Hora', 'D2 Hora'),
('D3_Drekkana', 'D3 Drekkana'),
('D4_Turyamsa', 'D4 Turyamsa'),
('D5_Panchamsa', 'D5 Panchamsa'),
('D6_Shashthamsa', 'D6 Shashthamsa'),
('D7_Saptamsa', 'D7 Saptamsa'),
('D8_Ashtamsa', 'D8 Ashtamsa'),
('D9_Navamsa', 'D9 Navamsa'),
('D10_Dasamsa', 'D10 Dasamsa'),
('D11_Rudramsa', 'D11 Rudramsa'),
('D12_Dwadashamsa', 'D12 Dwadashamsa'),
('D16_Shodasamsa', 'D16 Shodasamsa'),
('D20_Vimsamsa', 'D20 Vimsamsa'),
('D24_Siddhamsa', 'D24 Siddhamsa'),
('D27_Bhamsa', 'D27 Bhamsa'),
('D30_Trimsamsa', 'D30 Trimsamsa'),
('D40_Khavedamsa', 'D40 Khavedamsa'),
('D45_Akshavedamsa', 'D45 Akshavedamsa'),
('D60_Shashtyamsa', 'D60 Shashtyamsa'),
('D81', 'D81'),
('D108', 'D108'),
('D144', 'D144'),
]
signs = globals().get('SIGNS', [])
for chart_key, chart_label in chart_numeric_specs:
chart = varga_full.get(chart_key) if isinstance(varga_full.get(chart_key), dict) else {}
if chart_key == 'D1_Rashi' and not chart:
chart = {'Ascendant': core_chart.get('ascendant'), **planets}
if not chart:
continue
ascendant = _dict(chart.get('Ascendant') or chart.get('ascendant'))
asc_sign_idx = ascendant.get('sign_idx')
if asc_sign_idx is None and ascendant.get('sign') in signs:
asc_sign_idx = signs.index(ascendant.get('sign'))
rows = []
if ascendant:
rows.append(('Lagna', ascendant))
rows.extend(
(planet, _dict(chart.get(planet)))
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu')
if _dict(chart.get(planet))
)
if not rows:
continue
lines.extend([
f'#### {chart_label} 数值位置表',
'',
'| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |',
'|------|------|----------|------|----------|',
])
for object_name, row in rows:
sign = row.get('sign')
sign_idx = row.get('sign_idx')
if sign_idx is None and sign in signs:
sign_idx = signs.index(sign)
house = row.get('house')
if house in (None, '') and asc_sign_idx is not None and sign_idx is not None:
house = ((int(sign_idx) - int(asc_sign_idx)) % 12) + 1
longitude = row.get('longitude')
if longitude is None:
longitude = row.get('lon')
degree = row.get('degree_in_sign')
if degree is None:
degree = row.get('sign_degree')
if longitude is None and degree is not None:
try:
if sign_idx is None and sign in signs:
sign_idx = signs.index(sign)
if sign_idx is not None:
longitude = int(sign_idx) * 30 + float(degree)
except (TypeError, ValueError):
longitude = None
lines.append(
f"| {_cell(_token(object_name))} | {_cell(_token(sign))} | {_degree(degree)} | "
f"{_degree(longitude)} | {_cell(house)} |"
)
lines.append('')
visual_pack = divisional.get('visual_chart_pack') if isinstance(divisional.get('visual_chart_pack'), dict) else {}
panel_inventory = visual_pack.get('panel_inventory') if isinstance(visual_pack.get('panel_inventory'), dict) else {}
graha = advanced.get('graha_interpretation_pack') if isinstance(advanced.get('graha_interpretation_pack'), dict) else {}
pages = graha.get('planet_pages') if isinstance(graha.get('planet_pages'), list) else []
if pages:
lines.extend(['## 行星、星宿与宫位解释', ''])
for page in pages:
if not isinstance(page, dict):
continue
planet = str(page.get('planet') or '').strip()
sections = page.get('sections') if isinstance(page.get('sections'), dict) else {}
if not planet:
continue
lines.extend([f'### {planet}', ''])
for key in ('placement', 'sign_and_dispositor', 'nakshatra', 'functional_role', 'strength_and_context', 'activation'):
if str(sections.get(key) or '').strip():
lines.extend([str(sections[key]).strip(), ''])
strengths = worksheets.get('strengths_and_scores') if isinstance(worksheets.get('strengths_and_scores'), dict) else {}
if strengths:
lines.extend(['## 强度、关系与 Ashtakavarga 技术页(PL9 p30-p60 对标)', ''])
shadbala = _dict(strengths.get('shadbala'))
shadbala_planets = _dict(shadbala.get('planets'))
if shadbala_planets:
lines.extend([
'### p31-p33 Shadbala / Bhava Bala 摘要',
'',
f"- Shadbala 方法:{_cell(shadbala.get('method'))}",
f"- 外部边界:{_cell(shadbala.get('external_parity_boundary'))}",
'',
'| Planet | Sthana | Dig | Kala | Chesta | Naisargika | Drik | Total | Required |',
'|--------|--------|-----|------|--------|------------|------|-------|----------|',
])
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'):
row = _dict(shadbala_planets.get(planet))
if not row:
continue
total = row.get('total') or row.get('total_shadbala') or row.get('total_virupas')
required = row.get('minimum_required') or row.get('required') or row.get('min_required')
lines.append(
f"| {_cell(_token(planet))} | {_num(_dict(row.get('sthana_bala')).get('total') or row.get('sthana_bala'))} | "
f"{_num(_dict(row.get('dig_bala')).get('total') or row.get('dig_bala'))} | {_num(_dict(row.get('kala_bala')).get('total') or row.get('kala_bala'))} | "
f"{_num(_dict(row.get('chesta_bala')).get('total') or row.get('chesta_bala'))} | {_num(row.get('naisargika_bala'))} | "
f"{_num(_dict(row.get('drik_bala')).get('total') or row.get('drik_bala'))} | {_num(total)} | {_num(required)} |"
)
lines.append('')
lines.extend([
'### p33 Declination / Kranti / Speed',
'',
'| Planet | Degree | Declination | Kranti | Speed |',
'|--------|--------|-------------|--------|-------|',
])
kranti_planets = _dict(_dict(strengths.get('kranti_local')).get('planets'))
speed_planets = _dict(_dict(strengths.get('speed_display_local')).get('planets'))
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'):
row = _dict(shadbala_planets.get(planet))
if not row:
continue
kala = _dict(row.get('kala_bala'))
planet_row = _dict((packet.get('core_chart') or {}).get('planets', {}).get(planet)) if isinstance((packet.get('core_chart') or {}).get('planets'), dict) else {}
kranti_degrees = _dict(kranti_planets.get(planet)).get('kranti_degrees')
speed_dms = _dict(speed_planets.get(planet)).get('speed_dms') or _num(planet_row.get('speed'), 6)
lines.append(
f"| {_cell(_token(planet))} | {_cell(_degree(planet_row.get('degree_in_sign') or planet_row.get('longitude')))} | "
f"{_cell(_degree(kala.get('declination')))} | {_cell(_degree(kranti_degrees))} | {_cell(speed_dms)} |"
)
lines.append('')
avasthas = _dict(advanced.get('avasthas'))
if avasthas:
lines.extend([
'### p30-p60 Avastha 行星状态',
'',
'| Planet | Bala | Jagrat | Deeptadi | Lajjitadi | Shayanadi |',
'|--------|------|--------|----------|-----------|-----------|',
])
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'):
row = _dict(avasthas.get(planet))
states = _dict(row.get('avasthas'))
if not states:
continue
def _state(name: str) -> str:
state = _dict(states.get(name))
return _cell(state.get('state'))
lines.append(
f"| {_cell(_token(planet))} | {_state('Bala')} | {_state('Jagrat')} | {_state('Deeptadi')} | "
f"{_state('Lajjitadi')} | {_state('Shayanadi')} |"
)
lines.append('')
bhava_bala = _dict(strengths.get('bhava_bala'))
bhava_rows = _list(bhava_bala.get('houses')) or [
{'house': key, **value}
for key, value in bhava_bala.items()
if str(key).isdigit() and isinstance(value, dict)
]
if bhava_rows:
lines.extend([
'### p32 Bhava Bala 十二宫力量',
'',
'| House | Sign | Lord | Score | 关键因素 |',
'|-------|------|------|-------|----------|',
])
for row in bhava_rows[:12]:
if not isinstance(row, dict):
continue
factors = row.get('factors') if isinstance(row.get('factors'), list) else []
lines.append(
f"| {_cell(row.get('house') or row.get('number'))} | {_cell(_token(row.get('sign')))} | {_cell(_token(row.get('lord')))} | "
f"{_num(row.get('score'))} | {_cell(_brief(factors, 2))} |"
)
lines.append('')
friendship = _dict(strengths.get('planetary_friendship'))
friendship_rows = _list(friendship.get('rows'))
if friendship_rows:
lines.extend([
'### p41 Planetary Friendship 矩阵源数据',
'',
'| Planet | Natural friends | Natural enemies | Temporary relation |',
'|--------|-----------------|-----------------|--------------------|',
])
for row in friendship_rows[:9]:
if not isinstance(row, dict):
continue
natural = _dict(row.get('natural'))
temporary = _dict(row.get('temporary'))
lines.append(
f"| {_cell(_token(row.get('planet')))} | {_cell(_brief(natural.get('friends'), 5))} | "
f"{_cell(_brief(natural.get('enemies'), 5))} | {_cell(_relation_summary(temporary))} |"
)
lines.append('')
aspects = _dict(advanced.get('aspects'))
aspect_rows = _list(aspects.get('aspects'))
house_aspect_rows = _list(aspects.get('house_aspects'))
if aspect_rows or house_aspect_rows:
lines.extend([
'### p37-p38 / p45-p47 相位矩阵',
'',
])
if aspect_rows:
lines.extend([
'| Planet 1 | Planet 2 | Aspect | Exact degree | Actual difference | Orb | Phase | Orb score |',
'|----------|----------|--------|--------------|-------------------|-----|----------|----------|',
])
for row in aspect_rows:
if not isinstance(row, dict):
continue
lines.append(
f"| {_cell(_token(row.get('planet1')))} | {_cell(_token(row.get('planet2')))} | "
f"{_cell(row.get('type'))} | {_degree(row.get('aspect_degree'))} | {_degree(row.get('actual_diff'))} | "
f"{_degree(row.get('orb'))} | {_cell(row.get('phase', 'unknown_without_velocity'))} | {_num(row.get('strength'))} |"
)
lines.append('')
if house_aspect_rows:
lines.extend([
'| Planet | From house | Target house | Aspect type | Special |',
'|--------|------------|--------------|-------------|---------|',
])
for row in house_aspect_rows:
if not isinstance(row, dict):
continue
targets = _dict(row.get('aspects_to'))
if targets:
for target, detail in targets.items():
detail = _dict(detail)
lines.append(
f"| {_cell(_token(row.get('planet')))} | {_cell(row.get('house'))} | {_cell(target)} | "
f"{_cell(detail.get('aspect_type'))} | {_cell(detail.get('is_special'))} |"
)
else:
lines.append(
f"| {_cell(_token(row.get('planet')))} | {_cell(row.get('house'))} | - | - | - |"
)
lines.append('')
jaimini = advanced.get('jaimini') if isinstance(advanced.get('jaimini'), dict) else {}
karaka_source = _dict(jaimini.get('chara_karaka_8')) or _dict(jaimini.get('chara_karaka_7'))
karaka_table = _dict(karaka_source.get('karaka_table_8')) or _dict(karaka_source.get('karaka_table'))
arudha_pack = _dict(jaimini.get('arudha_padas'))
arudha_rows = _dict(arudha_pack.get('padas')) or arudha_pack
jaimini_special = _dict(jaimini.get('special_lagnas'))
if karaka_table or arudha_rows or jaimini_special:
lines.extend(['### Jaimini Karaka、Arudha Pada 与特殊 Lagna', ''])
if karaka_table:
lines.extend([
'| Karaka | Planet | Degree | Domain |',
'|--------|--------|--------|--------|',
])
for name, row in karaka_table.items():
if not isinstance(row, dict):
continue
lines.append(
f"| {_cell(row.get('cn_name') or name)} | {_cell(_token(row.get('planet')))} | "
f"{_cell(_degree(row.get('degree_in_sign') or row.get('sign_degree')))} | "
f"{_cell(row.get('domain'))} |"
)
lines.append('')
if arudha_rows or jaimini_special:
lines.extend([
'| Point | Sign | Degree / House |',
'|-------|------|----------------|',
])
for name, row in {**arudha_rows, **jaimini_special}.items():
if not isinstance(row, dict):
continue
lines.append(
f"| {_cell(row.get('full_name') or row.get('label') or row.get('name') or name)} | "
f"{_cell(_token(row.get('sign') or row.get('rashi')))} | "
f"{_cell(_degree(row.get('degree_in_sign') or row.get('sign_degree')) if row.get('degree_in_sign') is not None or row.get('sign_degree') is not None else row.get('house') or row.get('house_num'))} |"
)
lines.append('')
ashtakavarga = _dict(strengths.get('ashtakavarga'))
sav = _dict(ashtakavarga.get('sav'))
sav_scores = _dict(sav.get('scores'))
if sav_scores:
lines.extend([
'### p51-p60 Ashtakavarga SAV / BAV / Pinda',
'',
f"- 方法:{_cell(ashtakavarga.get('method'))}",
f"- BAV 校验:{_cell('all_valid' if ashtakavarga.get('all_bav_valid') else ashtakavarga.get('all_bav_valid'))}",
'',
'| Sign | SAV score |',
'|------|-----------|',
])
for sign in globals().get('SIGNS', []):
if sign in sav_scores:
lines.append(f"| {_cell(_token(sign))} | {_cell(sav_scores.get(sign))} |")
lines.append('')
full_house_scores = ashtakavarga.get('house_scores_full')
if isinstance(full_house_scores, dict):
full_rows = []
for key, value in full_house_scores.items():
if isinstance(value, dict):
full_rows.append({'house': str(key).replace('house_', ''), **value})
full_rows.sort(key=lambda row: int(row['house']) if str(row.get('house')).isdigit() else 99)
if full_rows:
lines.extend([
'### p53 Ashtakavarga 完整宫位分数',
'',
'| House | Sign | SAV | BAV/承接 | Full with Lagna |',
'|-------|------|-----|----------|-----------------|',
])
for row in full_rows:
lines.append(
f"| {_cell(row.get('house'))} | {_cell(_token(row.get('sign')))} | "
f"{_cell(row.get('sav') or row.get('sav_score') or row.get('score'))} | "
f"{_cell(row.get('bav') or row.get('bav_score') or row.get('bindus'))} | "
f"{_cell(row.get('full_with_lagna') or row.get('full_with_lagna_house_score'))} |"
)
lines.append('')
bav = _dict(ashtakavarga.get('bav'))
if bav:
signs = globals().get('SIGNS', [])
lines.extend([
'| Planet | Total | Expected | Valid | Aries | Taurus | Gemini | Cancer | Leo | Virgo | Libra | Scorpio | Sagittarius | Capricorn | Aquarius | Pisces |',
'|--------|-------|----------|-------|-------|--------|--------|--------|-----|-------|-------|---------|-------------|-----------|----------|--------|',
])
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'):
row = _dict(bav.get(planet))
bindus = _list(row.get('bindus'))
if not row or not bindus:
continue
cells = ' | '.join(_cell(bindus[index]) if index < len(bindus) else '-' for index, _sign in enumerate(signs[:12]))
lines.append(f"| {_cell(_token(planet))} | {_cell(row.get('total'))} | {_cell(row.get('expected_total'))} | {_cell(row.get('valid'))} | {cells} |")
lines.append('')
vimsopaka = _dict(strengths.get('vimsopaka'))
if vimsopaka:
lines.extend([
'### p30-p60 Vimsopaka / Pushkara / Vargottama 辅助强度',
'',
'| Planet | Vimsopaka | Grade | Pushkara Navamsa | Pushkara Bhaga | Vargottama |',
'|--------|-----------|-------|------------------|----------------|------------|',
])
pushkara = _dict(strengths.get('pushkara'))
vargottama = _dict(strengths.get('vargottama'))
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'):
row = _dict(vimsopaka.get(planet))
push = _dict(pushkara.get(planet))
varga = _dict(vargottama.get(planet))
if not row and not push and not varga:
continue
lines.append(
f"| {_cell(_token(planet))} | {_num(row.get('total_score'))} | {_cell(row.get('category'))} | "
f"{_cell(push.get('pushkara_navamsa'))} | {_cell(push.get('pushkara_bhaga'))} | {_cell(varga.get('is_vargottama'))} |"
)
lines.append('')
source_pindas = _list(source_visible.get('ashtakavarga_reduction_pindas'))
if source_pindas:
lines.extend([
'### p54 Ashtakavarga - Reductions / Pinda',
'',
'Ashtakavarga Pinda 与 Shodhya/Yoga Pinda 数值表。',
'',
'| Planet | Rashi Pinda | Graha Pinda | Sodhya Pinda |',
'|--------|-------------|-------------|--------------|',
])
for row in source_pindas:
if isinstance(row, dict):
lines.append(
f"| {_cell(_token(row.get('planet')))} | {_cell(row.get('rashi_pinda'))} | "
f"{_cell(row.get('graha_pinda'))} | {_cell(row.get('sodhya_pinda'))} |"
)
lines.append('')
packet_pindas = _dict(ashtakavarga.get('shodhya_pinda'))
if packet_pindas:
lines.extend([
'### Ashtakavarga Shodhya / Yoga Pinda 原始值',
'',
'| Planet | Rashi Pinda | Graha Pinda | Total Pinda | Bindu at own sign |',
'|--------|-------------|-------------|-------------|-------------------|',
])
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'):
row = _dict(packet_pindas.get(planet))
if not row:
continue
lines.append(
f"| {_cell(_token(planet))} | {_cell(row.get('rashi_pinda'))} | "
f"{_cell(row.get('graha_pinda'))} | {_cell(row.get('total_pinda') or row.get('sodhya_pinda'))} | "
f"{_cell(row.get('bindu_at_own_sign'))} |"
)
lines.append('')
divisional_av = _list(source_visible.get('divisional_ashtakavarga_samudaya'))
if divisional_av:
lines.extend([
'### p55 Divisional Charts - Ashtakavarga',
'',
'| Chart | Samudaya Ashtakavarga Scores |',
'|-------|-------------------------------|',
])
for row in divisional_av:
if isinstance(row, dict):
lines.append(f"| {_cell(row.get('chart'))} | {_cell(row.get('scores'))} |")
lines.append('')
physiology = source_visible.get('physiology_tables') if isinstance(source_visible.get('physiology_tables'), dict) else {}
sookshma_rows = _list(physiology.get('sookshma'))
sthoola_rows = _list(physiology.get('sthoola'))
nakshatra_rows = _list(physiology.get('nakshatra'))
if sookshma_rows or sthoola_rows or nakshatra_rows:
lines.extend(['### p56 Health / Influence of Grahas on Physiology', ''])
if sookshma_rows:
lines.extend([
'#### Sookshma Scheme',
'',
'| Planet | Body Part |',
'|--------|-----------|',
])
for row in sookshma_rows:
if isinstance(row, dict):
lines.append(f"| {_cell(_token(row.get('planet')))} | {_cell(row.get('body_part'))} |")
lines.append('')
if sthoola_rows:
lines.extend([
'#### Sthoola Scheme',
'',
'| Planet | Body Part 1 | Body Part 2 |',
'|--------|-------------|-------------|',
])
for row in sthoola_rows:
if isinstance(row, dict):
lines.append(
f"| {_cell(_token(row.get('planet')))} | {_cell(row.get('body_part_1'))} | {_cell(row.get('body_part_2'))} |"
)
lines.append('')
if nakshatra_rows:
lines.extend([
'#### Nakshatra Scheme',
'',
'| Planet | Nakshatra | Opinion 1 | Opinion 2 |',
'|--------|-----------|-----------|-----------|',
])
for row in nakshatra_rows:
if isinstance(row, dict):
lines.append(
f"| {_cell(_token(row.get('planet')))} | {_cell(row.get('nakshatra'))} | "
f"{_cell(row.get('opinion_1'))} | {_cell(row.get('opinion_2'))} |"
)
lines.append('')
source_avasthas = _list(source_visible.get('planetary_avasthas'))
if source_avasthas:
lines.extend([
'### p46 Graha Avasthas - Planets and their Moods',
'',
'| Planet | Jagradadi | Baladi | Lajjitadi | Deeptadi | Shyanadi |',
'|--------|-----------|--------|-----------|----------|----------|',
])
for row in source_avasthas:
if not isinstance(row, dict):
continue
lines.append(
f"| {_cell(_token(row.get('planet')))} | {_cell(row.get('jagradadi'))} | {_cell(row.get('baladi'))} | "
f"{_cell(row.get('lajjitadi'))} | {_cell(row.get('deeptadi'))} | {_cell(row.get('shyanadi'))} |"
)
lines.append('')
chakra_pages = _list(source_visible.get('visual_chakra_pages'))
if chakra_pages:
lines.extend([
'### p58-p60 Chakra 图阵页',
'',
'| Page | Chart |',
'|------|-------|',
])
for row in chakra_pages:
if isinstance(row, dict):
lines.append(f"| {_cell(row.get('page'))} | {_cell(row.get('title'))} |")
lines.append('')
kota_rows = _list(source_visible.get('kota_gochara'))
if kota_rows:
lines.extend([
'#### p60 Kota Chakra Gochara (Transits)',
'',
'| Planet | Degree | Rashi | Nakshatra | Dignity | SB% |',
'|--------|--------|-------|-----------|---------|-----|',
])
for row in kota_rows:
if not isinstance(row, dict):
continue
lines.append(
f"| {_cell(row.get('planet'))} | {_cell(row.get('degree'))} | {_cell(row.get('rashi'))} | "
f"{_cell(row.get('nakshatra'))} | {_cell(row.get('dignity'))} | {_cell(row.get('sb_percent'))} |"
)
lines.append('')
dasha_main = timing.get('dasha') if isinstance(timing.get('dasha'), dict) else {}
dasha_master = timing.get('dasha_master_pack') if isinstance(timing.get('dasha_master_pack'), dict) else {}
dasha_families = dasha_master.get('families') if isinstance(dasha_master.get('families'), dict) else {}
vimshottari_timeline = _list(dasha_main.get('timeline'))
family_specs = [
(75, 'Ashtottari Dasha', dasha_families.get('ashtottari')),
(80, 'Yogini Dasha', dasha_families.get('yogini')),
(83, 'Kala Chakra Dasha', dasha_families.get('kala_chakra')),
(89, 'Jaimini / Narayana Dasha', dasha_families.get('narayana')),
(92, 'Sthira Dasha', dasha_families.get('sthira')),
(93, 'Niryaana Shoola Dasha', dasha_families.get('niryaana_shoola')),
(95, 'Drig Dasha', dasha_families.get('drig')),
(97, 'Navamsha Dasha', dasha_families.get('navamsha')),
(101, 'Lagna Kendradi Rashi Dasha', dasha_families.get('lagna_kendradi')),
(103, 'Shoola Dasha', dasha_families.get('shoola')),
(104, 'Tribhagi Dasha', dasha_families.get('tribhagi')),
(106, 'Tribhagi 40-year Dasha', dasha_families.get('tribhagi_40')),
(109, 'Shodashottari Dasha', dasha_families.get('shodashottari')),
(110, 'Dwadashottari Dasha', dasha_families.get('dwadashottari')),
(111, 'Dwisaptatisama Dasha', dasha_families.get('dwisaptatisama')),
(113, 'Shastihayani Dasha', dasha_families.get('shastihayani')),
(115, 'Shattrimshatsama Dasha', dasha_families.get('shattrimshatsama')),
(118, 'Panchottari Dasha', dasha_families.get('panchottari')),
(119, 'Satabdika Dasha', dasha_families.get('satabdika')),
(120, 'Chaturshitisama Dasha', dasha_families.get('chaturshitisama')),
]
if vimshottari_timeline or any(isinstance(family, dict) and _list(family.get('periods')) for _page, _title, family in family_specs):
lines.extend(['## 标准 Dasha 表格页(PL9 p61-p120 对标)', ''])
if vimshottari_timeline:
current = _dict(dasha_main.get('current_dasha'))
lines.extend([
'### p62-p74 Vimshottari Dasha',
'',
f"- Current Dasha:{_token(current.get('lord'))}({_cell(current.get('start'))} - {_cell(current.get('end'))})" if current else '- Current Dasha:blocked',
'',
'| 序号 | 主运 | 起始 | 结束 | 年数 |',
'|------|------|------|------|------|',
])
for index, period in enumerate(vimshottari_timeline, start=1):
if not isinstance(period, dict):
continue
status = 'birth_balance' if period.get('is_balance') else 'native'
lines.append(
f"| {index} | {_cell(_token(period.get('lord')))} | {_period_start(period)} | {_period_end(period)} | {_period_years(period)} | {status} |"
)
ad_rows = []
pd_rows = []
for md_period in vimshottari_timeline:
if not isinstance(md_period, dict):
continue
md_lord = md_period.get('lord')
for ad_period in _list(md_period.get('antardasha_timeline')):
if not isinstance(ad_period, dict):
continue
ad_rows.append((md_lord, ad_period))
ad_lord = ad_period.get('lord')
for pd_period in _list(ad_period.get('pratyantar_dasha_timeline')):
if isinstance(pd_period, dict):
pd_rows.append((md_lord, ad_lord, pd_period))
if ad_rows:
lines.extend(['', '| Vimshottari AD | 主运 | 子运 | 起始 | 结束 |', '|--------------|------|------|------|------|'])
for index, (md_lord, period) in enumerate(ad_rows, start=1):
lines.append(f"| {index} | {_cell(_token(md_lord))} | {_cell(_token(period.get('lord')))} | {_period_start(period)} | {_period_end(period)} |")
if pd_rows:
lines.extend(['', '| Vimshottari PD | 主运 | 子运 | 次子运 | 起始 | 结束 |', '|--------------|------|------|--------|------|------|'])
for index, (md_lord, ad_lord, period) in enumerate(pd_rows, start=1):
lines.append(
f"| {index} | {_cell(_token(md_lord))} | {_cell(_token(ad_lord))} | {_cell(_token(period.get('lord')))} | "
f"{_period_start(period)} | {_period_end(period)} |"
)
lines.append('')
for page, title_text, family in family_specs:
if not isinstance(family, dict):
continue
periods = _list(family.get('periods'))
if not periods:
continue
lines.extend([
f'### p{page} {title_text}',
'',
'| 序号 | 主星/星座 | 起始 | 结束 | 年数 |',
'|------|-----------|------|------|------|',
])
for index, period in enumerate(periods, start=1):
if not isinstance(period, dict):
continue
raw = _dict(period.get('raw_period'))
lord = period.get('lord') or period.get('planet') or period.get('sign') or period.get('rashi') or raw.get('lord') or raw.get('planet') or raw.get('sign') or raw.get('rashi')
lines.append(
f"| {index} | {_cell(_token(lord))} | {_period_start(period) or _cell(raw.get('start_date') or raw.get('start'))} | "
f"{_period_end(period) or _cell(raw.get('end_date') or raw.get('end'))} | {_period_years(period)} |"
)
child_rows = []
for period in periods:
if not isinstance(period, dict):
continue
raw = _dict(period.get('raw_period'))
for key in ('antardasha_periods', 'antardasha', 'antardashas', 'children'):
rows = _list(period.get(key))
if not rows:
rows = _list(raw.get(key))
for row in rows:
if not isinstance(row, dict):
continue
identity = (
str(row.get('main') or row.get('mahadasha_lord') or row.get('parent_lord') or ''),
str(row.get('lord') or row.get('sub') or row.get('antardasha_lord') or row.get('planet') or ''),
str(_period_start(row)),
str(_period_end(row)),
)
if any(
identity == (
str(existing.get('main') or existing.get('mahadasha_lord') or existing.get('parent_lord') or ''),
str(existing.get('lord') or existing.get('sub') or existing.get('antardasha_lord') or existing.get('planet') or ''),
str(_period_start(existing)),
str(_period_end(existing)),
)
for existing in child_rows
):
continue
child_rows.append(row)
if child_rows:
lines.extend(['', '| AD/child | 主层 | 子层 | 起始 | 结束 |', '|----------|------|------|------|------|'])
for child_index, child in enumerate(child_rows, start=1):
child_raw = _dict(child.get('raw_period'))
main_lord = (
child.get('main')
or child.get('mahadasha_lord')
or child.get('parent_lord')
or child.get('parent_sign')
or child_raw.get('main')
or child_raw.get('parent_sign')
or '-'
)
sub_lord = (
child.get('lord')
or child.get('sub')
or child.get('antardasha_lord')
or child.get('planet')
or child.get('sign')
or child_raw.get('lord')
or child_raw.get('planet')
or child_raw.get('sign')
)
lines.append(
f"| {child_index} | {_cell(_token(main_lord))} | {_cell(_token(sub_lord))} | "
f"{_period_start(child) or _cell(child_raw.get('start_date') or child_raw.get('start'))} | "
f"{_period_end(child) or _cell(child_raw.get('end_date') or child_raw.get('end'))} |"
)
lines.append('')
source_dasha_pages = _list(source_visible.get('dasha_pages'))
if source_dasha_pages:
source_titles = {
92: 'Sthira Dasha',
93: 'Niryaana Shoola Dasha',
94: 'Niryaana Shoola Dasha',
95: 'Drig Dasha',
96: 'Drig Dasha',
97: 'Navamsha Dasha',
98: 'Navamsha Dasha',
99: 'Narayana Dasha',
100: 'Narayana Dasha',
101: 'Lagna Kendradi Rashi Dasha',
102: 'Lagna Kendradi Rashi Dasha',
103: 'Shoola Dasha',
}
lines.extend([
'## 原版 Dasha 日期表(同案 PL9 数据)',
'',
'以下表格保留同一命盘原版报告中的完整可见日期和星座边界,便于逐项读取与校对。',
'',
])
for source_page in source_dasha_pages:
if not isinstance(source_page, dict):
continue
page_number = int(source_page.get('page_number') or 0)
groups = _list(source_page.get('groups'))
if not groups:
continue
lines.extend([
f"### p{page_number} {source_titles.get(page_number, 'Dasha')}",
'',
'| 主运 | 起始年龄 | 子段 | 起始 | 结束 |',
'|------|----------|------|------|------|',
])
for group in groups:
if not isinstance(group, dict):
continue
group_title = group.get('title') or ''
annotations = _list(group.get('title_annotations'))
age = annotations[0] if annotations else ''
group_rows = _list(group.get('rows'))
boundaries = [
str((_dict(row.get('cells')).get('Boundary')) or '')
for row in group_rows
if isinstance(row, dict)
]
for index, row in enumerate(group_rows):
if not isinstance(row, dict):
continue
cells = _dict(row.get('cells'))
beginning = cells.get('Boundary') or ''
ending = boundaries[index + 1] if index + 1 < len(boundaries) else group.get('period_end') or ''
lines.append(
f"| {_cell(group_title)} | {_cell(age)} | {_cell(cells.get('Period'))} | "
f"{_cell(beginning)} | {_cell(ending)} |"
)
lines.append('')
kp = advanced.get('kp') if isinstance(advanced.get('kp'), dict) else {}
yogas_doshas = advanced.get('yogas_doshas') if isinstance(advanced.get('yogas_doshas'), dict) else {}
if kp or yogas_doshas.get('sade_sati'):
lines.extend(['## Saturn 与 KP 支持页(PL9 p121-p125 对标)', ''])
sade_sati = _dict(yogas_doshas.get('sade_sati'))
if sade_sati:
phases = _dict(sade_sati.get('phases'))
dosha_pack = _dict(advanced.get('dosha_remedy_pack'))
saturn_status = _dict(dosha_pack.get('saturn_status'))
timeline = _dict(saturn_status.get('cycle_timeline'))
display_rows = _dict(timeline.get('pl9_display_rows'))
sadhesati_rows = _list(timeline.get('sadhesati_rows')) or _list(display_rows.get('sadhesati'))
# Customer tables retain computed boundaries and reference points;
# print-layout rows deliberately blank repeated values.
kantaka_rows = _list(timeline.get('kantaka_rows')) or _list(display_rows.get('kantaka'))
lines.extend([
'### p121-p122 Sade Sati / Dhayya / Kantaka Saturn',
'',
f"- Moon sign:{_cell(_token(sade_sati.get('moon_sign')))}",
f"- 当前阶段:{_cell(sade_sati.get('phase') or sade_sati.get('status'))}",
f"- 是否 active:{_cell(sade_sati.get('is_active'))}",
f"- 摘要:{_cell(sade_sati.get('summary'))}",
'',
'| Phase | Sign |',
'|-------|------|',
f"| rising | {_cell(_token(phases.get('rising')))} |",
f"| peak | {_cell(_token(phases.get('peak')))} |",
f"| setting | {_cell(_token(phases.get('setting')))} |",
'',
])
if sadhesati_rows:
lines.extend([
'#### p121 Results of the Dhayya of Sadhesati',
'',
'| Cycle / phase | Transit of Saturn | Beginning date | Ending date | Duration Yr-Mn-Dy | Saturn AV | Sarva AV |',
'|---------------|-------------------|----------------|-------------|-------------------|-----------|----------|',
])
for item in sadhesati_rows:
if not isinstance(item, dict):
continue
lines.append(
f"| {_cell(item.get('cycle_phase'))} | {_cell(item.get('transit_sign'))} | {_cell(item.get('beginning_date'))} | "
f"{_cell(item.get('ending_date'))} | {_cell(item.get('duration'))} | {_cell(item.get('saturn_av'))} | {_cell(item.get('sarva_av'))} |"
)
lines.append('')
if kantaka_rows:
lines.extend([
'#### p122 Results of Laghu Kalyani Dhayya and Kantaka Saturn',
'',
'| Cycle / phase | Transit of Saturn | Beginning date | Ending date | Duration Yr-Mn-Dy | Ashtakavarga |',
'|---------------|-------------------|----------------|-------------|-------------------|--------------|',
])
for item in kantaka_rows:
if not isinstance(item, dict):
continue
av_value = ' '.join(str(value) for value in (item.get('saturn_av'), item.get('sarva_av')) if value not in (None, ''))
lines.append(
f"| {_cell(item.get('cycle_phase'))} | {_cell(item.get('transit_sign'))} | {_cell(item.get('beginning_date'))} | "
f"{_cell(item.get('ending_date'))} | {_cell(item.get('duration'))} | {_cell(av_value)} |"
)
lines.append('')
if kp:
lines.extend([
'### p123 KP Chart / Cusps',
'',
f"- House basis:{_cell(kp.get('house_basis'))}",
'',
'| House | Sign | Cusp longitude | KP lord chain |',
'|-------|------|----------------|---------------|',
])
houses = _dict(kp.get('houses'))
for house in [str(i) for i in range(1, 13)]:
row = _dict(houses.get(house))
if not row:
continue
lines.append(
f"| {house} | {_cell(_token(row.get('sign')))} | {_num(row.get('cusp_longitude'), 4)} | {_cell(_lord_chain(row.get('kp_lords')))} |"
)
lines.extend(['', '### p124 KP Significators / Planet Lords', '', '| Planet | KP lord chain | A | B | C | D |', '|--------|---------------|---|---|---|---|'])
planets = _dict(kp.get('planets'))
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'):
row = _dict(planets.get(planet))
if not row:
continue
sig = _dict(row.get('significators'))
lines.append(
f"| {_cell(_token(planet))} | {_cell(_lord_chain(row.get('kp_lords')))} | {_cell(sig.get('A'))} | {_cell(sig.get('B'))} | {_cell(sig.get('C'))} | {_cell(sig.get('D'))} |"
)
ruling = _dict(kp.get('ruling_planets'))
if ruling:
lines.extend(['', '### p125 Natal / KP Ruling Planets', '', '| Field | Planet |', '|-------|--------|'])
for key in ('day_lord', 'moon_sign_lord', 'moon_star_lord', 'moon_sub_lord', 'ascendant_sign_lord', 'ascendant_star_lord', 'ascendant_sub_lord'):
lines.append(f"| {_cell(key)} | {_cell(_token(ruling.get(key)))} |")
lines.append('')
kp_source_cells = _dict(source_visible.get('kp_p124_visual_cells'))
if kp_source_cells:
lines.extend(['', '### p124 KP 原版细格', ''])
for table_key, title, columns in (
('house_significations', 'House significators', ('house', 'planets_in_nak_of_occupants', 'occupants', 'planets_in_nak_of_cusp_sign_lord', 'cusp_sign_lord')),
('planet_significations', 'Planet significators', ('planet', 'very_strong', 'strong', 'normal', 'weak')),
('ruling_planets', 'Ruling planets', ()),
):
table = _dict(kp_source_cells.get(table_key))
rows = _list(table.get('rows'))
if not rows:
fields = _dict(table.get('fields'))
rows = [{'field': key, 'value': value} for key, value in fields.items()]
if not rows:
continue
lines.extend([f'#### {title}', ''])
if table_key == 'ruling_planets':
lines.extend(['| Field | Value |', '|-------|-------|'])
for row in rows:
lines.append(f"| {_cell(row.get('field'))} | {_cell(row.get('value'))} |")
else:
available = [key for key in columns if any(row.get(key) not in (None, '') for row in rows)]
if not available:
continue
lines.extend([
'| ' + ' | '.join(available) + ' |',
'|' + '|'.join('---' for _ in available) + '|',
])
for row in rows:
lines.append('| ' + ' | '.join(_cell(row.get(key)) for key in available) + ' |')
lines.append('')
annual = timing.get('annual_tajika_pack') if isinstance(timing.get('annual_tajika_pack'), dict) else {}
if annual:
lines.extend(['## 年度图盘与 Tajika(PL9 p126-p155 对标)', ''])
annual_chart = _dict(annual.get('annual_chart'))
annual_chart_data = _dict(annual_chart.get('data'))
solar_return = _dict(annual.get('solar_return'))
solar_return_data = _dict(solar_return.get('data') or solar_return)
if solar_return_data or annual_chart_data:
lines.extend([
'### p126-p127 Varshaphala 基础资料与年度行星表',
'',
f"- Varshapravesha:{solar_return_data.get('dt_local') or solar_return_data.get('local_time') or solar_return_data.get('date') or '-'}",
f"- 年返上升:{_token(annual_chart_data.get('asc_sign'))}",
'',
])
year_lord = _dict(annual.get('year_lord'))
year_lord_data = _dict(year_lord.get('data') or year_lord)
if year_lord_data:
candidates = _list(year_lord_data.get('candidates'))
lines.extend([
'#### Varshesha / Year Lord',
'',
'| Field | Value |',
'|-------|-------|',
f"| Varshesha | {_cell(_token(year_lord_data.get('year_lord') or year_lord_data.get('lord')))} |",
f"| Selection basis | {_cell(year_lord_data.get('selection_basis'))} |",
f"| Annual ascendant sign | {_cell(_token(year_lord_data.get('annual_asc_sign')))} |",
f"| Muntha sign | {_cell(_token(year_lord_data.get('muntha_sign')))} |",
])
if candidates:
lines.append(f"| Candidate count | {_cell(len(candidates))} |")
lines.append('')
if candidates:
lines.extend([
'| Candidate source | Planet | House | Sign | Panchavargiya score |',
'|------------------|--------|-------|------|---------------------|',
])
for row in candidates:
if not isinstance(row, dict):
continue
lines.append(
f"| {_cell(row.get('source'))} | {_cell(_token(row.get('planet')))} | {_cell(row.get('planet_house'))} | "
f"{_cell(_token(row.get('planet_sign')))} | {_cell(row.get('panchavargiya_score'))} |"
)
lines.append('')
planet_longitudes = _dict(annual_chart_data.get('planet_longitudes'))
if planet_longitudes:
signs = globals().get('SIGNS', [])
lines.extend([
'| Planet | Sign | Degree | Longitude |',
'|--------|------|--------|-----------|',
])
if annual_chart_data.get('asc_sign') is not None:
lines.append(f"| Lagna | {_cell(_token(annual_chart_data.get('asc_sign')))} | - | - |")
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'):
longitude = planet_longitudes.get(planet)
if not isinstance(longitude, (int, float)):
continue
sign = signs[int(float(longitude) // 30) % 12] if signs else '-'
lines.append(
f"| {_cell(_token(planet))} | {_cell(_token(sign))} | {_cell(_degree(float(longitude) % 30))} | "
f"{_cell(_degree(longitude))} |"
)
lines.append('')
current_year = _dict(annual.get('pl9_current_year_results'))
current_sections = _list(current_year.get('sections'))
if current_sections:
lines.extend([
'### p139-p141 Varshaphala 年度结果解释',
'',
'| 项目 | 锚点 | 年度主题 |',
'|------|------|----------|',
])
for section in current_sections:
if not isinstance(section, dict):
continue
anchor = _dict(section.get('anchor'))
anchor_text = ', '.join(
f"{_token(key)}={_token(value)}"
for key, value in anchor.items()
if key != 'field_status' and value not in (None, '')
)
lines.append(
f"| {_cell(section.get('title'))} | {_cell(anchor_text)} | "
f"{_cell(_brief(section.get('themes'), 3))} |"
)
lines.append('')
annual_dasha = _dict(annual.get('pl9_annual_dasha_results'))
dasha_rows = _list(annual_dasha.get('rows'))
if dasha_rows:
lines.extend([
'### p144-p145 Annual Dasha 结果解释',
'',
'| 阶段 | 主星 | 年盘宫位 | 日期边界 | 主题 |',
'|------|------|----------|----------|------|',
])
for row in dasha_rows:
if not isinstance(row, dict):
continue
boundary = f"{row.get('start')} - {row.get('end')}" if row.get('start') and row.get('end') else row.get('start') or '-'
lines.append(
f"| {_cell(row.get('order'))} | {_cell(_token(row.get('lord')))} | {_cell(row.get('annual_whole_sign_house'))} | "
f"{_cell(boundary)} | {_cell(_brief(row.get('themes'), 2))} |"
)
lines.append('')
normalized = _dict(annual.get('normalized_tables'))
mudda_rows = _list(normalized.get('mudda_rows'))
if mudda_rows:
lines.extend([
'### p132-p135 Mudda / Patyayini 年度 Dasha 表',
'',
'| balance profile | 主星 | 开始 | 结束 | 边界口径 |',
'|-----------------|------|------|------|----------|',
])
for row in mudda_rows:
if not isinstance(row, dict):
continue
lines.append(
f"| {_cell(row.get('balance_profile'))} | {_cell(_token(row.get('lord')))} | "
f"{_cell(row.get('start'))} | {_cell(row.get('end'))} | {_public_status_note(row.get('boundary_status'))} |"
)
lines.append('')
patyayini_rows = _list(normalized.get('patyayini_rows'))
if patyayini_rows:
lines.extend([
'### p134 Patyayini / Patyamsha ending-date 表',
'',
'| 主层 | 子层 | 边界 | 边界语义 |',
'|------|------|------|----------|',
])
for row in patyayini_rows:
if not isinstance(row, dict):
continue
lines.append(
f"| {_cell(_token(row.get('main')))} | {_cell(_token(row.get('sub')))} | "
f"{_cell(row.get('boundary'))} | {_public_status_note(row.get('boundary_status'))} |"
)
lines.append('')
tajika_rows = _list(normalized.get('tajika_yoga_rows')) or _list(normalized.get('pyjhora_tajika_yoga_rows'))
if tajika_rows:
lines.extend([
'### p136-p138 Tajika Yoga Presence 对照表',
'',
'| Yoga | 结果 | 形成行星 | 相位运动(入相/分离) |',
'|------|------|----------|----------------------|',
])
for row in tajika_rows[:32]:
if not isinstance(row, dict):
continue
forming = row.get('forming_planets') or row.get('planets') or row.get('raw_relationships')
lines.append(
f"| {_cell(row.get('yoga') or row.get('name'))} | {_cell(_public_report_text(row.get('observation_status') or row.get('condition_status') or row.get('present')))} | "
f"{_cell(forming)} | {_cell({'applying': '入相', 'separating': '分离'}.get(str(row.get('motion') or row.get('aspect_motion') or '-'), row.get('motion') or row.get('aspect_motion') or '-'))} |"
)
lines.append('')
saham_rows = _list(normalized.get('saham_rows'))
if saham_rows:
lines.extend([
'### p130 Sahams 点位表',
'',
'| Saham | 星座 | 星座内度数 |',
'|-------|------|------------|',
])
for row in saham_rows[:48]:
if not isinstance(row, dict):
continue
lines.append(
f"| {_cell(row.get('name') or row.get('saham'))} | {_cell(_token(row.get('sign')))} | "
f"{_cell(_degree(row.get('degree_in_sign')))} |"
)
lines.append('')
public_replay = _dict(annual.get('public_panchavargiya_replay'))
replay_coverage = _dict(public_replay.get('coverage'))
if public_replay:
lines.extend([
'### 公开 Panchavargiya 数值复核边界',
'',
'| 项目 | 结果 |',
'|------|------|',
f"| 公开复核状态 | {_cell(public_replay.get('status'))} |",
f"| 覆盖行星 | {_cell(replay_coverage.get('planet_count'))} |",
f"| 已匹配分量 | {_cell(replay_coverage.get('exact_component_match_count'))} / {_cell(replay_coverage.get('component_count'))} |",
f"| 已匹配总分 | {_cell(replay_coverage.get('exact_total_match_count'))} / {_cell(replay_coverage.get('planet_count'))} |",
f"| 可直接用于公式升级 | {_cell(public_replay.get('promotion_allowed'))} |",
'',
])
annual_extra_specs = (
('annual_anchor_rows', 'p126 年度锚点', ('name', 'label', 'value', 'sign', 'degree', 'house')),
('annual_planet_rows', 'p127 年度行星资料', ('planet', 'sign', 'degree', 'longitude', 'nakshatra', 'pada')),
('true_solar_return_rows', 'True Solar Return', ('field', 'value', 'date', 'time', 'dt_local')),
('panchadhikari_rows', 'Panchadhikari', ('planet', 'role', 'value', 'sign', 'house')),
('tripataki_placement_rows', 'Tripataki Placement', ('planet', 'sign', 'house', 'degree', 'placement')),
('tripataki_vedha_rows', 'Tripataki Vedha', ('planet', 'target', 'sign', 'house', 'result')),
('current_year_result_rows', 'p139-p141 年度结果', ('title', 'value', 'theme', 'summary', 'anchor')),
('tajika_presence_rows', 'Tajika Presence', ('yoga', 'name', 'present', 'forming_planets')),
('tajika_condition_status_rows', 'Tajika Condition', ('yoga', 'name', 'condition', 'present')),
('tajika_forming_pair_rows', 'Tajika Forming Pair', ('yoga', 'name', 'planet_a', 'planet_b', 'forming_planets')),
('tajika_strength_rows', 'Tajika Strength', ('planet', 'yoga', 'strength', 'value')),
('raman_saham_rows', 'Raman Sahams', ('name', 'saham', 'sign', 'degree_in_sign', 'degree')),
)
for table_key, title, preferred_keys in annual_extra_specs:
rows = _list(normalized.get(table_key))
if not rows:
continue
scalar_rows = [row for row in rows if isinstance(row, dict)]
if table_key == 'true_solar_return_rows':
hidden_fields = {'error_deg', 'iterations', 'method', 'raw_root', 'status'}
scalar_rows = [
row for row in scalar_rows
if str(row.get('field') or '').strip() not in hidden_fields
]
if not scalar_rows:
continue
keys = [key for key in preferred_keys if any(row.get(key) not in (None, '') for row in scalar_rows)]
if not keys:
keys = [
key for key in scalar_rows[0]
if key not in {'source_path', 'execution_status', 'confidence_status', 'boundary_status', 'status', 'selection_status'}
and not isinstance(scalar_rows[0].get(key), (dict, list, tuple))
][:8]
if not keys:
continue
lines.extend([
f'### {title}',
'',
'| ' + ' | '.join(_cell(key) for key in keys) + ' |',
'|' + '|'.join('---' for _key in keys) + '|',
])
for row in scalar_rows:
lines.append('| ' + ' | '.join(_cell(row.get(key)) for key in keys) + ' |')
lines.append('')
annual_special = _dict(annual.get('annual_special_points'))
annual_special_data = _dict(annual_special.get('data'))
if annual_special_data:
lines.extend([
'### 年度特殊点',
'',
'| Point | Sign | Degree | Longitude | Lord |',
'|-------|------|--------|-----------|------|',
])
for name, row in annual_special_data.items():
if not isinstance(row, dict):
continue
lines.append(
f"| {_cell(row.get('name') or name)} | {_cell(_token(row.get('sign')))} | "
f"{_cell(_degree(row.get('degree_in_sign')))} | {_cell(_degree(row.get('longitude')))} | "
f"{_cell(_token(row.get('lord')))} |"
)
lines.append('')
tripataki = _dict(annual.get('tripataki_chakra'))
tripataki_placements = _list(tripataki.get('placements'))
tripataki_vedha = _list(tripataki.get('vedha_rows'))
if tripataki_placements:
lines.extend([
'### Tripataki Chakra 年盘落点',
'',
'| Body | Sign | Degree | Longitude |',
'|------|------|--------|-----------|',
])
for row in tripataki_placements:
if not isinstance(row, dict):
continue
lines.append(
f"| {_cell(row.get('body'))} | {_cell(_token(row.get('sign')))} | "
f"{_cell(_degree(row.get('degree_in_sign')))} | {_cell(_degree(row.get('longitude')))} |"
)
lines.append('')
if tripataki_vedha:
lines.extend([
'| Target | Vedha sources |',
'|--------|---------------|',
])
for row in tripataki_vedha:
if not isinstance(row, dict):
continue
sources = row.get('sources') if isinstance(row.get('sources'), list) else []
lines.append(f"| {_cell(row.get('target'))} | {_cell('、'.join(map(str, sources)))} |")
lines.append('')
monthly_pack = _dict(annual.get('monthly_chart_pack'))
monthly_rows = _list(monthly_pack.get('rows'))
if monthly_rows:
lines.extend([
'### 年度月返照图盘数据',
'',
'| 月份 | 本地时刻 | 年盘上升 | 上升度数 | 行星黄经(度) |',
'|------|----------|----------|----------|-----------------|',
])
for row in monthly_rows:
if not isinstance(row, dict):
continue
asc = _dict(row.get('ascendant'))
longitudes = _dict(row.get('planet_longitudes'))
longitude_text = ';'.join(
f"{_token(planet)} {_degree(value)}"
for planet, value in longitudes.items()
if isinstance(value, (int, float))
)
lines.append(
f"| {_cell(row.get('month_index'))} | {_cell(row.get('local_datetime'))} | "
f"{_cell(_token(asc.get('sign')))} | {_cell(_degree(asc.get('degree_in_sign') or asc.get('degree')))} | "
f"{_cell(longitude_text)} |"
)
lines.append('')
lifetime_pack = _dict(annual.get('lifetime_annual_chart_pages'))
lifetime_pages = _list(lifetime_pack.get('pages'))
lifetime_rows = [
row for page in lifetime_pages if isinstance(page, dict)
for row in _list(page.get('rows')) if isinstance(row, dict)
]
if lifetime_rows:
lines.extend([
'### 生命周期年度返照图盘数据',
'',
'| 年龄 | 年份 | 年返时刻 | 上升 | 上升度数 | 行星黄经(度) |',
'|------|------|----------|------|----------|-----------------|',
])
for row in lifetime_rows:
asc = _dict(row.get('ascendant'))
longitudes = _dict(row.get('planet_longitudes'))
longitude_text = ';'.join(
f"{_token(planet)} {_degree(value)}"
for planet, value in longitudes.items()
if isinstance(value, (int, float))
)
lines.append(
f"| {_cell(row.get('completed_age'))} | {_cell(row.get('target_year'))} | "
f"{_cell(row.get('local_datetime'))} | {_cell(_token(asc.get('sign')))} | "
f"{_cell(_degree(asc.get('degree_in_sign') or asc.get('degree')))} | {_cell(longitude_text)} |"
)
lines.append('')
annual_series = _dict(annual.get('annual_chart_series_pack'))
for series_key, title in (
('birth_place_eight_year_overview', '出生地八年年度总览'),
('local_eight_year_overview', '现居地八年年度总览'),
):
series = _dict(annual_series.get(series_key))
series_rows = _list(series.get('rows'))
if not series_rows:
continue
lines.extend([
f'### {title}',
'',
'| 年份 | 年龄 | 年返时刻 | 年盘上升 |',
'|------|------|----------|----------|',
])
for row in series_rows:
if not isinstance(row, dict):
continue
lines.append(
f"| {_cell(row.get('target_year'))} | {_cell(row.get('age'))} | "
f"{_cell(row.get('solar_return_local') or row.get('local_datetime'))} | "
f"{_cell(_token(row.get('annual_ascendant')))} |"
)
lines.append('')
annual_visual = annual.get('pl9_annual_visual_panels') if isinstance(annual.get('pl9_annual_visual_panels'), dict) else {}
panels = annual_visual.get('panels') if isinstance(annual_visual.get('panels'), list) else []
if panels:
lines.extend(['### p139-p155 年度北印度图盘', ''])
for panel in panels:
if not isinstance(panel, dict):
continue
lines.append(f"##### p{panel.get('pl9_page')} · {panel.get('chart_label') or panel.get('panel_id')}")
lines.append('')
annual_profile = _dict(annual.get('profile'))
primary_target_year = str(annual_profile.get('target_year') or '') if annual_profile else ''
annual_series = _dict(timing.get('annual_tajika_series'))
annual_series_years = _dict(annual_series.get('years'))
future_year_items = [
(year, _dict(pack))
for year, pack in sorted(annual_series_years.items(), key=lambda item: str(item[0]))
if str(year) and str(year) != primary_target_year and isinstance(pack, dict)
]
for year, annual_pack in future_year_items:
annual_chart = _dict(annual_pack.get('annual_chart'))
annual_chart_data = _dict(annual_chart.get('data'))
solar_return = _dict(annual_pack.get('solar_return'))
solar_return_data = _dict(solar_return.get('data') or solar_return)
lines.extend([f'### {year} 年度 Varshaphala / Tajika 细项', ''])
if solar_return_data or annual_chart_data:
lines.extend([
'| 项目 | 数值 |',
'|------|------|',
f"| Varshapravesha | {_cell(solar_return_data.get('dt_local') or solar_return_data.get('local_time') or solar_return_data.get('date') or '-')} |",
f"| 年返上升 | {_cell(_token(annual_chart_data.get('asc_sign')))} |",
])
year_lord = _dict(annual_pack.get('year_lord'))
year_lord_data = _dict(year_lord.get('data') or year_lord)
if year_lord_data:
lines.extend([
f"| Varshesha | {_cell(_token(year_lord_data.get('year_lord') or year_lord_data.get('lord')))} |",
f"| Muntha sign | {_cell(_token(year_lord_data.get('muntha_sign')))} |",
])
lines.append('')
planet_longitudes = _dict(annual_chart_data.get('planet_longitudes'))
if planet_longitudes:
signs = globals().get('SIGNS', [])
lines.extend([
'#### 年度行星资料',
'',
'| 行星 | 星座 | 落座度数 | 黄经 |',
'|------|------|----------|------|',
])
if annual_chart_data.get('asc_sign') is not None:
lines.append(f"| Lagna | {_cell(_token(annual_chart_data.get('asc_sign')))} | - | - |")
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'):
longitude = planet_longitudes.get(planet)
if not isinstance(longitude, (int, float)):
continue
sign = signs[int(float(longitude) // 30) % 12] if signs else '-'
lines.append(
f"| {_cell(_token(planet))} | {_cell(_token(sign))} | {_cell(_degree(float(longitude) % 30))} | "
f"{_cell(_degree(longitude))} |"
)
lines.append('')
annual_dasha = _dict(annual_pack.get('pl9_annual_dasha_results'))
dasha_rows = _list(annual_dasha.get('rows'))
if dasha_rows:
lines.extend([
'#### Annual Dasha 阶段表',
'',
'| 阶段 | 主星 | 年盘宫位 | 日期边界 | 主题 |',
'|------|------|----------|----------|------|',
])
for row in dasha_rows:
if not isinstance(row, dict):
continue
boundary = f"{row.get('start')} - {row.get('end')}" if row.get('start') and row.get('end') else row.get('start') or '-'
lines.append(
f"| {_cell(row.get('order'))} | {_cell(_token(row.get('lord')))} | {_cell(row.get('annual_whole_sign_house'))} | "
f"{_cell(boundary)} | {_cell(_brief(row.get('themes'), 2))} |"
)
lines.append('')
normalized = _dict(annual_pack.get('normalized_tables'))
future_specs = (
('mudda_rows', 'Mudda / Patyayini 年度 Dasha 表', ('balance_profile', 'lord', 'start', 'end', 'boundary_status')),
('patyayini_rows', 'Patyayini / Patyamsha ending-date 表', ('main', 'sub', 'boundary', 'boundary_status')),
('saham_rows', 'Sahams 点位表', ('name', 'saham', 'sign', 'degree_in_sign')),
('tajika_yoga_rows', 'Tajika Yoga Presence 表', ('yoga', 'name', 'present', 'forming_planets')),
('pyjhora_tajika_yoga_rows', 'Tajika Yoga Presence 表', ('yoga', 'name', 'present', 'forming_planets')),
('annual_planet_rows', '年度行星扩展字段', ('planet', 'sign', 'degree', 'longitude', 'nakshatra', 'pada')),
('annual_anchor_rows', '年度锚点', ('name', 'label', 'value', 'sign', 'degree', 'house')),
)
rendered_titles = set()
for table_key, title, preferred_keys in future_specs:
rows = [row for row in _list(normalized.get(table_key)) if isinstance(row, dict)]
if not rows:
continue
if title in rendered_titles and table_key == 'pyjhora_tajika_yoga_rows':
continue
rendered_titles.add(title)
keys = [key for key in preferred_keys if any(row.get(key) not in (None, '') for row in rows)]
if not keys:
keys = [
key for key in rows[0]
if key not in {'source_path', 'execution_status', 'confidence_status', 'boundary_status', 'status', 'selection_status'}
and not isinstance(rows[0].get(key), (dict, list, tuple))
][:6]
if not keys:
continue
lines.extend([
f'#### {title}',
'',
'| ' + ' | '.join(_cell(key) for key in keys) + ' |',
'|' + '|'.join('---' for _key in keys) + '|',
])
for row in rows:
lines.append('| ' + ' | '.join(_cell(row.get(key)) for key in keys) + ' |')
lines.append('')
special = _dict(annual_pack.get('annual_special_points'))
special_data = _dict(special.get('data'))
if special_data:
lines.extend([
'#### 年度特殊点',
'',
'| 点位 | 星座 | 落座度数 | 黄经 | 守护星 |',
'|------|------|----------|------|--------|',
])
for name, row in special_data.items():
if not isinstance(row, dict):
continue
lines.append(
f"| {_cell(row.get('name') or name)} | {_cell(_token(row.get('sign')))} | "
f"{_cell(_degree(row.get('degree_in_sign')))} | {_cell(_degree(row.get('longitude')))} | "
f"{_cell(_token(row.get('lord')))} |"
)
lines.append('')
monthly_pack = _dict(annual_pack.get('monthly_chart_pack'))
monthly_rows = _list(monthly_pack.get('rows'))
if monthly_rows:
lines.extend([
'#### 年度月返照图盘数据',
'',
'| 月份 | 本地时刻 | 年盘上升 | 上升度数 |',
'|------|----------|----------|----------|',
])
for row in monthly_rows:
if not isinstance(row, dict):
continue
asc = _dict(row.get('ascendant'))
lines.append(
f"| {_cell(row.get('month_index'))} | {_cell(row.get('local_datetime'))} | "
f"{_cell(_token(asc.get('sign')))} | {_cell(_degree(asc.get('degree_in_sign') or asc.get('degree')))} |"
)
lines.append('')
d1_sheet = worksheets.get('d1_rasi_bhava') if isinstance(worksheets.get('d1_rasi_bhava'), dict) else {}
d1_houses = _dict(d1_sheet.get('houses')) or _dict(core_chart.get('houses'))
d1_planets = _dict(d1_sheet.get('planets')) or _dict(core_chart.get('planets'))
advanced_yoga = advanced.get('yoga') if isinstance(advanced.get('yoga'), dict) else {}
detected_yogas = _list(advanced_yoga.get('detected_yogas')) or _list(advanced_yoga.get('yogas'))
dosha_pack = _dict(advanced.get('dosha_remedy_pack'))
bhavesh_pack = _dict(advanced.get('bhavesh_interpretation_pack'))
if d1_houses or yogas_doshas or detected_yogas or bhavesh_pack or dosha_pack:
lines.extend(['## Bhavesh、Dosha 与 Yoga 解释页(PL9 p156-p189 对标)', ''])
if d1_houses and d1_planets:
lines.extend([
'### p156-p162 宫主(Bhavesh)落宫解释',
'',
'| House | Sign | Lord | Lord placement | Lord dignity | Dasha activation |',
'|-------|------|------|----------------|--------------|------------------|',
])
active_lords = set()
current_dasha = _dict(timing.get('dasha')).get('current_dasha') if isinstance(timing.get('dasha'), dict) else {}
if isinstance(current_dasha, dict) and current_dasha.get('lord'):
active_lords.add(str(current_dasha.get('lord')))
for ad in _list(current_dasha.get('antardasha_timeline')):
if isinstance(ad, dict) and ad.get('is_current') and ad.get('lord'):
active_lords.add(str(ad.get('lord')))
for index in range(1, 13):
house = _dict(d1_houses.get(f'house_{index}') or d1_houses.get(str(index)))
lord = house.get('lord')
lord_row = _dict(d1_planets.get(str(lord)))
placement = '-'
if lord_row:
placement = f"{_token(lord_row.get('sign'))} H{lord_row.get('house') or '-'} {_degree(lord_row.get('degree_in_sign'))}"
lines.append(
f"| {index} | {_cell(_token(house.get('cusp_sign') or house.get('sign')))} | {_cell(_token(lord))} | "
f"{_cell(placement)} | {_cell(lord_row.get('status') or lord_row.get('dignity'))} | {_cell('active' if str(lord) in active_lords else 'background')} |"
)
lines.append('')
bhavesh_pages = _list(bhavesh_pack.get('pages'))
if bhavesh_pages:
lines.extend([
'### Bhavesh 宫主逐宫解释',
'',
])
for row in bhavesh_pages:
if not isinstance(row, dict):
continue
sections = _dict(row.get('sections'))
title = f"第{row.get('house')}宫主:{_token(row.get('lord'))}"
lines.extend([f'#### {title}', ''])
for key in ('house_context', 'lord_placement', 'result', 'context', 'activation'):
value = str(sections.get(key) or '').strip()
if value:
lines.extend([_public_report_text(value), ''])
if yogas_doshas:
lines.extend([
'### p163-p171 Mangala / Sade Sati / Dosha Results',
'',
'| Topic | Detected / phase | Key facts | Summary |',
'|-------|------------------|-----------|---------|',
])
for key, label in (
('mangal_dosha', 'Mangal / Kuja Dosha'),
('kaal_sarp_dosha', 'Kaal Sarp Dosha'),
('pitra_dosha', 'Pitra Dosha'),
('sade_sati', 'Sade Sati'),
):
row = _dict(yogas_doshas.get(key))
if not row:
continue
detected = row.get('has_dosha')
if detected is None:
detected = row.get('is_active') if row.get('is_active') is not None else row.get('phase')
fact_bits = []
for fact_key in ('mars_house', 'mars_sign', 'sun_house', 'sun_sign', 'rahu_house', 'ketu_house', 'moon_sign', 'severity', 'type'):
if row.get(fact_key) not in (None, ''):
fact_bits.append(f"{fact_key}={_token(row.get(fact_key))}")
lines.append(
f"| {_cell(label)} | {_cell(detected)} | {_cell(';'.join(map(str, fact_bits)) or '-')} | {_cell(row.get('summary'))} |"
)
lines.append('')
dosha_rows = _list(dosha_pack.get('dosha_rows'))
if dosha_rows:
lines.extend([
'### Dosha 逐项事实与解释',
'',
])
for row in dosha_rows:
if not isinstance(row, dict):
continue
title = str(row.get('title') or row.get('id') or '').strip()
sections = _dict(row.get('sections'))
if not title:
continue
lines.extend([f'#### {title}', ''])
for key in ('detected', 'consideration', 'facts', 'result', 'cancellation'):
value = str(sections.get(key) or '').strip()
if value:
lines.extend([_public_report_text(value), ''])
practices = _dict(dosha_pack.get('supportive_practices'))
recommendations = _list(practices.get('recommendations'))
if recommendations:
lines.extend([
'### 传统辅助建议',
'',
'| 对象 | 建议 | 宝石/次数 |',
'|------|------|-----------|',
])
for row in recommendations:
if not isinstance(row, dict):
continue
actions = row.get('actions') if isinstance(row.get('actions'), list) else []
advice = row.get('note') or ';'.join(str(item) for item in actions if item)
lines.append(
f"| {_cell(_token(row.get('planet') or row.get('dosha')))} | "
f"{_cell(_public_report_text(advice))} | "
f"{_cell(row.get('gem') or row.get('repetitions') or ('No gem or repetition count supplied' if _pl9_report_language(packet) == 'en' else '源数据未提供宝石或次数'))} |"
)
lines.append('')
if detected_yogas:
lines.extend([
'### p172-p189 Applicable Benefic / Mixed / Malefic Yogas',
'',
f"- Yoga detected count:{len(detected_yogas)}",
f"- Context layers:{_cell(_brief(advanced_yoga.get('context_layers'), 6))}",
'',
])
def _yoga_group_label(row: dict) -> str:
category = str(row.get('category') or row.get('subtype') or '').lower()
name = str(row.get('name') or row.get('name_cn') or row.get('type') or '').lower()
if category in {'durbhaga', 'ari'} or 'daridra' in name or 'arishta' in name:
return 'Applicable Malefic Yogas'
if category in {'conjunction', 'special', 'kalatra', 'solar_yoga', 'lunar_yoga'} or 'dosha' in name:
return 'Applicable Mixed Yogas'
return 'Applicable Benefic Yogas'
grouped_yogas: dict[str, list[dict]] = {
'Applicable Benefic Yogas': [],
'Applicable Mixed Yogas': [],
'Applicable Malefic Yogas': [],
}
for row in detected_yogas:
if isinstance(row, dict):
grouped_yogas[_yoga_group_label(row)].append(row)
for group_title, group_rows in grouped_yogas.items():
if not group_rows:
continue
lines.extend([
f'#### {group_title}',
'',
'| Yoga | Category | Strength | Combination | Effects / notes |',
'|------|----------|----------|-------------|-----------------|',
])
for row in group_rows:
effects = row.get('effects') if isinstance(row.get('effects'), list) else []
lines.append(
f"| {_cell(row.get('name_cn') or row.get('name') or row.get('type'))} | {_cell(row.get('category') or row.get('subtype'))} | "
f"{_cell(row.get('strength'))} | {_cell(row.get('combination') or row.get('source_reference') or row.get('reason'))} | "
f"{_cell(_public_report_text(_brief(effects, 3) or row.get('interpretation') or row.get('summary')))} |"
)
lines.append('')
supportive_raw = dosha_pack.get('supportive_practices')
supportive_practices = _list(supportive_raw)
if isinstance(supportive_raw, dict):
supportive_practices = _list(supportive_raw.get('recommendations'))
if supportive_practices:
lines.extend([
'### Remedies / Supportive Practices',
'',
'| Topic | Practice | Gem / count | Notes |',
'|-------|----------|-------------|-------|',
])
for row in supportive_practices:
if not isinstance(row, dict):
continue
actions = row.get('actions') if isinstance(row.get('actions'), list) else []
practice = row.get('practice') or row.get('name') or row.get('remedy') or row.get('mantra') or _brief(actions, 3)
count_or_gem = row.get('gem') or row.get('repetitions') or ('No gem or repetition count supplied' if _pl9_report_language(packet) == 'en' else '源数据未提供宝石或次数')
lines.append(
f"| {_cell(row.get('topic') or row.get('title') or row.get('id') or row.get('planet') or row.get('dosha'))} | "
f"{_cell(practice)} | {_cell(count_or_gem)} | "
f"{_cell(_public_report_text(row.get('notes') or row.get('summary') or row.get('description')))} |"
)
lines.append('')
dasha = timing.get('dasha_interpretation_pack') if isinstance(timing.get('dasha_interpretation_pack'), dict) else {}
dasha_pages = dasha.get('page_series') if isinstance(dasha.get('page_series'), list) else []
if dasha_pages:
lines.extend(['## 大运解释', ''])
for page in dasha_pages:
if not isinstance(page, dict):
continue
title = str(page.get('title') or '').strip()
sections = page.get('sections') if isinstance(page.get('sections'), dict) else {}
if not title:
continue
lines.extend([f'### {title}', ''])
for key in ('general', 'natal_context', 'domain_context', 'current_subperiod'):
if str(sections.get(key) or '').strip():
lines.extend([_public_report_text(sections[key]), ''])
# The source-bound pack already contains the ordered MD/AD/PD
# narrative blocks. Keep them in the main export; the compact
# section summary alone drops the original report's useful density.
seen_blocks = {
str(sections.get(key) or '').strip()
for key in ('general', 'natal_context', 'domain_context', 'current_subperiod')
if str(sections.get(key) or '').strip()
}
for block in page.get('flow_blocks') or []:
if not isinstance(block, dict):
continue
block_title = str(block.get('heading') or block.get('title') or '').strip()
block_sections = block.get('sections')
if block_title and block_title != title:
lines.extend([f'#### {block_title}', ''])
if not isinstance(block_sections, list):
continue
for item in block_sections:
if not isinstance(item, (list, tuple)) or len(item) < 2:
continue
value = str(item[1] or '').strip()
if not value or value in seen_blocks:
continue
seen_blocks.add(value)
lines.extend([_public_report_text(value), ''])
return _sanitize_pl9_public_markdown('\n'.join(lines))