Files
Jyotisha/scripts/narayana_legacy_label.py
T
Jesse_ChenandCursor baf4ae8b34
Independent Staging Quality Gate / validate (push) Failing after 16m17s
Independent Staging Quality Gate / publish (push) Skipped
feat(narayana-rath): follow book p.43 for dual lords and node years (BUG-1227~1229)
Round-7 option A is text-first, so Scorpio and Aquarius lords, the Ketu-only degree reversal, and node exaltation years now live in the Rath display layer. Scoring files stay unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-04 18:37:34 +08:00

220 lines
11 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 版(并列参考,未进打分与时间轴结论;按原书 p.43 正文(第七轮裁定))"
RATH_LABEL_EN = "Sanjay Rath version (shown alongside for reference; not used in scoring or timing conclusions; book p.43 text, round-7 ruling)"
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", "base_years", "dignity_adjustment", "adjustment_source")}
out["start_age"] = round(float(row["start_age"]), 2)
if row.get("end_age") is not None:
out["end_age"] = round(float(row["end_age"]), 2)
if isinstance(out["years"], float):
out["years"] = round(out["years"], 2)
decision = row.get("dual_lord_decision") or {}
if decision.get("basis_text"):
out["dual_lord_basis"] = decision["basis_text"]
out["dual_lord_basis_en"] = decision.get("basis_text_en")
if birth_year and out.get("end_age") is not None:
out["start_year"] = round(birth_year + float(row["start_age"]), 2)
out["end_year"] = round(birth_year + float(out["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("| {index} | {sign} | {lord} | {start} | {end} | {base} | {adjustment} | {years} |".format(
index=index, sign=row.get("sign"), lord=row.get("lord") or "未决",
start=row.get("start_age"), end=row.get("end_age"),
base="—" if row.get("base_years") is None else row.get("base_years"),
adjustment="—" if row.get("dignity_adjustment") is None else row.get("dignity_adjustment"),
years="—" if row.get("years") is None else row.get("years")))
seen = set()
for row in rath["mahadasha"]:
sign = row.get("sign")
basis = row.get("dual_lord_basis")
if sign in ("Scorpio", "Aquarius") and basis and sign not in seen:
seen.add(sign)
lines.append(f"- 双主星 {sign}:{basis}")
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 等分",
*_ad_direction_lines_zh(rule.get("direction")),
"",
"| 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
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:
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') or '未决'}" if md else ""
return (f"- 并列:{RATH_LABEL_ZH}:起运宫 {rath.get('seed_sign')}"
f"(第 {rath.get('strength_level_decided')} 级:{rath.get('strength_level_text')}){running}")