From 5eb11e7cd12a95bbfc01d2f5934cab9f9726717b Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Wed, 7 Oct 2026 17:42:13 +0800 Subject: [PATCH] fix(narayana): show the source-aware seed beside the historical sequence (BUG-1264, upstream 1918ffaf) --- docs/BUG_HISTORY.md | 16 ++ scripts/cmd_narayana_dasha.py | 13 +- scripts/jyotish_engine.py | 8 +- scripts/narayana_dasha.py | 179 ++++++++++++++++++++++- tests/test_narayana_method_comparison.py | 61 ++++++++ 5 files changed, 271 insertions(+), 6 deletions(-) create mode 100644 tests/test_narayana_method_comparison.py diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 3f4fc091..370d3ff6 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -17062,3 +17062,19 @@ - 相关记录:BUG-1205、BUG-1261 - 复发自:无 - 修复版本:本分支待发布 + +## BUG-1264 | Narayana 报告只给历史起运,不标明所选规则 + +- 状态:resolved +- 首次发现:2026-10-07 +- 最近更新:2026-10-07 +- 影响面:`scripts/narayana_dasha.py` 的展示;全盘报告与 `cmd_narayana_dasha`。生时校正打分仍走原有默认序列 +- 用户现象:报告里的 Narayana 只有从上升顺排的历史序列,解释句把起运写成 Lagna,看不出实际用了哪条起运规则 +- 触发条件:生成 Narayana 大运报告 +- 根因:默认计算固定从上升顺排。上游 `1918ffaf` 另有按来源选择的起运、序列和双主星,用来对照,不替换历史序列。本仓序列表没有 Ketu 的第 12 表 +- 修复:展示层并列历史序列和来源对照。四个 profile 都是历史默认时,返回形状与原来的循环相同。来源对照写在 `method_comparison`,不替换 `mahadasha_sequence`,也不改 `score_candidates`。解释句写出实际选中的起运规则和星座。Ketu 落入需要第 12 表的情形时返回 blocked,原因 `rath_ketu_table12_v1_not_in_local_sequence_profiles`,不编序列。原有 Rath 对照保留 +- 验证:`tests/test_narayana_method_comparison.py`。默认调用与显式历史 profile 的序列相同;解释含所选规则名 +- 防复发:上述测试;打分调用仍是两参数的 `calc_narayana_mahadasha` +- 相关记录:BUG-1215、BUG-1218 +- 复发自:无 +- 修复版本:本分支待发布 diff --git a/scripts/cmd_narayana_dasha.py b/scripts/cmd_narayana_dasha.py index b1ac51ac..679b9453 100644 --- a/scripts/cmd_narayana_dasha.py +++ b/scripts/cmd_narayana_dasha.py @@ -26,7 +26,7 @@ def cmd_narayana_dasha(args, chart_data): "Narayana Dasha 需要完整的行星经度数据。", ]) - from narayana_dasha import narayana_dasha_full_report + from narayana_dasha import attach_narayana_method_comparison, narayana_dasha_full_report planet_lons = {} for pn, pd in chart_data.get('planets', {}).items(): @@ -59,6 +59,7 @@ def cmd_narayana_dasha(args, chart_data): 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) + attach_narayana_method_comparison(result, asc_idx, planet_lons, current_age, birth_year) # 格式化输出 lines = [ @@ -99,6 +100,16 @@ def cmd_narayana_dasha(args, chart_data): for line in result.get('interpretation', []): lines.append(f" {line}") + source = (result.get('method_comparison') or {}).get('source_aware') or {} + if source: + lines.append("") + lines.append("── 来源对照 ──") + if source.get('status') == 'blocked': + lines.append(f" 来源算法:blocked({source.get('reason')})") + else: + seed = source.get('seed_selection') or {} + lines.append(f" 来源起运规则:{seed.get('profile')};起运星座:{seed.get('selected_sign')}") + rath = result.get('rath') or {} if rath: lines.append("") diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 9162495b..77d7f3ae 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -66,6 +66,7 @@ except ModuleNotFoundError: # pragma: no cover - minimal environments 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, rath_report_lines_zh # BUG-1215 / BUG-1218 +from narayana_dasha import narayana_comparison_lines_zh as _narayana_comparison_lines_zh from capability_evidence_pool import build_capability_evidence_pool_summary from guided_topic_discovery import build_guided_topics @@ -9522,6 +9523,7 @@ def render_pl9_markdown(packet: dict) -> str: '', 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 + *_narayana_comparison_lines_zh((worksheets.get('timing_and_predictive_systems') or {}).get('narayana_dasha')), '', '#### Patyayini 年内节奏支持', '', @@ -16244,7 +16246,7 @@ def cmd_full_reading(args): # ── Step 4.13: Narayana Dasha(Rishi Dasha,v6.0.20)── try: - from narayana_dasha import narayana_dasha_full_report + from narayana_dasha import attach_narayana_method_comparison, narayana_dasha_full_report # 计算当前年龄(从出生到 today) birth_dt = _birth_datetime_from_args(args) today_dt = datetime.strptime(args.today, '%Y-%m-%d') if hasattr(args, 'today') and args.today else datetime.now() @@ -16256,8 +16258,10 @@ 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( # + Rath view, display only (BUG-1218) + labeled = 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) + report['modules']['narayana_dasha'] = attach_narayana_method_comparison( + labeled, asc_idx, planet_lons, current_age, args.year) except Exception as e: report['errors'].append(f"narayana-dasha: {e}") diff --git a/scripts/narayana_dasha.py b/scripts/narayana_dasha.py index 69252a98..11be67e4 100644 --- a/scripts/narayana_dasha.py +++ b/scripts/narayana_dasha.py @@ -429,10 +429,64 @@ def _resolve_narayana_lord( return first if first_years >= second_years else second +def _calc_narayana_mahadasha_profiled( + lagna_sign_idx: int, + planet_lons: Dict[str, float], + start_year: float, + *, + duration_profile: str, + sequence_profile: str, + seed_profile: str, + dual_lord_profile: str, +) -> List[Dict]: + """Source-aware Mahadasha. The legacy two-argument path does not call this.""" + if seed_profile == "jaimini_lagna_seventh_strength_v1": + selection = select_narayana_seed_sign(lagna_sign_idx, planet_lons) + if selection.get("status") != "selected": + raise ValueError(selection.get("reason") or "seed_not_selected") + seed = int(selection["selected_sign_idx"]) + elif seed_profile == "legacy_lagna_v0": + seed = lagna_sign_idx + else: + raise ValueError(f"unsupported Narayana seed profile: {seed_profile}") + sequence = build_narayana_sign_sequence(seed, profile=sequence_profile) + periods = [] + cum_years = start_year + for sign_idx in sequence: + lord = _resolve_narayana_lord(sign_idx, planet_lons, dual_lord_profile=dual_lord_profile) + lord_sign_idx = _get_planet_sign(lord, planet_lons) + if lord_sign_idx is None: + lord_sign_idx = sign_idx + years, _adjustment = calculate_narayana_period_years( + sign_idx, lord, lord_sign_idx, profile=duration_profile, + ) + periods.append({ + 'sign': SIGNS[sign_idx], + 'sign_idx': sign_idx, + 'lord': lord, + 'lord_in_sign': SIGNS[lord_sign_idx], + 'lord_sign_idx': lord_sign_idx, + 'years': years, + 'count_from_to': f'{SIGNS[sign_idx]}({sign_idx})→{SIGNS[lord_sign_idx]}({lord_sign_idx})', + 'start_age': round(cum_years, 2), + 'end_age': round(cum_years + years, 2), + 'duration_profile': duration_profile, + 'sequence_profile': sequence_profile, + 'seed_profile': seed_profile, + }) + cum_years += years + return periods + + def calc_narayana_mahadasha( lagna_sign_idx: int, planet_lons: Dict[str, float], start_year: float = 0.0, + *, + duration_profile: str = "legacy_forward_v0", + sequence_profile: str = "legacy_zodiacal_v0", + seed_profile: str = "legacy_lagna_v0", + dual_lord_profile: str = "legacy_primary_lord_v0", ) -> List[Dict]: """ 计算 Narayana Dasha 大运序列(第1周期)。 @@ -447,7 +501,24 @@ def calc_narayana_mahadasha( 'sign': str, 'sign_idx': int, 'lord': str, 'years': int, 'start_age': float, 'end_age': float, } + + 默认四个 profile 走原来的 Lagna 顺行循环,返回字段与打分调用保持一致。 """ + if not ( + duration_profile == "legacy_forward_v0" + and sequence_profile == "legacy_zodiacal_v0" + and seed_profile == "legacy_lagna_v0" + and dual_lord_profile == "legacy_primary_lord_v0" + ): + return _calc_narayana_mahadasha_profiled( + lagna_sign_idx, + planet_lons, + start_year, + duration_profile=duration_profile, + sequence_profile=sequence_profile, + seed_profile=seed_profile, + dual_lord_profile=dual_lord_profile, + ) periods = [] cum_years = start_year @@ -864,8 +935,23 @@ def narayana_dasha_full_report( """ result = {} - # 1. 完整大运序列 - mahadasha = calc_narayana_mahadasha(lagna_sign_idx, planet_lons) + # 1. 完整大运序列。默认 profile 仍走原来的两参数调用。 + if ( + duration_profile == "legacy_forward_v0" + and sequence_profile == "legacy_zodiacal_v0" + and seed_profile == "legacy_lagna_v0" + and dual_lord_profile == "legacy_primary_lord_v0" + ): + mahadasha = calc_narayana_mahadasha(lagna_sign_idx, planet_lons) + else: + mahadasha = calc_narayana_mahadasha( + lagna_sign_idx, + planet_lons, + duration_profile=duration_profile, + sequence_profile=sequence_profile, + seed_profile=seed_profile, + dual_lord_profile=dual_lord_profile, + ) total_cycle = sum(p['years'] for p in mahadasha) result['mahadasha_sequence'] = mahadasha result['total_cycle_years'] = total_cycle @@ -912,6 +998,90 @@ def narayana_dasha_full_report( return result +def narayana_report_with_variants(lagna_sign_idx, planet_lons, current_age=0, birth_year=0): + """Report the historical sequence and a source-aware sequence side by side. + + The returned top-level Mahadasha stays the historical profile. Rectification + scoring calls ``calc_narayana_mahadasha`` directly and does not read this. + Rath Ketu table 12 is not in the local sequence profiles, so that case is + blocked instead of replaced with another table. + """ + legacy = narayana_dasha_full_report(lagna_sign_idx, planet_lons, current_age, birth_year) + selection = select_narayana_seed_sign(lagna_sign_idx, planet_lons) + if selection.get('status') != 'selected': + blocked = {'status': 'blocked', 'reason': selection.get('reason')} + return { + **legacy, + 'selected_method': 'legacy_comparison_only', + 'method_comparison': {'source_aware': blocked, 'legacy': legacy}, + 'comparison_required': True, + } + seed = selection['selected_sign_idx'] + if _get_planet_sign('Saturn', planet_lons) == seed: + sequence = 'rath_saturn_table11_v1' + elif _get_planet_sign('Ketu', planet_lons) == seed: + blocked = {'status': 'blocked', 'reason': 'rath_ketu_table12_v1_not_in_local_sequence_profiles'} + return { + **legacy, + 'selected_method': 'legacy_comparison_only', + 'method_comparison': {'source_aware': blocked, 'legacy': legacy}, + 'comparison_required': True, + } + else: + sequence = 'rath_general_table10_v1' + settings = dict( + duration_profile='jaimini_odd_footed_dignity_v2', + sequence_profile=sequence, + seed_profile='jaimini_lagna_seventh_strength_v1', + dual_lord_profile='jaimini_dual_lord_source_v1', + antardasha_profile='parashara_equal_v1', + ) + try: + native = narayana_dasha_full_report( + lagna_sign_idx, planet_lons, current_age, birth_year, **settings, + ) + except ValueError as exc: + blocked = {'status': 'blocked', 'reason': str(exc), 'settings': settings} + return { + **legacy, + 'selected_method': 'legacy_comparison_only', + 'method_comparison': {'source_aware': blocked, 'legacy': legacy}, + 'comparison_required': True, + } + return { + **legacy, + 'selected_method': 'legacy_with_source_aware_comparison', + 'method_comparison': {'source_aware': native, 'legacy': legacy}, + 'comparison_required': True, + } + + +def narayana_comparison_lines_zh(narayana) -> List[str]: + """One display line naming the source-aware seed rule beside the legacy sequence.""" + if not isinstance(narayana, dict): + return [] + source = (narayana.get('method_comparison') or {}).get('source_aware') + if not isinstance(source, dict): + return [] + if source.get('status') == 'blocked': + return [f"- 来源对照:blocked({source.get('reason')})"] + seed = source.get('seed_selection') or {} + return [f"- 来源对照:所选起运规则 {seed.get('profile')},起运星座 {seed.get('selected_sign')}"] + + +def attach_narayana_method_comparison(result, lagna_sign_idx, planet_lons, current_age=0, birth_year=0): + """Attach the side-by-side view without replacing the historical sequence.""" + if not isinstance(result, dict): + return result + comparison = narayana_report_with_variants( + lagna_sign_idx, planet_lons, current_age, birth_year, + ) + result['method_comparison'] = comparison.get('method_comparison') + result['comparison_required'] = True + result['selected_method'] = comparison.get('selected_method') + return result + + def _interpret_narayana(result: Dict, current_age: float) -> List[str]: """Narayana Dasha 简要解读""" lines = [] @@ -920,7 +1090,10 @@ def _interpret_narayana(result: Dict, current_age: float) -> List[str]: if md_seq: total = sum(p['years'] for p in md_seq) lines.append(f"Narayana Dasha 完整周期: {total} 年(12 星座 × 可变年数)") - lines.append(f"起运星座: {result.get('lagna_sign', '?')}(Lagna)") + seed = result.get('seed_selection') or {} + rule = seed.get('profile') or result.get('seed_profile') or 'legacy_lagna_v0' + sign = seed.get('selected_sign') or result.get('lagna_sign', '?') + lines.append(f"起运星座: {sign}(所选起运规则 {rule})") curr = result.get('current_dasha', {}) md = curr.get('md') diff --git a/tests/test_narayana_method_comparison.py b/tests/test_narayana_method_comparison.py new file mode 100644 index 00000000..2ee88d58 --- /dev/null +++ b/tests/test_narayana_method_comparison.py @@ -0,0 +1,61 @@ +"""Narayana display keeps the historical sequence and names the selected seed (BUG-1264).""" + +from __future__ import annotations + +from scripts.narayana_dasha import ( + calc_narayana_mahadasha, + narayana_dasha_full_report, + narayana_report_with_variants, +) + + +LONGS = { + "Sun": 10.0, + "Moon": 40.0, + "Mars": 70.0, + "Mercury": 100.0, + "Jupiter": 130.0, + "Venus": 160.0, + "Saturn": 15.0, + "Rahu": 250.0, + "Ketu": 70.0, +} + + +def test_default_calc_matches_the_explicit_legacy_profiles() -> None: + plain = calc_narayana_mahadasha(0, LONGS) + explicit = calc_narayana_mahadasha( + 0, + LONGS, + 0.0, + duration_profile="legacy_forward_v0", + sequence_profile="legacy_zodiacal_v0", + seed_profile="legacy_lagna_v0", + dual_lord_profile="legacy_primary_lord_v0", + ) + assert plain == explicit + report = narayana_dasha_full_report(0, LONGS, current_age=30, birth_year=1990) + assert [row["sign"] for row in report["mahadasha_sequence"]] == [row["sign"] for row in plain] + assert report["interpretation"][1].startswith("起运星座: Aries(所选起运规则 legacy_lagna_v0)") + assert "(Lagna)" not in report["interpretation"][1] + + +def test_source_aware_comparison_does_not_replace_the_historical_sequence() -> None: + compared = narayana_report_with_variants(0, LONGS, current_age=30, birth_year=1990) + historical = narayana_dasha_full_report(0, LONGS, current_age=30, birth_year=1990) + assert [row["years"] for row in compared["mahadasha_sequence"]] == [row["years"] for row in historical["mahadasha_sequence"]] + source = compared["method_comparison"]["source_aware"] + assert source["seed_selection"]["profile"] == "jaimini_lagna_seventh_strength_v1" + assert "jaimini_lagna_seventh_strength_v1" in source["interpretation"][1] + assert compared["comparison_required"] is True + + +def test_ketu_in_the_seed_is_blocked_without_table_12() -> None: + longs = dict(LONGS) + longs["Saturn"] = 200.0 + longs["Ketu"] = 5.0 + compared = narayana_report_with_variants(0, longs, current_age=10, birth_year=1990) + source = compared["method_comparison"]["source_aware"] + assert source["status"] == "blocked" + assert "rath_ketu_table12_v1" in source["reason"] + assert compared["mahadasha_sequence"][0]["sign"] == "Aries"