同一份真实盘包在屏蔽与不屏蔽 PyJHora 下新旧渲染 sha 都一致,各 24 项且退出码 0。指定 pytest 233 通过、3 跳过。 Co-Authored-By: Grok (agent loop)
256 lines
11 KiB
Python
256 lines
11 KiB
Python
"""Helpers moved unchanged out of render_pl9_markdown (phase 1)."""
|
||
|
||
import re
|
||
|
||
try:
|
||
from scripts.report_render.envelope import _unwrap
|
||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
|
||
from report_render.envelope import _unwrap
|
||
|
||
SIGNS = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
|
||
SIGNS_CN = {'Aries': '白羊座', 'Taurus': '金牛座', 'Gemini': '双子座', 'Cancer': '巨蟹座', 'Leo': '狮子座', 'Virgo': '处女座', 'Libra': '天秤座', 'Scorpio': '天蝎座', 'Sagittarius': '射手座', 'Capricorn': '摩羯座', 'Aquarius': '水瓶座', 'Pisces': '双鱼座'}
|
||
|
||
PLANET_LABEL_CN = {
|
||
'Sun': '太阳',
|
||
'Moon': '月亮',
|
||
'Mars': '火星',
|
||
'Mercury': '水星',
|
||
'Jupiter': '木星',
|
||
'Venus': '金星',
|
||
'Saturn': '土星',
|
||
'Rahu': '北交点',
|
||
'Ketu': '南交点',
|
||
}
|
||
|
||
def _humanize_reader_token(token: str) -> str:
|
||
value = str(token or '').strip()
|
||
if not value:
|
||
return value
|
||
if value in SIGNS_CN:
|
||
return SIGNS_CN[value]
|
||
if value in PLANET_LABEL_CN:
|
||
return PLANET_LABEL_CN[value]
|
||
return value
|
||
|
||
def _humanize_reader_group(raw_group: str) -> str:
|
||
seen = set()
|
||
parts = []
|
||
for item in str(raw_group or '').split(','):
|
||
humanized = _humanize_reader_token(item)
|
||
if humanized and humanized not in seen:
|
||
seen.add(humanized)
|
||
parts.append(humanized)
|
||
return ' / '.join(parts)
|
||
|
||
def _humanize_tajika_candidate(name: str) -> str:
|
||
return {
|
||
'Ithasala_candidate': '趋近相位候选',
|
||
'Easarapha_candidate': '分离相位候选',
|
||
}.get(str(name or ''), '年度相位候选')
|
||
|
||
def _humanize_tajika_motion(motion: str) -> str:
|
||
return {'applying': '趋近', 'separating': '分离'}.get(str(motion or ''), str(motion or '-'))
|
||
|
||
def _display_rashi_from_value(raw_value):
|
||
if isinstance(raw_value, int) and 0 <= raw_value < len(SIGNS):
|
||
return _humanize_reader_token(SIGNS[raw_value])
|
||
return _humanize_reader_token(raw_value)
|
||
|
||
def _humanize_reader_summary_line(summary_line: str) -> str:
|
||
summary_line = _unwrap(summary_line)
|
||
if isinstance(summary_line, dict):
|
||
summary_line = (
|
||
summary_line.get('brief')
|
||
or summary_line.get('text')
|
||
or summary_line.get('summary')
|
||
or ''
|
||
)
|
||
text = str(summary_line or '').strip()
|
||
if not text:
|
||
return text
|
||
if text == "年度层已经有可读壳层,但仍需要带着冲突标签阅读。":
|
||
return "这一年的年度主线已经形成。"
|
||
if text == "This pack converts Dasha trigger facts into report-ready interpretation seeds without final event claims.":
|
||
return "这层已经把 Dasha 触发事实压成可读提示,但仍不直接升级为具体事件断语。"
|
||
if text == "Source: dasha_master_pack.interpretation_triggers.":
|
||
return "当前解释仍然直接来自 dasha_master_pack.interpretation_triggers 的证据层。"
|
||
if text == "All claims remain evidence-labeled and parameter-sensitive where the source pack is unclosed.":
|
||
return "只要 source pack 还没闭环,相关结论就继续保持 evidence-labeled 与 parameter_sensitive。"
|
||
if text == "Tajika Yogas: annual candidate structures visible":
|
||
return "Tajika Yogas 当前已经能读到年度候选交互结构。"
|
||
if text == "Sahams: annual sensitive points visible":
|
||
return "Sahams 当前已经能读到年度敏感点。"
|
||
match = re.match(r'^([^:]+):\s*conflict between\s+(.+?)\s+vs\s+(.+)$', text)
|
||
if match:
|
||
topic = match.group(1).strip()
|
||
left = _humanize_reader_group(match.group(2))
|
||
right = _humanize_reader_group(match.group(3))
|
||
topic_label = {
|
||
'Muntha': 'Muntha',
|
||
'Year Lord': 'Year Lord',
|
||
'Annual Asc': '返照上升',
|
||
}.get(topic, topic)
|
||
if left == right:
|
||
return f"{topic_label} 当前结果指向{left}。"
|
||
return f"{topic_label} 当前存在候选分歧:一组结果指向{left},另一组结果指向{right}。"
|
||
simple_match = re.match(r'^([^:]+):\s*(.+)$', text)
|
||
if simple_match:
|
||
topic = simple_match.group(1).strip()
|
||
value = _humanize_reader_group(simple_match.group(2))
|
||
if topic == 'Year Lord' and value:
|
||
return f"Year Lord 当前优先候选指向{value}。"
|
||
if topic == 'Muntha' and value:
|
||
return f"Muntha 当前优先候选指向{value}。"
|
||
if topic == 'Tajika Yogas' and 'annual candidate structures visible' in str(simple_match.group(2)).lower():
|
||
return "Tajika Yogas 当前已经能读到年度候选交互结构。"
|
||
if topic == 'Sahams' and 'annual sensitive points visible' in str(simple_match.group(2)).lower():
|
||
return "Sahams 当前已经能读到年度敏感点。"
|
||
return text
|
||
|
||
def _humanize_annual_highlight_line(highlight: str) -> str:
|
||
text = str(highlight or '').strip()
|
||
if not text:
|
||
return text
|
||
if text == "Solar Return / Tajika annual shell is available.":
|
||
return "本年度返照与 Tajika 时间结构已经生成。"
|
||
if text == "Muntha and Year Lord are visible, but Year Lord remains producer-disagree sensitive.":
|
||
return "Muntha 和 Year Lord 现在都已经可见,但 Year Lord 仍要带着分歧标签阅读。"
|
||
if text == "The report should read annual structure first, then the evidence appendix.":
|
||
return "这一层适合从年度结构展开,再回头核对证据附录。"
|
||
return text
|
||
|
||
def _humanize_annual_narrative_highlight(highlight: str) -> str:
|
||
return _humanize_annual_highlight_line(str(highlight)).rstrip('。')
|
||
|
||
def _humanize_domain_title(title: str) -> str:
|
||
text = str(title or '').strip()
|
||
if not text:
|
||
return text
|
||
mapping = {
|
||
"Career and public direction": "事业与公众方向",
|
||
"Wealth and growth opportunities": "财富与增长机会",
|
||
"Relationship and alliance themes": "关系与合作主题",
|
||
"Health and vitality caution": "健康与精力提醒",
|
||
"Risk, delay, and friction markers": "阻力、延迟与摩擦提示",
|
||
"Timing windows and cross-check needs": "时间窗口与交叉核对重点",
|
||
}
|
||
return mapping.get(text, text)
|
||
|
||
def _workbook_readable_label(worksheet_id: str) -> str:
|
||
if worksheet_id == 'birth_and_parameters':
|
||
return '出生与计算口径'
|
||
if worksheet_id == 'd1_rasi_bhava':
|
||
return '本命盘主结构'
|
||
if worksheet_id == 'strengths_and_scores':
|
||
return '力量与技术表'
|
||
if worksheet_id == 'divisional_and_special_charts':
|
||
return '分盘与特殊点位'
|
||
if worksheet_id == 'timing_and_predictive_systems':
|
||
return '时间系统与流年层'
|
||
if worksheet_id == 'advanced_systems':
|
||
return '进阶体系'
|
||
if worksheet_id == 'ai_and_audit':
|
||
return '审计与证据附录'
|
||
return '补充工作簿'
|
||
|
||
def _workbook_readable_sentence(worksheet_id: str) -> str:
|
||
if worksheet_id == 'birth_and_parameters':
|
||
return '出生参数、岁差、交点模式与计算口径已经固定,是整份报告的可信底座。'
|
||
if worksheet_id == 'd1_rasi_bhava':
|
||
return '本命盘主结构已经就位,适合先看性格主轴、房屋结构和基础趋势。'
|
||
if worksheet_id == 'strengths_and_scores':
|
||
return '力量、功能性吉凶与技术表可以直接用来判断支撑与压力来源。'
|
||
if worksheet_id == 'divisional_and_special_charts':
|
||
return '分盘与特殊点位可作为扩展阅读,适合补充看细节。'
|
||
if worksheet_id == 'timing_and_predictive_systems':
|
||
return '大运、行运与年度层已经可以形成时间主线。'
|
||
if worksheet_id == 'advanced_systems':
|
||
return 'Jaimini、KP、Yoga 等进阶体系可作为补充主轴。'
|
||
if worksheet_id == 'ai_and_audit':
|
||
return 'AI 读写、审计与证据层用于核对来源与边界,不直接替代正文。'
|
||
return f'{worksheet_id} 已纳入当前报告。'
|
||
|
||
def _workbook_reading_paragraph(worksheet_ids: list[str]) -> str:
|
||
parts = []
|
||
for worksheet_id in worksheet_ids:
|
||
sentence = _workbook_readable_sentence(worksheet_id).rstrip('。')
|
||
parts.append(f"先看{_workbook_readable_label(worksheet_id)},这里会告诉你{sentence}")
|
||
if not parts:
|
||
return '当前暂无可用工作簿阅读顺序。'
|
||
return ';'.join(parts) + '。'
|
||
|
||
def _md_cell(value) -> str:
|
||
text = str(value if value not in (None, '') else '-').replace('\n', ' ').strip()
|
||
return text.replace('|', '/')
|
||
|
||
def _render_compact_payload(value, depth: int = 0) -> str:
|
||
if value in (None, '', [], {}):
|
||
return '-'
|
||
if depth >= 2:
|
||
return _md_cell(type(value).__name__)
|
||
if isinstance(value, dict):
|
||
parts = []
|
||
for key in sorted(value, key=str):
|
||
if key in {
|
||
'narrative_items',
|
||
'domain_paragraphs',
|
||
'response_envelope',
|
||
'input_hash',
|
||
'profile_hash',
|
||
'raw_calculation',
|
||
'shared_full_report_authority',
|
||
}:
|
||
continue
|
||
parts.append(f"{key}={_render_compact_payload(value.get(key), depth + 1)}")
|
||
return '; '.join(parts) if parts else '-'
|
||
if isinstance(value, (list, tuple)):
|
||
items = [_render_compact_payload(item, depth + 1) for item in list(value)[:8]]
|
||
suffix = ' …' if len(value) > 8 else ''
|
||
return ', '.join(items) + suffix if items else '-'
|
||
return _md_cell(value)
|
||
|
||
def _sign_text(row: dict, sign_key: str = 'sign', cn_key: str = 'sign_cn') -> str:
|
||
if not isinstance(row, dict):
|
||
return '-'
|
||
sign = row.get(cn_key) or _humanize_reader_token(row.get(sign_key)) or row.get(sign_key)
|
||
return _md_cell(sign)
|
||
|
||
def _value_text(*values):
|
||
for value in values:
|
||
if value not in (None, '', [], {}):
|
||
return str(value)
|
||
return None
|
||
|
||
def _compose_narrative_paragraph(*parts: str | None) -> str | None:
|
||
cleaned = []
|
||
for part in parts:
|
||
text = str(part or '').strip()
|
||
if not text:
|
||
continue
|
||
text = text.rstrip()
|
||
if text.endswith('。'):
|
||
text = text[:-1]
|
||
cleaned.append(text)
|
||
if not cleaned:
|
||
return None
|
||
return ';'.join(cleaned) + '。'
|
||
|
||
def _planet_list_text(items: list[str], limit: int = 3) -> str | None:
|
||
names = []
|
||
seen = set()
|
||
for item in items:
|
||
humanized = _humanize_reader_token(item)
|
||
if humanized and humanized not in seen:
|
||
seen.add(humanized)
|
||
names.append(humanized)
|
||
if len(names) >= limit:
|
||
break
|
||
if not names:
|
||
return None
|
||
return '、'.join(names)
|
||
|
||
def _append_unique_text(items: list[str], value: str) -> None:
|
||
text = str(value or '').strip().rstrip('。')
|
||
if text and text not in items:
|
||
items.append(text)
|