Removing the English-to-Chinese back-translation left every BVR semantic note and every yogas_doshas raj/dhana "H*_Lord(...) in H*" sentence as 未判定 in the Chinese yoga table and pages. - yoga_engine: semantic conditions return BiText(Chinese, old note). The Chinese comes from references/yoga_semantic_notes_zh.json, one sentence per note, so dedup keeps the same equality classes. English-only custom templates get the same lookup. combination_en is unchanged. - yogas_doshas: raj/dhana rows add combination_zh; combination is unchanged. - chinese_combination prefers a clean combination_zh. Which yogas fire, their strength and category are unchanged (fictional charts 0-19, same rows as staging; only set-iteration order differs). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HwMNZEzdzmrbPhuRNmZe3C
371 lines
16 KiB
Python
371 lines
16 KiB
Python
"""Six parity chapters for issue #91 section A.
|
|
|
|
C-class sections stay absent. Labels are bilingual. Narayana's two methods
|
|
are separate headings. Fictional charts only.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from scripts.pl9_full_data_export import render_full_data_markdown
|
|
from scripts.pl9_reader_english import HAN
|
|
from scripts.professional_report_reference import _english_edition
|
|
from scripts.report_render.appendix_language import chinese_prose_gates
|
|
from scripts.report_render.parity_chapters import (
|
|
CHAPTERS,
|
|
FORBIDDEN_TITLES,
|
|
LABELS,
|
|
SECTIONS,
|
|
_chapter_sections,
|
|
lab,
|
|
render_parity_chapters,
|
|
)
|
|
from tests.report_english_fictional import _load_packet
|
|
|
|
_SNAKE = re.compile(r"[A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+")
|
|
_KAAL_ZH = re.compile(r"\d+ 颗中 \d+ 颗")
|
|
_KAAL_EN = re.compile(r"\d+ of \d+")
|
|
|
|
|
|
def _chapter_text(markdown: str, language: str) -> str:
|
|
marker = "\n## 大运附表\n" if language == "zh" else "\n## Dasha Appendix\n"
|
|
assert marker in markdown
|
|
return markdown.split(marker, 1)[1]
|
|
|
|
|
|
def _section_body(markdown: str, heading: str) -> str:
|
|
marker = f"### {heading}\n"
|
|
start = markdown.index(marker) + len(marker)
|
|
rest = markdown[start:]
|
|
ends = [pos for pos in (rest.find("\n### "), rest.find("\n## "), rest.find("\n# ")) if pos >= 0]
|
|
return rest[: min(ends)] if ends else rest
|
|
|
|
|
|
def _heading_lines(markdown: str, level: int) -> list[str]:
|
|
prefix = "#" * level + " "
|
|
return [line[len(prefix):].strip() for line in markdown.splitlines() if line.startswith(prefix)]
|
|
|
|
|
|
def test_chapter_titles_follow_the_task_groups() -> None:
|
|
groups = _chapter_sections()
|
|
assert len(groups) == len(CHAPTERS) == 6
|
|
assert [title for group in groups for title in group] == [title for title, _zh in SECTIONS]
|
|
for (chapter_en, chapter_zh), titles in zip(CHAPTERS, groups):
|
|
assert chapter_en and chapter_zh
|
|
assert titles
|
|
|
|
|
|
def test_labels_cover_both_languages_and_a_missing_label_fails() -> None:
|
|
for key, pair in LABELS.items():
|
|
assert pair[0] and pair[1], key
|
|
assert lab(key, "en") == pair[0]
|
|
assert lab(key, "zh") == pair[1]
|
|
assert "_" not in pair[0] and "_" not in pair[1]
|
|
with pytest.raises(KeyError):
|
|
lab("not_a_real_column", "zh")
|
|
|
|
|
|
def test_empty_packet_chapters_are_headed_and_do_not_print_raw_keys() -> None:
|
|
for language in ("zh", "en"):
|
|
text = render_parity_chapters({}, language)
|
|
titles = SECTIONS
|
|
cursor = 0
|
|
for title_en, title_zh in titles:
|
|
heading = title_zh if language == "zh" else title_en
|
|
body = _section_body(text, heading)
|
|
assert body.strip(), heading
|
|
cursor_next = text.index(f"### {heading}\n", cursor)
|
|
assert cursor_next >= cursor
|
|
cursor = cursor_next
|
|
if "|---|" in body or "| --- |" in body:
|
|
lines = [line for line in body.splitlines() if line.startswith("|")]
|
|
assert len(lines) >= 3, heading
|
|
for title in FORBIDDEN_TITLES:
|
|
assert f"### {title}" not in text
|
|
assert f"## {title}" not in text
|
|
assert f"# {title}" not in text
|
|
assert _SNAKE.search(text) is None
|
|
assert _heading_lines(text, 1) == []
|
|
chapters = [chapter_zh if language == "zh" else chapter_en for chapter_en, chapter_zh in CHAPTERS]
|
|
assert _heading_lines(text, 2) == chapters
|
|
zh = render_parity_chapters({}, "zh")
|
|
en = render_parity_chapters({}, "en")
|
|
assert zh.index("#### 来源感知口径") < zh.index("#### 既有口径")
|
|
assert "来源感知口径" not in zh.split("#### 既有口径", 1)[1]
|
|
assert en.index("#### Source-aware method") < en.index("#### Legacy method")
|
|
assert "Source-aware method" not in en.split("#### Legacy method", 1)[1]
|
|
assert "印刷页 10" in zh
|
|
assert "printed pages 10-17" in en
|
|
for banned in ("source_locator", "profile_id", "niryaana_shoola_strength", "source_discrepancy", "sign_idx"):
|
|
assert banned not in zh
|
|
assert banned not in en
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def fictional_reports() -> dict[int, tuple[str, str]]:
|
|
reports = {}
|
|
for index in (0, 1, 9):
|
|
packet = _load_packet(index)
|
|
edition = _english_edition(packet, True)
|
|
chinese = render_full_data_markdown(packet)
|
|
english = edition.get("markdown_en") or ""
|
|
assert edition.get("english_unavailable") in (None, "")
|
|
assert english
|
|
reports[index] = (chinese, english)
|
|
return reports
|
|
|
|
|
|
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}
|
|
assert HAN.findall(english) == []
|
|
assert "[translation missing:" not in english
|
|
zh = _chapter_text(chinese, "zh")
|
|
en = _chapter_text(english, "en")
|
|
zh_at = 0
|
|
en_at = 0
|
|
for title_en, title_zh in SECTIONS:
|
|
zh_at = zh.index(f"### {title_zh}\n", zh_at)
|
|
en_at = en.index(f"### {title_en}\n", en_at)
|
|
for body, heading in ((_section_body(zh, title_zh), title_zh), (_section_body(en, title_en), title_en)):
|
|
assert body.strip(), f"{index} {heading}"
|
|
for chapter_en, chapter_zh in CHAPTERS:
|
|
assert _heading_lines(chinese, 2).count(chapter_zh) == 1
|
|
assert _heading_lines(english, 2).count(chapter_en) == 1
|
|
for title in FORBIDDEN_TITLES:
|
|
assert title not in zh
|
|
assert title not in en
|
|
assert _SNAKE.search(zh) is None
|
|
assert _SNAKE.search(en) is None
|
|
assert "印刷页 10" in zh and "printed pages 10-17" in en
|
|
assert "#### 来源感知口径" in zh and "#### 既有口径" in zh
|
|
assert "#### Source-aware method" in en and "#### Legacy method" in en
|
|
assert "再小运" in _section_body(zh, "Chara 大运时长")
|
|
assert "Pratyantardasha" in _section_body(en, "Chara Dasha Period Durations")
|
|
assert "marriage_windows" not in _section_body(zh, "Bhrigu 近似推进序列")
|
|
assert "venus_encounter" not in _section_body(en, "Bhrigu Approximate Progression")
|
|
assert "Raja Yoga" not in zh
|
|
assert "blocked" not in zh.lower()
|
|
|
|
|
|
_UNDETERMINED = {"zh": "未判定", "en": "Not determined"}
|
|
_GAP_LINE = {
|
|
"zh": "公开报告资料包未带此模块",
|
|
"en": "This module is not in the public report packet",
|
|
}
|
|
# These four modules are absent from the public worksheet. The reason is why
|
|
# the section may stay undetermined. Anything else must show a real cell.
|
|
_PACKET_GAPS = {
|
|
"Navamsha AD Duration": "navamsha_dasha is computed on the reading but is not copied into the public worksheet",
|
|
"Pancha Pakshi": "pancha_pakshi is not copied into the public worksheet",
|
|
"Rashi Tulya Navamsa": "rashi_tulya_navamsa is not copied into the public worksheet",
|
|
"Current Dasha Convergence": "dasa_convergence per-domain rows are not copied into the public worksheet",
|
|
}
|
|
|
|
|
|
def _table_blocks(body: str) -> list[list[list[str]]]:
|
|
blocks = []
|
|
current: list[str] = []
|
|
for line in body.splitlines():
|
|
if line.startswith("|"):
|
|
current.append(line)
|
|
continue
|
|
if current:
|
|
blocks.append(current)
|
|
current = []
|
|
if current:
|
|
blocks.append(current)
|
|
parsed = []
|
|
for block in blocks:
|
|
rows = []
|
|
for index, line in enumerate(block):
|
|
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
if cells and all(set(cell) <= set("-: ") and cell for cell in cells):
|
|
continue
|
|
if index == 0:
|
|
continue
|
|
rows.append(cells)
|
|
parsed.append(rows)
|
|
return parsed
|
|
|
|
|
|
_NOT_VALUES = {"未判定", "Not determined", "—", "-", ""}
|
|
# A whole cell that looks like a packet key (`summary`, `p5`, `house_7`).
|
|
_RAW_KEY_CELL = re.compile(r"^[a-z][a-z0-9]*(_[a-z0-9]+)*$")
|
|
|
|
|
|
def _section_has_real_cell(body: str, undetermined: str) -> bool:
|
|
"""A table must carry a value outside its first (label) column.
|
|
|
|
The first column holds row labels, so it never proves that the packet was
|
|
read. A one-column table therefore never passes. A body with no table at all
|
|
may be a plain sentence (for example "no boundary within 90 days").
|
|
"""
|
|
blocks = _table_blocks(body)
|
|
if blocks:
|
|
return any(
|
|
cell not in _NOT_VALUES and cell != undetermined
|
|
for rows in blocks
|
|
for row in rows
|
|
for cell in row[1:]
|
|
)
|
|
prose = [line.strip() for line in body.splitlines() if line.strip() and not line.startswith("#")]
|
|
return any(line != undetermined for line in prose)
|
|
|
|
|
|
def _all_cells(markdown: str) -> list[str]:
|
|
cells = []
|
|
for line in markdown.splitlines():
|
|
if not line.startswith("|"):
|
|
continue
|
|
parts = [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
if parts and all(set(cell) <= set("-: ") and cell for cell in parts):
|
|
continue
|
|
cells.extend(parts)
|
|
return cells
|
|
|
|
|
|
def test_every_section_has_a_real_cell_except_the_four_public_gaps(fictional_reports: dict[int, tuple[str, str]]) -> None:
|
|
titles = dict(SECTIONS)
|
|
for reason in _PACKET_GAPS.values():
|
|
assert reason
|
|
for index, (chinese, english) in fictional_reports.items():
|
|
for language, markdown in (("zh", chinese), ("en", english)):
|
|
chapter = _chapter_text(markdown, language)
|
|
for title_en, title_zh in SECTIONS:
|
|
heading = title_zh if language == "zh" else title_en
|
|
body = _section_body(chapter, heading)
|
|
if title_en in _PACKET_GAPS:
|
|
first = next(line.strip() for line in body.splitlines() if line.strip())
|
|
assert first == _GAP_LINE[language], f"{index} {heading}"
|
|
continue
|
|
assert _section_has_real_cell(body, _UNDETERMINED[language]), f"{index} {heading}"
|
|
|
|
|
|
def test_every_section_has_a_real_cell_outside_the_label_column_unit() -> None:
|
|
one_column = "| 角色 |\n|---|\n| 未判定 |\n| summary |\n"
|
|
assert not _section_has_real_cell(one_column, "未判定")
|
|
labels_only = "| 项目 | 内容 |\n|---|---|\n| 状态 | 未判定 |\n| 宫位 | — |\n"
|
|
assert not _section_has_real_cell(labels_only, "未判定")
|
|
real = "| 项目 | 内容 |\n|---|---|\n| 宫位 | 7 |\n"
|
|
assert _section_has_real_cell(real, "未判定")
|
|
assert _section_has_real_cell("参考日起 90 天内没有大运或小运交界。", "未判定")
|
|
assert not _section_has_real_cell("未判定", "未判定")
|
|
|
|
|
|
def test_no_table_cell_is_a_raw_packet_key(fictional_reports: dict[int, tuple[str, str]]) -> None:
|
|
for index, (chinese, english) in fictional_reports.items():
|
|
for language, markdown in (("zh", chinese), ("en", english)):
|
|
chapter = _chapter_text(markdown, language)
|
|
keys = sorted({cell for cell in _all_cells(chapter) if _RAW_KEY_CELL.fullmatch(cell)})
|
|
assert keys == [], f"{index} {language} {keys[:10]}"
|
|
|
|
|
|
def test_six_chapters_are_reader_chapters_and_sections_sit_below_them(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""The reader splits on `## ` and ignores `# `. The six chapters must be `##`."""
|
|
import scripts.pl9_full_data_export as export
|
|
|
|
zh_chapters = [chapter_zh for _chapter_en, chapter_zh in CHAPTERS]
|
|
en_chapters = [chapter_en for chapter_en, _chapter_zh in CHAPTERS]
|
|
packet = _load_packet(0)
|
|
full_zh = render_full_data_markdown(copy.deepcopy(packet))
|
|
full_en = _english_edition(copy.deepcopy(packet), True).get("markdown_en") or ""
|
|
monkeypatch.setattr(export, "_append_parity_chapters", lambda markdown, _packet, _language: markdown)
|
|
base_zh = render_full_data_markdown(copy.deepcopy(packet))
|
|
base_en = _english_edition(copy.deepcopy(packet), True).get("markdown_en") or ""
|
|
assert base_en
|
|
for full, base, chapters in ((full_zh, base_zh, zh_chapters), (full_en, base_en, en_chapters)):
|
|
h1 = _heading_lines(full, 1)
|
|
assert len(h1) == 1, h1
|
|
assert full.startswith("# " + h1[0])
|
|
assert _heading_lines(base, 1) == h1
|
|
full_h2 = _heading_lines(full, 2)
|
|
base_h2 = _heading_lines(base, 2)
|
|
assert len(full_h2) == len(base_h2) + len(chapters)
|
|
assert full_h2[: len(base_h2)] == base_h2
|
|
assert full_h2[len(base_h2):] == chapters
|
|
tail = full.split("\n## " + chapters[0] + "\n", 1)[1]
|
|
assert _heading_lines(tail, 3)[: len(_chapter_sections()[0])] == [
|
|
(dict(SECTIONS)[title] if chapters is zh_chapters else title) for title in _chapter_sections()[0]
|
|
]
|
|
assert len(_heading_lines(tail, 3)) == len(SECTIONS)
|
|
|
|
|
|
def test_parity_chapters_go_through_local_conventions() -> None:
|
|
from scripts.pl9_full_data_export import _apply_local_report_conventions
|
|
|
|
packet = {
|
|
"report_language": "zh",
|
|
"worksheets": {
|
|
"strengths_and_scores": {
|
|
"vimsopaka": {
|
|
"Sun": {"planet": "Sun", "total_score": 12.5, "category": "必然"},
|
|
}
|
|
}
|
|
},
|
|
}
|
|
chinese = render_full_data_markdown(packet)
|
|
zh_body = _section_body(chinese, "Vimsopaka 分盘贡献")
|
|
zh_replaced = _apply_local_report_conventions("必然", language="zh")
|
|
assert zh_replaced == "可能"
|
|
assert zh_replaced in zh_body
|
|
assert "必然" not in zh_body
|
|
|
|
english_packet = copy.deepcopy(packet)
|
|
english_packet["report_language"] = "en"
|
|
english_packet["worksheets"]["strengths_and_scores"]["vimsopaka"]["Sun"]["category"] = "Divorce risk"
|
|
# A bare lower-case word is now treated as an internal key and is not
|
|
# printed, so the probe is a reader-style phrase (fix2 F1).
|
|
english = render_full_data_markdown(english_packet)
|
|
en_body = _section_body(english, "Vimsopaka Contribution")
|
|
en_replaced = _apply_local_report_conventions("Divorce risk", language="en")
|
|
assert en_replaced == "relationship strain risk"
|
|
assert en_replaced in en_body
|
|
assert "divorce" not in en_body.lower()
|
|
|
|
|
|
def _combination_cells(markdown: str) -> list[str]:
|
|
cells: list[str] = []
|
|
column = None
|
|
for line in markdown.splitlines():
|
|
if not line.startswith("|"):
|
|
column = None
|
|
continue
|
|
parts = [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
if column is None:
|
|
column = parts.index("组合条件") if "组合条件" in parts else -1
|
|
continue
|
|
if column < 0 or all(set(cell) <= set("-: ") and cell for cell in parts):
|
|
continue
|
|
if column < len(parts):
|
|
cells.append(parts[column])
|
|
return cells
|
|
|
|
|
|
def test_chinese_yoga_combinations_are_not_blanked(fictional_reports: dict[int, tuple[str, str]]) -> None:
|
|
"""BUG-1418: staging printed Chinese text in these cells; none may fall back to 未判定."""
|
|
for index, (chinese, english) in fictional_reports.items():
|
|
cells = _combination_cells(chinese)
|
|
assert cells, index
|
|
assert [cell for cell in cells if cell == "未判定"] == [], index
|
|
assert "组合:未判定" not in chinese
|
|
assert HAN.findall(english) == []
|
|
|
|
|
|
def test_chart_nine_partial_kaal_and_friendship(fictional_reports: dict[int, tuple[str, str]]) -> None:
|
|
chinese, english = fictional_reports[9]
|
|
assert _KAAL_ZH.search(chinese)
|
|
assert _KAAL_EN.search(english)
|
|
zh_friends = _section_body(_chapter_text(chinese, "zh"), "行星中性与复合友敌关系")
|
|
en_friends = _section_body(_chapter_text(english, "en"), "Neutral and Compound Relationships")
|
|
zh_rows = [line for line in zh_friends.splitlines() if line.startswith("| 太阳") or line.startswith("| 月亮") or line.startswith("| 水星")]
|
|
assert len([line for line in zh_friends.splitlines() if line.startswith("| ") and "---" not in line]) >= 10
|
|
assert zh_rows
|
|
assert "极友" in zh_friends
|
|
assert "Great friend" in en_friends
|
|
assert "没有大运或小运交界" in chinese or "距参考日天数" in _section_body(_chapter_text(chinese, "zh"), "大运与小运交界检查")
|