fix(yoga): D9 debilitation only weakens Pancha Mahapurusha (BUG-1213)

Round-4 astrologer ruling 2026-10-03 Q1: the yoga stays formed with
d9_dignity_modifier='weakened' and the reason 'D9 落陷,兑现减弱'; only a failed
D1 condition is not_formed. The D9 sign is derived from the longitude when the
caller does not pass navamsa_sign, so the card, report and /api/pancha_mahapurusha
paths all check it. None of the nine public celebrity charts change.

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-03 16:18:06 +08:00
co-authored by Claude Opus 5.5
parent 52b3061fe5
commit bc03bba26b
4 changed files with 117 additions and 47 deletions
+53 -16
View File
@@ -2,7 +2,7 @@
`scripts/pancha_mahapurusha.py` is the only detector: kendras from the Lagna,
retrograde does not cancel, combustion and malefic conjunction / aspect only
reduce the yoga, D9 debilitation still cancels it (current behaviour). The
reduce the yoga, D9 debilitation only weakens it (round-4 ruling Q1, BUG-1213). The
data-driven `mahapurusha_*`, `*_own`, `moolatrikona_*`, `hamsa_trikona` and the
BVR Maalavya duplicate are `reference_only`, and `yogas_doshas` keeps its old
aggregate for reference only, so a yoga the main detector rejects cannot come
@@ -69,12 +69,42 @@ def test_malefic_aspect_reduces() -> None:
assert _one({"Jupiter": _row("Cancer", 5.0), "Ketu": _row("Capricorn", 10.0)}, "Hamsa Yoga")["strength"] == "full"
def test_d9_debilitation_still_cancels_when_the_caller_gives_d9() -> None:
# Current behaviour kept (ruling silent): checked only when `navamsa_sign`
# is supplied; product paths do not supply it.
hamsa = _one({"Jupiter": _row("Cancer", 21.0, navamsa_sign="Capricorn")}, "Hamsa Yoga")
assert hamsa["is_valid"] is False and hamsa["strength"] == "cancelled"
assert _one({"Jupiter": _row("Cancer", 21.0)}, "Hamsa Yoga")["is_valid"] is True
def test_d9_debilitation_only_weakens() -> None:
# 原值:传 navamsa_sign 落陷 → is_valid False、strength cancelled(不传则不检查)
# 新值:formed 不变,d9_dignity_modifier = weakened,原因「D9 落陷,兑现减弱」;
# 不传 navamsa_sign 时由经度推出 D9(巨蟹 21° 的 D9 是摩羯)
# 原因:占星师第四轮裁定 2026-10-03 问 1(BUG-1213)
for row in (_row("Cancer", 21.0, navamsa_sign="Capricorn"), _row("Cancer", 21.0)):
hamsa = _one({"Jupiter": row}, "Hamsa Yoga")
assert (hamsa["status"], hamsa["is_valid"], hamsa["strength"]) == ("formed", True, "reduced")
assert hamsa["d9_dignity_modifier"] == "weakened" and hamsa["d9_sign"] == "Capricorn"
assert hamsa["reductions"] == ["D9 落陷,兑现减弱"]
strong = _one({"Jupiter": _row("Cancer", 5.0)}, "Hamsa Yoga")
assert strong["d9_dignity_modifier"] is None and strong["strength"] == "full"
def test_d1_condition_not_met_is_not_formed() -> None:
# Jupiter exalted but in the 9th from a Pisces Lagna (trikona, not kendra).
rows = detect_pancha_mahapurusha({"Jupiter": _row("Cancer", 5.0, asc="Pisces")}, include_not_formed=True)
hamsa = next(item for item in rows if item["planet"] == "Jupiter")
assert (hamsa["status"], hamsa["is_valid"]) == ("not_formed", False)
# Kendra but neither own nor exalted.
rows = detect_pancha_mahapurusha({"Jupiter": _row("Leo", 5.0, asc="Leo")}, include_not_formed=True)
assert next(item for item in rows if item["planet"] == "Jupiter")["status"] == "not_formed"
assert detect_pancha_mahapurusha({"Jupiter": _row("Leo", 5.0, asc="Leo")}) == []
def test_d9_weakened_yoga_reaches_the_card_and_the_report_rows_alike() -> None:
planets = {"Jupiter": _row("Cancer", 21.0), "Moon": _row("Taurus", 3.0)}
rows = pancha_mahapurusha_rows(planets)
assert rows[0]["d9_dignity_modifier"] == "weakened"
assert "D9 落陷,兑现减弱" in rows[0]["combination"]
engine_rows = [row for row in YogaEngine(str(RULES)).detect(planets, "Cancer") if row.get("category") == "mahapurusha"]
assert engine_rows == rows
chart_rows = JyotishAPIHandler._detect_yogas(None, planets, SIGNS.index("Cancer"))
entry = _compact_yoga_entry(next(row for row in chart_rows if row.get("category") == "PMC"))
layer = build_traditional_yogas({"raman_support": {"yogas": {"status": "executed", "hits": [], "ungoverned_detected": [entry]}}})
assert layer["names"] == ["Hamsa Yoga(Jupiter;部分减弱:D9 落陷,兑现减弱)"]
def test_reference_only_rules_never_run() -> None:
@@ -85,14 +115,17 @@ def test_reference_only_rules_never_run() -> None:
def test_a_rejected_yoga_does_not_come_back_by_another_path() -> None:
# Jupiter exalted in the Lagna (Cancer) but debilitated in D9; also in a
# trikona, which the retired hamsa_trikona rule used to accept.
planets = {"Jupiter": _row("Cancer", 21.0, navamsa_sign="Capricorn"), "Moon": _row("Taurus", 3.0), "Sun": _row("Aries", 10.0),
"Mars": _row("Gemini", 1.0), "Mercury": _row("Pisces", 2.0), "Venus": _row("Aries", 2.0),
"Saturn": _row("Leo", 2.0), "Rahu": _row("Virgo", 2.0), "Ketu": _row("Pisces", 2.0)}
engine_rows = YogaEngine(str(RULES)).detect(planets, "Cancer")
# 原值:木星在上升巨蟹入旺但 D9 落陷(被判不成立)作为「被拒格局」
# 新值:木星入旺巨蟹但在双鱼上升的第 9 宫(三方宫,非角宫),D1 条件不成立
# 原因:第四轮问 1(BUG-1213)D9 落陷不再拒绝格局;被拒样例改为 D1 不成立,
# 仍是已退役的 hamsa_trikona 规则曾接受的三方宫
asc = "Pisces"
planets = {"Jupiter": _row("Cancer", 5.0, asc=asc), "Moon": _row("Taurus", 3.0, asc=asc), "Sun": _row("Aries", 10.0, asc=asc),
"Mars": _row("Gemini", 1.0, asc=asc), "Mercury": _row("Aquarius", 2.0, asc=asc), "Venus": _row("Aries", 2.0, asc=asc),
"Saturn": _row("Leo", 2.0, asc=asc), "Rahu": _row("Virgo", 2.0, asc=asc), "Ketu": _row("Pisces", 2.0, asc=asc)}
engine_rows = YogaEngine(str(RULES)).detect(planets, asc)
assert not [row for row in engine_rows if "Hamsa" in row["name"] or "汉萨" in row.get("name_cn", "")]
chart_rows = JyotishAPIHandler._detect_yogas(None, planets, SIGNS.index("Cancer"))
chart_rows = JyotishAPIHandler._detect_yogas(None, planets, SIGNS.index(asc))
assert not [row for row in chart_rows if "Hamsa" in row["name"]]
merged = merge_detected_yogas(
[entry for entry in map(_compact_yoga_entry, chart_rows) if entry],
@@ -100,13 +133,17 @@ def test_a_rejected_yoga_does_not_come_back_by_another_path() -> None:
)
layer = build_traditional_yogas({"raman_support": {"yogas": {"status": "executed", "hits": [], "ungoverned_detected": merged}}})
assert not [name for name in layer["names"] if "Hamsa" in name]
aggregate = calc_all_yogas_doshas(planets, {}, "Cancer", "Moon", "Taurus", 11, 12, 10, "Gemini")
aggregate = calc_all_yogas_doshas(planets, {}, asc, "Moon", "Taurus", 11, 12, 10, "Gemini")
assert aggregate["pancha_mahapurusha"]["status"] == "reference_only"
assert aggregate["pancha_mahapurusha"]["yogas"] == []
def test_reduced_yoga_reaches_the_card_with_its_reasons() -> None:
planets = {"Jupiter": _row("Cancer", 5.0), "Saturn": _row("Capricorn", 10.0)}
# 原值:土星摩羯 10°(D9 白羊,落陷;旧规则只在给 navamsa_sign 时检查)
# 新值:土星摩羯 2°(D9 摩羯,本宫),断言不变
# 原因:BUG-1213 起 D9 由经度推出,摩羯 10° 的萨萨会多一条「D9 落陷」减弱;
# 本用例只测凶星照的原因上卡,换成 D9 不落陷的度数
planets = {"Jupiter": _row("Cancer", 5.0), "Saturn": _row("Capricorn", 2.0)}
all_rows = pancha_mahapurusha_rows(planets)
engine_rows = [row for row in YogaEngine(str(RULES)).detect(planets, "Cancer") if row.get("category") == "mahapurusha"]
assert engine_rows == all_rows