From 5be864a8e4cf9e347cd9561298c14a94b172f99d Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Sun, 18 Oct 2026 16:17:47 +0800 Subject: [PATCH] fix(dasha): judge antardasha sandhi from the unclipped start Upstream 3c2dad95, rewritten for this tree. A birth-clipped antardasha start was reported as a transition. The first mahadasha start stays, because BUG-1268 already stores the real pre-birth boundary there. BUG-1382. Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor --- docs/BUG_HISTORY.md | 15 +++++++ scripts/jyotish_engine.py | 10 +++-- tests/test_dasha_sandhi_birth_clip.py | 60 +++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 tests/test_dasha_sandhi_birth_clip.py diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 5c35c066..7f5473f4 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -36518,3 +36518,18 @@ - 相关记录:BUG-1268、BUG-1380。 - 复发自:无。 - 修复版本:分支 `codex/upstream-sync8-20261017`。上游提交 `3c2dad95`、`8643bc9a`、`9e04f0db`。 + +## BUG-1382 | 出生日被当成小运交接 + +- 状态:resolved +- 首次发现 / 最近更新:2026-10-17 / 2026-10-18 +- 来源:issue #80,任务书 T3。上游 `3c2dad95` 的 Sandhi 部分,按我方数据改写。检索「dasha_sandhi」「unclipped_start」,没有更早的同类记录。 +- 现象:出生后 30 天内的盘,Sandhi 表把出生日写成一次小运交接。那一天只是第一段小运被裁到出生日,不是两段小运的交界。 +- 触发条件:`_calc_dasha_sandhi` 用裁剪后的 `start` 判断小运交接。默认参考时刻还带着当前的时分秒。 +- 根因:第一段小运的可见起点被改成了出生日。BUG-1268 之后,第一大运自己的 `start` 已经是出生前的真实交接,不能连大运起点一起跳过。 +- 修复:小运起点改用 `unclipped_start_datetime`。第一大运起点仍参与判断。没给参考日期时,参考时刻归零到当天 0 点。 +- 验证:`tests/test_dasha_sandhi_birth_clip.py`。虚构盘在出生后 30 天不再把出生日算成小运交接;出生前的大运起点仍在容限内出现。 +- 防复发:上述测试。 +- 相关记录:BUG-1268、BUG-1380。 +- 复发自:无。 +- 修复版本:分支 `codex/upstream-sync8-20261017`。上游提交 `3c2dad95`。 diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index d5c8daf1..06ef4782 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -464,7 +464,7 @@ def _calc_sensitive_points(planets): def _calc_dasha_sandhi(dasha_result, reference_date=None, orb_days=90): - ref = datetime.strptime(reference_date, "%Y-%m-%d") if reference_date else datetime.now() + ref = datetime.strptime(reference_date, "%Y-%m-%d") if reference_date else datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) sandhi = [] timeline = dasha_result.get('timeline', []) if isinstance(dasha_result, dict) else [] for md in timeline: @@ -486,7 +486,11 @@ def _calc_dasha_sandhi(dasha_result, reference_date=None, orb_days=90): for boundary_key in ['start', 'end']: if boundary_key not in ad: continue - bdt = datetime.strptime(ad[boundary_key], "%Y-%m-%d") + # 被出生裁掉的小运起点不是交接。用完整起点判断,第一大运自己的 start 仍是出生前的真实交接,不跳过。 + boundary_date = ad[boundary_key] + if boundary_key == 'start' and ad.get('unclipped_start_datetime'): + boundary_date = datetime.fromisoformat(ad['unclipped_start_datetime']).strftime('%Y-%m-%d') + bdt = datetime.strptime(boundary_date, "%Y-%m-%d") delta = (bdt - ref).days if abs(delta) <= orb_days: sandhi.append({ @@ -494,7 +498,7 @@ def _calc_dasha_sandhi(dasha_result, reference_date=None, orb_days=90): 'mahadasha_lord': md.get('lord'), 'lord': ad.get('lord'), 'boundary': boundary_key, - 'date': ad.get(boundary_key), + 'date': boundary_date, 'days_from_reference': delta, 'within_orb': True, }) diff --git a/tests/test_dasha_sandhi_birth_clip.py b/tests/test_dasha_sandhi_birth_clip.py new file mode 100644 index 00000000..fc217199 --- /dev/null +++ b/tests/test_dasha_sandhi_birth_clip.py @@ -0,0 +1,60 @@ +"""A clipped birth balance is not an actual Dasha transition. + +Upstream ``tests/test_dasha_sandhi_birth_clip.py`` (3c2dad95) skips the first +mahadasha start whenever ``is_balance`` is set. This tree does not: BUG-1268 +already stores the real pre-birth mahadasha boundary in ``start``. + +| 处 | 原值 | 新值 | 原因 | +| --- | --- | --- | --- | +| 第一大运 ``start`` | ``2000-01-01``,并跳过 ``is_balance`` 起点 | ``1998-01-01``,不跳过 | 我方第一大运起点是出生前的真实交接 | +| 断言 | 跳过起点后窗口为空 | 起点超出容限,窗口仍为空 | 单独证明被裁剪的小运起点不算交接 | +""" +from types import SimpleNamespace + +from scripts.jyotish_engine import _calc_dasha_sandhi, cmd_dasha + + +def test_birth_clip_does_not_create_a_transition(): + data = {'timeline': [{'lord': 'Saturn', 'is_balance': True, + 'start': '1998-01-01', 'end': '2010-01-01', + 'antardasha_timeline': [{'lord': 'Mercury', 'start': '2000-01-01', 'end': '2002-01-01', + 'unclipped_start_datetime': '1999-01-01T12:30:00'}]}]} + assert _calc_dasha_sandhi(data, '2000-01-01')['sandhi_windows'] == [] + + +def test_actual_unclipped_start_is_still_found(): + data = {'timeline': [{'lord': 'Saturn', 'is_balance': True, + 'start': '1998-01-01', 'end': '2010-01-01', + 'antardasha_timeline': [{'lord': 'Mercury', 'start': '2000-01-01', 'end': '2002-01-01', + 'unclipped_start_datetime': '1999-12-20T12:30:00'}]}]} + windows = _calc_dasha_sandhi(data, '2000-01-01', 30)['sandhi_windows'] + assert len(windows) == 1 + assert windows[0]['date'] == '1999-12-20' + assert windows[0]['days_from_reference'] == -12 + + +def test_real_boundaries_are_inclusive_at_orb_limit(): + data = {'timeline': [{'lord': 'Sun', 'start': '2000-01-01', 'end': '2000-02-01'}]} + windows = _calc_dasha_sandhi(data, '2000-01-02', 1)['sandhi_windows'] + assert len(windows) == 1 and windows[0]['date'] == '2000-01-01' + + +def test_prebirth_mahadasha_start_is_not_discarded(): + data = {'timeline': [{'lord': 'Saturn', 'is_balance': True, + 'start': '1999-12-01', 'end': '2010-01-01', + 'antardasha_timeline': []}]} + windows = _calc_dasha_sandhi(data, '2000-01-01', 90)['sandhi_windows'] + assert [row['date'] for row in windows] == ['1999-12-01'] + + +def test_thirty_day_chart_does_not_list_the_birth_day_as_an_antardasha_join(): + packet = cmd_dasha(SimpleNamespace( + moon_lon=20 / 3, nakshatra=None, pada=None, + birthdate='2000-01-01', year=2000, month=1, day=1, hour=12, minute=0, second=0, + lat=None, lon=None, tz=0, today='2000-01-31', dasha_year_days=365.25)) + windows = _calc_dasha_sandhi(packet, '2000-01-31', 30)['sandhi_windows'] + clipped = [ + row for row in windows + if row['level'] == 'antardasha' and row['boundary'] == 'start' and row['date'] == '2000-01-01' + ] + assert clipped == []