diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 5c16165a..d35bd2b4 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -21240,3 +21240,20 @@ - 关联记录:BUG-1541、BUG-1220(年盘保留秒)。 - 复发自:无 - 修复版本:分支 `codex/upstream-skill-sync-20261104`,待合并。 + +## BUG-1543 | KP 子主星与宫头用的是显示用的四舍五入值 + +- 状态:resolved(代码 + 回归测试;待部署) +- 首次发现 / 最近更新:2026-11-04(上游同步第十轮核对上游 `26d5a52d`)/ 2026-11-04 +- 影响面:`scripts/jyotish_engine.py` `cmd_kp`(命令 `kp`、`/api/kp` 一类入口)。KP 在商业覆盖层仍是 `reference_only`。 +- 现象:行星落在星主 / 子主 / 子子主分界附近、或宫头附近时,KP 表可能差一格。 +- 触发条件:行星度数或宫头离 KP 分界小于约 0.0001 度;出生时间带秒。 +- 根因:`cmd_kp` 把 `calculate_bhava_boundaries()` 返回的宫头(保留 4 位小数)、行星的 `degree_in_sign`(保留 4 位小数)和星盘结果里的 `julian_day`(保留 6 位小数,约 0.09 秒)交给 KP 分析,KP 分界是按原始经度切的。上游同一缺陷,10-16 修复,第九轮没核对。 +- 修复:按出生时刻重算儒略日(与 `compute_chart_data` 同一公式,含秒);宫头改用 `calculate_cusps()` 原值;行星度数用 `degree_raw % 30`,没有原值时才退回显示值。 +- 验证:新测试 `tests/test_upstream_sync10_kp_unrounded_inputs.py`(移植上游 `tests/test_kp_unrounded_inputs.py`):儒略日、宫头、行星度数都是原值;改前失败、改后通过。`test_kp_system.py`、`test_kp_cusp_house_assignment.py`、`test_kp_monthly_report_packet.py` 5 条通过、2 条跳过;公开名人盘实跑 `cmd_kp` 正常返回。 +- 影响输出:只有贴着分界的行星或宫头会换一格,其余逐字节相同。不涉及冻结身份文件。 +- 未搬:同一上游提交里的 Vivaha Saham 昼夜公式与 Yoga 燃烧状态透传,见本轮进度记录。 +- 防复发:上述测试;显示用的取整值不进计算。 +- 关联记录:BUG-1383(KP 边界,第八轮)、BUG-1541、BUG-1542。 +- 复发自:无 +- 修复版本:分支 `codex/upstream-skill-sync-20261104`,待合并。 diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index ecd9b65b..bfbe45a5 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -13749,6 +13749,12 @@ def cmd_kp(args): _apply_ayanamsa(requested_report_ayanamsa) return {"error": "swisseph未安装"} + # The chart's julian_day is rounded for display. KP cusp and sub-lord + # boundaries use the civil instant itself, seconds included (upstream + # 26d5a52d, BUG-1543). Same formula as compute_chart_data. + jd = swe.julday(args.year, args.month, args.day, + _birth_hour_decimal(args.hour, args.minute, _arg_second(args)) - args.tz) + asc_sign = chart.get("ascendant", {}).get("sign", "Aries") planets = chart.get("planets", {}) planet_lons = { @@ -13762,7 +13768,8 @@ def cmd_kp(args): from bhava_chalit import BhavaChalitCalculator kp_house_calculator = BhavaChalitCalculator() asc_lon = chart.get('ascendant', {}).get('degree_raw', chart.get('ascendant', {}).get('degree', 0.0)) - boundaries = kp_house_calculator.calculate_bhava_boundaries( + # Unrounded cusps; calculate_bhava_boundaries rounds them to 4 decimals. + raw_cusps = kp_house_calculator.calculate_cusps( float(asc_lon), 0.0, 'placidus', jd, args.lat, args.lon, ) bhava_chart = kp_house_calculator.get_bhava_chalit_chart( @@ -13776,16 +13783,19 @@ def cmd_kp(args): for pname, pdata in planets.items(): if isinstance(pdata, dict) and 'sign' in pdata: bhava_row = bhava_planets.get(pname) if isinstance(bhava_planets.get(pname), dict) else {} + raw_lon = planet_lons.get(pname) planet_positions[pname] = { 'sign': pdata['sign'], - 'degree': pdata.get('degree_in_sign', pdata.get('degree', 0) % 30), + # degree_in_sign is rounded to 4 decimals for display. + 'degree': (float(raw_lon) % 30 if raw_lon is not None + else pdata.get('degree_in_sign', pdata.get('degree', 0) % 30)), 'house': bhava_row.get('bhava_house', pdata.get('house', 1)), } result = calc_kp_analysis( planet_positions, asc_sign, - house_cusps=boundaries.get('cusps'), + house_cusps=raw_cusps, house_occupancy_basis='cusp_to_cusp', ) moon_lon = planets.get('Moon', {}).get('degree_raw', planets.get('Moon', {}).get('degree', 0.0)) if isinstance(planets.get('Moon'), dict) else 0.0 diff --git a/tests/test_upstream_sync10_kp_unrounded_inputs.py b/tests/test_upstream_sync10_kp_unrounded_inputs.py new file mode 100644 index 00000000..9db80a38 --- /dev/null +++ b/tests/test_upstream_sync10_kp_unrounded_inputs.py @@ -0,0 +1,48 @@ +"""KP must not reuse display-rounded time, cusps or planet degrees (BUG-1543). + +Ported from upstream yinduzhanxing 26d5a52d tests/test_kp_unrounded_inputs.py +(sync 10). Synthetic chart only. +""" +import sys +from pathlib import Path +from types import SimpleNamespace + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / 'scripts')) +import jyotish_engine as engine # noqa: E402 +from bhava_chalit import BhavaChalitCalculator # noqa: E402 + + +def test_kp_passes_raw_time_cusps_and_longitudes(monkeypatch): + args = SimpleNamespace(year=2000, month=1, day=2, hour=3, minute=4, second=5, + lat=20., lon=75., tz=5.5, ayanamsa='raman') + chart = {'ascendant': {'sign': 'Aries', 'degree_raw': 12.123456789}, + 'planets': {'Sun': {'sign': 'Aries', 'degree_raw': 1.234567891, 'degree_in_sign': 1.2346}, + 'Moon': {'sign': 'Taurus', 'degree_raw': 42.987654321, 'degree_in_sign': 12.9877}}} + raw_cusps = [12.123456789 + 30 * i for i in range(12)] + seen = {} + monkeypatch.setattr(engine, '_compute_chart_from_args', lambda a: (chart, 0, 2451545., 23.)) + + def cusps(self, asc, mc, hs, jd, lat, lon): + seen['jd'] = jd + return raw_cusps + + monkeypatch.setattr(BhavaChalitCalculator, 'calculate_cusps', cusps) + monkeypatch.setattr(BhavaChalitCalculator, 'get_bhava_chalit_chart', + lambda *a, **k: {'planets': {'Sun': {'bhava_house': 12}, 'Moon': {'bhava_house': 2}}}) + + def analyze(planets, asc, **kwargs): + seen['planets'] = planets + seen.update(kwargs) + return {} + + monkeypatch.setattr(engine, 'calc_kp_analysis', analyze) + monkeypatch.setattr(engine, 'attach_calculation_profile', lambda result, args: result) + monkeypatch.setattr(engine, '_build_response_envelope', lambda command, result, **k: result) + engine.cmd_kp(args) + expected = engine.swe.julday(2000, 1, 2, 3 + 4 / 60 + 5 / 3600 - 5.5) + assert seen['jd'] == expected and seen['jd'] != round(expected, 6) + assert seen['house_cusps'] == raw_cusps + assert seen['planets']['Sun']['degree'] == 1.234567891 + assert seen['planets']['Moon']['degree'] == 42.987654321 % 30 + assert seen['house_occupancy_basis'] == 'cusp_to_cusp'