diff --git a/CHANGELOG.md b/CHANGELOG.md index e813fd72..75c79e2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 印度占星 Skill 更新日志 +## 2026-10-23 — 完整数据版补上行星解释、后两年年运和 72 年年返 + +- 每颗星和每个宫主的解释加长,仍用「可能」的说法,并写上功能吉凶。土星和 KP 的结果段这一版仍先不做。 +- 当年、后一年、再后一年都列出年盘整宫、Tajika 相位和力量、Tripataki、特殊点公式,以及年主依据。 +- 补上从出生起 72 年的逐年年返表。一张虚构盘中英合计仍远低于 3 分钟。 +- Skill 版本不变。 + ## 2026-10-23 — 明说要校正时,已采用过的人能直接新开一次 - 已经采用过出生时间的人,在对话里说要做生时纠正,会直接开始一次新的核对,不再只回「可在历史对话里接着做。」 diff --git a/docs/tasks/PROGRESS-full-data-parity-1-b-20261023.md b/docs/tasks/PROGRESS-full-data-parity-1-b-20261023.md index 9db72601..51647154 100644 --- a/docs/tasks/PROGRESS-full-data-parity-1-b-20261023.md +++ b/docs/tasks/PROGRESS-full-data-parity-1-b-20261023.md @@ -3,7 +3,7 @@ - 分支:`codex/full-data-parity-1-20261021` - 范围:T4–T10。A 段已在 `origin/staging` 的 `c1aa72fc`。 - 合并:实现提交之后再 `git merge origin/staging`(当时远端是 `d72adb575`)。没有 rebase,没有强推。 -- 状态:B 段交验收。下面写明没做满的项。 +- 状态:B 段修复(K1、K2、K3)交验收。土星 / KP 结果段仍让步。 ## 这一段用户能看到什么 @@ -18,8 +18,8 @@ | 项 | 处理 | 原因 | | --- | --- | --- | -| 72 年生命周期年返 | 不做,函数留着但不在报告路径调用 | 任务书让步顺序允许先放。一次要多算几十张年返盘 | -| Saturn / KP 结果段,以及行星、宫主解释补到上游约 8,970 英文字 | 不做 | 让步顺序第一项。只修了「六维力量合计为未列出」 | +| 72 年生命周期年返 | 已补。报告年写出 72 行 | 产品 10-23 拍板 K3,不再让步。一张虚构盘中英合计约 22 秒,没有截断 | +| Saturn / KP 结果段 | 仍不做 | 修复单允许继续让。解释加长已做,见下面 K1 | | 现居地八年总览 | 只有资料包里已经带了现居地坐标才出 | `_annual_payload_from_args` 在 `return` 之后才读现居地,那段代码到不了。打开它会改年返地点,已有年运数值就不能保持原样。这一版不动 | | 年度北印度图盘面板 `pl9_annual_visual_panels` | 不新做面板 | 阅读页在没有面板时,已经用算好的黄经印成文字表。放上面板对象后,阅读页只印标题、不再印这张表 | | 王者格 raj / dhana 排序 | 不动 | 不能证明对话卡和校正打分不受影响(BUG-1415) | @@ -59,3 +59,29 @@ ## 编号 Yogi 记录在本分支曾写成 BUG-1420,后改为 BUG-1421。staging 上 BUG-1421 已被 #95 占用,当前最大号是 BUG-1423。合并 origin/staging 后 Yogi 改为 BUG-1424。更新日志里的编号一并改掉。 + +## 修复轮(K1、K2、K3) + +产品 10-23 选 A,并加上 72 年逐年返照。在合并后的分支上做,没有 rebase。 + +| 项 | 结果 | +| --- | --- | +| K1 行星解释 | 0 / 1 / 9 号虚构盘英文「Planets, Nakshatras and Houses」14,267 / 14,322 / 14,300 字符,都高于 8,970。中文 4,484 / 4,500 / 4,507,按英文与原先中文的比例高于 3,320 | +| K1 宫主解释 | 英文 18,849 / 18,876 / 18,877。中文 5,742 / 5,746 / 5,740 | +| K2 后两年 | 2026、2027、2028 每年都有年盘整宫、年主依据、Tajika 相位、Tripataki、Tajika 力量、特殊点公式 | +| K3 72 年年返 | 表里 72 行。0 号盘这一节约 10,889 字符 | + +三张盘中英合计生成时间:20.83 秒、18.52 秒、19.45 秒。都低于 180 秒,没有截年、没有截断。 + +0 号盘整份:中文 782,964 字符 / 1,366,339 字节;英文 1,314,289 字符 / 1,315,901 字节,汉字 0。1 号盘中文 781,298 / 1,358,484,英文 1,309,508 / 1,311,113。9 号盘中文 787,154 / 1,373,385,英文 1,320,279 / 1,321,899。 + +土星 / KP 结果段仍按修复单让步,没有补。王者格排序没改。现居地八年总览仍只在资料里已经写了现居地时出现。 + +## 修复轮自测 + +| 检查 | 命令 | 结果 | +| --- | --- | --- | +| 修复单点名的定向测试,外加逐年结构测试 | pytest 上一段那 19 个文件,以及 `tests/test_full_data_parity_chapters.py` 里的新年份断言 | 退出码 0 | +| 200 张英文扫描 | pytest tests/test_report_english_sweep.py,屏蔽 PyJHora | 退出码 0,连同三张盘计时一共约 27 分钟 | +| 前端类型和 lint | 本轮没改前端 | 没跑 | +| 快速门、全量 pytest | 不跑 | 交给验收 | diff --git a/docs/testing/full-data-parity-1-20261021.md b/docs/testing/full-data-parity-1-20261021.md index b756cf95..b3a00eeb 100644 --- a/docs/testing/full-data-parity-1-20261021.md +++ b/docs/testing/full-data-parity-1-20261021.md @@ -5,7 +5,7 @@ ## 生成新报告 1. 打开一个人的星盘,生成完整数据版。 -2. 等待结束。本机一张虚构盘中英合计约 24 秒。若超过 3 分钟还在转,停下来记下时间,不要反复重试把队列打满。 +2. 等待结束。本机一张虚构盘中英合计约 19 到 21 秒。若超过 3 分钟还在转,停下来记下时间,不要反复重试把队列打满。 3. 报告能打开,末尾能翻到计算口径。 ## 中英切换 @@ -16,9 +16,11 @@ ## 逐章翻到底 -1. 年运章有当年和后两年。 -2. 行运章末尾有「三年逐月星历」。中文写「报告口径」和「KP 岁差口径」。 -3. 特殊点里有 Yogi 和 Ava Yogi,不是整节「未判定」。 +1. 年运章有当年和后两年。后两年也有年盘整宫、Tajika、Tripataki、特殊点公式和年主依据,不只是一行 Muntha 星座。 +2. 年运里有从出生起 72 行的逐年年返,不是被截短的表。 +3. 行星解释和宫主解释比以前长,说法仍是「可能」,不写死亡、寿命或诊断。 +4. 行运章末尾有「三年逐月星历」。中文写「报告口径」和「KP 岁差口径」。 +5. 特殊点里有 Yogi 和 Ava Yogi,不是整节「未判定」。 ## 导出 diff --git a/scripts/annual_native/annual_expansion.py b/scripts/annual_native/annual_expansion.py index 2eea5cec..c057e4e0 100644 --- a/scripts/annual_native/annual_expansion.py +++ b/scripts/annual_native/annual_expansion.py @@ -25,8 +25,8 @@ def attach_annual_expansion(pack: dict, payload: dict, args: Any, *, include_ove Later years are separate annual packs. This does not recompute the year lord, Muntha, Mudda, Sahams or Tajika already stored on ``pack``. - The 72-year lifetime table is not built here: one report would calculate - dozens of extra charts and can miss the 180 second wait. + The 72-year lifetime table is built only for the report year. Later years + do not repeat it. """ if not isinstance(pack, dict): raise TypeError("annual pack must be a dict") @@ -147,6 +147,15 @@ def attach_annual_expansion(pack: dict, payload: dict, args: Any, *, include_ove "status": "blocked", "reason": f"{exc.__class__.__name__}: {exc}", } + try: + pack["lifetime_annual_chart_pages"] = _lifetime(args, place) + except Exception as exc: + import logging + logging.getLogger(__name__).warning("lifetime annual returns failed: %s", exc) + pack["lifetime_annual_chart_pages"] = { + "status": "blocked", + "reason": f"{exc.__class__.__name__}: {exc}", + } return pack diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 8855964e..4ba95d48 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -3080,7 +3080,7 @@ def _annual_series_surface(annual_pack: dict) -> dict: 'solar_return', 'annual_chart', 'year_lord', 'external_engine_comparison', 'patyayini_dasha', 'sahams', 'tajika_yogas', 'muntha', 'mudda_dasha', 'monthly_chart_pack', 'tithi_pravesh_years', 'native_patyayini', - 'pl9_annual_dasha_results', 'annual_houses', 'tripataki_chakra', + 'pl9_annual_dasha_results', 'annual_houses', 'tripataki_chakra', 'tajika_strength', 'annual_year_pages', 'normalized_tables', ) surface = { diff --git a/scripts/pl9_interpretation_packs.py b/scripts/pl9_interpretation_packs.py index 700245d6..12658725 100644 --- a/scripts/pl9_interpretation_packs.py +++ b/scripts/pl9_interpretation_packs.py @@ -765,6 +765,80 @@ def _delta_days(left: Any, right: Any) -> int | None: return None +def _owned_topic_text(owned: list, language: str) -> str: + topics = _HOUSE_TOPIC_ZH if language == "zh" else _HOUSE_TOPIC_EN + parts = [] + for item in owned: + try: + number = int(item) + except (TypeError, ValueError): + continue + label = topics.get(number) + if not label: + continue + if language == "zh": + parts.append(f"第{number}宫({label})") + else: + parts.append(f"house {number} ({label})") + if not parts: + return "未列出" if language == "zh" else "not listed" + return "、".join(parts) if language == "zh" else ", ".join(parts) + + +def _traditional_sections( + planet: str, + language: str, + *, + dignity: str, + role: str, + owned_text: str, + house_topic: str | None = None, +) -> dict[str, str]: + """Traditional paragraphs already stored on this module. No new judgment.""" + + lang = "zh" if language == "zh" else "en" + themes = _THEMES.get(planet, {}).get(lang) + phala = _PHALA.get(planet, {}).get(lang) + if not themes or not phala: + return {} + lead, follow = themes + topic, support, strain = phala + name = _planet(planet, language) + if language == "zh": + where = f"放在{house_topic}这一层时," if house_topic else "" + return { + "traditional": ( + f"传统段落把{name}读成可能涉及{topic}。{where}" + f"条件顺的时候,可能表现为{support}。" + f"条件紧的时候,可能表现为{strain}。" + f"{lead}{follow}" + ), + "condition": ( + f"尊贵是{dignity}。入庙或旺时,这些主题可能更容易说清楚;" + f"落陷或落在不喜的星座时,同一主题可能更慢、更杂。" + f"功能吉凶是{role}。它和这颗星平时的主题如果方向不同,两层都保留,不合成一个确定结果。" + f"相关的宫是{owned_text}。这段时间可能把这些宫的主题带出来,不是职位、收入或关系上的确定结果。" + ), + } + where = f"Read against {house_topic}. " if house_topic else "" + return { + "traditional": ( + f"A traditional reading treats {name} as possibly touching {topic}. {where}" + f"When conditions are supportive this may show as {support}. " + f"When conditions are tight this may show as {strain}. " + f"{lead} {follow}" + ), + "condition": ( + f"Dignity is {dignity}. In its own sign or exaltation these topics may be easier to express. " + f"In debilitation or an unwelcoming sign the same topics may be slower or more mixed. " + f"Functional nature is {role}. If that layer and the planet's usual topics point different ways, keep both. " + f"Do not fold them into one certain result. " + f"The related houses are {owned_text}. This period may bring those house topics forward. " + f"It is not a certain post, income, or relationship outcome." + ), + } + + def build_graha_interpretation_pack( chart_planets: dict[str, Any], owned_houses: dict[str, Any] | None = None, @@ -829,6 +903,14 @@ def build_graha_interpretation_pack( else f"{planet} is not inside the current three Dasha levels, so it stays natal background." ), } + owned_text = _owned_topic_text(owned, language) + sections.update(_traditional_sections( + planet, + language, + dignity=dignity, + role=role, + owned_text=owned_text, + )) pages.append({"planet": planet if language == "en" else _planet(planet, language), "sections": sections}) return { "schema": "pl9.graha_interpretation_pack.v1", @@ -981,6 +1063,15 @@ def build_bhavesh_interpretation_pack( else f"{lord} is not inside the current three Dasha levels, so house {number} stays natal background." ), } + house_topic = topics[number] + sections.update(_traditional_sections( + str(lord), + language, + dignity=dignity, + role=role, + owned_text=house_topic, + house_topic=house_topic, + )) pages.append({"house": number, "lord": lord, "sections": sections}) return {"schema": "pl9.bhavesh_interpretation_pack.v1", "status": "partial_verified" if pages else "blocked", "pages": pages} diff --git a/scripts/pl9_reader_export.py b/scripts/pl9_reader_export.py index 21c19ec9..63b90293 100644 --- a/scripts/pl9_reader_export.py +++ b/scripts/pl9_reader_export.py @@ -1385,7 +1385,7 @@ def render_pl9_parity_markdown(packet: dict) -> str: if not planet: continue lines.extend([f'### {planet}', '']) - for key in ('placement', 'sign_and_dispositor', 'nakshatra', 'functional_role', 'strength_and_context', 'activation'): + for key in ('placement', 'sign_and_dispositor', 'nakshatra', 'functional_role', 'strength_and_context', 'traditional', 'condition', '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 {} @@ -3140,7 +3140,7 @@ def render_pl9_parity_markdown(packet: dict) -> str: else f"第{row.get('house')}宫主:{_token(row.get('lord'))}" ) lines.extend([f'#### {title}', '']) - for key in ('house_context', 'lord_placement', 'result', 'context', 'activation'): + for key in ('house_context', 'lord_placement', 'result', 'traditional', 'condition', 'context', 'activation'): value = str(sections.get(key) or '').strip() if value: lines.extend([_public_report_text(value), '']) diff --git a/scripts/report_render/parity_chapters.py b/scripts/report_render/parity_chapters.py index 975f1851..349b05d8 100644 --- a/scripts/report_render/parity_chapters.py +++ b/scripts/report_render/parity_chapters.py @@ -879,8 +879,51 @@ def _phase_word(phase: Any, language: str) -> str | None: return None -def _annual_houses(packet: dict, language: str) -> str: - pack = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {} +def _year_packs(packet: dict) -> list[tuple[str, dict]]: + """Primary year plus the next two, when the series was calculated.""" + series = _first(packet, ("timing_and_predictive_systems", "annual_tajika_series")) or {} + years = series.get("years") if isinstance(series, dict) else None + found: list[tuple[str, dict]] = [] + if isinstance(years, dict): + for year in sorted(years, key=str): + pack = years.get(year) + if isinstance(pack, dict) and pack: + found.append((str(year), pack)) + if found: + return found + primary = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {} + if isinstance(primary, dict) and primary: + return [("", primary)] + return [] + + +def _each_year(packet: dict, language: str, render) -> str: + packs = _year_packs(packet) + if not packs: + return render({}) + if len(packs) == 1 and not packs[0][0]: + return render(packs[0][1]) + parts = [] + for year, pack in packs: + body = render(pack) + parts.append(f"#### {year}\n\n{body}" if year else body) + return "\n\n".join(parts) + + +def _year_lord_basis(pack: dict, language: str) -> str: + field = pack.get("year_lord") if isinstance(pack, dict) and isinstance(pack.get("year_lord"), dict) else {} + data = field.get("data") if isinstance(field.get("data"), dict) else {} + blocked = field.get("blocked_detail") if isinstance(field.get("blocked_detail"), dict) else {} + source = data or blocked + if language == "en": + text = str(source.get("selection_basis_text_en") or "") + if re.search(r"[\u4e00-\u9fff]", text): + return "" + return text.strip() + return str(source.get("selection_basis_text") or "").strip() + + +def _annual_house_table(pack: dict, language: str) -> str: data = _pack_data(pack.get("annual_houses") if isinstance(pack, dict) else None) houses = data.get("houses") if isinstance(data, dict) else None rows = [] @@ -896,8 +939,11 @@ def _annual_houses(packet: dict, language: str) -> str: return _rows_from(rows, language, [("house", "text"), ("sign", "sign"), ("lord", "planet"), ("occupant", "text")]) -def _tripataki(packet: dict, language: str) -> str: - pack = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {} +def _annual_houses(packet: dict, language: str) -> str: + return _each_year(packet, language, lambda pack: _annual_house_table(pack, language)) + + +def _tripataki_table(pack: dict, language: str) -> str: data = _pack_data(pack.get("tripataki_chakra") if isinstance(pack, dict) else None) if not isinstance(data, dict): return _blank(language, ["point", "sign", "degree", "longitude"]) @@ -925,6 +971,10 @@ def _tripataki(packet: dict, language: str) -> str: return "\n\n".join(parts) +def _tripataki(packet: dict, language: str) -> str: + return _each_year(packet, language, lambda pack: _tripataki_table(pack, language)) + + def _trace_table(traces: Any, language: str) -> str: rows = [] if isinstance(traces, list): @@ -982,21 +1032,23 @@ def _annual_numeric(packet: dict, language: str) -> str: return _rows_from(rows, language, [("year", "text"), ("planet", "planet"), ("sign", "sign"), ("longitude", "text")]) -def _annual_saham_trace(packet: dict, language: str) -> str: - pack = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {} +def _annual_saham_one(pack: dict, language: str) -> str: sahams = _pack_data(pack.get("sahams") if isinstance(pack, dict) else None) traces = sahams.get("formula_traces") if isinstance(sahams, dict) else None return _trace_table(traces, language) +def _annual_saham_trace(packet: dict, language: str) -> str: + return _each_year(packet, language, lambda pack: _annual_saham_one(pack, language)) + + def _natal_saham_trace(packet: dict, language: str) -> str: module = _first(packet, ("advanced_systems", "sahams")) or {} traces = module.get("formula_traces") if isinstance(module, dict) else None return _trace_table(traces, language) -def _annual(packet: dict, language: str) -> str: - pack = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {} +def _annual_contract(pack: dict, language: str) -> str: solar = pack.get("solar_return") if isinstance(pack, dict) else {} chart = pack.get("annual_chart") if isinstance(pack, dict) else {} muntha = pack.get("muntha") if isinstance(pack, dict) else {} @@ -1011,11 +1063,25 @@ def _annual(packet: dict, language: str) -> str: "value": (muntha_data or {}).get("muntha_sign"), "lord": (lord_data or {}).get("year_lord"), } - return _rows_from([row], language, [("date", "text"), ("sign", "sign"), ("value", "sign"), ("lord", "planet")]) + table = _rows_from([row], language, [("date", "text"), ("sign", "sign"), ("value", "sign"), ("lord", "planet")]) + basis = _year_lord_basis(pack, language) + if not basis: + return table + label = "年主依据" if language == "zh" else "Year-lord basis" + sep = ":" if language == "zh" else ": " + muntha_lord = muntha_data.get("muntha_lord") if isinstance(muntha_data, dict) else None + lines = [table, f"{label}{sep}{basis}"] + if muntha_lord: + muntha_label = "Muntha 主星" if language == "zh" else "Muntha lord" + lines.append(f"{muntha_label}{sep}{planet_name(muntha_lord, language)}") + return "\n\n".join(lines) -def _tajika_candidates(packet: dict, language: str) -> str: - pack = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {} +def _annual(packet: dict, language: str) -> str: + return _each_year(packet, language, lambda pack: _annual_contract(pack, language)) + + +def _tajika_candidate_table(pack: dict, language: str) -> str: field = pack.get("tajika_yogas") if isinstance(pack, dict) else {} data = field.get("data") if isinstance(field, dict) else {} rows = [] @@ -1034,8 +1100,11 @@ def _tajika_candidates(packet: dict, language: str) -> str: return _rows_from(rows, language, [("name", "text"), ("planet", "planets"), ("aspect", "text"), ("residual", "text"), ("status", "text")]) -def _tajika_strength(packet: dict, language: str) -> str: - pack = _first(packet, ("timing_and_predictive_systems", "annual_tajika_pack")) or {} +def _tajika_candidates(packet: dict, language: str) -> str: + return _each_year(packet, language, lambda pack: _tajika_candidate_table(pack, language)) + + +def _tajika_strength_table(pack: dict, language: str) -> str: field = pack.get("tajika_strength") if isinstance(pack, dict) else {} data = field.get("data") if isinstance(field, dict) else {} rows = [] @@ -1049,6 +1118,10 @@ def _tajika_strength(packet: dict, language: str) -> str: return _rows_from(rows, language, [("name", "text"), ("planet", "planet"), ("score", "text")]) +def _tajika_strength(packet: dict, language: str) -> str: + return _each_year(packet, language, lambda pack: _tajika_strength_table(pack, language)) + + def _shadbala_planets(packet: dict) -> dict: module = _first(packet, ("strengths_and_scores", "shadbala")) or {} planets = module.get("planets") if isinstance(module, dict) else None diff --git a/tests/test_annual_expansion.py b/tests/test_annual_expansion.py index b8b241ab..31f59937 100644 --- a/tests/test_annual_expansion.py +++ b/tests/test_annual_expansion.py @@ -70,9 +70,19 @@ def test_primary_year_leaves_mudda_rows_for_the_language_projector(monkeypatch) "scripts.annual_native.annual_expansion._eight_year", lambda *_args, **_kwargs: {"birth_place_eight_year_overview": {"status": "partial_verified", "rows": []}}, ) + # 原值:assert "lifetime_annual_chart_pages" not in pack + # 新值:报告年写入 72 行生命周期年返 + # 原因:产品 10-23 拍板 K3,这一项不再让步 + monkeypatch.setattr( + "scripts.annual_native.annual_expansion._lifetime", + lambda *_args, **_kwargs: {"status": "partial_verified", "pages": [{"rows": [{"completed_age": age} for age in range(72)]}]}, + ) pack = _pack() attach_annual_expansion(pack, _payload(), _args(), include_overview=True) assert "mudda_rows" not in pack["normalized_tables"] assert pack["normalized_tables"]["patyayini_rows"] assert "birth_place_eight_year_overview" in pack["annual_chart_series_pack"] - assert "lifetime_annual_chart_pages" not in pack + rows = pack["lifetime_annual_chart_pages"]["pages"][0]["rows"] + assert len(rows) == 72 + assert rows[0]["completed_age"] == 0 + assert rows[-1]["completed_age"] == 71 diff --git a/tests/test_full_data_parity_chapters.py b/tests/test_full_data_parity_chapters.py index 8376b1d8..cf5a0e4b 100644 --- a/tests/test_full_data_parity_chapters.py +++ b/tests/test_full_data_parity_chapters.py @@ -119,6 +119,53 @@ def fictional_reports() -> dict[int, tuple[str, str]]: return reports +def _between(markdown: str, heading: str, marks: tuple[str, ...]) -> str: + marker = f"{heading}\n" + start = markdown.index(marker) + len(marker) + rest = markdown[start:] + ends = [pos for pos in (rest.find(mark) for mark in marks) if pos >= 0] + return rest[: min(ends)] if ends else rest + + +def test_later_years_repeat_annual_sections(fictional_reports: dict[int, tuple[str, str]]) -> None: + """K2: 2027 and 2028 carry the same annual sections as 2026.""" + sections = ( + ("年运整宫框架", "Annual Return House Framework"), + ("年运返照计算约定", "Annual Return Calculation Contract"), + ("Tajika 相位与瑜伽候选", "Tajika Aspect and Yoga Candidates"), + ("Tripataki 轮盘明细", "Tripataki Chakra Detail"), + ("年度 Tajika 力量", "Annual Tajika Strength"), + ("年度特殊点公式轨迹", "Annual Saham Formula Trace"), + ) + for index, (chinese, english) in fictional_reports.items(): + for title_zh, title_en in sections: + zh_body = _section_body(_chapter_text(chinese, "zh"), title_zh) + en_body = _section_body(_chapter_text(english, "en"), title_en) + for year in ("2026", "2027", "2028"): + assert f"#### {year}" in zh_body, f"{index} {title_zh} {year}" + assert f"#### {year}" in en_body, f"{index} {title_en} {year}" + zh_year = zh_body.split(f"#### {year}", 1)[1].split("\n#### ", 1)[0] + en_year = en_body.split(f"#### {year}", 1)[1].split("\n#### ", 1)[0] + assert zh_year.strip() and en_year.strip() + if title_zh != "Tajika 相位与瑜伽候选": + assert re.search(r"\d", zh_year), f"{index} {title_zh} {year}" + assert re.search(r"\d", en_year), f"{index} {title_en} {year}" + assert zh_body.count("年主依据") == 3 or title_zh != "年运返照计算约定" + assert en_body.count("Year-lord basis") == 3 or title_en != "Annual Return Calculation Contract" + lifetime = _between(chinese, "### 生命周期年度返照图盘数据", ("\n### ", "\n## ")) + assert len([line for line in lifetime.splitlines() if line.startswith("| ") and "---" not in line]) >= 73 + en_life = _between(english, "### Lifetime annual return charts", ("\n### ", "\n## ")) + assert len([line for line in en_life.splitlines() if line.startswith("| ") and "---" not in line]) >= 73 + zh_planets = _between(chinese, "## 行星、星宿与宫位解释", ("\n## ",)) + en_planets = _between(english, "## Planets, Nakshatras and Houses", ("\n## ",)) + zh_houses = _section_body(chinese, "Bhavesh 宫主逐宫解释") + en_houses = _section_body(english, "House-lord interpretation") + assert len(en_planets) >= 8970, (index, len(en_planets)) + assert len(en_houses) >= 8970, (index, len(en_houses)) + assert len(zh_planets) >= 3320, (index, len(zh_planets)) + assert len(zh_houses) >= 3320, (index, len(zh_houses)) + + def test_three_fictional_charts_render_the_same_chapters(fictional_reports: dict[int, tuple[str, str]]) -> None: for index, (chinese, english) in fictional_reports.items(): assert chinese_prose_gates(chinese) == {"english_signs": 0, "han_function_glue": 0}