feat(narayana): Sanjay Rath profile replayed against the book tables; not wired (BUG-1215)

Round-4 ruling Q4. scripts/narayana_rath.py builds the Rath sequence from the
rule ledger and book rows ported verbatim from upstream yinduzhanxing c7117f92
(sanjay_rath_narayana_book_ledger JSON + md, chara_narayana_book_golden_cases):
Table 10 order (Saturn -> Table 11, Ketu -> reversed), years by
jaimini_odd_footed_dignity_v2 with jaimini_dual_lord_source_v1, ADs twelve
equal parts from the MD sign (Table 13 / 11 / 14), start sign by the existing
select_narayana_seed_sign, ties reported as parameter_sensitive with both
sequences.

Given the book's start sign, Tables 15 / 17 / 18 match 36/36 MD signs and
years. The start sign does not: Table 17 picks Virgo (book Pisces), Table 18
ties (book Cancer). Task red line 2: the profile stays out of every
user-visible surface and out of scoring; T5 trial not run. A test pins that no
script imports it.

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:36:31 +08:00
co-authored by Claude Opus 5.5
parent 0afcc2d9a3
commit 7cb332bce6
5 changed files with 4037 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
"""Sanjay Rath Narayana Dasha profile vs the book tables (BUG-1215, round-4 Q4).
The rule ledger and the book rows are ported verbatim from upstream
yinduzhanxing c7117f92 (`references/oracle/sanjay_rath_narayana_book_ledger_2026_08_28.json`,
Tables 15 / 17 / 18). Birth data are the published charts the book tables are
computed for (public figures / the book's own example chart):
- Table 15: Sri Aurobindo, 1872-08-15 05:00 LMT, Calcutta 22N34 88E22;
- Table 17: the book's "standard nativity", 1963-08-07 21:15 IST, Sambalpur 21N28 84E01;
- Table 18: Indira Gandhi, 1917-11-19 23:11 IST, Allahabad 25N27 81E51.
Lahiri ayanamsa. With the book's start sign, all 36 MD signs and years match.
The start sign itself is not reproduced by the existing strength rule for
Tables 17 and 18 — pinned below as the current state (task red line 2: the
profile stays out of every user-visible surface and out of scoring).
"""
from __future__ import annotations
import hashlib
import json
import re
from pathlib import Path
import pytest
from scripts.jyotish_engine import compute_chart_data
from scripts.narayana_rath import SIGNS, antardasha, calc_narayana_rath, md_order
ROOT = Path(__file__).resolve().parents[1]
LEDGER = ROOT / "references/oracle/sanjay_rath_narayana_book_ledger_2026_08_28.json"
UPSTREAM_BLOBS = { # c7117f92
"references/oracle/sanjay_rath_narayana_book_ledger_2026_08_28.json": "a52f181c22ba1b1e6a330b73711eb0e791785e56",
"references/oracle/chara_narayana_book_golden_cases_2026_08_28.json": "552c40448502c15a47035da064914dd76a4d919b",
"docs/research/sanjay_rath_narayana_book_ledger_2026_08_28.md": "8d18c2320d32dcd364784351ac52dc8bcb4ef053",
}
CHARTS = {
"Table 15": (1872, 8, 15, 5, 0, 22.5667, 88.3667, 88.3667 / 15),
"Table 17": (1963, 8, 7, 21, 15, 21.4667, 84.0167, 5.5),
"Table 18": (1917, 11, 19, 23, 11, 25.45, 81.85, 5.5),
}
def _book(table: str) -> dict:
return next(case for case in json.loads(LEDGER.read_text(encoding="utf-8"))["golden_cases"] if case["source_table"] == table)
def _chart(table: str):
chart, asc, *_ = compute_chart_data(*CHARTS[table], ayanamsa_name="lahiri")
return asc, {name: row["degree"] for name, row in chart["planets"].items()}
@pytest.mark.parametrize("path,blob", sorted(UPSTREAM_BLOBS.items()))
def test_ported_files_are_the_upstream_blobs(path: str, blob: str) -> None:
data = (ROOT / path).read_bytes()
assert hashlib.sha1(b"blob %d\0" % len(data) + data).hexdigest() == blob
@pytest.mark.parametrize("table", sorted(CHARTS))
def test_book_start_sign_reproduces_order_and_years(table: str) -> None:
book = _book(table)
asc, lons = _chart(table)
result = calc_narayana_rath(asc, lons, seed_sign_idx=SIGNS.index(book["start_sign"]), cycle_count=1)
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"]]
def test_saturn_in_the_start_sign_gives_the_zodiacal_order() -> None:
asc, lons = _chart("Table 18") # Saturn in Cancer
assert md_order(SIGNS.index("Cancer"), lons)["direction"] == "table11_saturn_zodiacal"
asc, lons = _chart("Table 15")
assert md_order(SIGNS.index("Cancer"), lons)["direction"] == "table10_general"
def test_start_sign_selection_current_state() -> None:
"""Existing strength rule (occupants, then aspect by Jupiter / Mercury /
lord): Table 15 right; Table 17 picks Virgo (book Pisces: Pisces holds its
own lord Jupiter, which the aspect-only rule does not count); Table 18 ties
(book Cancer). Update this test together with the rule once the astrologer
answers; until then the profile is not wired anywhere."""
asc, lons = _chart("Table 15")
assert (calc_narayana_rath(asc, lons)["status"], calc_narayana_rath(asc, lons)["seed_sign"]) == ("partial_verified", "Cancer")
asc, lons = _chart("Table 17")
assert calc_narayana_rath(asc, lons)["seed_sign"] == "Virgo"
asc, lons = _chart("Table 18")
result = calc_narayana_rath(asc, lons)
assert result["status"] == "parameter_sensitive" and result["reason"] == "later_narayana_sign_strength_rules_required"
assert set(result["alternatives"]) == {"Cancer", "Capricorn"}
assert [row["sign"] for row in result["alternatives"]["Cancer"]["mahadasha"][:12]] == [row[0] for row in _book("Table 18")["rows"]]
def test_antardasha_is_twelve_equal_parts_from_the_md_sign() -> None:
asc, lons = _chart("Table 15")
md = calc_narayana_rath(asc, lons, cycle_count=1)["mahadasha"][0] # Cancer, 7 years; Mars and Jupiter there
rows = antardasha(md, lons)
assert [row["sign"] for row in rows][:3] == ["Cancer", "Gemini", "Taurus"] # Table 13, even sign reverse
assert all(abs(row["years"] - 7 / 12) < 1e-6 for row in rows)
asc, lons = _chart("Table 18")
md = calc_narayana_rath(asc, lons, seed_sign_idx=SIGNS.index("Cancer"), cycle_count=1)["mahadasha"][0]
assert [row["sign"] for row in antardasha(md, lons)][:3] == ["Cancer", "Leo", "Virgo"] # Saturn in Cancer: Table 11
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")
if path.name != "narayana_rath.py" and pattern.search(path.read_text(encoding="utf-8")))
assert users == []