From 0a4e6d0aa4d82984ae0af1e1536bece1ad44d82e Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Fri, 2 Oct 2026 14:08:21 +0800 Subject: [PATCH] fix(yogas): restore English combination text for the five-planet BVR rules; Kemadruma text says nodes are not counted (BUG-1187) Follow-up to f3ed9509 (TASK-upstream-sync4-20261002 T5). The port dropped combo_template_en from bvr_002/003/004/016/017/018, so the English edition had no combination text for them; restored. kemadruma_yoga's text still said "only the Sun and Moon" although the nodes no longer count. New tests: Ketu alone next to the Moon leaves Kemadruma in place in both the rule and detect_kemadruma; Saturn there cancels it and forms Sunaphaa. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE --- references/yoga_rules.json | 28 ++++++--- ...ar_solar_yoga_templates_kemadruma_nodes.py | 62 +++++++++++++++++++ 2 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 tests/test_lunar_solar_yoga_templates_kemadruma_nodes.py diff --git a/references/yoga_rules.json b/references/yoga_rules.json index 8e187375..560dbae3 100644 --- a/references/yoga_rules.json +++ b/references/yoga_rules.json @@ -1306,8 +1306,8 @@ "logic": { "type": "custom", "expr": "\n# PyJHora/BVR-5: none of the five true planets (Mars..Saturn) in 1st/2nd/12th from Moon;\n# the Sun and the nodes do not count (sync4 T5, BPHS / Raman 300 Combinations no. 5),\n# and no planets other than Moon in quadrants from Lagna.\nif 'Moon' not in ctx.planets:\n False\nelse:\n moon_h = house_of('Moon')\n moon_zone = {moon_h, ((moon_h - 1 + 1) % 12) + 1, ((moon_h - 1 + 11) % 12) + 1}\n ky1 = all(p in ['Moon', 'Sun', 'Rahu', 'Ketu'] for p, info in ctx.planets.items() if info.get('house') in moon_zone)\n ky2 = all(p == 'Moon' for p, info in ctx.planets.items() if info.get('house') in [1, 4, 7, 10])\n ky1 and ky2\n", - "combo_template": "月亮1/2/12宫仅日月,且Lagna角宫仅月亮", - "combo_template_en": "Only the Sun and Moon in the 1st/2nd/12th from the Moon, and only the Moon in the Kendras from the Lagna" + "combo_template": "月亮1/2/12宫无火水木金土(太阳与罗计不计),且Lagna角宫仅月亮", + "combo_template_en": "None of Mars to Saturn in the 1st/2nd/12th from the Moon (the Sun and the nodes are not counted), and only the Moon in the Kendras from the Lagna" }, "effects": [ "人生艰辛", @@ -9894,7 +9894,8 @@ "houses": [ 2 ], - "combo_template": "{planets}在太阳的第2宫" + "combo_template": "{planets}在太阳的第2宫", + "combo_template_en": "{planets} in the 2nd from the Sun" }, "effects": [ "太阳后方有行星支持", @@ -9936,7 +9937,8 @@ "houses": [ 12 ], - "combo_template": "{planets}在太阳的第12宫" + "combo_template": "{planets}在太阳的第12宫", + "combo_template_en": "{planets} in the 12th from the Sun" }, "effects": [ "太阳前方有行星铺垫", @@ -9983,7 +9985,8 @@ "houses": [ 2 ], - "combo_template": "{planets}在太阳的第2宫" + "combo_template": "{planets}在太阳的第2宫", + "combo_template_en": "{planets} in the 2nd from the Sun" }, { "type": "has_planet_in_house_from", @@ -10001,7 +10004,8 @@ "houses": [ 12 ], - "combo_template": "{planets}在太阳的第12宫" + "combo_template": "{planets}在太阳的第12宫", + "combo_template_en": "{planets} in the 12th from the Sun" } ] }, @@ -10045,7 +10049,8 @@ "houses": [ 2 ], - "combo_template": "{planets}在月亮的第2宫" + "combo_template": "{planets}在月亮的第2宫", + "combo_template_en": "{planets} in the 2nd from the Moon" }, "effects": [ "自力更生", @@ -10089,7 +10094,8 @@ "houses": [ 12 ], - "combo_template": "{planets}在月亮的第12宫" + "combo_template": "{planets}在月亮的第12宫", + "combo_template_en": "{planets} in the 12th from the Moon" }, "effects": [ "心智有背景支持", @@ -10136,7 +10142,8 @@ "houses": [ 2 ], - "combo_template": "{planets}在月亮的第2宫" + "combo_template": "{planets}在月亮的第2宫", + "combo_template_en": "{planets} in the 2nd from the Moon" }, { "type": "has_planet_in_house_from", @@ -10154,7 +10161,8 @@ "houses": [ 12 ], - "combo_template": "{planets}在月亮的第12宫" + "combo_template": "{planets}在月亮的第12宫", + "combo_template_en": "{planets} in the 12th from the Moon" } ] }, diff --git a/tests/test_lunar_solar_yoga_templates_kemadruma_nodes.py b/tests/test_lunar_solar_yoga_templates_kemadruma_nodes.py new file mode 100644 index 00000000..1a304cf1 --- /dev/null +++ b/tests/test_lunar_solar_yoga_templates_kemadruma_nodes.py @@ -0,0 +1,62 @@ +"""sync4 T5 follow-up (BUG-1187). + +* Kemadruma: a node alone next to the Moon does not cancel it, in both the rule + version (kemadruma_yoga) and yoga_expansion.detect_kemadruma. +* The six five-planet BVR rules keep their English combination text (the port + dropped combo_template_en, so the English edition fell back to no text). +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from scripts.yoga_engine import YogaEngine +from scripts.yoga_expansion import detect_kemadruma +from tests.test_lunar_solar_yogas_count_five_planets import _chart + +RULES = Path(__file__).resolve().parents[1] / "references" / "yoga_rules.json" + +# Aries lagna; Moon in Taurus, Ketu alone in Gemini (2nd from Moon), Aries (12th) empty, +# no planet in Aries / Cancer / Libra / Capricorn (kendras from lagna). +ISOLATED = dict(Moon="Taurus", Ketu="Gemini", Rahu="Sagittarius", Sun="Leo", Mars="Leo", + Mercury="Virgo", Jupiter="Virgo", Venus="Leo", Saturn="Scorpio") + + +def test_ketu_next_to_the_moon_does_not_cancel_kemadruma() -> None: + planets = _chart(**ISOLATED) + names = {row["name"] for row in YogaEngine(str(RULES)).detect(planets, "Aries")} + assert "Kemadruma Yoga" in names + assert not {"Sunaphaa Yoga", "Anaphaa Yoga", "Duradhara Yoga"} & names + assert detect_kemadruma(planets)["present"] is True + + +def test_saturn_next_to_the_moon_cancels_kemadruma() -> None: + planets = _chart(**{**ISOLATED, "Saturn": "Gemini"}) + names = {row["name"] for row in YogaEngine(str(RULES)).detect(planets, "Aries")} + assert "Kemadruma Yoga" not in names + assert "Sunaphaa Yoga" in names + assert detect_kemadruma(planets)["present"] is False + + +def _templates(node, acc): + if isinstance(node, dict): + if "combo_template" in node: + acc.append(node) + for value in node.values(): + _templates(value, acc) + elif isinstance(node, list): + for value in node: + _templates(value, acc) + return acc + + +def test_five_planet_bvr_rules_keep_english_combination_text() -> None: + rules = {rule["id"]: rule for rule in json.loads(RULES.read_text(encoding="utf-8"))["rules"]} + for rule_id in ("bvr_002_sunapha_precise", "bvr_003_anapha_precise", "bvr_004_duradhara_precise", + "bvr_016_vesi_precise", "bvr_017_vosi_precise", "bvr_018_ubhayachara_precise", + "kemadruma_yoga"): + conditions = _templates(rules[rule_id]["logic"], []) + assert conditions, rule_id + for cond in conditions: + assert cond.get("combo_template_en"), rule_id