Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
324 lines
13 KiB
Python
324 lines
13 KiB
Python
"""Report completion for the full-data edition (TASK-report-full-data-fix-20261008).
|
|
|
|
Template wording, annual field projection, the opening focus, and the two
|
|
mistranslations. Chart numbers come from the packet. These tests do not
|
|
calculate a new dasha or import PyJHora.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import re
|
|
|
|
from scripts.pl9_full_data_export import _sanitize_pl9_ai_density_markdown, render_full_data_markdown
|
|
from scripts.pl9_interpretation_packs import (
|
|
attach_full_data_interpretation,
|
|
missing_field_count,
|
|
opening_focus_block,
|
|
section_coverage,
|
|
)
|
|
from scripts.pl9_reader_english import HAN
|
|
|
|
FORBIDDEN_ZH = (
|
|
"再婚",
|
|
"几段关系",
|
|
"早夭",
|
|
"一定会",
|
|
"必然",
|
|
"Neecha Bhanga Raja Yoga",
|
|
"落陷取消王",
|
|
"疾病诊断",
|
|
"疾病",
|
|
)
|
|
FORBIDDEN_EN = (
|
|
"divorce",
|
|
"miscarriage",
|
|
"infertility",
|
|
"guarantee",
|
|
"guaranteed",
|
|
)
|
|
HALF_TRANSLATED = re.compile(r"[A-Za-z]+\s*[\u4e00-\u9fff]+s")
|
|
|
|
|
|
def _planet(sign: str, house: int, nakshatra: str, status: str = "Own Sign") -> dict:
|
|
return {"sign": sign, "house": house, "status": status, "nakshatra": nakshatra}
|
|
|
|
|
|
def _packet(language: str = "zh") -> dict:
|
|
planets = {
|
|
"Sun": _planet("Aries", 10, "Ashwini", "入旺(Exalted)"),
|
|
"Moon": _planet("Taurus", 2, "Rohini"),
|
|
"Mars": _planet("Capricorn", 7, "Exalted"),
|
|
"Mercury": _planet("Pisces", 6, "Debilitated"),
|
|
"Jupiter": _planet("Cancer", 1, "Exalted"),
|
|
"Venus": _planet("Libra", 4, "Own Sign"),
|
|
"Saturn": _planet("Aquarius", 8, "Own Sign"),
|
|
"Rahu": _planet("Gemini", 12, "Ardra", ""),
|
|
"Ketu": _planet("Sagittarius", 6, "Mula", ""),
|
|
}
|
|
lords = ["Mars", "Venus", "Mercury", "Moon", "Sun", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Saturn", "Jupiter"]
|
|
houses = {f"house_{index}": {"lord": lord, "cusp_sign": "Aries"} for index, lord in enumerate(lords, start=1)}
|
|
timeline = []
|
|
for lord in ("Sun", "Moon", "Mars"):
|
|
current = lord == "Moon"
|
|
timeline.append({
|
|
"lord": lord,
|
|
"start": "2000-01-01",
|
|
"end": "2006-01-01",
|
|
"is_current": current,
|
|
"antardasha_timeline": [{
|
|
"lord": "Mars",
|
|
"start": "2001-01-01",
|
|
"end": "2002-01-01",
|
|
"is_current": current,
|
|
"pratyantar_dasha_timeline": [{
|
|
"lord": "Mercury",
|
|
"start": "2001-02-01",
|
|
"end": "2001-04-01",
|
|
"is_current": current,
|
|
}],
|
|
}],
|
|
})
|
|
return {
|
|
"report_language": language,
|
|
"birth_info": {"name": "虚构甲" if language == "zh" else "Fictional"},
|
|
"worksheets": {
|
|
"d1_rasi_bhava": {"planets": planets, "houses": houses, "ascendant": {"sign": "Cancer"}},
|
|
"strengths_and_scores": {
|
|
"functional_benefic_malefic": {
|
|
"functional_benefics": ["Jupiter", "Mars"],
|
|
"functional_malefics": ["Saturn"],
|
|
"functional_neutrals": ["Moon"],
|
|
},
|
|
"shadbala": {"planets": {"Sun": {"total": 1.25}}},
|
|
},
|
|
"timing_and_predictive_systems": {
|
|
"dasha": {"timeline": timeline, "current_dasha": timeline[1]},
|
|
"annual_tajika_pack": {
|
|
"solar_return": {"status": "partial_verified", "data": {"dt_local": "2026-04-07T09:00:00"}},
|
|
"mudda_dasha": {
|
|
"status": "partial_verified",
|
|
"periods": [{"lord": "Sun", "start": "2026-04-07", "end": "2026-05-07", "months": 1}],
|
|
},
|
|
"patyayini_dasha": {"status": "blocked", "reason": "not_in_engine"},
|
|
"sahams": {"status": "partial_verified", "data": [{"name": "Punya", "sign": "Aries", "degree_in_sign": 1.2}]},
|
|
"tajika_yogas": {
|
|
"status": "partial_verified",
|
|
"data": {"yogas": [{"name": "Ithasala", "present": True, "planets": ["Sun", "Moon"]}]},
|
|
},
|
|
"monthly_windows": [{
|
|
"index": 1,
|
|
"lord": "Sun",
|
|
"duration_months": 1,
|
|
"start": "2026-04-07",
|
|
"end": "2026-05-07",
|
|
}],
|
|
},
|
|
},
|
|
"advanced_systems": {
|
|
"yoga": {
|
|
"detected_yogas": [{
|
|
"name": "Gajakesari Yoga",
|
|
"name_cn": "象鹿瑜伽",
|
|
"category": "raja",
|
|
"effects": ["名声可见"],
|
|
"effects_en": ["visible reputation"],
|
|
"planets": ["Moon", "Jupiter"],
|
|
"combination": "Moon and Jupiter",
|
|
}, {
|
|
"name": "Neecha Bhanga Raja Yoga",
|
|
"name_cn": "落陷取消王瑜伽",
|
|
"category": "neecha_bhanga",
|
|
"effects": ["传统条件成立"],
|
|
"effects_en": ["the cancellation conditions hold"],
|
|
"planets": ["Mercury"],
|
|
}],
|
|
},
|
|
"yogas_doshas": {"mangal_dosha": {"has_dosha": False, "mars_house": 7, "severity": "none"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def test_t5_repairs_bhava_bala_and_planets_suffix() -> None:
|
|
raw = "\n".join([
|
|
"# 印度占星星盘报告",
|
|
"",
|
|
"### p31-p33 Shadbala / Bhava Bala 摘要",
|
|
"",
|
|
"### p32 Bhava Bala 十二宫力量",
|
|
"",
|
|
"### p125 Natal / KP Ruling Planets",
|
|
"",
|
|
"### Upagraha and Sub-Planets Points",
|
|
"",
|
|
"### Planetary Friendship Matrix",
|
|
"",
|
|
])
|
|
text = _sanitize_pl9_ai_density_markdown(raw)
|
|
assert "Bhava 年龄状态" not in text
|
|
assert "六维力量 / Bhava Bala" in text
|
|
assert "Bhava Bala 十二宫力量" in text
|
|
assert "行星s" not in text
|
|
# Ruling planets are not significators (Claude acceptance 2026-10-08): was 本命 / KP 征象星.
|
|
assert "本命 / KP 主宰星" in text
|
|
assert HALF_TRANSLATED.search(text) is None
|
|
assert "年龄状态" not in text.split("Graha Avastha", 1)[-1] or "Baladi" not in raw
|
|
|
|
|
|
def test_templates_cover_required_sections_and_forbidden_wording() -> None:
|
|
packet = _packet("zh")
|
|
attach_full_data_interpretation(packet)
|
|
focus = opening_focus_block(packet)
|
|
assert focus.startswith("重点:")
|
|
assert "MD" in focus and "AD" in focus and "PD" in focus
|
|
assert "第10宫" in focus and "第2宫" in focus and "第7宫" in focus
|
|
text = render_full_data_markdown(packet)
|
|
for heading in ("大运解释", "行星、星宿与宫位解释", "宫主逐宫解释", "Dosha 逐项事实与解释", "瑜伽解释"):
|
|
assert heading in text
|
|
assert "Patyayini 没有实现" in text
|
|
assert "Mudda 月段窗口" in text
|
|
assert "blocked" not in text.lower()
|
|
for phrase in FORBIDDEN_ZH:
|
|
assert phrase not in text
|
|
assert "落陷取消(Neecha Bhanga" in text
|
|
assert "Raja Yoga" not in text
|
|
assert HALF_TRANSLATED.search(text) is None
|
|
assert "行星s" not in text
|
|
coverage = {row["section"]: row["status"] for row in section_coverage(packet)}
|
|
assert coverage["sthira"] == "out_of_scope"
|
|
assert "Sthira Dasha" not in text
|
|
assert missing_field_count(packet) <= 10
|
|
|
|
|
|
def test_english_focus_and_new_sections_have_no_han() -> None:
|
|
packet = _packet("en")
|
|
text = render_full_data_markdown(packet)
|
|
assert "Focus: current Maha Dasha MD" in text
|
|
assert "AD" in text and "PD" in text
|
|
assert "Dasha Interpretation" in text
|
|
assert "House-lord interpretation" in text
|
|
assert "Yoga interpretation" in text
|
|
assert "Patyayini is not implemented" in text
|
|
assert "Monthly mudda windows" in text
|
|
for phrase in FORBIDDEN_EN:
|
|
assert phrase not in text.lower()
|
|
assert "Neecha Bhanga Raja Yoga" not in text
|
|
assert "not a Raja Yoga" in text
|
|
leaked = HAN.findall(text)
|
|
assert leaked == [], leaked[:12]
|
|
|
|
|
|
def test_yoga_effect_does_not_assert_lifespan() -> None:
|
|
# 原值:作用列写「长寿;健康良好」/ Longevity。
|
|
# 新值:作用列改成体质与恢复力的参考句。名称「长寿格局」和 Ayur Yoga (Strong Longevity) 可以留下。
|
|
# 原因:格局名称是传统叫法,作用说明不再把寿命当断语。
|
|
packet = _packet("zh")
|
|
packet["worksheets"]["advanced_systems"]["yoga"]["detected_yogas"].append({
|
|
"name": "Ayur Yoga (Strong Longevity)",
|
|
"name_cn": "长寿格局",
|
|
"category": "ayur",
|
|
"strength": "strong",
|
|
"effects": ["长寿", "健康良好"],
|
|
"effects_en": ["Longevity", "Good health"],
|
|
"combination": "traditional name only",
|
|
})
|
|
chinese = render_full_data_markdown(packet)
|
|
english_packet = copy.deepcopy(packet)
|
|
english_packet["report_language"] = "en"
|
|
english = render_full_data_markdown(english_packet)
|
|
|
|
def without_names(text: str) -> str:
|
|
text = text.replace("长寿格局", "")
|
|
return re.sub(r"Ayur Yoga \(Strong Longevity\)", "", text, flags=re.IGNORECASE)
|
|
|
|
for phrase in ("长寿", "寿命长", "短寿"):
|
|
assert phrase not in without_names(chinese)
|
|
assert "longevity" not in without_names(english).lower()
|
|
assert "传统上与体质和恢复力有关,仅作参考" in chinese
|
|
assert "constitution and recovery" in english.lower()
|
|
|
|
|
|
def test_term_replacement_does_not_enter_identifiers() -> None:
|
|
# 原值:functional_benefics / Kendras / conditions 变成「功能吉星s」「角宫s」「宫条件s」,字段名里的 sign 被换成星座。
|
|
# 新值:散文里的复数 s 吃进英文词;反引号和标识符保持原文。
|
|
# 原因:术语替换只能作用于自然语言。
|
|
raw = "\n".join([
|
|
"# 标题",
|
|
"",
|
|
"strict_functional_benefic_malefic_v2",
|
|
"bphs_ch34_general_with_sign_exceptions_v2",
|
|
"functional_benefics and Kendras and house conditions",
|
|
"`functional_benefics`",
|
|
])
|
|
text = _sanitize_pl9_ai_density_markdown(raw)
|
|
assert "strict_functional_benefic_malefic_v2" in text
|
|
assert "bphs_ch34_general_with_sign_exceptions_v2" in text
|
|
assert "`functional_benefics`" in text
|
|
assert "功能吉星s" not in text
|
|
assert "角宫s" not in text
|
|
assert "宫条件s" not in text
|
|
assert "功能吉星" in text
|
|
assert re.search(r"[一-鿿]+s\b", text) is None
|
|
rendered = render_full_data_markdown(_packet("zh"))
|
|
assert re.search(r"[一-鿿]+s\b", rendered) is None
|
|
mixed = re.search(r"[A-Za-z0-9_]*_[一-鿿]|[一-鿿]_[A-Za-z0-9_]", rendered)
|
|
assert mixed is None, mixed.group(0) if mixed else ""
|
|
|
|
|
|
def test_rahu_ketu_do_not_rule_an_unlisted_house() -> None:
|
|
from scripts.pl9_interpretation_packs import _natal_en, _natal_zh, _sub_paragraph
|
|
|
|
# 原值:空守护列表写成「守护第未列出宫」。
|
|
# 新值:罗睺、计都写「不守护宫位」/ rules no house。中文全文该病句计数为 0。
|
|
# 原因:这两颗星不守护宫位。其他「未列出」按句意保留。
|
|
empty = {"ownership": [], "house": 6, "nakshatra": "Mula"}
|
|
filled = {"ownership": [1], "house": 6}
|
|
row = {"start": "2001-01-01", "end": "2001-02-01"}
|
|
assert "不守护宫位" in _sub_paragraph("Sun", "Rahu", row, empty, "zh", level="PD")
|
|
assert "守护第未列出宫" not in _sub_paragraph("Sun", "Rahu", row, empty, "zh", level="PD")
|
|
assert "不守护宫位" in _natal_zh("Ketu", filled, {})
|
|
assert "守护第1宫" in _natal_zh("Sun", filled, {})
|
|
assert "守护宫未单独列出" in _sub_paragraph("Sun", "Mars", row, empty, "zh", level="AD")
|
|
assert "rules no house" in _natal_en("Rahu", filled, {})
|
|
text = render_full_data_markdown(_packet("zh"))
|
|
assert text.count("守护第未列出宫") == 0
|
|
|
|
|
|
def test_chinese_render_stays_stable_when_english_uses_a_copy() -> None:
|
|
packet = _packet("zh")
|
|
before = render_full_data_markdown(copy.deepcopy(packet))
|
|
english = copy.deepcopy(packet)
|
|
english["report_language"] = "en"
|
|
render_full_data_markdown(english)
|
|
after = render_full_data_markdown(copy.deepcopy(packet))
|
|
assert after == before
|
|
|
|
|
|
def test_zh_headings_carry_no_untranslated_kp_or_row_words() -> None:
|
|
# Claude acceptance 2026-10-08: after the term pass stopped rewriting inside
|
|
# words, appendix headings such as "KP 行星 Significators" and
|
|
# "Annual Tajika瑜伽 Rows" were left half translated.
|
|
raw = "\n".join([
|
|
"# 印度占星星盘报告",
|
|
"",
|
|
"## KP Planet Significators",
|
|
"",
|
|
"## KP House Cusps and Significators",
|
|
"",
|
|
"### KP Significators / Planet Lords",
|
|
"",
|
|
"### KP Chart / Cusps",
|
|
"",
|
|
"## Annual Tajika Yoga Rows",
|
|
"",
|
|
"#### Dhana Yoga",
|
|
"",
|
|
])
|
|
text = _sanitize_pl9_ai_density_markdown(raw)
|
|
headings = re.findall(r"(?m)^#{2,6} (.*)$", text)
|
|
pattern = re.compile(r"\b(Significators|Cusps|Rows|Lords|Ruling|Planets?)\b|[A-Za-z]{3,}[\u4e00-\u9fff]|[\u4e00-\u9fff]s\b")
|
|
assert [h for h in headings if pattern.search(h)] == []
|
|
assert "KP 行星征象星" in headings and "KP 宫头与征象星" in headings and "年度 Tajika 瑜伽行" in headings
|
|
|