#!/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}")