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
This commit is contained in:
Jesse_Chen
2026-10-03 21:09:40 +08:00
co-authored by Claude Opus 5.5
parent 3264831a23
commit bf8d704708
13 changed files with 387 additions and 16 deletions
+19 -2
View File
@@ -56,8 +56,9 @@ def cmd_narayana_dasha(args, chart_data):
current_age=current_age,
birth_year=birth_year,
)
from narayana_legacy_label import label_legacy_narayana # BUG-1215
label_legacy_narayana(result)
from narayana_legacy_label import label_legacy_narayana # BUG-1215; Rath view BUG-1218
label_legacy_narayana(result, lagna_sign_idx=asc_idx, planet_lons=planet_lons,
current_age=current_age, birth_year=birth_year)
# 格式化输出
lines = [
@@ -98,4 +99,20 @@ def cmd_narayana_dasha(args, chart_data):
for line in result.get('interpretation', []):
lines.append(f" {line}")
rath = result.get('rath') or {}
if rath:
lines.append("")
lines.append(f"── {rath.get('algorithm_label')} ──")
if rath.get('seed_sign'):
lines.append(f" 起运宫: {rath['seed_sign']}(第 {rath.get('strength_level_decided')} 级:{rath.get('strength_level_text')});{rath.get('direction_text')}")
if rath.get('method_note'):
lines.append(f" {rath['method_note']}")
for p in rath.get('mahadasha', [])[:12]:
lines.append(f" {p['sign']:>12s} ({p['lord']:>7s}): {p['years']:2d}年 [{p['start_age']:5.1f}→{p['end_age']:5.1f}]")
rmd = (rath.get('current_dasha') or {}).get('md')
if rmd:
lines.append(f" 当前大运: {rmd['sign']}({rmd['lord']}){rmd['years']}年")
else:
lines.append(f" 起运宫未定({rath.get('reason') or rath.get('status')})")
return "\n".join(lines)
+1 -1
View File
@@ -1623,7 +1623,7 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
)
if isinstance(narayana_result, dict):
# BUG-1215: legacy algorithm, labelled until the Rath profile replaces it.
modules['narayana_dasha'] = _load_local_module('narayana_legacy_label').label_legacy_narayana(narayana_result)
modules['narayana_dasha'] = _load_local_module('narayana_legacy_label').label_legacy_narayana(narayana_result, lagna_sign_idx=asc_sign_idx, planet_lons=planet_lons, current_age=_consultation_current_age(birth_payload, body), birth_year=int(birth_payload.get('year', 0) or 0))
except Exception as exc:
diagnostics.append({'layer': 'narayana_dasha', 'status': 'unavailable', 'reason': exc.__class__.__name__})
+4 -2
View File
@@ -65,7 +65,7 @@ except ModuleNotFoundError: # pragma: no cover - minimal environments
lines.extend(" | ".join(str(item) for item in row) for row in rows)
return "\n".join(lines)
from life_stage_hook import generate_life_stage_hooks
from narayana_legacy_label import LEGACY_NOTE_ZH as LEGACY_NARAYANA_NOTE_ZH # BUG-1215
from narayana_legacy_label import LEGACY_NOTE_ZH as LEGACY_NARAYANA_NOTE_ZH, rath_report_lines_zh # BUG-1215 / BUG-1218
from capability_evidence_pool import build_capability_evidence_pool_summary
from guided_topic_discovery import build_guided_topics
@@ -9449,6 +9449,7 @@ def render_pl9_markdown(packet: dict) -> str:
'本地年龄轴与 PL9 日期边界/起运序列的字段级 parity 尚未闭环,因此不替代主时间判断。',
'',
f'- 算法:{LEGACY_NARAYANA_NOTE_ZH}', # BUG-1215
*rath_report_lines_zh(((worksheets.get('timing_and_predictive_systems') or {}).get('narayana_dasha') or {}).get('rath')), # BUG-1218
'',
'#### Patyayini 年内节奏支持',
'',
@@ -16143,7 +16144,8 @@ def cmd_full_reading(args):
birth_year=args.year,
)
from narayana_legacy_label import label_legacy_narayana # BUG-1215
report['modules']['narayana_dasha'] = label_legacy_narayana(narayana_result)
report['modules']['narayana_dasha'] = label_legacy_narayana( # + Rath view, display only (BUG-1218)
narayana_result, lagna_sign_idx=asc_idx, planet_lons=planet_lons, current_age=current_age, birth_year=args.year)
except Exception as e:
report['errors'].append(f"narayana-dasha: {e}")
+156 -3
View File
@@ -8,11 +8,16 @@ 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
from typing import Any, Dict, List, Optional
LEGACY_PROFILE = "legacy_reference_only"
LEGACY_NOTE_ZH = "旧算法(从上升起、一律顺行),待替换为 Sanjay Rath 版"
@@ -21,11 +26,159 @@ LEGACY_NOTE_EN = "Legacy algorithm (starts from the Lagna, always zodiacal); to
LEGACY_LABEL_ZH = "旧算法(从上升起、一律顺行)"
def label_legacy_narayana(result: Any) -> Any:
"""Add the legacy profile and note to a `narayana_dasha_full_report` result."""
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}")
+30
View File
@@ -150,6 +150,31 @@ _PHRASES: dict[str, str] = {
# BUG-1215: narayana_legacy_label.LEGACY_NOTE_ZH
"旧算法(从上升起、一律顺行),待替换为 Sanjay Rath 版": "Legacy algorithm (starts from the Lagna, always zodiacal); to be replaced by Sanjay Rath's version",
"- 算法:": "- Algorithm: ",
# BUG-1218: narayana_legacy_label rath_report_lines_zh
"Sanjay Rath 版(并列参考,未进打分与时间轴结论):": "Sanjay Rath version (shown alongside for reference; not used in scoring or timing conclusions): ",
"Sanjay Rath 版(并列参考,未进打分与时间轴结论)": "Sanjay Rath version (shown alongside for reference; not used in scoring or timing conclusions)",
",宫主 ": ", lord ",
"起运宫未定,": "start sign undecided, ",
"- 并列:": "- Alongside: ",
"- 顺序:": "- Order: ",
"起运宫 ": "start sign ",
";当前大运 ": "; running MD ",
"- 当前大运 ": "- Running MD ",
"按 Table 10": "Table 10 order",
"土星在起运宫:顺行(Table 11)": "Saturn in the start sign: zodiacal (Table 11)",
"计都在起运宫:反向(Table 12)": "Ketu in the start sign: reversed (Table 12)",
"PyJHora 兼容:计都优先": "PyJHora-compatible: Ketu takes precedence",
"占据星数": "occupants",
"木星 / 水星 / 宫主三角色": "Jupiter / Mercury / lord roles",
"占据星尊贵": "occupant dignity",
"星座性质": "modality",
"宫主星座内度数": "lord's degree in sign",
"宫主落奇偶相反星座": "lord in a sign of opposite parity",
"大运年数": "dasha years",
"| Rath MD | 星座 | 宫主 | 起始年龄 | 结束年龄 | 年数 |": "| Rath MD | Sign | Lord | Start age | End age | Years |",
"| Rath AD | 星座 | 起始年龄 | 结束年龄 |": "| Rath AD | Sign | Start age | End age |",
" 的子运:比较 ": " sub-periods: compare ",
",12 等分": ", twelve equal parts",
# BUG-1214: tajika_year_lord.selection_basis_text (zh) → its en twin.
"年盘不全,暂用 Muntha 主星": "Annual chart incomplete; Muntha sign lord used for now",
"(Panchavargiya 为本地近似计算)": " (Panchavargiya is a local approximation)",
@@ -240,6 +265,11 @@ REGEX_EN: list[tuple[re.Pattern[str], str]] = [
f"the strongest by Panchavargiya, {m.group(2)}, is the year lord")),
(re.compile(r"五类候选都不照年盘上升,取全体 Panchavargiya 最强的([A-Za-z]+)"),
r"None of the five candidates aspects the annual Lagna; the strongest of all by Panchavargiya, \1, is the year lord"),
# BUG-1218 Rath Narayana report lines
(re.compile(r"(第 (\d) 级:([^)]+))"), r" (level \1: \2)"),
(re.compile(r"(第 (\d) 级)"), r" (level \1)"),
(re.compile(r" 与 ([^,]+),较强 ([^(]+)"), r" and \1; stronger \2"),
(re.compile(r",从宫主 (\S+) 所在 (\S+) 起"), r", from the sign of its lord \1, \2"),
# scripts/pl9_reader_export.py: f"第{house}宫主:{lord}"
(re.compile(r"第(\d+)宫主:"), r"Lord of house \1: "),
# scripts/aspects.py / jyotish_engine.py: f"{n}宫相位"
@@ -60,8 +60,9 @@ MODULE_KEEP = (
"functional_benefic_malefic", "kakshya", "yogas", "chara_dasha", "transits",
)
# BUG-1215: + profile / algorithm_note / algorithm_label (the legacy-algorithm label the card carries).
# BUG-1218: + rath (the Sanjay Rath view the card shows beside the legacy one).
NARAYANA_KEEP = ("lagna_sign", "current_dasha", "current_year", "current_age", "mahadasha_sequence",
"profile", "algorithm_note", "algorithm_label")
"profile", "algorithm_note", "algorithm_label", "rath")
def _pick(value: dict[str, Any], keys: tuple[str, ...]) -> dict[str, Any]: