From ac3dccbac89c0b5e3d98dbe84cbf2a99eeb3420f Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 22 Oct 2026 21:55:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(yoga):=20Chinese=20combination=20text=20fro?= =?UTF-8?q?m=20a=20Chinese=20source,=20not=20=E6=9C=AA=E5=88=A4=E5=AE=9A?= =?UTF-8?q?=20(#91=20fix2=20F3,=20BUG-1418)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01HwMNZEzdzmrbPhuRNmZe3C --- references/yoga_semantic_notes_zh.json | 169 +++++++++++++++++++++ scripts/report_render/appendix_language.py | 4 + scripts/yoga_engine.py | 49 +++++- scripts/yogas_doshas.py | 30 ++++ tests/test_full_data_parity_chapters.py | 28 ++++ tests/test_yoga_combination_chinese.py | 105 +++++++++++++ 6 files changed, 382 insertions(+), 3 deletions(-) create mode 100644 references/yoga_semantic_notes_zh.json create mode 100644 tests/test_yoga_combination_chinese.py diff --git a/references/yoga_semantic_notes_zh.json b/references/yoga_semantic_notes_zh.json new file mode 100644 index 00000000..491a1d95 --- /dev/null +++ b/references/yoga_semantic_notes_zh.json @@ -0,0 +1,169 @@ +{ + "10th house occupied by lords of 10th, 2nd and 7th": "第10宫有10宫主、2宫主与7宫主", + "10th lord conjoined with 9th lord": "10宫主与9宫主合相", + "10th lord in 3rd house with Saturn": "10宫主与土星同在第3宫", + "10th lord in 6th, 8th or 12th house": "10宫主在第6、8或12宫", + "10th lord in trine from 7th house": "10宫主在第7宫起的三方宫", + "10th lord's navamsa dispositor is exalted in 10th and lagna lord joins him": "10宫主 D9 定位星擢升落第10宫,且上升主与之相会", + "11th lord conjoins Venus": "11宫主与金星相会", + "11th lord in deep exaltation": "11宫主在深度擢升点", + "12th lord occupies the 12th house (Vipareeta Raja Yoga)": "12宫主落第12宫(逆转王瑜伽)", + "1st and 11th lords exchange": "1宫主与11宫主互换星座", + "2nd and 10th lords exchange": "2宫主与10宫主互换星座", + "2nd lord in 10th with malefics": "2宫主与凶星同在第10宫", + "2nd lord in house of Saturn or Mars": "2宫主落在土星或火星守护的星座", + "2nd lord joins Jupiter in 8th house": "2宫主与木星同在第8宫", + "3rd house is a benefic sign aspected by benefic planets": "第3宫是吉星星座并受吉星相位", + "3rd lord joins a benefic amsa (with or aspected by benefic)": "3宫主落吉星分度(与吉星同宫或受其相位)", + "3rd lord joins the Moon": "3宫主与月亮相会", + "4th and 10th lords exchange signs (Parivartana)": "4宫主与10宫主互换星座(互容)", + "4th lord aspected by malefic": "4宫主受凶星相位", + "4th lord exalted": "4宫主擢升", + "5th house occupied by malefics": "第5宫有凶星", + "5th lord aspected by another benefic or in benefic sign": "5宫主受另一吉星相位或落吉星星座", + "5th lord conjunct malefics": "5宫主与凶星合相", + "5th lord in 6th, 8th or 12th": "5宫主在第6、8或12宫", + "5th lord is a natural benefic": "5宫主是天然吉星", + "5th lord with benefic and Mars/Saturn in 5th": "5宫主与吉星同宫,且火星或土星在第5宫", + "6th house further aspected by another malefic": "第6宫另受一颗凶星相位", + "6th lord in conjunction with Venus": "6宫主与金星合相", + "6th lord is a natural malefic": "6宫主是天然凶星", + "6th lord occupies the 6th house (Vipareeta Raja Yoga)": "6宫主落第6宫(逆转王瑜伽)", + "7 visible planets occupy exactly 3 distinct signs": "七颗可见行星恰好分布在三个星座", + "7 visible planets occupy exactly 4 distinct signs": "七颗可见行星恰好分布在四个星座", + "7 visible planets occupy exactly 5 distinct signs": "七颗可见行星恰好分布在五个星座", + "7 visible planets occupy exactly 6 distinct signs": "七颗可见行星恰好分布在六个星座", + "7 visible planets occupy exactly 7 distinct signs": "七颗可见行星恰好分布在七个星座", + "7th lord conjunct Venus, aspected by Saturn": "7宫主与金星合相,并受土星相位", + "7th lord exalted in 10th": "7宫主擢升落第10宫", + "8th house is not Jupiter's own or exaltation sign": "第8宫不是木星的本座或擢升座", + "8th lord occupies the 8th house (Vipareeta Raja Yoga)": "8宫主落第8宫(逆转王瑜伽)", + "9th lord aspected by benefic": "9宫主受吉星相位", + "9th lord exalted": "9宫主擢升", + "All 7 visible planets exclusively in movable signs (Ar/Cn/Li/Cp)": "七颗可见行星只在基本星座(白羊、巨蟹、天秤、摩羯)", + "All 7 visible planets in 7 consecutive houses starting from non-Kendra, each house occupied": "七颗可见行星从非角宫起连续占据七个宫,每宫都有星", + "All planets exclusively in dual signs (Ge/Vi/Sg/Pi)": "所有行星都只在双体星座(双子、处女、射手、双鱼)", + "All planets exclusively in fixed signs (Ta/Le/Sc/Aq)": "所有行星都只在固定星座(金牛、狮子、天蝎、水瓶)", + "All planets in even houses (2,4,6,8,10,12) from Lagna": "所有行星都在上升起的偶数宫(2、4、6、8、10、12)", + "All planets in odd houses (1,3,5,7,9,11) from Lagna": "所有行星都在上升起的奇数宫(1、3、5、7、9、11)", + "Aspected by benefics": "受吉星相位", + "BVR definition: TBD": "BVR 定义待补", + "Benefic in 4th house": "吉星在第4宫", + "Benefic in 9th house": "吉星在第9宫", + "Benefics in Upachaya houses (3,6,10,11) from Lagna or Moon": "吉星在上升或月亮起的成长宫(3、6、10、11)", + "Benefics in both 2nd and 12th from Lagna, not aspected by malefics": "上升两侧第2、12宫都有吉星,且不受凶星相位", + "Born during daytime with waxing Moon": "白天出生且月亮渐盈", + "Both 11th lord and Venus in a quadrant from lagna lord": "11宫主与金星都在上升主起的角宫", + "Jupiter and Venus in kendras": "木星与金星在角宫", + "Jupiter conjunct Saturn and Moon in 6th house": "木星与土星、月亮同在第6宫", + "Jupiter in 12th house": "木星在第12宫", + "Jupiter in 2nd or 5th house, conjoined or aspected by Mercury and Venus": "木星在第2或5宫,与水星和金星合相或受其相位", + "Jupiter in 3rd house": "木星在第3宫", + "Jupiter in 5th or 9th from Moon": "木星在月亮起第5或第9宫", + "Jupiter in 5th or 9th from Venus": "木星在金星起第5或第9宫", + "Jupiter in Lagna or aspects Lagna from watery sign": "木星在上升宫,或从水象星座相位上升宫", + "Jupiter in own, friendly or exaltation sign": "木星在本座、友座或擢升座", + "Jupiter is lord of 5th house (Lagna in Leo or Scorpio)": "木星守护第5宫(狮子或天蝎上升)", + "Jupiter is weak (debilitated, combust, or enemy sign)": "木星偏弱(落陷、燃烧或在敌座)", + "Jupiter weak or afflicted": "木星弱或受克", + "Jupiter, Venus, Mercury and Moon all in kendras (1,4,7,10) from Lagna": "木星、金星、水星与月亮都在上升起的角宫(1、4、7、10)", + "Lagna has benefics and not affected by malefics": "上升宫有吉星且不受凶星影响", + "Lagna has malefics and not affected by benefics": "上升宫有凶星且不受吉星影响", + "Lagna in a fixed sign": "上升在固定星座", + "Lagna in fixed sign aspected by 6th lord": "上升在固定星座,并受6宫主相位", + "Lagna in malefic sign (Ar/Sc/Cp/Aq) or Sg/Ta aspected by malefics": "上升在凶星星座(白羊、天蝎、摩羯、水瓶),或在射手、金牛并受凶星相位", + "Lagna in watery sign with benefics OR Lagna lord is watery planet": "上升在水象星座且有吉星,或上升主是水象行星", + "Lagna lord and 2nd lord exchange their places (Parivartana)": "上升主与2宫主互换星座(互容)", + "Lagna lord and navamsa lord both in watery signs": "上升主与其 D9 所落星座的主星都在水象星座", + "Lagna lord aspected by Saturn": "上升主受土星相位", + "Lagna lord conjoined or aspected by Jupiter": "上升主与木星合相或受木星相位", + "Lagna lord conjunct/aspected by malefics": "上升主与凶星合相或受凶星相位", + "Lagna lord exalted in a quadrant with Jupiter's aspect": "上升主在角宫擢升,并受木星相位", + "Lagna lord in Mercury sign with Mercury": "上升主落在水星守护的星座且与水星同宫", + "Lagna lord in a dry sign": "上升主落在干燥星座", + "Lagna lord is exalted": "上升主擢升", + "Lagna lord joins Mars and Mercury in 4th or 12th": "上升主与火星、水星同在第4或12宫", + "Lagna lord, his dispositor, the latter's dispositor in rasi and navamsa, all in kendras/trines/exaltation": "上升主、其定位星及再下一层定位星在本命盘与 D9 中都落角宫、三方宫或擢升", + "Lagna malefic/Gulika in trine OR Gulika with Kendra/Trine lords OR L1 with Rahu/Sat/Ket": "上升宫有凶星且 Gulika 在三方宫,或 Gulika 与角宫、三方宫主同宫,或上升主与罗睺、土星、计都同宫", + "Lord of navamsa occupied by 4th lord in 12th": "4宫主所落 D9 星座的主星在第12宫", + "Lord of navamsa occupied by 4th lord in kendra from Lagna and Chandra Lagna": "4宫主所落 D9 星座的主星在上升与月亮上升起的角宫", + "Lord of navamsa occupied by Moon is exalted in rasi": "月亮所落 D9 星座的主星在本命盘擢升", + "Lord of sign occupied by lagna lord, or navamsa dispositor thereof, in kendra/trine/own/exaltation": "上升主所落星座的主星,或其 D9 定位星,在角宫、三方宫、本座或擢升座", + "Lord of sign occupied in navamsa by 2nd/5th/11th lord, exalted and joins the 9th lord": "2/5/11宫主在 D9 所落星座的主星擢升,并与9宫主相会", + "Lords of 10th and 6th in Lagna with 2nd lord": "10宫主、6宫主与2宫主同在上升宫", + "Lords of 10th and 6th in Neechamsat (debilitation)": "10宫主与6宫主在 D9 落陷", + "Lords of 4th and 10th conjoined with Saturn and Mars": "4宫主与10宫主和土星、火星合相", + "Lords of Aroodha Lagna (A1/AL) and Aroodha Dwadasa (A12/UL) in conjunction": "Arudha 上升(A1/AL)与 Arudha 第12宫(A12/UL)的主星合相", + "Lords of Lagna, 7th and 5th are weak": "上升主、7宫主与5宫主都弱", + "Malefic 6th lord in Lagna, 8th or 10th": "凶性6宫主在上升宫、第8或10宫", + "Malefics in both 2nd and 12th from Lagna, not aspected by benefics": "上升两侧第2、12宫都有凶星,且不受吉星相位", + "Malefics join Rasi Lagna": "凶星落本命上升宫", + "Malefics join kendras and trines": "凶星同时落入角宫与三方宫", + "Malefics join trines": "凶星落入三方宫", + "Malefics occupy 12th, 7th and Lagna": "凶星占据第12宫、第7宫与上升宫", + "Malika Yoga (planets in consecutive houses) starting from 11th house": "从第11宫起的 Malika 格局(行星连续占宫)", + "Mandi and Rahu in 3rd house": "Mandi 与罗睺同在第3宫", + "Mandi in 2nd house": "Mandi 在第2宫", + "Mars and Saturn in 5th house": "火星与土星同在第5宫", + "Mars conjoins 3rd lord and Rahu/Saturn/Mandi in cruel shashtiamsas": "火星与3宫主及罗睺、土星或 Mandi 同在凶性六十分", + "Mars in 2nd house": "火星在第2宫", + "Mars in 3rd in Preta Puriha sashtiamsa (cruel)": "火星在第3宫,落 Preta Puriha 六十分(凶)", + "Mars in 5/7/9, malefic moon, Saturn in 12th": "火星在第5、7或9宫,月亮受凶,土星在第12宫", + "Mars in 7th from Sun": "火星在太阳起第7宫", + "Mars is lord of 6th or 8th": "火星守护第6或第8宫", + "Mars joins Saturn": "火星与土星相会", + "Mercury 2nd from Sun": "水星在太阳起第2宫", + "Mercury conjunct or aspected by a malefic": "水星与凶星合相或受凶星相位", + "Mercury in 3rd house": "水星在第3宫", + "Mercury in a quadrant": "水星在角宫", + "Mercury is lord of both Lagna and 4th (Gemini or Virgo Lagna)": "水星同时守护上升与第4宫(双子或处女上升)", + "Mercury, Jupiter and Venus each in kendra, trine or 2nd house": "水星、木星、金星各在角宫、三方宫或第2宫", + "Method 1: 9th lord from 11th in 11th with Moon, aspected by 11th lord": "算法一:11宫起的9宫主与月亮同在第11宫,并受11宫主相位", + "Method 2: 9th lord from Lagna and Moon in 11th": "算法二:上升起的9宫主与月亮同在第11宫", + "Moon 11th from Mercury (BVR method)": "月亮在水星起第11宫(BVR 算法)", + "Moon and Mars in conjunction in any house": "月亮与火星在同一宫合相", + "Moon and Venus in 7th, malefics in 4th and 10th": "月亮与金星在第7宫,凶星在第4宫与第10宫", + "Moon aspected by Lagna lord in 6th, 8th or 12th": "月亮受落在第6、8或12宫的上升主相位", + "Moon conjunct Saturn, Mandi or Rahu": "月亮与土星、Mandi 或罗睺合相", + "Moon in 10th from Sun": "月亮在太阳起第10宫", + "Moon in 10th, Venus in 7th, malefics in 4th": "月亮在第10宫,金星在第7宫,凶星在第4宫", + "Moon in 5th from Jupiter": "月亮在木星起第5宫", + "Moon in 9th house": "月亮在第9宫", + "Moon in Lagna": "月亮在上升宫", + "Moon in conjunction with Saturn": "月亮与土星合相", + "Moon is strong": "月亮有力", + "Moon joins malefics in navamsa of Cancer or Scorpio": "月亮在巨蟹座或天蝎座 D9 与凶星同宫", + "Navamsa Lagna owned by dry planet": "D9 上升由干燥行星守护", + "No Jupiter aspect on Moon": "月亮不受木星相位", + "Powerful lords of 5th and 7th join with 6th lord": "有力的5宫主与7宫主和6宫主相会", + "PyJHora aligned source logic": "按 PyJHora 源码条件判定", + "Rahu in 2nd house": "罗睺在第2宫", + "Rahu in Lagna": "罗睺在上升宫", + "Rahu in Lagna in conjunction with Moon": "罗睺在上升宫与月亮合相", + "Raja Yoga disrupted by malefic associations or aspects": "王瑜伽因凶星同宫或相位受损", + "Saturn aspects 5th lord OR Moon in 5th house": "土星相位5宫主,或月亮在第5宫", + "Saturn exalted and in kendra": "土星擢升且在角宫", + "Saturn in 12th house": "土星在第12宫", + "Saturn in 2nd from Sun": "土星在太阳起第2宫", + "Saturn in 5th house": "土星在第5宫", + "Saturn in 9th house": "土星在第9宫", + "Strong benefic in Lagna, 3rd or 5th, simultaneously aspecting 9th": "有力吉星在第1、3或5宫,同时相位第9宫", + "Sun and Mandi in 2nd house": "太阳与 Mandi 同在第2宫", + "Sun and Mercury in the same house (Budha-Aditya conjunction)": "太阳与水星同宫(水日合相)", + "Sun and Saturn in 5th house": "太阳与土星同在第5宫", + "Sun exalted and strong": "太阳擢升且有力", + "Sun in 6th house in conjunction with a malefic": "太阳在第6宫与凶星合相", + "Sun in 7th house": "太阳在第7宫", + "Sun in kendra from Moon": "太阳在月亮起的角宫", + "Sun in own, exalted or friendly sign": "太阳在本座、擢升座或友座", + "Sun in the 10th house": "太阳在第10宫", + "Sun joins lords of navamsa signs occupied by 6th and 9th lords": "太阳与6宫主、9宫主所落 D9 星座的主星相会", + "Sun or Saturn with Rahu in cruel shashtiamsa": "太阳或土星与罗睺同在凶性六十分", + "Sun strong in Leo": "太阳在狮子座有力", + "Sun, Moon and Mars are in mutual trines (trikona) from each other": "太阳、月亮、火星彼此成三方关系", + "Two benefics join in 7th, 9th or 10th": "两颗吉星同在第7、9或10宫", + "Venus in own/exaltation sign (Ta/Li/Pi) and in kendra (1/4/7/10) from Lagna": "金星在本座或擢升座(金牛、天秤、双鱼)且在上升起的角宫(1、4、7、10)", + "Venus, Saturn and Mars join Moon in 7th house": "金星、土星、火星与月亮同在第7宫", + "Waning Moon in 5th house": "亏月在第5宫", + "Weak Moon in 7th house": "弱月在第7宫" +} diff --git a/scripts/report_render/appendix_language.py b/scripts/report_render/appendix_language.py index edf5e000..9f08d918 100644 --- a/scripts/report_render/appendix_language.py +++ b/scripts/report_render/appendix_language.py @@ -647,6 +647,10 @@ def chinese_yoga_label(item: dict) -> str: def chinese_combination(item: dict) -> str: """Use the Chinese combination sentence. An English sentence is not translated word by word.""" + authored = str(item.get("combination_zh") or "").strip() + if authored and re.search(r"[一-鿿]", authored) and not _has_prose_glue(authored): + # Written in Chinese by the detector itself (yogas_doshas raj / dhana rows). + return authored combination = str(item.get("combination") or "").strip() han = bool(re.search(r"[\u4e00-\u9fff]", combination)) function = bool(_HAN_FUNCTION_RE.search(combination)) if combination else False diff --git a/scripts/yoga_engine.py b/scripts/yoga_engine.py index a9d56ecd..3a84aeb3 100644 --- a/scripts/yoga_engine.py +++ b/scripts/yoga_engine.py @@ -162,6 +162,48 @@ STRENGTH_EN = { } +# Chinese wording for the English ``note`` of semantic (BVR / PyJHora batch) +# conditions. The note stays the English twin. One note maps to one Chinese +# sentence, so the dedup key keeps the same equality classes (BUG-1418). +_SEMANTIC_NOTES_ZH_PATH = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "..", "references", "yoga_semantic_notes_zh.json") +# A condition without a note used to print its type key. Keep one fixed +# sentence per type so two matches of the same type still dedupe together. +_SEMANTIC_TYPE_ZH = { + "planet_in_house": "行星落在规则指定的宫位", + "planet_in_lagna": "行星落在上升宫", +} + + +@functools.lru_cache(maxsize=1) +def _semantic_notes_zh() -> Dict[str, str]: + try: + with open(_SEMANTIC_NOTES_ZH_PATH, "r", encoding="utf-8") as handle: + data = json.load(handle) + except (OSError, ValueError): + return {} + return {str(key): str(value) for key, value in data.items()} if isinstance(data, dict) else {} + + +def _semantic_text(cond: Dict, ctype: str): + """``cond.get("note", ctype)`` with Chinese text; the old value stays as ``.en``.""" + if "note" in cond: + note = cond.get("note") + zh = _semantic_notes_zh().get(str(note)) if note else None + return BiText(zh, note) if zh else note + zh = _SEMANTIC_TYPE_ZH.get(str(ctype)) + return BiText(zh, ctype) if zh else ctype + + +def _template_text(zh: str, en) -> BiText: + """``BiText(zh, en)``; a rule template written only in English gets its Chinese sentence (BUG-1418).""" + if not any("\u4e00" <= char <= "\u9fff" for char in zh): + chinese = _semantic_notes_zh().get(zh) + if chinese: + return BiText(chinese, zh if en is None else en) + return BiText(zh, en) + + def _combo(cond: Dict, default_zh: str, **values) -> BiText: """Format the rule's combination template in Chinese (unchanged) and in English.""" zh = cond.get("combo_template", default_zh).format(**values) @@ -172,7 +214,7 @@ def _combo(cond: Dict, default_zh: str, **values) -> BiText: en = en_template.format(**{key: (value.en if isinstance(value, BiText) else value) for key, value in values.items()}) except (KeyError, IndexError, ValueError): en = None - return BiText(zh, en) + return _template_text(zh, en) def _get_dignity_level(planet: str, sign: str) -> str: @@ -1465,7 +1507,7 @@ class YogaEngine: combo = cond.get("combo_template", "自定义条件满足") if isinstance(combo, str): # Custom templates were never formatted; keep that, and pair the English text. - combo = BiText(combo, cond.get("combo_template_en") if "combo_template" in cond else DEFAULT_COMBO_EN["自定义条件满足"]) + combo = _template_text(combo, cond.get("combo_template_en") if "combo_template" in cond else DEFAULT_COMBO_EN["自定义条件满足"]) if callable(combo): combo = combo(ctx, bindings) strength = cond.get("strength", rule.get("strength", "中")) @@ -1788,7 +1830,8 @@ class YogaEngine: def _eval_semantic_condition(self, ctype, cond, ctx, rule, bindings): """兼容批量抽取规则中的语义型条件。返回保守布尔匹配。""" def success(combo=None): - return [{"combination": combo or cond.get("note", ctype), "strength": rule.get("strength", "中")}] + text = combo or _semantic_text(cond, ctype) + return [{"combination": text, "strength": rule.get("strength", "中")}] def house(ref): if isinstance(ref, int): diff --git a/scripts/yogas_doshas.py b/scripts/yogas_doshas.py index 47367901..71817a2b 100644 --- a/scripts/yogas_doshas.py +++ b/scripts/yogas_doshas.py @@ -29,6 +29,24 @@ NATURAL_BENEFICS = {'Jupiter', 'Venus', 'Mercury', 'Moon'} NATURAL_MALEFICS = {'Saturn', 'Mars', 'Sun', 'Rahu', 'Ketu'} +# Chinese twins of the raj / dhana combination strings. ``combination`` keeps its +# old text; the Chinese report reads ``combination_zh`` (BUG-1418). +_PLANET_ZH = { + 'Sun': '太阳', 'Moon': '月亮', 'Mars': '火星', 'Mercury': '水星', 'Jupiter': '木星', + 'Venus': '金星', 'Saturn': '土星', 'Rahu': '罗睺', 'Ketu': '计都', +} + + +def _cn(planet) -> str: + return _PLANET_ZH.get(str(planet), str(planet)) + + +def _houses_cn(houses) -> str: + if isinstance(houses, (list, tuple)): + return '、'.join(str(house) for house in houses) + return str(houses) + + def calc_raj_yogas(planets_data: Dict, houses: Dict) -> Dict: """ 计算 Raj Yogas(王者瑜伽)——权力、地位、社会影响力格局 v7.0 @@ -109,6 +127,7 @@ def calc_raj_yogas(planets_data: Dict, houses: Dict) -> Dict: 'type': 'Raj Yoga', 'subtype': 'dual_lordship', 'combination': f'{lord}(H{k_houses}主+H{t_houses}主)', + 'combination_zh': f'{_cn(lord)}同时是第{_houses_cn(k_houses)}宫主(角宫)与第{_houses_cn(t_houses)}宫主(三方宫)', 'formation_house': lord_house, 'strength': 'strong' if lord_house in kendras else 'moderate', 'interpretation': f'Raj Yoga——{lord}同时掌管角宫{k_houses}和三方宫{t_houses},且在{lord_house}宫,双重权力格局', @@ -134,6 +153,7 @@ def calc_raj_yogas(planets_data: Dict, houses: Dict) -> Dict: 'type': 'Raj Yoga', 'subtype': 'conjunction', 'combination': f'{kl}(角宫主) + {tl}(三方宫主)', + 'combination_zh': f'角宫主{_cn(kl)}与三方宫主{_cn(tl)}同宫', 'formation_house': house_from_asc, 'strength': 'strong' if house_from_asc in kendras else 'moderate', 'interpretation': f'Raj Yoga——{kl}(角宫主)与{tl}(三方宫主)同宫在{SIGNS_LIST[kl_sign]},权力格局', @@ -146,6 +166,7 @@ def calc_raj_yogas(planets_data: Dict, houses: Dict) -> Dict: 'type': 'Raj Yoga', 'subtype': 'mutual_aspect', 'combination': f'{kl}(角宫主) ↔ {tl}(三方宫主)', + 'combination_zh': f'角宫主{_cn(kl)}与三方宫主{_cn(tl)}对宫互视', 'interpretation': f'Raj Yoga——{kl}(角宫主)与{tl}(三方宫主)互看(对宫相位),权力格局', } results['yogas'].append(yoga) @@ -166,6 +187,7 @@ def calc_raj_yogas(planets_data: Dict, houses: Dict) -> Dict: 'type': 'Raj Yoga', 'subtype': 'parivartana', 'combination': f'{kl}(角宫主) ⇄ {tl}(三方宫主)', + 'combination_zh': f'角宫主{_cn(kl)}与三方宫主{_cn(tl)}互换星座', 'interpretation': f'Raj Yoga——{kl}(角宫主)与{tl}(三方宫主)互容交换,权力格局强化', } results['yogas'].append(yoga) @@ -185,6 +207,7 @@ def calc_raj_yogas(planets_data: Dict, houses: Dict) -> Dict: 'type': 'Viparita Raja Yoga', 'subtype': 'dusthana_in_dusthana', 'combination': f'H{h}_Lord({lord}) in H{lord_house}', + 'combination_zh': f'{h}宫主{_cn(lord)}落第{lord_house}宫', 'interpretation': f'Viparita Raja Yoga——{h}宫主{lord}落入{lord_house}宫(凶中凶),先苦后甜逆转格局', } results['yogas'].append(yoga) @@ -254,6 +277,7 @@ def calc_dhana_yogas(planets_data: Dict, houses: Dict) -> Dict: 'type': 'Dhana Yoga', 'subtype': '2nd_11th_lords_strong', 'combination': f'H2_Lord({lord_2}) in H{h2} + H11_Lord({lord_11}) in H{h11}', + 'combination_zh': f'2宫主{_cn(lord_2)}落第{h2}宫,11宫主{_cn(lord_11)}落第{h11}宫', 'strength': 'strong' if h2 in KENDRA and h11 in KENDRA else 'moderate', 'interpretation': f'Dhana Yoga——2宫主{lord_2}(H{h2})与11宫主{lord_11}(H{h11})均落强宫,财富积累格局', }) @@ -268,6 +292,7 @@ def calc_dhana_yogas(planets_data: Dict, houses: Dict) -> Dict: 'type': 'Dhana Yoga', 'subtype': '5th_9th_conjunction', 'combination': f'H5_Lord({lord_5}) + H9_Lord({lord_9})', + 'combination_zh': f'5宫主{_cn(lord_5)}与9宫主{_cn(lord_9)}同宫', 'strength': 'strong', 'interpretation': f'Dhana Yoga——5宫主{lord_5}与9宫主{lord_9}同宫在{s},财富+幸运格局', }) @@ -281,6 +306,7 @@ def calc_dhana_yogas(planets_data: Dict, houses: Dict) -> Dict: 'type': 'Dhana Yoga', 'subtype': '5th_9th_mutual_aspect', 'combination': f'H5_Lord({lord_5}) ↔ H9_Lord({lord_9})', + 'combination_zh': f'5宫主{_cn(lord_5)}与9宫主{_cn(lord_9)}对宫互视', 'strength': 'moderate', 'interpretation': f'Dhana Yoga——5宫主{lord_5}(H{h5})与9宫主{lord_9}(H{h9})互看,财富格局', }) @@ -290,6 +316,7 @@ def calc_dhana_yogas(planets_data: Dict, houses: Dict) -> Dict: 'type': 'Dhana Yoga', 'subtype': '5th_9th_strong_houses', 'combination': f'H5_Lord({lord_5}) in H{h5} + H9_Lord({lord_9}) in H{h9}', + 'combination_zh': f'5宫主{_cn(lord_5)}落第{h5}宫,9宫主{_cn(lord_9)}落第{h9}宫', 'strength': 'moderate', 'interpretation': f'Dhana Yoga——5宫主{lord_5}与9宫主{lord_9}均落强宫,财富格局', }) @@ -302,6 +329,7 @@ def calc_dhana_yogas(planets_data: Dict, houses: Dict) -> Dict: 'type': 'Dhana Yoga', 'subtype': '2nd_9th_conjunction', 'combination': f'H2_Lord({lord_2}) + H9_Lord({lord_9})', + 'combination_zh': f'2宫主{_cn(lord_2)}与9宫主{_cn(lord_9)}同宫', 'strength': 'strong', 'interpretation': f'Dhana Yoga——2宫主{lord_2}与9宫主{lord_9}同宫在{s},财富+幸运组合', }) @@ -314,6 +342,7 @@ def calc_dhana_yogas(planets_data: Dict, houses: Dict) -> Dict: 'type': 'Dhana Yoga', 'subtype': '11th_9th_conjunction', 'combination': f'H11_Lord({lord_11}) + H9_Lord({lord_9})', + 'combination_zh': f'11宫主{_cn(lord_11)}与9宫主{_cn(lord_9)}同宫', 'strength': 'strong', 'interpretation': f'Dhana Yoga——11宫主{lord_11}与9宫主{lord_9}同宫在{s},收益+幸运组合', }) @@ -329,6 +358,7 @@ def calc_dhana_yogas(planets_data: Dict, houses: Dict) -> Dict: 'type': 'Dhana Yoga', 'subtype': f'{w_house}th_lord_benefic_conjunction', 'combination': f'H{w_house}_Lord({wealth_lord}) + {benefic}', + 'combination_zh': f'{w_house}宫主{_cn(wealth_lord)}与{_cn(benefic)}同宫', 'strength': 'moderate', 'interpretation': f'Dhana Yoga——{w_house}宫主{wealth_lord}与吉星{benefic}同宫在{s},财富助力格局', }) diff --git a/tests/test_full_data_parity_chapters.py b/tests/test_full_data_parity_chapters.py index f5edf451..ef1f0bdb 100644 --- a/tests/test_full_data_parity_chapters.py +++ b/tests/test_full_data_parity_chapters.py @@ -328,6 +328,34 @@ def test_parity_chapters_go_through_local_conventions() -> None: 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) diff --git a/tests/test_yoga_combination_chinese.py b/tests/test_yoga_combination_chinese.py new file mode 100644 index 00000000..b9dc9cc6 --- /dev/null +++ b/tests/test_yoga_combination_chinese.py @@ -0,0 +1,105 @@ +"""BUG-1418: Chinese yoga combination text comes from a Chinese source. + +The English note or template of a rule stays the English twin. The Chinese +report must not print 「未判定」 for a combination that has text, and it must +not translate English back word by word. +""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +sys.modules.setdefault("jhora", None) + +from scripts.report_render.appendix_language import _has_prose_glue, chinese_combination +from scripts.yoga_engine import BiText, YogaEngine, _semantic_notes_zh, _semantic_text, _template_text +from scripts.yogas_doshas import calc_dhana_yogas, calc_raj_yogas + +ROOT = Path(__file__).resolve().parents[1] +RULES = ROOT / "references" / "yoga_rules.json" +HAN = re.compile(r"[一-鿿]") + + +def _semantic_notes() -> dict[str, set[str]]: + engine = YogaEngine(str(RULES)) + notes: dict[str, set[str]] = {} + + def walk(node, rule_id): + if isinstance(node, dict): + kind = node.get("type") or node.get("op") + if kind and kind not in ("compound_conditions", "compound") and not hasattr(engine, f"_eval_{kind}"): + if node.get("note"): + notes.setdefault(node["note"], set()).add(rule_id) + for value in node.values(): + walk(value, rule_id) + elif isinstance(node, list): + for value in node: + walk(value, rule_id) + + for rule in json.loads(RULES.read_text(encoding="utf-8"))["rules"]: + walk(rule.get("logic", {}), rule.get("id")) + return notes + + +def test_every_semantic_note_has_one_chinese_sentence() -> None: + table = _semantic_notes_zh() + notes = _semantic_notes() + assert notes + missing = sorted(note for note in notes if note not in table) + assert missing == [] + chinese = [table[note] for note in notes] + # One-to-one, so the detector's dedup key keeps the same equality classes. + assert len(set(chinese)) == len(chinese) + for text in table.values(): + assert HAN.search(text), text + assert not _has_prose_glue(text), text + assert "; " not in text + + +def test_semantic_text_keeps_the_old_value_as_english() -> None: + note = next(iter(_semantic_notes())) + text = _semantic_text({"note": note}, "any_type") + assert isinstance(text, BiText) + assert HAN.search(str(text)) + assert text.en == note + # No note: the old value was the type key; dedup still groups by type. + keyed = _semantic_text({}, "planet_in_house") + assert HAN.search(str(keyed)) and keyed.en == "planet_in_house" + assert _semantic_text({"note": ""}, "planet_in_house") == "" + english_template = _template_text("PyJHora aligned source logic", None) + assert HAN.search(str(english_template)) + assert english_template.en == "PyJHora aligned source logic" + chinese_template = _template_text("满足自定义条件", "Custom condition met") + assert str(chinese_template) == "满足自定义条件" and chinese_template.en == "Custom condition met" + + +def _planets(**signs: str) -> dict: + order = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", + "Sagittarius", "Capricorn", "Aquarius", "Pisces"] + asc = order.index("Aries") + planets = {} + for planet, sign in signs.items(): + index = order.index(sign) + planets[planet] = {"sign": sign, "house": (index - asc) % 12 + 1, "degree": index * 30 + 10.0} + return planets + + +def test_raj_and_dhana_rows_carry_chinese_combination() -> None: + # Fictional placements, Aries ascendant: Mars with Saturn, Jupiter in the 9th. + planets = _planets(Sun="Leo", Moon="Cancer", Mars="Capricorn", Mercury="Virgo", + Jupiter="Sagittarius", Venus="Libra", Saturn="Capricorn", Rahu="Gemini", Ketu="Sagittarius") + houses = {"asc_sign": "Aries"} + rows = calc_raj_yogas(planets, houses)["yogas"] + calc_dhana_yogas(planets, houses)["yogas"] + assert rows + for row in rows: + text = row.get("combination_zh") + assert text and HAN.search(text), row + assert not _has_prose_glue(text), text + assert not re.search(r"[A-Za-z]{3,}", text), text + assert chinese_combination(row) == text + english = [row for row in rows if "_Lord(" in row["combination"]] + for row in english: + assert chinese_combination(row) != "未判定"