From 4d2506c8c1c8937369044c4072ac0905c1eda369 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Mon, 19 Oct 2026 12:21:35 +0800 Subject: [PATCH] fix(reading): give Niryaana Shoola its own PVR table The report used the same nine-year rule as Shoola. Start from the stronger second or eighth sign and list mahadashas only. Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor --- CHANGELOG.md | 4 + docs/BUG_HISTORY.md | 15 ++ .../PROGRESS-niryaana-shoola-pvr-20261018.md | 36 ++++ docs/tasks/README.md | 2 +- scripts/jyotish_engine.py | 14 +- scripts/rashi_dasha_report.py | 7 + scripts/rashi_dashas_native.py | 186 +++++++++++++++++- .../rashi_dasha_pyjhora_20261008.json | 125 +++++------- tests/test_full_reading_module_parity.py | 13 +- tests/test_rashi_dashas_native.py | 153 +++++++++++++- 10 files changed, 451 insertions(+), 104 deletions(-) create mode 100644 docs/tasks/PROGRESS-niryaana-shoola-pvr-20261018.md diff --git a/CHANGELOG.md b/CHANGELOG.md index ae4bbdc3..486fcca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 印度占星 Skill 更新日志 +## 2026-10-19 — 完整数据版报告的 Niryaana Shoola 大运表改了 + +- 这张表不再和 Shoola 相同。起运从第二宫和第八宫里较强的星座开始;奇数星座顺行,偶数星座逆行;动、固、双星座分别是 7、8、9 年。只列主运,子运暂不列出。两宫分不出强弱时,这张表本轮不列出。合盘仍不用它做应期。 + ## 2026-10-18 — 完整解读补上西占对照和五处缺失模块 - 个人报告里的西占对照层不再因为缺文件而整段空白。它仍只作对照,不拿来改印度占星的结论。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index be2561d6..1a2e15d5 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -36840,3 +36840,18 @@ - 相关记录:BUG-423、BUG-1367。 - 复发自:无。 - 修复版本:分支 `codex/rectification-narrower-delivery-20261016`。 + +## BUG-1393 | Niryaana Shoola 和 Shoola 用了同一套算法,报告里两张表逐行相同 + +- 状态:resolved +- 首次发现 / 最近更新:2026-10-18 / 2026-10-19 +- 来源:issue #84。任务书 `docs/tasks/TASK-niryaana-shoola-pvr-20261018.md`。检索「niryaana_shoola」「goel_nirayana_shoola_v1」,命中 BUG-1283。 +- 现象:完整数据版报告里的 Niryaana Shoola 与 Shoola 逐行相同。两张表都从上升和第七宫里取较强的星座起运,每宫 9 年,一律顺行。 +- 触发条件:打开带本命盘的完整数据版报告,或读取 full-reading 的 `niryaana_shoola_dasha`。 +- 根因:BUG-1283 把报告改走 `goel_nirayana_shoola_v1` 时,所选规则其实是 Shoola 的。当时没有和同文件里的 Shoola 对过输出。本条推翻该 profile。BUG-1283 的另一半不变:新路径仍不得调用旧的 `scripts/niryaana_shoola_dasha.py`。 +- 修复:在 `scripts/rashi_dashas_native.py` 内改成 `pvr_niryaana_shoola_v1`。起运比较第二宫和第八宫,依次看宫内九曜个数、星座相位数、有没有入旺行星;三层都相同则本轮不上表。奇数星座顺行,偶数星座逆行。动、固、双星座分别 7、8、9 年。只出主运。full-reading 的 `niryaana_shoola_dasha` 改填这张表。合盘校验器对这一行仍固定为不适用。 +- 验证:`tests/test_rashi_dashas_native.py` 在屏蔽 PyJHora 后 18 通过;`tests/test_full_reading_module_parity.py` 与 `tests/test_niryaana_shoola_dasha.py` 合计 10 通过。同一张样例盘上,Niryaana Shoola 与 Shoola 的首运或年数不再全同。语法树断言仍确认新路径不导入旧文件。 +- 防复发:上述测试。不得再把 Niryaana Shoola 接回 `goel_nirayana_shoola_v1` 或 `_nine_year`。平手不得默认挑一个星座。不得为这张表编造子运。 +- 相关记录:BUG-1283。 +- 复发自:无。这是推翻 BUG-1283 所选 profile,不是旧文件被重新调用。 +- 修复版本:分支 `codex/niryaana-shoola-pvr-20261018`。 diff --git a/docs/tasks/PROGRESS-niryaana-shoola-pvr-20261018.md b/docs/tasks/PROGRESS-niryaana-shoola-pvr-20261018.md new file mode 100644 index 00000000..5eb16516 --- /dev/null +++ b/docs/tasks/PROGRESS-niryaana-shoola-pvr-20261018.md @@ -0,0 +1,36 @@ +# 进度:Niryaana Shoola 改为 PVR 口径(2026-10-19) + +- 基线:`origin/staging` `b827901c1`。分支 `codex/niryaana-shoola-pvr-20261018`。 +- 前置:#81 已在 staging(`900581068`),issue 标签为 `agent/待真机`。本单在其后开工。 +- BUG 编号:开工时最大号 BUG-1392,本单使用 BUG-1393。关联 BUG-1283,并写明推翻其所选 `goel_nirayana_shoola_v1`。旧文件仍不得被新路径调用。 + +## 做了什么 + +- `scripts/rashi_dashas_native.py`:Niryaana Shoola 改为 `pvr_niryaana_shoola_v1`。起运比较第二宫和第八宫(九曜个数、星座相位数、入旺行星);奇数星座顺行,偶数星座逆行;动 / 固 / 双星座 7 / 8 / 9 年。只出主运。三层平手则本轮不上表。没有搬上游文件,也没有 import 旧的 `niryaana_shoola_dasha.py`。 +- `scripts/jyotish_engine.py`:full-reading 的 `niryaana_shoola_dasha` 改填上面的结果,替换 #81 的占位。 +- `scripts/rashi_dasha_report.py`:该节列出主运,并写「子运规则未定,暂不列出。」 +- 对照文件里 Niryaana Shoola 的本站行和差异步骤按新表重记。PyJHora 行没有重跑。 + +## 断言三栏 + +| 位置 | 原值 | 新值 | 原因 | +| --- | --- | --- | --- | +| `test_rashi_dashas_native.py` 同盘 Niryaana | 首运 Virgo,十二段都是 9 年,有子运 | `pvr_niryaana_shoola_v1`,首运 Aries、顺行,年数从 7/8/9 起,无子运 | 推翻与 Shoola 相同的旧 profile | +| `test_full_reading_module_parity.py` | `execution_status=blocked`,原因「口径更正中」 | `parameter_sensitive`,profile `pvr_niryaana_shoola_v1`,periods 为主运 | #84 用真结果替换占位 | +| fixture `native.niryaana_shoola` 与 `diff_steps` | 与 Shoola 相同的 9 年顺行表 | PVR 主运星座和年数;差异步骤按新表重记 | 只改本站期望,PyJHora 行不动 | + +## 命令 + +工作树没有 `.venv`。下面用已装 Swiss Ephemeris 的 Anaconda 3.11:`C:\Users\74082\anaconda3\python.exe`。 + +| 命令 | 结果 | +| --- | --- | +| `tests/test_rashi_dashas_native.py`(屏蔽 PyJHora 导入) | 18 通过,退出码 0 | +| `tests/test_full_reading_module_parity.py` + `tests/test_niryaana_shoola_dasha.py` | 10 通过,退出码 0 | +| 全量 pytest、`run_quality_gate.py --profile quick` | 未跑。Windows 分工把这两项留给 Claude 验收 | + +## 偏离 + +- 没有整份搬上游 `niryaana_shoola_dasha.py`。力量比较、方向和年数按上游 `9f08b57b` / `258e08e3` 写在本仓文件里。 +- 两位公开名人用仓库里已有的公开盘:一张起射手并顺行,一张起水瓶并顺行,主运序列与该起运下的 7/8/9 年规则一致。 +- 平手样例(七曜都在巨蟹、白羊上升)结果为「本轮不上表」。节点占宫、节点星座相位、外行星不计数,与上游 `258e08e3` 的样例一致。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 8110db22..1779bf39 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -505,6 +505,6 @@ | `TASK-marriage-match-p1-fix-20261018.md` | #79 | 只存两个 profile 的配对结果;旧历史列表不读新行;并发重复写入改为重读 | 待验收 | `codex/marriage-match-p1-20261017` | | `TASK-marriage-match-p1-fix-20261018-2.md` | #79 | 胶水挪进子包,根目录文件数回到 446;调用清单改指向 marriage_match.py | 待验收 | `codex/marriage-match-p1-20261017` | | `TASK-full-reading-module-parity-20261018.md` | #81 | full-reading 补齐上游 5 个模块(西占层文件没搬致永远 blocked、Navamsha / Niryaana 用我们自己的实现填键、Lagna Vargottamamsa / Pushkara / Punarphoo)(BUG-1375 起) | 待执行(可与 #78 并行) | `codex/full-reading-module-parity-20261018` | -| `TASK-niryaana-shoola-pvr-20261018.md` | #84 | Niryaana Shoola 改 PVR 口径(我们的表与 Shoola 逐行相同;推翻 BUG-1283 所选 profile;占星师 7a773d13) | 排队(等 #81) | `codex/niryaana-shoola-pvr-20261018` | +| `TASK-niryaana-shoola-pvr-20261018.md` | `PROGRESS-niryaana-shoola-pvr-20261018.md` | #84 Niryaana Shoola 改 PVR 口径(我们的表与 Shoola 逐行相同;推翻 BUG-1283 所选 profile;占星师 7a773d13) | 待验收 | `codex/niryaana-shoola-pvr-20261018` | | `TASK-synastry-full-report-20261018.md` | #82 | 婚恋合盘:选人页 + 完整合盘报告(关系画像九节:结论+折叠的分析逻辑+关系时间线+技术依据摘要;英文按需;计费键 synastry.full) | 排队(等 #78、#79、#81) | `codex/synastry-full-report-20261018`;动表 | | `TASK-synastry-report-chat-20261018.md` | #83 | 从合盘报告某一节进入对话问依据(绑定报告与节、对话里可用术语、只依据已存计算结果并逐条核对、计费照普通对话) | 排队(等 #82) | `codex/synastry-report-chat-20261018`;动表 | diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 94f9a916..d3332577 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -16124,18 +16124,20 @@ def cmd_full_reading(args): planet_lons, ) report['modules']['navamsha_dasha'] = _public_secondary_dasha_module(secondary_dashas.get('navamsha')) + report['modules']['niryaana_shoola_dasha'] = _public_secondary_dasha_module( + secondary_dashas.get('niryaana_shoola') + ) except Exception as e: report['modules']['navamsha_dasha'] = { 'status': 'blocked', 'reason': f'navamsha_dasha_producer_failed:{e}', 'periods': [], } - # F2b: do not publish the current Niryaana Shoola table. It matches Shoola. - # The synastry validator marks this row not applicable, so the pair task stays open. - report['modules']['niryaana_shoola_dasha'] = { - 'execution_status': 'blocked', - 'reason': '口径更正中,见 TASK-niryaana-shoola-pvr-20261018', - } + report['modules']['niryaana_shoola_dasha'] = { + 'status': 'blocked', + 'reason': f'niryaana_shoola_dasha_producer_failed:{e}', + 'periods': [], + } _record_stage_timing( stage_timings, 'dasha_and_core_varga_stack', diff --git a/scripts/rashi_dasha_report.py b/scripts/rashi_dasha_report.py index 6bd0bb13..a9062d48 100644 --- a/scripts/rashi_dasha_report.py +++ b/scripts/rashi_dasha_report.py @@ -140,6 +140,13 @@ def render_rashi_dasha_chapter(packet: Mapping) -> str: section = sections[key] title = title_en if language == "en" else title_zh lines.extend([f"### {title}", "", _method_line(section, language), ""]) + if key == "niryaana_shoola" and section.get("status") == "listed": + lines.extend([ + "Child periods stay off this table: the cited book does not give a separate rule for them." + if language == "en" + else "子运规则未定,暂不列出。", + "", + ]) if key == "chara" and section.get("status") == "listed": lines.extend([ "This section is the K.N. Rao Chara table. It is not the Narayana table." diff --git a/scripts/rashi_dashas_native.py b/scripts/rashi_dashas_native.py index cd83737d..0430b42c 100644 --- a/scripts/rashi_dashas_native.py +++ b/scripts/rashi_dashas_native.py @@ -38,6 +38,17 @@ except ImportError: # pragma: no cover - package import _MOVABLE = {0, 3, 6, 9} _FIXED = {1, 4, 7, 10} _DUAL = {2, 5, 8, 11} +# Nine grahas only. Outer planets and upagrahas are ignored. Nodes are not weighted. +_AYUR_RASI_STRENGTH_BODIES = ( + "Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu", +) +_EXALTED_SIGNS = { + "Sun": {0}, "Moon": {1}, "Mars": {9}, "Mercury": {5}, + "Jupiter": {3}, "Venus": {11}, "Saturn": {6}, +} +_NIRYAANA_SOURCE_ZH = "PVR《Vedic Astrology: An Integrated Approach》" +_NIRYAANA_SOURCE_EN = "PVR, Vedic Astrology: An Integrated Approach" +_NIRYAANA_PROFILE = "pvr_niryaana_shoola_v1" def _as_date(value: date | datetime | str) -> date: @@ -303,6 +314,171 @@ def _drig_children(md_sign: int, start: date, end: date, graha: Mapping, planet_ return _equal_children(order, start, end) +def niryaana_shoola_years(sign_idx: int) -> int: + """Movable, fixed, and dual signs last 7, 8, and 9 years.""" + sign_idx = int(sign_idx) % 12 + if sign_idx in _FIXED: + return 8 + if sign_idx in _DUAL: + return 9 + return 7 + + +def select_niryaana_shoola_start_sign( + *, + ascendant_longitude: float, + planet_longitudes: Mapping[str, float], +) -> dict: + """Pick the stronger of the second and eighth signs for a longevity dasha. + + Compare occupant count, then every planet's Jaimini sign aspect (the sign + lord is not weighted), then whether an occupant is exalted. A three-way + tie stays off the table. Count only the nine grahas. + """ + lagna = sign_index(float(ascendant_longitude)) + second, eighth = (lagna + 1) % 12, (lagna + 7) % 12 + positions = { + name: float(longitude) + for name, longitude in planet_longitudes.items() + if name in _AYUR_RASI_STRENGTH_BODIES and isinstance(longitude, (int, float)) + } + candidates = {} + for sign_idx in (second, eighth): + occupants = sorted( + name for name, longitude in positions.items() + if sign_index(longitude) == sign_idx + ) + aspecting = sorted( + name for name, longitude in positions.items() + if sign_index(longitude) != sign_idx and sign_idx in _aspecting_signs(sign_index(longitude)) + ) + exalted = sorted( + name for name in occupants + if sign_idx in _EXALTED_SIGNS.get(name, set()) + ) + candidates[sign_idx] = { + "sign_index": sign_idx, + "sign": SIGNS[sign_idx], + "occupants": occupants, + "occupant_count": len(occupants), + "rasi_aspecting_planets": aspecting, + "rasi_aspect_count": len(aspecting), + "exalted_occupants": exalted, + } + second_score = ( + candidates[second]["occupant_count"], + candidates[second]["rasi_aspect_count"], + bool(candidates[second]["exalted_occupants"]), + ) + eighth_score = ( + candidates[eighth]["occupant_count"], + candidates[eighth]["rasi_aspect_count"], + bool(candidates[eighth]["exalted_occupants"]), + ) + payload = { + "profile": "pvr_niryaana_shoola_ayur_rasi_strength_v1", + "included_bodies": list(_AYUR_RASI_STRENGTH_BODIES), + "ascendant_sign_index": lagna, + "second_sign_index": second, + "eighth_sign_index": eighth, + "candidates": candidates, + "selection_levels": [ + "occupant_count", + "all_planet_rasi_aspect_count", + "exalted_occupant_presence", + ], + } + if second_score == eighth_score: + return { + **payload, + "status": "blocked", + "reason": "niryaana_shoola_ayur_rasi_strength_tie_requires_source_rule", + } + selected = second if second_score > eighth_score else eighth + return { + **payload, + "status": "selected", + "selected_sign_idx": selected, + "selected_sign": SIGNS[selected], + } + + +def niryaana_shoola_applicability( + *, + ascendant_longitude: float, + second_strength: float, + eighth_strength: float, +) -> dict: + """Start from the stronger given second or eighth strength. Equal strength blocks.""" + lagna = sign_index(float(ascendant_longitude)) + second, eighth = (lagna + 1) % 12, (lagna + 7) % 12 + second_strength, eighth_strength = float(second_strength), float(eighth_strength) + if second_strength == eighth_strength: + return { + "applicable": False, + "execution_status": "blocked", + "ascendant_sign_index": lagna, + "second_sign_index": second, + "eighth_sign_index": eighth, + "second_strength": second_strength, + "eighth_strength": eighth_strength, + "rule_family": "pvr_niryaana_shoola_2nd_8th_modality", + "reason": "The cited Niryaana Shoola rule does not provide a tie-break for equal second/eighth strength.", + } + start = second if second_strength > eighth_strength else eighth + return { + "applicable": True, + "ascendant_sign_index": lagna, + "second_sign_index": second, + "eighth_sign_index": eighth, + "second_strength": second_strength, + "eighth_strength": eighth_strength, + "starting_sign_index": start, + "starting_sign": SIGNS[start], + "direction": "forward" if start % 2 == 0 else "reverse", + "rule_family": "pvr_niryaana_shoola_2nd_8th_modality", + "reason": "Niryaana Shoola starts from the stronger second/eighth sign; odd seeds run forward and even seeds reverse.", + } + + +def niryaana_shoola_mahadashas(birth: date, start_sign: int) -> list[dict]: + """Twelve mahadashas only. Odd signs go forward; even signs go backward.""" + start_sign = int(start_sign) % 12 + order = _forward(start_sign) if start_sign % 2 == 0 else _reverse(start_sign) + periods = [] + cursor = birth + for sign_idx in order: + years = niryaana_shoola_years(sign_idx) + end = add_years(cursor, years) + periods.append(_period(sign_idx, years, cursor, end)) + cursor = end + return periods + + +def _niryaana_shoola(birth: date, ascendant_longitude: float, planet_lons: Mapping[str, float]) -> dict: + picked = select_niryaana_shoola_start_sign( + ascendant_longitude=ascendant_longitude, + planet_longitudes=planet_lons, + ) + if picked.get("status") != "selected": + return _omitted( + _NIRYAANA_PROFILE, + _NIRYAANA_SOURCE_ZH, + _NIRYAANA_SOURCE_EN, + "本轮不上表:二宫与八宫力量相同,原书没有给出进一步的裁决规则。", + "This report does not include Niryaana Shoola: the second and eighth signs are equal, and the source gives no further tie-break.", + ) + start_sign = int(picked["selected_sign_idx"]) + return _listed( + _NIRYAANA_PROFILE, + _NIRYAANA_SOURCE_ZH, + _NIRYAANA_SOURCE_EN, + niryaana_shoola_mahadashas(birth, start_sign), + direction="forward" if start_sign % 2 == 0 else "reverse", + strength_profile=picked["profile"], + ) + + def _nine_year(birth: date, lagna: int, planet_lons: Mapping[str, float], rashi: Mapping, profile: str, source_zh: str, source_en: str) -> dict: seventh = (lagna + 6) % 12 picked = stronger_sign(lagna, seventh, rashi, planet_lons) @@ -397,15 +573,7 @@ def calculate_rashi_dashas(birth: date | datetime | str, ascendant_longitude: fl "Crux of Vedic Astrology, printed page 395", ), "navamsha": _navamsha(birth_day, float(ascendant_longitude), longitudes), - "niryaana_shoola": _nine_year( - birth_day, - lagna, - longitudes, - rashi, - "goel_nirayana_shoola_v1", - "Predicting through Jaimini Astrology,印页 162", - "Predicting through Jaimini Astrology, printed page 162", - ), + "niryaana_shoola": _niryaana_shoola(birth_day, float(ascendant_longitude), longitudes), "lagna_kendradi": _omitted( "", "", diff --git a/tests/fixtures/rashi_dasha_pyjhora_20261008.json b/tests/fixtures/rashi_dasha_pyjhora_20261008.json index 3f99bd44..b142d915 100644 --- a/tests/fixtures/rashi_dasha_pyjhora_20261008.json +++ b/tests/fixtures/rashi_dasha_pyjhora_20261008.json @@ -564,32 +564,32 @@ }, "niryaana_shoola": { "sign_indexes": [ - 10, 11, - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, + 10, + 9, 8, - 9 + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0 ], "years": [ 9, + 8, + 7, 9, + 8, + 7, 9, + 8, + 7, 9, - 9, - 9, - 9, - 9, - 9, - 9, - 9, - 9 + 8, + 7 ] }, "lagna_kendradi": { @@ -647,7 +647,6 @@ "subperiod" ], "niryaana_shoola": [ - "start_sign", "direction", "years", "subperiod" @@ -1224,7 +1223,6 @@ }, "niryaana_shoola": { "sign_indexes": [ - 3, 4, 5, 6, @@ -1235,21 +1233,22 @@ 11, 0, 1, - 2 + 2, + 3 ], "years": [ + 8, 9, + 7, + 8, 9, + 7, + 8, 9, + 7, + 8, 9, - 9, - 9, - 9, - 9, - 9, - 9, - 9, - 9 + 7 ] }, "lagna_kendradi": { @@ -1308,7 +1307,6 @@ "subperiod" ], "niryaana_shoola": [ - "start_sign", "direction", "years", "subperiod" @@ -1909,32 +1907,32 @@ }, "niryaana_shoola": { "sign_indexes": [ - 2, - 3, - 4, - 5, - 6, - 7, - 8, 9, - 10, - 11, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, 0, - 1 + 11, + 10 ], "years": [ + 7, 9, + 8, + 7, 9, + 8, + 7, 9, + 8, + 7, 9, - 9, - 9, - 9, - 9, - 9, - 9, - 9, - 9 + 8 ] }, "lagna_kendradi": { @@ -1992,7 +1990,6 @@ "subperiod" ], "niryaana_shoola": [ - "start_sign", "direction", "years", "subperiod" @@ -2568,34 +2565,8 @@ ] }, "niryaana_shoola": { - "sign_indexes": [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 0 - ], - "years": [ - 9, - 9, - 9, - 9, - 9, - 9, - 9, - 9, - 9, - 9, - 9, - 9 - ] + "sign_indexes": [], + "years": [] }, "lagna_kendradi": { "status": "omitted", diff --git a/tests/test_full_reading_module_parity.py b/tests/test_full_reading_module_parity.py index a4f26066..8143d492 100644 --- a/tests/test_full_reading_module_parity.py +++ b/tests/test_full_reading_module_parity.py @@ -147,10 +147,15 @@ def test_secondary_dashas_use_native_profiles_and_match_goel_method_1(reports) - niryaana = modules["niryaana_shoola_dasha"] assert navamsha["status"] == "parameter_sensitive" assert navamsha["profile_id"] == "goel_method_1_v1" - assert niryaana == { - "execution_status": "blocked", - "reason": "口径更正中,见 TASK-niryaana-shoola-pvr-20261018", - } + # 原值:niryaana == {execution_status: blocked, reason: 口径更正中,见 TASK-niryaana-shoola-pvr-20261018} + # 新值:status parameter_sensitive,profile pvr_niryaana_shoola_v1,periods 是 PVR 主运 + # 原因:#84 用真结果换掉 #81 的占位。合盘校验器仍把这一行固定判为不适用。 + assert niryaana["status"] == "parameter_sensitive" + assert niryaana["profile_id"] == "pvr_niryaana_shoola_v1" + assert niryaana["periods"] == niryaana["native_result"]["periods"] + assert niryaana["native_result"]["status"] == "listed" + assert "antardasha" not in niryaana["periods"][0] + assert sum(row["years"] for row in niryaana["periods"]) == 96 assert navamsha["native_result"]["status"] == "listed" first = navamsha["periods"][0] d9 = modules["varga_full"]["D9_Navamsa"] diff --git a/tests/test_rashi_dashas_native.py b/tests/test_rashi_dashas_native.py index 61955750..59a0c104 100644 --- a/tests/test_rashi_dashas_native.py +++ b/tests/test_rashi_dashas_native.py @@ -13,6 +13,9 @@ from scripts.rashi_dashas_native import ( drig_aspect_sequence, navamsha_sign, navamsha_years, + niryaana_shoola_applicability, + niryaana_shoola_mahadashas, + select_niryaana_shoola_start_sign, ) from scripts.rashi_dasha_report import render_rashi_dasha_chapter @@ -63,15 +66,23 @@ def test_goel_short_method_matches_the_printed_totals_and_direct_sthira() -> Non assert "pratyantar" not in aquarius -def test_same_goel_chart_niryaana_uses_short_bala_not_the_printed_jaimini_start() -> None: - # Printed Jaimini rashi bala on this chart starts Pisces. Short-method totals tie, - # and the karaka rank of the lords selects Virgo. The report keeps the short method. +def test_same_goel_chart_niryaana_uses_pvr_not_the_shoola_rule() -> None: + # 原值:首运 Virgo,十二段都是 9 年,并有从 Virgo 起的子运。 + # 新值:profile pvr_niryaana_shoola_v1,首运 Aries、顺行,年数 7/8/9,没有子运。 + # 原因:产品 2026-10-18 推翻 BUG-1283 所选的 goel_nirayana_shoola_v1。那套和 Shoola 是同一张表。 packet = calculate_rashi_dashas(date(2001, 1, 1), 5 * 30 + 1.55, GOEL_LONS) niryaana = packet["niryaana_shoola"] - assert niryaana["periods"][0]["sign"] == "Virgo" - assert [row["years"] for row in niryaana["periods"]] == [9] * 12 - assert niryaana["periods"][0]["antardasha"][0]["sign"] == "Virgo" - assert niryaana["periods"][0]["antardasha"][0]["end"] == "2001-10-01" + shoola = packet["shoola"] + assert niryaana["profile_id"] == "pvr_niryaana_shoola_v1" + assert niryaana["status"] == "listed" + assert niryaana["direction"] == "forward" + assert niryaana["periods"][0]["sign"] == "Aries" + assert [row["years"] for row in niryaana["periods"][:3]] == [7, 8, 9] + assert "antardasha" not in niryaana["periods"][0] + assert ( + niryaana["periods"][0]["sign"] != shoola["periods"][0]["sign"] + or [row["years"] for row in niryaana["periods"]] != [row["years"] for row in shoola["periods"]] + ) def test_drig_aspect_order_for_a_cancer_lagna_and_narayana_style_years() -> None: @@ -152,7 +163,128 @@ def test_report_modules_do_not_call_the_old_shoola_or_niryaana_helpers() -> None assert all(not item.split(".")[0] == "jhora" for item in imported) +def test_niryaana_shoola_starts_from_the_stronger_second_or_eighth() -> None: + taurus = select_niryaana_shoola_start_sign(ascendant_longitude=0.0, planet_longitudes={"Mars": 31.0}) + scorpio = select_niryaana_shoola_start_sign(ascendant_longitude=0.0, planet_longitudes={"Mars": 211.0}) + assert taurus["selected_sign"] == "Taurus" + assert scorpio["selected_sign"] == "Scorpio" + taurus_rows = niryaana_shoola_mahadashas(date(2000, 1, 1), taurus["selected_sign_idx"]) + scorpio_rows = niryaana_shoola_mahadashas(date(2000, 1, 1), scorpio["selected_sign_idx"]) + assert [row["sign"] for row in taurus_rows[:2]] == ["Taurus", "Aries"] + assert [row["sign"] for row in scorpio_rows[:2]] == ["Scorpio", "Libra"] + + +def test_niryaana_shoola_odd_signs_go_forward_and_even_signs_go_backward() -> None: + even_seed = niryaana_shoola_applicability(ascendant_longitude=0.0, second_strength=2.0, eighth_strength=1.0) + odd_seed = select_niryaana_shoola_start_sign(ascendant_longitude=45.0, planet_longitudes={"Mars": 61.0}) + assert even_seed["starting_sign"] == "Taurus" + assert even_seed["direction"] == "reverse" + assert odd_seed["selected_sign"] == "Gemini" + odd_rows = niryaana_shoola_mahadashas(date(2000, 1, 1), odd_seed["selected_sign_idx"]) + assert [row["sign"] for row in odd_rows[:2]] == ["Gemini", "Cancer"] + assert [row["years"] for row in odd_rows[:3]] == [9, 7, 8] + + +def test_niryaana_shoola_given_strengths_match_the_upstream_mahadasha_golden() -> None: + # Golden copied from upstream 9f08b57b tests/test_niryaana_shoola_dasha.py + # test_niryaana_shoola_uses_modality_years_and_parity_direction. + picked = niryaana_shoola_applicability( + ascendant_longitude=15.0, second_strength=2.0, eighth_strength=1.0, + ) + rows = niryaana_shoola_mahadashas(date(2000, 1, 1), picked["starting_sign_index"]) + assert [row["sign"] for row in rows[:3]] == ["Taurus", "Aries", "Pisces"] + assert [row["years"] for row in rows[:3]] == [8, 7, 9] + assert [row["sign"] for row in rows] == [ + "Taurus", "Aries", "Pisces", "Aquarius", "Capricorn", "Sagittarius", + "Scorpio", "Libra", "Virgo", "Leo", "Cancer", "Gemini", + ] + assert [row["years"] for row in rows] == [8, 7, 9, 8, 7, 9, 8, 7, 9, 8, 7, 9] + assert sum(row["years"] for row in rows) == 96 + assert all("antardasha" not in row for row in rows) + + +def test_two_public_charts_match_the_upstream_start_signs() -> None: + # Claude 10-18, upstream 9f08b57b: one chart starts in Sagittarius, one in Aquarius. + # These two public charts are the pair. Sequences are the PVR mahadashas from that start. + expected = { + "brad_pitt": ( + "Sagittarius", + ["Sagittarius", "Capricorn", "Aquarius", "Pisces", "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio"], + [9, 7, 8, 9, 7, 8, 9, 7, 8, 9, 7, 8], + ), + "jolie": ( + "Aquarius", + ["Aquarius", "Pisces", "Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn"], + [8, 9, 7, 8, 9, 7, 8, 9, 7, 8, 9, 7], + ), + } + sources = { + "brad_pitt": ROOT / "tests/indastro_cases.json", + "jolie": ROOT / "tests/celebrity_cases.json", + } + from scripts.jyotish_engine import compute_chart_data + + for case_id, (start, signs, years) in expected.items(): + rows = json.loads(sources[case_id].read_text(encoding="utf-8")) + birth = next(row for row in rows if row["id"] == case_id) + chart, _asc, _jd, _ayanamsa = compute_chart_data( + birth["year"], birth["month"], birth["day"], birth["hour"], birth["minute"], + birth["lat"], birth["lon"], birth["tz"], second=birth.get("second") or 0, + ayanamsa_name="lahiri", + ) + longitudes = { + name: float(row["degree_raw"]) + for name, row in chart["planets"].items() + if isinstance(row, dict) and "degree_raw" in row + } + section = calculate_rashi_dashas( + chart["birth_info"]["date"], float(chart["ascendant"]["degree_raw"]), longitudes, + )["niryaana_shoola"] + assert section["status"] == "listed" + assert section["direction"] == "forward" + assert [row["sign"] for row in section["periods"]] == signs + assert section["periods"][0]["sign"] == start + assert [row["years"] for row in section["periods"]] == years + assert all("antardasha" not in row for row in section["periods"]) + + +def test_niryaana_shoola_tie_stays_off_the_table() -> None: + tied = {name: 3 * 30 + 10.0 for name in ("Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn")} + section = calculate_rashi_dashas(date(2001, 1, 1), 0.5, tied)["niryaana_shoola"] + assert section["status"] == "omitted" + assert section["periods"] == [] + assert "本轮不上表" in section["reason_zh"] + assert "二宫与八宫力量相同" in section["reason_zh"] + + +def test_niryaana_shoola_counts_both_nodes_and_ignores_outer_planets() -> None: + # Same placements as upstream 258e08e3. + occupancy = select_niryaana_shoola_start_sign( + ascendant_longitude=0.0, + planet_longitudes={"Rahu": 31.0, "Ketu": 211.0, "Pluto": 31.0}, + ) + aspects = select_niryaana_shoola_start_sign( + ascendant_longitude=0.0, + planet_longitudes={"Rahu": 1.0, "Ketu": 181.0}, + ) + assert occupancy["included_bodies"][-2:] == ["Rahu", "Ketu"] + assert occupancy["candidates"][1]["occupants"] == ["Rahu"] + assert occupancy["candidates"][7]["occupants"] == ["Ketu"] + assert "Pluto" not in occupancy["candidates"][1]["occupants"] + assert aspects["candidates"][1]["rasi_aspecting_planets"] == ["Ketu"] + assert aspects["candidates"][7]["rasi_aspecting_planets"] == ["Rahu"] + without_outer = select_niryaana_shoola_start_sign(ascendant_longitude=0.0, planet_longitudes={"Mars": 31.0}) + with_outer = select_niryaana_shoola_start_sign( + ascendant_longitude=0.0, planet_longitudes={"Mars": 31.0, "Uranus": 211.0}, + ) + assert without_outer["selected_sign"] == with_outer["selected_sign"] == "Taurus" + assert "Uranus" not in with_outer["candidates"][7]["occupants"] + + def test_pyjhora_fixture_diffs_stay_on_the_recorded_steps() -> None: + # 原值:niryaana_shoola 的 native 行与 Shoola 一样,从上升或第七宫起、每段 9 年,并带子运差异。 + # 新值:同一键改为 PVR 主运的星座和年数;diff_steps 按新表相对 PyJHora 重记。 + # 原因:产品 2026-10-18 改口径。PyJHora 对照行没有重跑,只改我们自己的期望。 payload = json.loads(FIXTURE.read_text(encoding="utf-8")) assert payload["pyjhora_version"] == "4.8.7" assert payload["generator_python_has_pyjhora"] is True @@ -227,6 +359,10 @@ def test_jobs_chapter_names_the_method_and_leaves_kendradi_out() -> None: text = render_rashi_dasha_chapter(packet) for title in ("Sthira", "Niryaana Shoola", "Drig", "Navamsha", "Shoola", "Chara"): assert f"### {title}" in text + niryaana = text.split("### Niryaana Shoola", 1)[1].split("### ", 1)[0] + assert "pvr_niryaana_shoola_v1" in niryaana + assert "子运规则未定,暂不列出。" in niryaana + assert "| 序号 | 主运 | 子运 |" not in niryaana assert "本站选用的方法:" in text assert "未与外部软件逐一核对" in text assert "K.N. Rao" in text @@ -236,6 +372,9 @@ def test_jobs_chapter_names_the_method_and_leaves_kendradi_out() -> None: assert "| 序号 | 星座 |" not in kendradi english = render_rashi_dasha_chapter({**packet, "report_language": "en"}) assert "Method used on this site:" in english + english_niryaana = english.split("### Niryaana Shoola Dasha", 1)[1].split("### ", 1)[0] + assert "pvr_niryaana_shoola_v1" in english_niryaana + assert "Child periods stay off this table" in english_niryaana assert "This report does not include Lagna Kendradi" in english assert "not checked one-by-one against external software." in english leaked = [char for char in english if "\u4e00" <= char <= "\u9fff"]