From 9eb22f63634267290b88cfbfb5c945b00af8e679 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sat, 3 Oct 2026 16:42:15 +0800 Subject: [PATCH] feat(narayana): label the product Narayana as the legacy algorithm (T4, BUG-1215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE --- .../src/lib/consultation-evidence-card.ts | 3 + frontend/src/mastra/consultation-workflow.ts | 2 + scripts/cmd_narayana_dasha.py | 3 + scripts/jyotish_api_server.py | 3 +- scripts/jyotish_engine.py | 11 ++-- scripts/narayana_legacy_label.py | 28 +++++++++ scripts/pl9_reader_english_terms.py | 3 + scripts/pl9_reader_export.py | 10 ++- tests/test_narayana_legacy_label.py | 63 +++++++++++++++++++ 9 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 scripts/narayana_legacy_label.py create mode 100644 tests/test_narayana_legacy_label.py diff --git a/frontend/src/lib/consultation-evidence-card.ts b/frontend/src/lib/consultation-evidence-card.ts index 31a0a520..2242be21 100644 --- a/frontend/src/lib/consultation-evidence-card.ts +++ b/frontend/src/lib/consultation-evidence-card.ts @@ -615,6 +615,9 @@ function baseTiming(timing: Record, 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 }; } diff --git a/frontend/src/mastra/consultation-workflow.ts b/frontend/src/mastra/consultation-workflow.ts index 524d00bf..58331ad5 100644 --- a/frontend/src/mastra/consultation-workflow.ts +++ b/frontend/src/mastra/consultation-workflow.ts @@ -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([ diff --git a/scripts/cmd_narayana_dasha.py b/scripts/cmd_narayana_dasha.py index b9d30e0c..f9579cfe 100644 --- a/scripts/cmd_narayana_dasha.py +++ b/scripts/cmd_narayana_dasha.py @@ -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']}", "", "── 大运序列 ──", ] diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index d7076935..19b2d484 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -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__}) diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 2f9f6bbd..cab665b8 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -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 五要素)') diff --git a/scripts/narayana_legacy_label.py b/scripts/narayana_legacy_label.py new file mode 100644 index 00000000..62c4c541 --- /dev/null +++ b/scripts/narayana_legacy_label.py @@ -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 diff --git a/scripts/pl9_reader_english_terms.py b/scripts/pl9_reader_english_terms.py index 55e64680..61853c44 100644 --- a/scripts/pl9_reader_english_terms.py +++ b/scripts/pl9_reader_english_terms.py @@ -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)", diff --git a/scripts/pl9_reader_export.py b/scripts/pl9_reader_export.py index a966fdf5..4119de3a 100644 --- a/scripts/pl9_reader_export.py +++ b/scripts/pl9_reader_export.py @@ -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}', - '', '| 序号 | 主星/星座 | 起始 | 结束 | 年数 |', '|------|-----------|------|------|------|', ]) diff --git a/tests/test_narayana_legacy_label.py b/tests/test_narayana_legacy_label.py new file mode 100644 index 00000000..b891b959 --- /dev/null +++ b/tests/test_narayana_legacy_label.py @@ -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 == []