Files
Jyotisha/scripts/narayana_legacy_label.py
T
Jesse_ChenandClaude Opus 5.5 bf8d704708 feat(narayana): show the Sanjay Rath version beside the legacy one (T4, BUG-1218)
Display only; scoring and timing conclusions keep the legacy algorithm.
narayana_legacy_label attaches a Rath view (seed, deciding level, direction,
24 MDs, running MD/AD) under modules.narayana_dasha.rath for the consultation
modules, full reading Step 4.13 and the CLI. Reference report: one summary
line under the legacy line plus Rath MD / running-MD AD tables (English terms
added). Evidence card: narayana_rath (label, seed sign, level, running MD) on
timing / annual cards only — other cards sit at 11,979 of 12,000 characters.
Goldens re-captured from the real engine.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
2026-10-03 21:09:40 +08:00

185 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Honest label for the product Narayana Dasha (BUG-1215, round-4 ruling Q4).
`narayana_dasha.narayana_dasha_full_report` (consultation, report, CLI) runs
`calc_narayana_mahadasha`: always from the Lagna, always zodiacal. The
astrologer's ruling is Sanjay Rath's Narayana Dasha (stronger of Lagna / 7th,
order by the start sign); until the product switches (product decision after
the trial), every user-visible Narayana carries this label and the data carry
`profile: legacy_reference_only`. Kept outside `narayana_dasha.py`, which is in
the frozen rectification scoring identity (ERR-110 / ERR-115).
BUG-1218 (T4, product owner 2026-10-03): when the caller hands the chart, the
Sanjay Rath profile (`narayana_rath`, round-5 rules) rides along under `rath`
for display only. Scoring and timeline conclusions keep reading the legacy
fields; nothing here changes them.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
LEGACY_PROFILE = "legacy_reference_only"
LEGACY_NOTE_ZH = "旧算法(从上升起、一律顺行),待替换为 Sanjay Rath 版"
LEGACY_NOTE_EN = "Legacy algorithm (starts from the Lagna, always zodiacal); to be replaced by Sanjay Rath's version"
# Short form for the consultation evidence card (12,000-character budget).
LEGACY_LABEL_ZH = "旧算法(从上升起、一律顺行)"
RATH_LABEL_ZH = "Sanjay Rath 版(并列参考,未进打分与时间轴结论)"
RATH_LABEL_EN = "Sanjay Rath version (shown alongside for reference; not used in scoring or timing conclusions)"
STRENGTH_LEVEL_ZH = {
1: "占据星数", 2: "木星 / 水星 / 宫主三角色", 3: "占据星尊贵", 4: "星座性质",
5: "宫主星座内度数", 6: "宫主落奇偶相反星座", 7: "大运年数",
}
STRENGTH_LEVEL_EN = {
1: "occupants", 2: "Jupiter / Mercury / lord roles", 3: "occupant dignity", 4: "modality",
5: "lord's degree in sign", 6: "lord in a sign of opposite parity", 7: "dasha years",
}
DIRECTION_ZH = {
"table10_general": "按 Table 10", "table11_saturn_zodiacal": "土星在起运宫:顺行(Table 11)",
"table12_ketu_reversed": "计都在起运宫:反向(Table 12)",
}
DIRECTION_EN = {
"table10_general": "Table 10 order", "table11_saturn_zodiacal": "Saturn in the start sign: zodiacal (Table 11)",
"table12_ketu_reversed": "Ketu in the start sign: reversed (Table 12)",
}
def _period(row: Dict[str, Any], birth_year: Optional[int]) -> Dict[str, Any]:
out = {key: row.get(key) for key in ("sign", "lord", "years")}
out["start_age"] = round(float(row["start_age"]), 2)
out["end_age"] = round(float(row["end_age"]), 2)
if isinstance(out["years"], float):
out["years"] = round(out["years"], 2)
if birth_year:
out["start_year"] = round(birth_year + float(row["start_age"]), 2)
out["end_year"] = round(birth_year + float(row["end_age"]), 2)
return out
def _running(rows: List[Dict[str, Any]], age: Optional[float]) -> Optional[Dict[str, Any]]:
if age is None:
return None
return next((row for row in rows if float(row["start_age"]) <= age < float(row["end_age"])), None)
def rath_narayana_view(lagna_sign_idx: int, planet_lons: Dict[str, float], *,
current_age: Optional[float] = None, birth_year: Optional[int] = None) -> Dict[str, Any]:
"""Display-only Sanjay Rath Narayana: seed, basis, MDs, running MD / AD."""
try:
from narayana_rath import antardasha, calc_narayana_rath
except ImportError: # pragma: no cover - package import
from scripts.narayana_rath import antardasha, calc_narayana_rath
result = calc_narayana_rath(int(lagna_sign_idx) % 12, planet_lons, cycle_count=2)
view: Dict[str, Any] = {
"profile": result["profile"],
"algorithm_label": RATH_LABEL_ZH,
"algorithm_label_en": RATH_LABEL_EN,
"status": result["status"],
"used_for_scoring": False,
"source": result["source"],
"dual_lord_rule": result["dual_lord_rule"],
"antardasha_start_rule": result["antardasha_start_rule"],
}
if not result.get("mahadasha"):
view.update({"reason": result.get("reason"),
"alternative_seed_signs": sorted((result.get("alternatives") or {}).keys())})
return view
level = result.get("strength_level_decided")
view.update({
"seed_sign": result["seed_sign"],
"seed_basis": result["seed_basis"],
"strength_level_decided": level,
"strength_level_text": STRENGTH_LEVEL_ZH.get(level),
"strength_level_text_en": STRENGTH_LEVEL_EN.get(level),
"direction": result["direction"],
"direction_text": DIRECTION_ZH.get(result["direction"]),
"direction_text_en": DIRECTION_EN.get(result["direction"]),
"exception_applied": result.get("exception_applied"),
"method_note": result.get("method_note"),
"method_note_en": result.get("method_note_en"),
"mahadasha": [_period(row, birth_year) for row in result["mahadasha"]],
})
md = _running(result["mahadasha"], current_age)
if md is not None:
ad = antardasha(md, planet_lons)
current: Dict[str, Any] = {"md": _period(md, birth_year)}
rows = ad.get("rows") or []
running_ad = _running(rows, current_age)
if running_ad is not None:
current["ad"] = _period(running_ad, birth_year)
view["current_dasha"] = current
view["current_md_antardasha"] = [_period(row, birth_year) for row in rows]
view["current_md_antardasha_rule"] = ad.get("antardasha_start_rule")
return view
def label_legacy_narayana(result: Any, *, lagna_sign_idx: Optional[int] = None,
planet_lons: Optional[Dict[str, float]] = None,
current_age: Optional[float] = None,
birth_year: Optional[int] = None) -> Any:
"""Add the legacy profile and note to a `narayana_dasha_full_report` result.
With the chart (`lagna_sign_idx`, `planet_lons`) the Rath view is attached
under `rath` (display only, BUG-1218); a failure there never touches the
legacy result.
"""
if isinstance(result, dict):
result["profile"] = LEGACY_PROFILE
result["algorithm_note"] = LEGACY_NOTE_ZH
result["algorithm_note_en"] = LEGACY_NOTE_EN
result["algorithm_label"] = LEGACY_LABEL_ZH
if lagna_sign_idx is not None and planet_lons:
try:
result["rath"] = rath_narayana_view(lagna_sign_idx, planet_lons,
current_age=current_age, birth_year=birth_year)
except Exception as exc: # display layer only
result["rath"] = {"status": "blocked", "reason": exc.__class__.__name__,
"algorithm_label": RATH_LABEL_ZH, "algorithm_label_en": RATH_LABEL_EN}
return result
def rath_report_lines_zh(rath: Any) -> List[str]:
"""Reference-report block for the Sanjay Rath Narayana beside the legacy
line (BUG-1218, display only): summary, 24 MD rows, running MD's ADs."""
summary = rath_summary_line_zh(rath)
if summary is None:
return []
lines = [summary]
if not rath.get("mahadasha"):
return lines
if rath.get("direction_text"):
lines.append(f"- 顺序:{rath['direction_text']}" + (f";{rath['method_note']}" if rath.get("method_note") else ""))
lines.extend(["", "| Rath MD | 星座 | 宫主 | 起始年龄 | 结束年龄 | 年数 |", "|---------|------|------|----------|----------|------|"])
for index, row in enumerate(rath["mahadasha"], start=1):
lines.append(f"| {index} | {row.get('sign')} | {row.get('lord')} | {row.get('start_age')} | {row.get('end_age')} | {row.get('years')} |")
md = (rath.get("current_dasha") or {}).get("md")
rule = rath.get("current_md_antardasha_rule") or {}
if md and rath.get("current_md_antardasha"):
compared = rule.get("compared") or ["-", "-"]
lines.extend([
"",
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 等分",
"",
"| Rath AD | 星座 | 起始年龄 | 结束年龄 |",
"|---------|------|----------|----------|",
])
for index, row in enumerate(rath["current_md_antardasha"], start=1):
lines.append(f"| {index} | {row.get('sign')} | {row.get('start_age')} | {row.get('end_age')} |")
return lines
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:
return None
if not rath.get("seed_sign"):
return f"- 并列:{RATH_LABEL_ZH}:起运宫未定,{rath.get('reason') or rath.get('status')}"
md = (rath.get("current_dasha") or {}).get("md") or {}
running = f";当前大运 {md.get('sign')},宫主 {md.get('lord')}" if md else ""
return (f"- 并列:{RATH_LABEL_ZH}:起运宫 {rath.get('seed_sign')}"
f"(第 {rath.get('strength_level_decided')} 级:{rath.get('strength_level_text')}){running}")