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
110 lines
6.3 KiB
Python
110 lines
6.3 KiB
Python
"""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]
|