fix(yogas): lunar and solar yogas count only the five true planets; retire duplicate rules (upstream 2dd6062a)

bvr_002..004 (Sunaphaa/Anaphaa/Duradhara) and bvr_016..018 (Vesi/Vosi/
Ubhayachara) take upstream yinduzhanxing 2dd6062a's logic (Mars..Saturn
only). kemadruma_yoga also ignores the nodes (product decision: classical
definition; upstream still counts them). The six lunar/solar duplicates of
upstream's RETIRED_RULE_IDS are retired; upstream's four other retirements
are out of scope. TASK-upstream-sync4-20261002 T5.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
Jesse_Chen
2026-10-02 13:53:02 +08:00
co-authored by Claude Opus 5.5
parent 42d1582f4c
commit f3ed950982
4 changed files with 180 additions and 38 deletions
+116 -36
View File
@@ -1305,7 +1305,7 @@
"category": "chandra",
"logic": {
"type": "custom",
"expr": "\n# PyJHora/BVR-5: no planets other than Sun in 1st/2nd/12th from Moon,\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'] 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",
"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"
},
@@ -9880,16 +9880,21 @@
"category": "surya",
"logic": {
"type": "has_planet_in_house_from",
"planets": "all",
"planets": {
"role": "set",
"planets": [
"Mars",
"Mercury",
"Jupiter",
"Venus",
"Saturn"
]
},
"from": "Sun",
"houses": [
2
],
"exclude": [
"Moon"
],
"combo_template": "{planets}在太阳的第2宫",
"combo_template_en": "{planets} in the 2nd from the Sun"
"combo_template": "{planets}在太阳的第2宫"
},
"effects": [
"太阳后方有行星支持",
@@ -9917,16 +9922,21 @@
"category": "surya",
"logic": {
"type": "has_planet_in_house_from",
"planets": "all",
"planets": {
"role": "set",
"planets": [
"Mars",
"Mercury",
"Jupiter",
"Venus",
"Saturn"
]
},
"from": "Sun",
"houses": [
12
],
"exclude": [
"Moon"
],
"combo_template": "{planets}在太阳的第12宫",
"combo_template_en": "{planets} in the 12th from the Sun"
"combo_template": "{planets}在太阳的第12宫"
},
"effects": [
"太阳前方有行星铺垫",
@@ -9959,29 +9969,39 @@
"conditions": [
{
"type": "has_planet_in_house_from",
"planets": "all",
"planets": {
"role": "set",
"planets": [
"Mars",
"Mercury",
"Jupiter",
"Venus",
"Saturn"
]
},
"from": "Sun",
"houses": [
2
],
"exclude": [
"Moon"
],
"combo_template": "{planets}在太阳的第2宫",
"combo_template_en": "{planets} in the 2nd from the Sun"
"combo_template": "{planets}在太阳的第2宫"
},
{
"type": "has_planet_in_house_from",
"planets": "all",
"planets": {
"role": "set",
"planets": [
"Mars",
"Mercury",
"Jupiter",
"Venus",
"Saturn"
]
},
"from": "Sun",
"houses": [
12
],
"exclude": [
"Moon"
],
"combo_template": "{planets}在太阳的第12宫",
"combo_template_en": "{planets} in the 12th from the Sun"
"combo_template": "{planets}在太阳的第12宫"
}
]
},
@@ -10010,10 +10030,22 @@
"bvr_id": "BVR-2",
"category": "chandra",
"logic": {
"type": "custom",
"expr": "\n# PyJHora/BVR-2 Sunapha: target 2nd from Moon has at least one planet and Sun is absent.\nmoon_h = house_of('Moon')\nif moon_h is None:\n False\nelse:\n target = ((moon_h - 1 + 1) % 12) + 1\n ps = occupants(target)\n bool(ps) and 'Sun' not in ps\n",
"combo_template": "{planets}在月亮的第2宫",
"combo_template_en": "{planets} in the 2nd from the Moon"
"type": "has_planet_in_house_from",
"planets": {
"role": "set",
"planets": [
"Mars",
"Mercury",
"Jupiter",
"Venus",
"Saturn"
]
},
"from": "Moon",
"houses": [
2
],
"combo_template": "{planets}在月亮的第2宫"
},
"effects": [
"自力更生",
@@ -10042,10 +10074,22 @@
"bvr_id": "BVR-3",
"category": "chandra",
"logic": {
"type": "custom",
"expr": "\n# PyJHora/BVR-3 Anapha: target 12th from Moon has at least one planet and Sun is absent.\nmoon_h = house_of('Moon')\nif moon_h is None:\n False\nelse:\n target = ((moon_h - 1 + 11) % 12) + 1\n ps = occupants(target)\n bool(ps) and 'Sun' not in ps\n",
"combo_template": "{planets}在月亮的第12宫",
"combo_template_en": "{planets} in the 12th from the Moon"
"type": "has_planet_in_house_from",
"planets": {
"role": "set",
"planets": [
"Mars",
"Mercury",
"Jupiter",
"Venus",
"Saturn"
]
},
"from": "Moon",
"houses": [
12
],
"combo_template": "{planets}在月亮的第12宫"
},
"effects": [
"心智有背景支持",
@@ -10074,9 +10118,45 @@
"bvr_id": "BVR-4",
"category": "chandra",
"logic": {
"type": "custom",
"expr": "\n# PyJHora/BVR-4 Duradhara: both Sunapha and Anapha conditions are true.\nmoon_h = house_of('Moon')\nif moon_h is None:\n False\nelse:\n h2 = ((moon_h - 1 + 1) % 12) + 1\n h12 = ((moon_h - 1 + 11) % 12) + 1\n ps2 = occupants(h2)\n ps12 = occupants(h12)\n bool(ps2) and 'Sun' not in ps2 and bool(ps12) and 'Sun' not in ps12\n",
"combo_template": "PyJHora Chandra yoga logic"
"type": "and",
"conditions": [
{
"type": "has_planet_in_house_from",
"planets": {
"role": "set",
"planets": [
"Mars",
"Mercury",
"Jupiter",
"Venus",
"Saturn"
]
},
"from": "Moon",
"houses": [
2
],
"combo_template": "{planets}在月亮的第2宫"
},
{
"type": "has_planet_in_house_from",
"planets": {
"role": "set",
"planets": [
"Mars",
"Mercury",
"Jupiter",
"Venus",
"Saturn"
]
},
"from": "Moon",
"houses": [
12
],
"combo_template": "{planets}在月亮的第12宫"
}
]
},
"effects": [
"月亮前后均有支撑",
+11 -1
View File
@@ -694,6 +694,15 @@ def _run_custom_expr(expr, exec_globals):
# ============================================================================
# YogaEngine: 规则加载与检测
# ============================================================================
# Lunar / solar duplicates of the BVR precise rules (bvr_002..004, bvr_016..018),
# retired so a chart is not judged twice by two definitions (sync4 T5; same
# six ids as upstream yinduzhanxing 2dd6062a RETIRED_RULE_IDS; upstream's other
# four retirements are outside this task's scope and not taken).
RETIRED_RULE_IDS = frozenset({
'voshi_yoga', 'veshi_yoga', 'ubhayachari_yoga', 'anapha_yoga',
'sunapha_yoga', 'durudhura_yoga',
})
class YogaEngine:
"""Yoga 检测引擎。从 JSON 加载规则,对星盘执行条件匹配。"""
@@ -701,7 +710,8 @@ class YogaEngine:
with open(rules_path, 'r', encoding='utf-8') as f:
data = json.load(f)
self.schema_version = data.get("schema_version", "1.0")
self.rules = [r for r in data.get("rules", []) if r.get("enabled", True)]
self.rules = [r for r in data.get("rules", []) if r.get("enabled", True)
and r.get("id") not in RETIRED_RULE_IDS]
def detect(self, planets: Dict[str, Dict], ascendant: str, context: Optional[Dict] = None) -> List[Dict]:
"""
@@ -0,0 +1,48 @@
"""sync4 T5: Sunaphaa / Anaphaa / Duradhara (Moon) and Vesi / Vosi / Ubhayachara
(Sun) count only Mars, Mercury, Jupiter, Venus, Saturn; Rahu / Ketu alone next
to the Moon or Sun do not form them, and Kemadruma ignores the nodes too.
Rule logic for the six BVR rules is upstream yinduzhanxing 2dd6062a."""
from __future__ import annotations
from pathlib import Path
from scripts.yoga_engine import RETIRED_RULE_IDS, YogaEngine
RULES = Path(__file__).resolve().parents[1] / "references" / "yoga_rules.json"
SIGNS = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"]
def _chart(**signs: str) -> dict:
return {name: {"sign": sign, "house": SIGNS.index(sign) + 1, "degree": 15.0, "longitude": SIGNS.index(sign) * 30 + 15.0}
for name, sign in signs.items()}
def _names(planets: dict) -> set[str]:
return {row["name"] for row in YogaEngine(str(RULES)).detect(planets, "Aries")}
BASE = dict(Sun="Aquarius", Moon="Libra", Mars="Aquarius", Mercury="Aquarius",
Jupiter="Cancer", Venus="Pisces", Saturn="Capricorn", Rahu="Pisces")
def test_ketu_alone_behind_the_moon_is_not_anaphaa() -> None:
names = _names(_chart(**BASE, Ketu="Virgo"))
assert not {"Anaphaa Yoga", "Sunaphaa Yoga", "Duradhara Yoga"} & names
def test_saturn_behind_the_moon_is_anaphaa() -> None:
signs = {**BASE, "Saturn": "Virgo", "Ketu": "Virgo"}
assert "Anaphaa Yoga" in _names(_chart(**signs))
def test_ketu_alone_after_the_sun_is_not_vesi() -> None:
signs = {**BASE, "Sun": "Leo", "Ketu": "Virgo", "Mars": "Capricorn", "Mercury": "Capricorn"}
assert "Vesi Yoga" not in _names(_chart(**signs))
def test_duplicate_lunar_solar_rules_are_retired() -> None:
assert {"sunapha_yoga", "durudhura_yoga", "voshi_yoga", "veshi_yoga", "ubhayachari_yoga"} <= RETIRED_RULE_IDS
ids = {rule["id"] for rule in YogaEngine(str(RULES)).rules}
assert not ids & RETIRED_RULE_IDS
+5 -1
View File
@@ -161,7 +161,11 @@ def _globals_for(engine: YogaEngine, chart_index: int) -> dict:
# ---------------------------------------------------------------------------
def test_cached_compile_matches_pre_change_behaviour_for_every_rule_expression(engine) -> None:
"""全部 192 条去重表达式,缓存版与参照版在三张示例盘上逐条同值。"""
assert len(RULE_EXPRESSIONS) >= 190, "规则表表达式数量异常,等价证明的覆盖面不足"
# 原值: >= 190
# 新值: >= 184
# 原因: TASK-upstream-sync4-20261002 T5:bvr_002~004、bvr_016~018 六条从 custom 表达式改为
# 声明式 has_planet_in_house_from(上游 2dd6062a),表达式条数相应减少;等价证明仍覆盖其余全部表达式
assert len(RULE_EXPRESSIONS) >= 184, "规则表表达式数量异常,等价证明的覆盖面不足"
mismatches = []
for chart_index in range(len(SAMPLE_CHARTS)):