fix(yoga): Neecha Bhanga lists the conditions that hold, never Raja Yoga (T8, BUG-1205)
Astrologer ruling 乙14. One condition source, yogas_doshas.calc_nicha_bhanga_raj_yoga (conditions A-F, now also structured). New scripts/main_yoga_detectors.py turns it into yoga_engine-shaped rows (zh + en twins) that YogaEngine.detect appends; the four neechabhanga_* rules are reference_only (kept, renamed, not evaluated). Card: traditional_yogas canonical 落陷取消(Neecha Bhanga) with 成立条件; compact yoga entries keep conditions/reductions. Report: category 落陷取消, mixed group, not raja; personal-report alias neecha_bhanga -> special. Changed assertion (test_consultation_yoga_detection_merge): canonical name 'Neecha Bhanga Raja Yoga' -> '落陷取消(Neecha Bhanga)' (ruling 乙14). 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:
co-authored by
Claude Opus 5.5
parent
0cf2249e6b
commit
df21e591e8
@@ -83,7 +83,8 @@ def test_traditional_yogas_closed_list_only_and_fixed_label() -> None:
|
||||
))
|
||||
assert layer["status"] == "executed"
|
||||
assert layer["label"] == TRADITIONAL_YOGA_LABEL == "传统格局,本站未验证,只作参考"
|
||||
assert layer["names"] == ["Sasa Yoga", "Gajakesari Yoga", "Neecha Bhanga Raja Yoga", "Budha-Aditya Yoga"]
|
||||
# BUG-1205: the Neecha Bhanga canonical is no longer called Raja Yoga.
|
||||
assert layer["names"] == ["Sasa Yoga", "Gajakesari Yoga", "落陷取消(Neecha Bhanga)", "Budha-Aditya Yoga"]
|
||||
|
||||
|
||||
def test_traditional_yogas_skip_misses_and_empty() -> None:
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Neecha Bhanga lists the conditions that hold, never "Raja Yoga" (BUG-1205).
|
||||
|
||||
Astrologer rulings 2026-10-03 乙14 (TASK-astrologer-rulings-batch1-20261003 T8):
|
||||
only the satisfied cancellation conditions are shown; the four data-driven
|
||||
`neechabhanga_*` rules are `reference_only`; the one condition source is
|
||||
`yogas_doshas.calc_nicha_bhanga_raj_yoga` (conditions A-F). Public chart:
|
||||
Barack Obama, Astro-Databank AA (repository holdout v4), Raman, mean nodes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.consultation_native_layers import build_traditional_yogas
|
||||
from scripts.jyotish_engine import compute_chart_data
|
||||
from scripts.main_yoga_detectors import neecha_bhanga_rows, with_navamsa
|
||||
from scripts.yoga_engine import YogaEngine
|
||||
from scripts.yogas_doshas import calc_nicha_bhanga_raj_yoga
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
RULES = ROOT / "references" / "yoga_rules.json"
|
||||
RAJA = re.compile(r"(neecha|neech|nicha)[ -]?bhanga[ -]?raja?\b|neechabhanga raja", re.IGNORECASE)
|
||||
|
||||
|
||||
def _case(case_id: str, library: str) -> dict:
|
||||
cases = json.loads((ROOT / "references/real_case_calibration" / library).read_text(encoding="utf-8"))["cases"]
|
||||
return next(case for case in cases if case["case_id"] == case_id)["birth"]
|
||||
|
||||
|
||||
def _planets(birth: dict) -> tuple[dict, str]:
|
||||
year, month, day = (int(part) for part in birth["date"].split("-"))
|
||||
hour, minute = (int(part) for part in birth["time"].split(":"))
|
||||
chart, *_ = compute_chart_data(year, month, day, hour, minute, birth["latitude"], birth["longitude"],
|
||||
birth["timezone_offset"], ayanamsa_name="raman")
|
||||
planets = {name: {"sign": row["sign"], "house": row["house"], "degree": row["degree_in_sign"],
|
||||
"lon": row["degree_raw"], "retrograde": row["retrograde"]}
|
||||
for name, row in chart["planets"].items()}
|
||||
return planets, chart["ascendant"]["sign"]
|
||||
|
||||
|
||||
def test_reference_only_rules_are_not_evaluated_and_carry_no_raja_name() -> None:
|
||||
rules = {rule["id"]: rule for rule in json.loads(RULES.read_text(encoding="utf-8"))["rules"]}
|
||||
ids = {"neechabhanga_basic", "neechabhanga_kendra", "neechabhanga_lord_exalted", "neechabhanga_parivartana"}
|
||||
for rule_id in ids:
|
||||
assert rules[rule_id]["reference_only"] is True
|
||||
assert "BUG-1205" in rules[rule_id]["reference_only_reason"]
|
||||
for key in ("name", "name_cn", "dedup_key"):
|
||||
assert not RAJA.search(str(rules[rule_id].get(key) or "")), (rule_id, key)
|
||||
engine = YogaEngine(str(RULES))
|
||||
assert not ids & {rule["id"] for rule in engine.rules}
|
||||
assert ids <= set(engine.reference_only_rule_ids)
|
||||
|
||||
|
||||
def test_public_chart_lists_the_conditions_from_the_one_source() -> None:
|
||||
planets, asc = _planets(_case("barack_obama_1961_aa_v4_holdout", "minute_rectification_holdout_v4.json"))
|
||||
source = calc_nicha_bhanga_raj_yoga(with_navamsa(planets), {})["yogas"]
|
||||
rows = [row for row in YogaEngine(str(RULES)).detect(planets, asc) if row.get("category") == "neecha_bhanga"]
|
||||
assert rows == neecha_bhanga_rows(planets)
|
||||
assert [row["planets"] for row in rows] == [[item["planet"]] for item in source]
|
||||
for row, item in zip(rows, source):
|
||||
assert len(row["conditions"]) == len(item["cancellation_conditions"]) == len(item["cancellation_reasons"]) > 0
|
||||
assert row["name"] == "Neecha Bhanga" and row["name_cn"] == "落陷取消"
|
||||
assert "成立条件:" in row["combination"]
|
||||
assert not RAJA.search(json.dumps(row, ensure_ascii=False))
|
||||
assert "Raja" not in row["name"] and "王" not in row["name_cn"]
|
||||
# This chart: Jupiter debilitated in Capricorn, dispositor Saturn in the 1st.
|
||||
assert rows[0]["planets"] == ["Jupiter"]
|
||||
assert rows[0]["conditions"][0] == "落陷星座主星土星在上升起第1宫(角宫)"
|
||||
|
||||
|
||||
def test_card_label_lists_conditions() -> None:
|
||||
row = {"name": "Neecha Bhanga", "planets": ["Jupiter"], "conditions": ["条件甲", "条件乙"]}
|
||||
layer = build_traditional_yogas({"raman_support": {"yogas": {"status": "executed", "hits": [], "ungoverned_detected": [row]}}})
|
||||
assert layer["names"] == ["落陷取消(Neecha Bhanga)(Jupiter;成立条件:条件甲;条件乙)"]
|
||||
|
||||
|
||||
def test_no_raja_wording_on_user_visible_surfaces() -> None:
|
||||
frontend = [path for path in (ROOT / "frontend" / "src").rglob("*") if path.suffix in {".ts", ".tsx"}]
|
||||
for path in frontend + [RULES, ROOT / "scripts/consultation_native_layers.py", ROOT / "scripts/main_yoga_detectors.py"]:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
assert not RAJA.search(text), path
|
||||
personal = (ROOT / "frontend/src/lib/personal-report-generation.ts").read_text(encoding="utf-8")
|
||||
assert 'neecha_bhanga: "special"' in personal
|
||||
|
||||
|
||||
def test_reader_report_shows_conditions_not_raja() -> None:
|
||||
import copy
|
||||
|
||||
from scripts.jyotish_engine import build_professional_report_reference_packet, cmd_full_reading
|
||||
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
|
||||
from scripts.professional_report_reference import _export_args
|
||||
|
||||
birth = _case("barack_obama_1961_aa_v4_holdout", "minute_rectification_holdout_v4.json")
|
||||
year, month, day = (int(part) for part in birth["date"].split("-"))
|
||||
hour, minute = (int(part) for part in birth["time"].split(":"))
|
||||
case = {"name": "Barack Obama", "year": year, "month": month, "day": day, "hour": hour, "minute": minute,
|
||||
"second": 0, "lat": birth["latitude"], "lon": birth["longitude"], "tz": birth["timezone_offset"],
|
||||
"node_mode": "mean", "ayanamsa": "raman", "today": "2026-10-03", "target_year": 2026, "age": 65}
|
||||
packet = dict(build_professional_report_reference_packet(cmd_full_reading(type("Args", (), dict(case))()), _export_args(dict(case)), []))
|
||||
packet["report_version"] = "pl9_personal_long_report.v3"
|
||||
markdown = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main")
|
||||
rows = [line for line in markdown.splitlines() if line.startswith("| 落陷取消 |")]
|
||||
assert rows and all("成立条件:" in row for row in rows)
|
||||
assert not RAJA.search(markdown)
|
||||
assert "坏座位消王" not in markdown and "王权/事业 | 成立条件" not in markdown
|
||||
mixed = markdown[markdown.index("#### 可用混合型瑜伽"):]
|
||||
assert "| 落陷取消 | 落陷取消 |" in mixed.split("#### 可用凶性瑜伽")[0]
|
||||
Reference in New Issue
Block a user