feat(narayana-rath): AD direction checked against p.51, book divergence listed (BUG-1225)

Round-6 ruling Q7: default by the AD start sign's odd/even (Table 13, as in
Table 25); Saturn in the MD sign -> always zodiacal (Table 11); Ketu ->
reversed (Table 14); both -> Ketu first. Each AD rule now carries
direction.book_divergence (p.47 J.S. 1.1.34; J.S. 2.4.31-32), not applied;
reference report shows one line, English twins added. Table 25 Taurus 12/12.

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-04 07:35:11 +08:00
co-authored by Claude Opus 5.5
parent 61bd4425cd
commit 57ef4560ce
7 changed files with 148 additions and 4 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+16
View File
@@ -162,6 +162,7 @@ def rath_report_lines_zh(rath: Any) -> List[str]:
"",
f"- 当前大运 {md.get('sign')} 的子运:比较 {compared[0]} 与 {compared[-1]},较强 {rule.get('stronger_sign')}"
f"(第 {rule.get('strength_level_decided')} 级),从宫主 {rule.get('lord')} 所在 {rule.get('start_sign')} 起,12 等分",
*_ad_direction_lines_zh(rule.get("direction")),
"",
"| Rath AD | 星座 | 起始年龄 | 结束年龄 |",
"|---------|------|----------|----------|",
@@ -171,6 +172,21 @@ def rath_report_lines_zh(rath: Any) -> List[str]:
return lines
AD_DIRECTION_WORD_ZH = {"zodiacal": "顺行", "reverse": "逆行"}
def _ad_direction_lines_zh(direction: Any) -> List[str]:
"""AD direction applied + the book's two other statements, flagged as a
book divergence and not applied (round-6 ruling Q7, BUG-1225)."""
if not isinstance(direction, dict) or not direction.get("rule_text"):
return []
others = ";".join(
f"{row.get('rule_text')} → {AD_DIRECTION_WORD_ZH.get(row.get('direction'), '-')}"
for row in direction.get("book_divergence") or [] if isinstance(row, dict))
line = f"- 子运方向:{direction['rule_text']},{AD_DIRECTION_WORD_ZH.get(direction.get('direction'), '-')}"
return [line + (f"(书内分歧,未采用:{others})" if others else "")]
def rath_summary_line_zh(rath: Any) -> Optional[str]:
"""One report line naming the Rath start sign and running MD (BUG-1218)."""
if not isinstance(rath, dict) or not rath:
+53 -2
View File
@@ -42,8 +42,12 @@ exception, exalted +1 / debilitated -1); Scorpio / Aquarius lords by
AD [p.48 Arambha Rasi, p.50 note 2]: compare the MD sign and its 7th with the
same seven levels, take the stronger, start from the sign holding that sign's
lord (dual lords by the same rule), twelve equal parts. Order: Table 13 from
the start sign; Saturn in the MD sign -> Table 11 (zodiacal); Ketu in the MD
sign -> Table 14 (Ketu first when both).
the start sign (direction by the start sign's odd/even, as in Table 25;
round-6 ruling Q7); Saturn in the MD sign -> Table 11 (zodiacal); Ketu in the
MD sign -> Table 14 (Ketu first when both) [p.51]. The book's other two
statements (p.47 J.S. 1.1.34: by the MD sign; J.S. 2.4.31-32: by the MD sign
or its lord's sign) disagree with Table 25; each AD rule carries them as
`direction.book_divergence` (BUG-1225), not applied.
Ported verbatim from upstream yinduzhanxing c7117f92 `scripts/narayana_dasha.py`:
`_rath_ketu_table12_sequence` (the `rath_ketu_table12_v1` branch of
@@ -318,6 +322,52 @@ def _ad_order_profile(md_sign_idx: int, planet_lons: Dict[str, float]) -> Tuple[
return "rath_table13_v1", None
# AD direction (round-6 ruling Q7): default by the odd/even of the AD start
# sign (Table 13 practice, as the book's Table 25 shows); exceptions p.51:
# Saturn in the MD sign -> always zodiacal (Table 11); Ketu in the MD sign ->
# reversed (Table 14); both -> Ketu first (product owner 2026-10-03). Two
# other statements in the book disagree with Table 25 and are reported, not
# applied (`book_divergence`).
AD_DIRECTION_RULE = {
"rath_table13_v1": ("按子运起点星座奇偶(Table 13)", "by the odd/even of the AD start sign (Table 13)"),
"rath_saturn_table11_v1": ("大运星座有土星:一律顺行(Table 11)", "Saturn in the MD sign: always zodiacal (Table 11)"),
"rath_ketu_table14_v1": ("大运星座有计都:反向(Table 14)", "Ketu in the MD sign: reversed (Table 14)"),
}
AD_BOOK_DIVERGENCE = (
("rath_p47_js_1_1_34", "Rath p.47 (J.S. 1.1.34)",
"p.47(J.S. 1.1.34)看大运星座奇偶", "p.47 (J.S. 1.1.34) by the odd/even of the MD sign"),
("js_2_4_31_32", "J.S. 2.4.31-32 (Rath p.47-48)",
"J.S. 2.4.31–32 看大运星座或其宫主所落星座", "J.S. 2.4.31-32 by the MD sign or the sign its lord occupies"),
)
def _is_odd_sign(sign_idx: int) -> bool:
return sign_idx % 2 == 0 # Aries (index 0) is the first, odd sign
def ad_direction(md_sign_idx: int, start_sign_idx: int, profile: str, planet_lons: Dict[str, float]) -> Dict[str, Any]:
"""Applied AD direction plus the two book statements it does not follow."""
order = nd.build_narayana_antardasha_order_sequence(start_sign_idx, profile=profile)
applied = "zodiacal" if order[1] == (start_sign_idx + 1) % 12 else "reverse"
lord_sign = nd._get_planet_sign(_lord(md_sign_idx, planet_lons), planet_lons)
by_rule = {
"rath_p47_js_1_1_34": "zodiacal" if _is_odd_sign(md_sign_idx) else "reverse",
"js_2_4_31_32": "zodiacal" if _is_odd_sign(md_sign_idx) or (lord_sign is not None and _is_odd_sign(lord_sign)) else "reverse",
}
zh, en = AD_DIRECTION_RULE[profile]
return {
"direction": applied,
"rule": profile,
"rule_text": zh,
"rule_text_en": en,
"book_divergence": [
{"id": key, "source": source, "rule_text": rule_zh, "rule_text_en": rule_en,
"direction": by_rule[key], "differs": by_rule[key] != applied, "applied": False}
for key, source, rule_zh, rule_en in AD_BOOK_DIVERGENCE
],
}
def mahadasha_sequence(seed_sign_idx: int, planet_lons: Dict[str, float],
*, start_age: float = 0.0, cycle_count: int = 2) -> Dict[str, Any]:
"""Rath MD rows from one start sign (cycle 2 = twelve minus cycle 1)."""
@@ -384,6 +434,7 @@ def antardasha(md_row: Dict[str, Any], planet_lons: Dict[str, float]) -> Dict[st
"start_sign": SIGNS[start],
"order_profile": profile,
"method_note": note,
"direction": ad_direction(md_sign_idx, start, profile, planet_lons),
},
}
+14
View File
@@ -174,6 +174,20 @@ _PHRASES: dict[str, str] = {
"| Rath MD | 星座 | 宫主 | 起始年龄 | 结束年龄 | 年数 |": "| Rath MD | Sign | Lord | Start age | End age | Years |",
"| Rath AD | 星座 | 起始年龄 | 结束年龄 |": "| Rath AD | Sign | Start age | End age |",
" 的子运:比较 ": " sub-periods: compare ",
# BUG-1225: narayana_legacy_label._ad_direction_lines_zh (AD direction + book divergence)
"- 子运方向:": "- AD direction: ",
"按子运起点星座奇偶(Table 13)": "by the odd/even of the AD start sign (Table 13)",
"大运星座有土星:一律顺行(Table 11)": "Saturn in the MD sign: always zodiacal (Table 11)",
"大运星座有计都:反向(Table 14)": "Ketu in the MD sign: reversed (Table 14)",
"(书内分歧,未采用:": " (book divergence, not applied: ",
"p.47(J.S. 1.1.34)看大运星座奇偶": "p.47 (J.S. 1.1.34) by the odd/even of the MD sign",
"J.S. 2.4.31–32 看大运星座或其宫主所落星座": "J.S. 2.4.31-32 by the MD sign or the sign its lord occupies",
" → 顺行;": " -> zodiacal; ",
" → 逆行;": " -> reverse; ",
" → 顺行)": " -> zodiacal)",
" → 逆行)": " -> reverse)",
",顺行": ", zodiacal",
",逆行": ", reverse",
",12 等分": ", twelve equal parts",
# BUG-1214: tajika_year_lord.selection_basis_text (zh) → its en twin.
"年盘不全,暂用 Muntha 主星": "Annual chart incomplete; Muntha sign lord used for now",
+16
View File
@@ -248,3 +248,19 @@ def _production_files(record) -> list:
for value in record:
found.extend(_production_files(value))
return found
def test_table25_taurus_md_is_the_saturn_exception_and_carries_the_book_divergence() -> None:
"""Round-6 ruling Q7 / BUG-1225: Saturn in the Taurus MD sign -> ADs always
zodiacal (p.51), 12/12 with the book; the p.47 / J.S. 2.4.31-32 statements
are listed as a book divergence, not applied."""
chart, asc, *_ = compute_chart_data(*VERDICT_TABLES["Table 25"][0], ayanamsa_name="lahiri")
lons = {name: row["degree"] for name, row in chart["planets"].items()}
taurus = calc_narayana_rath(asc, lons, seed_sign_idx=SIGNS.index("Cancer"), cycle_count=1)["mahadasha"][2]
ad = antardasha(taurus, lons)
rule = ad["antardasha_start_rule"]
assert taurus["sign"] == "Taurus" and rule["order_profile"] == "rath_saturn_table11_v1"
assert [row["sign"] for row in ad["rows"]] == TABLE25_AD["Taurus"]
assert rule["direction"]["direction"] == "zodiacal"
p47, js = rule["direction"]["book_divergence"]
assert (p47["direction"], p47["differs"]) == ("reverse", True) # Taurus is even: p.47 would run the ADs backwards
+47
View File
@@ -248,3 +248,50 @@ def test_node_dignity_stays_inside_rath() -> None:
if path.name != "narayana_rath.py" and "RATH_NODE_DIGNITY" in path.read_text(encoding="utf-8"))
assert users == []
assert "RATH_NODE_DIGNITY" in inspect.getsource(rath._occupant_dignity)
# --- BUG-1225: AD direction vs p.51 exceptions and the book divergence (round-6 ruling Q7).
def _md(sign: str, years: float = 6.0) -> dict:
return {"sign": sign, "sign_idx": SIGNS.index(sign), "years": years, "start_age": 0.0, "end_age": years}
def test_ad_tables_match_the_p51_exceptions_for_every_start_sign() -> None:
for start in range(12):
table13 = nd.build_narayana_antardasha_order_sequence(start, profile="rath_table13_v1")
table14 = nd.build_narayana_antardasha_order_sequence(start, profile="rath_ketu_table14_v1")
table11 = nd.build_narayana_antardasha_order_sequence(start, profile="rath_saturn_table11_v1")
forward = [(start + i) % 12 for i in range(12)]
backward = [(start - i) % 12 for i in range(12)]
assert table13 == (forward if start % 2 == 0 else backward) # default: start sign odd -> zodiacal
assert table14 == (backward if start % 2 == 0 else forward) # Ketu: the opposite
assert table11 == forward # Saturn: always zodiacal
def test_ketu_in_the_md_sign_reverses_the_ads() -> None:
lons = _with(BASE, Rahu=215.0, Ketu=35.0) # Ketu in Taurus
ad = antardasha(_md("Taurus"), lons)
rule = ad["antardasha_start_rule"]
assert rule["order_profile"] == "rath_ketu_table14_v1" and rule["method_note"] is None
start = SIGNS.index(rule["start_sign"])
plain = nd.build_narayana_antardasha_order_sequence(start, profile="rath_table13_v1")
rows = [SIGNS.index(row["sign"]) for row in ad["rows"]]
assert rows[0] == plain[0] == start
assert (rows[1] - start) % 12 == (start - plain[1]) % 12 # same first step, opposite direction
direction = rule["direction"]
assert direction["rule"] == "rath_ketu_table14_v1"
assert direction["direction"] == ("reverse" if start % 2 == 0 else "zodiacal")
both = antardasha(_md("Taurus"), _with(lons, Saturn=40.0))
assert both["antardasha_start_rule"]["order_profile"] == "rath_ketu_table14_v1"
assert both["antardasha_start_rule"]["method_note"] == KETU_FIRST_NOTE
def test_book_divergence_lists_the_two_other_statements() -> None:
lons = _with(BASE, Rahu=215.0, Ketu=35.0)
direction = antardasha(_md("Taurus"), lons)["antardasha_start_rule"]["direction"]
ids = [row["id"] for row in direction["book_divergence"]]
assert ids == ["rath_p47_js_1_1_34", "js_2_4_31_32"]
assert all(row["applied"] is False and row["rule_text"] and row["rule_text_en"] for row in direction["book_divergence"])
p47 = direction["book_divergence"][0]
assert p47["direction"] == "reverse" # Taurus is an even sign
assert p47["differs"] == (p47["direction"] != direction["direction"])