完整数据版补上行星和宫主解释、后两年年运细项,以及 72 年逐年年返。

定向测试和 200 张英文扫描通过。一张虚构盘中英合计约 20 秒。土星和 KP 结果段仍按修复单让步。

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-23 15:04:25 +08:00
co-authored by Grok (agent loop)
parent 2a4646d8b2
commit cfd0ab68ac
10 changed files with 291 additions and 26 deletions
+7
View File
@@ -1,5 +1,12 @@
# 印度占星 Skill 更新日志
## 2026-10-23 — 完整数据版补上行星解释、后两年年运和 72 年年返
- 每颗星和每个宫主的解释加长,仍用「可能」的说法,并写上功能吉凶。土星和 KP 的结果段这一版仍先不做。
- 当年、后一年、再后一年都列出年盘整宫、Tajika 相位和力量、Tripataki、特殊点公式,以及年主依据。
- 补上从出生起 72 年的逐年年返表。一张虚构盘中英合计仍远低于 3 分钟。
- Skill 版本不变。
## 2026-10-23 — 明说要校正时,已采用过的人能直接新开一次
- 已经采用过出生时间的人,在对话里说要做生时纠正,会直接开始一次新的核对,不再只回「可在历史对话里接着做。」
@@ -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 | 不跑 | 交给验收 |
+6 -4
View File
@@ -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,不是整节「未判定」。
## 导出
+11 -2
View File
@@ -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
+1 -1
View File
@@ -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 = {
+91
View File
@@ -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}
+2 -2
View File
@@ -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), ''])
+86 -13
View File
@@ -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
+11 -1
View File
@@ -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
+47
View File
@@ -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}