fix(narayana): show the source-aware seed beside the historical sequence (BUG-1264, upstream 1918ffaf)

This commit is contained in:
jesse-ux
2026-10-07 17:42:13 +08:00
parent edeec41309
commit 5eb11e7cd1
5 changed files with 271 additions and 6 deletions
+16
View File
@@ -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
- 复发自:无
- 修复版本:本分支待发布
+12 -1
View File
@@ -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("")
+6 -2
View File
@@ -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}")
+176 -3
View File
@@ -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')
+61
View File
@@ -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"