feat(narayana): Table 18 as a flagged book example with the explicit seed; book software-verdict and Table 25 tests (BUG-1218)

Red line 3 revised by the product owner (2026-10-03 evening, audit da95d92d):
Indira Table 18 is a book-internal contradiction. calc_narayana_rath(...,
seed_sign_idx, book_example="rath_p66_indira_ak") reproduces it 12/12 flagged
book_example_exception and carries the general rule's Capricorn unchanged.
New tests: five D1 software-verdict tables 28/30 (two Wadiyar level-5 misses
pinned), co-lords 8/10, Table 25 sub-periods 24/36 (Gemini MD 0/12 pinned).

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 21:09:31 +08:00
co-authored by Claude Opus 5.5
parent f5d24f42d2
commit 735d66fd65
2 changed files with 140 additions and 9 deletions
+39 -6
View File
@@ -52,8 +52,10 @@ upstream function). Tables 10 / 11 / 13 / 14, `_rasi_aspects`, the year and
dual-lord functions are the same in both repositories and are reused from the
frozen local `narayana_dasha.py`, which this module does not modify.
Not wired into scoring, consultation or reports (task red line 3: Table 18 is
not reproduced by the general rule, see tests/test_narayana_rath_book_tables.py).
Table 18 (Indira) is a book-internal contradiction (BOOK_EXAMPLES): the
general rule's Capricorn is output as is; the book's Cancer only with the
start sign given explicitly and flagged (product owner, 2026-10-03 evening).
Not wired into scoring.
"""
from __future__ import annotations
@@ -72,6 +74,22 @@ DURATION_PROFILE = "jaimini_odd_footed_dignity_v2"
DUAL_LORD_PROFILE = "jaimini_dual_lord_source_v1"
ROLE_PROFILE = "rath_p36_role_conjunction_v1"
INDIRA_EXCEPTION = "rath_p66_indira_ak"
# Book examples whose printed start sign the general rule does not give.
# Product owner ruling 2026-10-03 (evening, red line 3 revised): reproduce
# them only with the start sign given explicitly, flagged; never by code that
# bends the general rule.
BOOK_EXAMPLES = {
INDIRA_EXCEPTION: {
"book_table": "Table 18",
"book_start_sign": "Cancer",
"book_internal_contradiction": True,
"evidence": ("Hand example (PDF Page 62; astrologer cites p.65-66) concludes Cancer because "
"'Saturn is the Atmakaraka and has gained the highest longitude', but Saturn sits in "
"Cancer and rules Capricorn, so rules 5 and 6 read literally point to Capricorn; the "
"book's own software verdict for the same native (Table 27) also has Capricorn "
"stronger than Cancer. Audit docs/research/shared_engine_reuse_audit_2026_10_03.md N1."),
},
}
KETU_FIRST_NOTE = "PyJHora 兼容:计都优先"
KETU_FIRST_NOTE_EN = "PyJHora-compatible: Ketu takes precedence"
SOURCE = {
@@ -339,13 +357,19 @@ def antardasha(md_row: Dict[str, Any], planet_lons: Dict[str, float]) -> Dict[st
def calc_narayana_rath(lagna_sign_idx: int, planet_lons: Dict[str, float],
*, seed_sign_idx: Optional[int] = None, cycle_count: int = 2,
allow_book_example: bool = False) -> Dict[str, Any]:
allow_book_example: bool = False,
book_example: Optional[str] = None) -> Dict[str, Any]:
"""Rath Narayana Dasha for one chart.
`seed_sign_idx` overrides the start sign (book-table replay only).
`seed_sign_idx` overrides the start sign (book-table replay only); with
`book_example` (a key of BOOK_EXAMPLES) the output is flagged
`book_example_exception` and still carries the general rule's own
verdict, unchanged, in `general_rule_selection`.
`allow_book_example` lets the Indira AK example decide a full seven-level
tie (flagged in the output).
"""
if book_example is not None and (seed_sign_idx is None or book_example not in BOOK_EXAMPLES):
raise ValueError("book_example needs an explicit seed_sign_idx and a known BOOK_EXAMPLES key")
base = {
"profile": PROFILE,
"duration_profile": DURATION_PROFILE,
@@ -356,7 +380,14 @@ def calc_narayana_rath(lagna_sign_idx: int, planet_lons: Dict[str, float],
}
if seed_sign_idx is not None:
selection = {"status": "explicit", "selected_sign_idx": seed_sign_idx,
"strength_level_decided": None, "book_example_exception": None}
"strength_level_decided": None, "book_example_exception": book_example}
if book_example is not None:
general = compare_signs(lagna_sign_idx, (lagna_sign_idx + 6) % 12, planet_lons)
base["general_rule_selection"] = {
key: general.get(key) for key in
("status", "selected_sign", "strength_level_decided", "strength_level_name", "reason")
}
base["book_example"] = BOOK_EXAMPLES[book_example]
else:
selection = compare_signs(lagna_sign_idx, (lagna_sign_idx + 6) % 12, planet_lons,
allow_book_example=allow_book_example)
@@ -365,7 +396,9 @@ def calc_narayana_rath(lagna_sign_idx: int, planet_lons: Dict[str, float],
base["book_example_exception"] = selection.get("book_example_exception")
if selection["status"] in ("selected", "explicit"):
seq = mahadasha_sequence(selection["selected_sign_idx"], planet_lons, cycle_count=cycle_count)
if selection["status"] == "explicit":
if selection["status"] == "explicit" and book_example is not None:
status, basis = "book_example_exception", f"explicit_seed:book_example:{book_example}"
elif selection["status"] == "explicit":
status, basis = "explicit_seed", "explicit_seed"
elif selection.get("book_example_exception"):
status, basis = "book_example_exception", f"book_example:{selection['book_example_exception']}"
+101 -3
View File
@@ -13,9 +13,14 @@ Lahiri ayanamsa. With the book's start sign, all 36 MD signs and years match.
Start sign by the round-5 seven-level rule (astrologer 2026-10-03): Table 15
Cancer (level 1) and Table 17 Pisces (level 2) reproduce; Table 18 does not —
the general rule decides Capricorn at level 5 (Cancer lord Moon 5.6 deg vs
Capricorn lord Saturn 21.8 deg), so the Indira AK book example (allowed only
after all seven levels tie) is never reached. Task red line 3: the profile
stays out of every user-visible surface and out of scoring.
Capricorn lord Saturn 21.8 deg). Red line 3 as revised by the product owner
(2026-10-03 evening, audit docs/research/shared_engine_reuse_audit_2026_10_03.md):
Table 18 is a book-internal contradiction (the hand example's reasoning reads
literally as Capricorn and the book's Table 27 software verdict is Capricorn),
reproduced only with the Cancer seed given explicitly and flagged
`book_example_exception`; the general rule's Capricorn is output unchanged.
Further acceptance evidence below: the book's five D1 software-verdict tables
(30 sign pairs, 28 match) and Table 25's 36 sub-period rows (24 match).
"""
from __future__ import annotations
@@ -124,6 +129,99 @@ def test_antardasha_starts_from_the_lord_of_the_stronger_of_md_and_seventh() ->
assert [row["sign"] for row in ad["rows"]][:3] == ["Cancer", "Leo", "Virgo"]
def test_table18_only_with_the_explicit_cancer_seed_and_flagged() -> None:
asc, lons = _chart("Table 18")
book = _book("Table 18")
result = calc_narayana_rath(asc, lons, seed_sign_idx=SIGNS.index("Cancer"), cycle_count=1,
book_example="rath_p66_indira_ak")
assert result["status"] == "book_example_exception"
assert result["book_example_exception"] == "rath_p66_indira_ak"
assert result["book_example"]["book_internal_contradiction"] is True
assert [row["sign"] for row in result["mahadasha"]] == [row[0] for row in book["rows"]]
assert [row["years"] for row in result["mahadasha"]] == [row[1] for row in book["rows"]]
# The general rule's own verdict travels with it, unchanged.
assert (result["general_rule_selection"]["selected_sign"], result["general_rule_selection"]["strength_level_decided"]) == ("Capricorn", 5)
with pytest.raises(ValueError):
calc_narayana_rath(asc, lons, book_example="rath_p66_indira_ak")
# Book software verdicts ("Stronger planets and signs") for five D1 charts:
# Rath, Narayana Dasa, Tables 22 / 23 / 24 / 25 / 27 (book example charts
# 14, 15 = the standard nativity, 16, 17 = Krishnaraja Wadiyar IV, 22 = Indira
# Gandhi). Inputs and verdicts as transcribed in the audit (da95d92d).
def _dms(d: float, m: float) -> float:
return d + m / 60
AB = dict(Ar=0, Ta=1, Ge=2, Cn=3, Le=4, Vi=5, Li=6, Sc=7, Sg=8, Cp=9, Aq=10, Pi=11)
VERDICT_TABLES = {
"Table 22": ((1962, 3, 28, 6, 29, _dms(22, 6), _dms(85, 17), 5.5), ("Saturn", "Mars"), "Ar Sc Ge Cp Aq Pi"),
"Table 23": ((1963, 8, 7, 21, 15, _dms(21, 28), _dms(84, 1), 5.5), ("Saturn", "Mars"), "Li Ta Sg Cn Le Pi"),
"Table 24": ((1954, 12, 19, 0, 20, _dms(28, 36), _dms(77, 12), 5.5), ("Saturn", "Mars"), "Li Sc Sg Cn Aq Vi"),
"Table 25": ((1884, 6, 4, 10, 18, 12.0, _dms(76, 38), _dms(76, 38) / 15), ("Saturn", "Ketu"), "Li Ta Ge Cn Le Vi"),
"Table 27": ((1917, 11, 19, 23, 8, _dms(25, 28), _dms(81, 52), 5.5), ("Rahu", "Mars"), "Ar Sc Sg Cp Le Pi"),
}
# Expected misses, documented: Wadiyar Gemini/Sagittarius and Virgo/Pisces —
# the book takes Gemini and Virgo; the general rule decides at level 5
# (Mercury 1.3 deg vs Jupiter 10.0 deg) for Sagittarius and Pisces. Not
# sensitive to node mode or ayanamsa (audit). Open question to the astrologer.
EXPECTED_SIGN_MISSES = {("Table 25", "Gemini", "Sagittarius"), ("Table 25", "Virgo", "Pisces")}
# Dual lords (Aquarius, Scorpio): book Table 23 Aquarius = Saturn (ours Rahu),
# Table 25 Scorpio = Ketu (ours Mars).
EXPECTED_CO_LORD_MISSES = {("Table 23", "Aquarius"), ("Table 25", "Scorpio")}
def test_book_software_verdicts_28_of_30_and_co_lords_8_of_10() -> None:
from scripts.narayana_rath import _lord, compare_signs
sign_misses, co_lord_misses, pairs = set(), set(), 0
for table, (birth, co_lords, stronger) in VERDICT_TABLES.items():
chart, _asc, *_ = compute_chart_data(*birth, ayanamsa_name="lahiri")
lons = {name: row["degree"] for name, row in chart["planets"].items()}
book = {AB[code] for code in stronger.split()}
for first in range(6):
second = first + 6
winner = first if first in book else second
result = compare_signs(first, second, lons)
pairs += 1
if result.get("selected_sign_idx") != winner:
assert result["strength_level_decided"] == 5
sign_misses.add((table, SIGNS[winner], SIGNS[second if winner == first else first]))
for sign_idx, want in zip((10, 7), co_lords):
if _lord(sign_idx, lons) != want:
co_lord_misses.add((table, SIGNS[sign_idx]))
assert pairs == 30
assert sign_misses == EXPECTED_SIGN_MISSES # 28/30
assert co_lord_misses == EXPECTED_CO_LORD_MISSES # 8/10
# Table 25 (Wadiyar) sub-periods for the first three MDs, from upstream
# c7117f92 references/oracle/chara_narayana_rath_wadiyar_table25_replay_2026_09_07.json.
TABLE25_AD = {
"Cancer": ["Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces", "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo"],
"Gemini": ["Taurus", "Aries", "Pisces", "Aquarius", "Capricorn", "Sagittarius", "Scorpio", "Libra", "Virgo", "Leo", "Cancer", "Gemini"],
"Taurus": ["Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces", "Aries", "Taurus", "Gemini"],
}
def test_table25_sub_periods_24_of_36() -> None:
"""Cancer and Taurus (Saturn inside, Table 11) MDs: 12/12. Gemini MD: 0/12 —
the Gemini/Sagittarius comparison picks Sagittarius (level 5, the same
Wadiyar miss as above), so the ADs start from Jupiter's sign Cancer
instead of Mercury's sign Taurus. Current state, pinned."""
chart, asc, *_ = compute_chart_data(*VERDICT_TABLES["Table 25"][0], ayanamsa_name="lahiri")
lons = {name: row["degree"] for name, row in chart["planets"].items()}
mds = calc_narayana_rath(asc, lons, seed_sign_idx=SIGNS.index("Cancer"), cycle_count=1)["mahadasha"][:3]
assert [row["sign"] for row in mds] == ["Cancer", "Gemini", "Taurus"]
matched = {}
for md in mds:
rows = [row["sign"] for row in antardasha(md, lons)["rows"]]
matched[md["sign"]] = sum(a == b for a, b in zip(rows, TABLE25_AD[md["sign"]]))
assert matched == {"Cancer": 12, "Gemini": 0, "Taurus": 12}
gemini = antardasha(mds[1], lons)["antardasha_start_rule"]
assert (gemini["stronger_sign"], gemini["strength_level_decided"], gemini["start_sign"]) == ("Sagittarius", 5, "Cancer")
def test_rath_profile_is_not_wired_into_any_product_path() -> None:
pattern = re.compile(r"\bnarayana_rath\b")
users = sorted(path.name for path in (ROOT / "scripts").rglob("*.py")