feat(narayana): label the product Narayana as the legacy algorithm (T4, BUG-1215)

Round-4 ruling Q4 / product decision A: scoring and the consultation and
report time axes keep the old Narayana (from the Lagna, always zodiacal) until
the Rath trial is decided; every user-visible Narayana now says so.
narayana_legacy_label adds profile=legacy_reference_only and the note (zh/en)
at the consumers (consultation modules, full reading Step 4.13, CLI), not in
the frozen narayana_dasha.py (ERR-110/115). The card's timing.narayana carries
'algorithm'; the projection allowlist passes algorithm_note; the reference
report's Narayana section and the dasha-family table show '算法:…' (English
twin in the phrase table). The unused narayana-dasha --variant-profile flag,
the one place offering a Chara Dasha author's profile for Narayana, is removed.

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:42:15 +08:00
co-authored by Claude Opus 5.5
parent 7cb332bce6
commit 9eb22f6363
9 changed files with 119 additions and 7 deletions
@@ -615,6 +615,9 @@ function baseTiming(timing: Record<string, unknown>, gaps: string[]) {
}
const remaining = copy(currentNarayana.remaining_years);
if (remaining !== undefined) narayana.remaining_years = remaining;
// BUG-1215: say which Narayana this is (legacy: from the Lagna, always zodiacal).
const algorithmNote = record(timing.narayana_dasha).algorithm_note;
if (narayana.md && typeof algorithmNote === "string" && algorithmNote) narayana.algorithm = algorithmNote;
if (!narayana.md) gaps.push("base.narayana.md");
return { vimshottari, narayana };
}
@@ -346,6 +346,8 @@ const timingKeys = new Set([
// Without these keys the projection kept `current_dasha` as an empty object
// and dropped the pratyantar dates, although the engine had both.
"md", "ad", "pd", "years", "startage", "endage", "remainingyears", "pratyantardashatimeline",
// BUG-1215: the running Narayana is the legacy algorithm; its label travels with it.
"algorithmnote",
]);
const validationKeys = new Set([
+3
View File
@@ -56,6 +56,8 @@ 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)
# 格式化输出
lines = [
@@ -65,6 +67,7 @@ def cmd_narayana_dasha(args, chart_data):
"",
f"Lagna: {result['lagna_sign']}({result['lagna_sign_idx']})",
f"总周期: {result['total_cycle_years']} 年",
f"算法: {result['algorithm_note']}",
"",
"── 大运序列 ──",
]
+2 -1
View File
@@ -1622,7 +1622,8 @@ def _attach_local_consultation_layers(handler, chart: dict, birth_payload: dict,
birth_year=int(birth_payload.get('year', 0) or 0),
)
if isinstance(narayana_result, dict):
modules['narayana_dasha'] = narayana_result
# 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)
except Exception as exc:
diagnostics.append({'layer': 'narayana_dasha', 'status': 'unavailable', 'reason': exc.__class__.__name__})
+7 -4
View File
@@ -65,6 +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 capability_evidence_pool import build_capability_evidence_pool_summary
from guided_topic_discovery import build_guided_topics
@@ -9447,6 +9448,8 @@ def render_pl9_markdown(packet: dict) -> str:
f"参考对齐状态:`{_md_cell(narayana_topic.get('reference_alignment_status') or 'not_closed')}`。"
'本地年龄轴与 PL9 日期边界/起运序列的字段级 parity 尚未闭环,因此不替代主时间判断。',
'',
f'- 算法:{LEGACY_NARAYANA_NOTE_ZH}', # BUG-1215
'',
'#### Patyayini 年内节奏支持',
'',
f"Patyayini 当前状态为 `{_md_cell(patyayini_topic.get('status') or 'blocked')}`;"
@@ -16111,7 +16114,8 @@ def cmd_full_reading(args):
current_age=current_age,
birth_year=args.year,
)
report['modules']['narayana_dasha'] = narayana_result
from narayana_legacy_label import label_legacy_narayana # BUG-1215
report['modules']['narayana_dasha'] = label_legacy_narayana(narayana_result)
except Exception as e:
report['errors'].append(f"narayana-dasha: {e}")
@@ -18353,9 +18357,8 @@ def main():
p = sub.add_parser('narayana-dasha', help='Narayana Dasha(Rishi Dasha)星座大运分析')
_add_chart_args(p)
p.add_argument('--age', type=float, default=None, help='当前年龄(用于定位大运位置)')
p.add_argument('--variant-profile', default='legacy_local',
choices=['legacy_local', 'kn_rao', 'goel_lagna_start', 'goel_gendered', 'goel_shastri_method2', 'pyjhora_blackbox'],
help='Narayana/Chara式星座大运profile:legacy保持旧输出;kn_rao/goel映射到显式本地规则;pyjhora_blackbox只输出PyJHora黑盒当前路径')
# BUG-1215: the unused --variant-profile flag is removed (it offered a
# Chara Dasha author's name as a profile of this command).
# 21.7 muhurta (v6.0.21新增)
p = sub.add_parser('muhurta', help='Muhurta 择时分析(Panchanga 五要素)')
+28
View File
@@ -0,0 +1,28 @@
#!/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).
"""
from __future__ import annotations
from typing import Any, Dict
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"
def label_legacy_narayana(result: Any) -> Any:
"""Add the legacy profile and note to a `narayana_dasha_full_report` result."""
if isinstance(result, dict):
result["profile"] = LEGACY_PROFILE
result["algorithm_note"] = LEGACY_NOTE_ZH
result["algorithm_note_en"] = LEGACY_NOTE_EN
return result
+3
View File
@@ -147,6 +147,9 @@ _PHRASES: dict[str, str] = {
"| 要素总数 |": "| Total elements |",
# ── annual labels and values ──
"Muntha 所在星座的守护星(当前本地口径)": "Ruler of the Muntha sign (current local method)",
# 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-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)",
+8 -2
View File
@@ -23,6 +23,10 @@ for the public reader rather than exposing empty or machine-key rows.
from __future__ import annotations
try: # scripts/ on sys.path
from narayana_legacy_label import LEGACY_NOTE_ZH
except ImportError: # pragma: no cover - package import
from scripts.narayana_legacy_label import LEGACY_NOTE_ZH
import re
SIGNS: list[str] = []
@@ -1652,9 +1656,11 @@ def render_pl9_parity_markdown(packet: dict) -> str:
periods = _list(family.get('periods'))
if not periods:
continue
lines.extend([f'### p{page} {title_text}', ''])
if 'Narayana' in title_text:
# BUG-1215: the product Narayana is the legacy algorithm.
lines.extend([f'- 算法:{LEGACY_NOTE_ZH}', ''])
lines.extend([
f'### p{page} {title_text}',
'',
'| 序号 | 主星/星座 | 起始 | 结束 | 年数 |',
'|------|-----------|------|------|------|',
])
+63
View File
@@ -0,0 +1,63 @@
"""The product Narayana Dasha is labelled as the legacy algorithm (BUG-1215, T4).
Round-4 ruling Q4: until the Sanjay Rath profile replaces it (product decision
after the trial), every user-visible Narayana says it is the old algorithm
(from the Lagna, always zodiacal) and the data carry
`profile: legacy_reference_only`. K.N. Rao is a Chara Dasha name only.
Fictional chart.
"""
from __future__ import annotations
import copy
import json
import re
from pathlib import Path
from scripts.narayana_legacy_label import LEGACY_NOTE_EN, LEGACY_NOTE_ZH, LEGACY_PROFILE
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
from tests.test_report_english_edition import CASES, _packet
ROOT = Path(__file__).resolve().parents[1]
def test_report_module_and_reference_edition_carry_the_label() -> None:
packet = _packet(CASES["day"])
zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reference")
assert f"- 算法:{LEGACY_NOTE_ZH}" in zh
from scripts.jyotish_engine import cmd_full_reading
reading = cmd_full_reading(type("Args", (), dict(CASES["day"]))())
module = reading["modules"]["narayana_dasha"]
assert (module["profile"], module["algorithm_note"], module["algorithm_note_en"]) == (LEGACY_PROFILE, LEGACY_NOTE_ZH, LEGACY_NOTE_EN)
def test_english_reader_translates_the_label() -> None:
from scripts.pl9_reader_english_terms import PHRASES_EN
text = f"- 算法:{LEGACY_NOTE_ZH}"
for source, target in PHRASES_EN:
text = text.replace(source, target)
assert text == f"- Algorithm: {LEGACY_NOTE_EN}"
def test_consultation_modules_carry_the_label() -> None:
golden = json.loads((ROOT / "frontend/tests/fixtures/consult-evidence-card-golden.json").read_text(encoding="utf-8"))
for chart in golden["charts"]:
for route in ("workflow", "annual_workflow", "timing_workflow"):
module = chart[route]["chart"]["modules"]["narayana_dasha"]
assert (module["profile"], module["algorithm_note"]) == (LEGACY_PROFILE, LEGACY_NOTE_ZH)
card = (ROOT / "frontend/src/lib/consultation-evidence-card.ts").read_text(encoding="utf-8")
assert "narayana.algorithm = algorithmNote" in card
workflow = (ROOT / "frontend/src/mastra/consultation-workflow.ts").read_text(encoding="utf-8")
assert '"algorithmnote"' in workflow
def test_kn_rao_never_names_narayana_on_user_visible_surfaces() -> None:
pattern = re.compile(r"(K\.?\s?N\.?\s?Rao|kn_rao)[^\n]{0,40}Narayana|Narayana[^\n]{0,40}(K\.?\s?N\.?\s?Rao|kn_rao)", re.IGNORECASE)
surfaces = [*(ROOT / "frontend/src").rglob("*.ts"), *(ROOT / "frontend/src").rglob("*.tsx"),
ROOT / "scripts/pl9_reader_export.py", ROOT / "scripts/pl9_reader_english_terms.py",
ROOT / "scripts/consultation_native_layers.py", ROOT / "scripts/jyotish_engine.py",
ROOT / "scripts/narayana_dasha.py", ROOT / "scripts/cmd_narayana_dasha.py"]
hits = [path.name for path in surfaces if pattern.search(path.read_text(encoding="utf-8"))]
assert hits == []