"""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"(? 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))