KP 改成宫头到宫头判宫,并写明口径(BUG-1319,上游 0337b77f)

本命 Bhava Chalit 表仍按中点,默认宫位不变。传了宫头却不声明就拒绝;没传宫头记整宫代理。报告多一行判宫口径。pytest tests/test_kp_cusp_house_assignment.py 3 通过,tests/test_bhava_chalit.py 仍通过。

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-14 00:48:38 +08:00
co-authored by Grok (agent loop)
parent d32ad7b7b1
commit 2748e43871
6 changed files with 111 additions and 3 deletions
+15
View File
@@ -17735,3 +17735,18 @@
- 复发自:无。
- 修复版本:分支 `codex/upstream-sync7-20261014`,尚未合入 staging。
## BUG-1319 | KP 落宫沿用本命中点判宫,传了宫头也不声明口径
- 状态:fixed-pending-verify
- 首次发现 / 最近更新:2026-10-14 / 2026-10-14
- 来源:issue #56。上游 `0337b77f`。任务书 T2。
- 现象:KP 用的行星落宫和本命 Bhava Chalit 表一样,按相邻宫头的中点判宫。宫头传进 KP 分析时,结果不写明判宫口径。
- 触发条件:打开 KP。本命 Bhava Chalit 表不走这条。
- 根因:同步第六轮漏了夹在上游隐私提交里的判宫修正。本命表和 KP 共用了中点函数。
- 修复:KP 改成宫头到下一宫头。本命表默认仍是中点,宫位数字不变。传了宫头却不声明口径就拒绝;没传宫头记整宫代理。KP 报告写出判宫口径。
- 验证:`tests/test_kp_cusp_house_assignment.py` 3 通过。同一颗星在两种口径下落入不同宫;默认中点与改前一致。
- 防复发:未知口径直接报错;报告正文必须出现 House occupancy basis。
- 相关记录:BUG-1318。
- 复发自:无。
- 修复版本:分支 `codex/upstream-sync7-20261014`,尚未合入 staging。
+23 -2
View File
@@ -272,6 +272,20 @@ class BhavaChalitCalculator:
# fallback: 最近的宫
return 1
def _planet_bhava_from_cusps(self, planet_lon: float, cusps: List[float]) -> int:
"""KP 宫头到宫头:宫 i(0 起)的范围是 cusp[i] → cusp[(i+1)%12]。
落在下一宫头上的点归下一宫。本命 Bhava Chalit 表不用这条。
"""
for i in range(12):
start = cusps[i]
end = cusps[(i + 1) % 12]
arc = _angular_dist(start, end)
pos = _angular_dist(start, planet_lon)
if arc > 0 and pos < arc:
return i + 1
return 1
def _planet_rashi_house(self, planet_lon: float, asc_lon: float) -> int:
"""整宫制宫位 (Rashi house)。"""
p_si = _sign_idx(planet_lon)
@@ -330,7 +344,8 @@ class BhavaChalitCalculator:
asc_lon: float, mc_lon: float,
house_system: str = 'sripati',
jd: float = None, lat: float = None,
lon: float = None) -> Dict:
lon: float = None,
house_assignment: str = 'sandhi') -> Dict:
"""根据 Bhava 边界重新分配行星宫位。
Parameters
@@ -346,13 +361,18 @@ class BhavaChalitCalculator:
dict with keys:
house_system, boundaries, planets
"""
if house_assignment not in ('sandhi', 'cusp_to_cusp'):
raise ValueError("house_assignment must be 'sandhi' or 'cusp_to_cusp'")
cusps = self.calculate_cusps(asc_lon, mc_lon, house_system,
jd, lat, lon)
sandhis = self.calculate_sandhis(cusps)
planets = {}
for pname, plon in planet_lons.items():
bhava = self._planet_bhava(plon, sandhis)
if house_assignment == 'cusp_to_cusp':
bhava = self._planet_bhava_from_cusps(plon, cusps)
else:
bhava = self._planet_bhava(plon, sandhis)
rashi = self._planet_rashi_house(plon, asc_lon)
si = _sign_idx(plon)
deg_in_sign = plon - si * 30
@@ -380,6 +400,7 @@ class BhavaChalitCalculator:
return {
'house_system': house_system,
'house_system_desc': self.HOUSE_SYSTEMS.get(house_system, ''),
'house_assignment': house_assignment,
'ascendant_lon': round(asc_lon, 4),
'mc_lon': round(mc_lon, 4),
'planets': planets,
+7 -1
View File
@@ -13963,6 +13963,7 @@ def cmd_kp(args):
)
bhava_chart = kp_house_calculator.get_bhava_chalit_chart(
planet_lons, float(asc_lon), 0.0, 'placidus', jd, args.lat, args.lon,
house_assignment='cusp_to_cusp',
)
bhava_planets = bhava_chart.get('planets') if isinstance(bhava_chart.get('planets'), dict) else {}
@@ -13977,7 +13978,12 @@ def cmd_kp(args):
'house': bhava_row.get('bhava_house', pdata.get('house', 1)),
}
result = calc_kp_analysis(planet_positions, asc_sign, house_cusps=boundaries.get('cusps'))
result = calc_kp_analysis(
planet_positions,
asc_sign,
house_cusps=boundaries.get('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
try:
birth_moment = datetime(int(args.year), int(args.month), int(args.day), int(args.hour), int(getattr(args, 'minute', 0) or 0))
+7
View File
@@ -371,6 +371,7 @@ def calc_kp_analysis(
planet_positions: Dict,
asc_sign: str = 'Aries',
house_cusps: Optional[List[float]] = None,
house_occupancy_basis: Optional[str] = None,
) -> Dict:
"""
完整KP分析(基于 diliprk/VedicAstro MIT 算法)。
@@ -379,6 +380,7 @@ def calc_kp_analysis(
planet_positions: 行星位置 {planet: {'sign': str, 'degree': float, 'house': int}}
asc_sign: 上升星座名称(无显式宫头时用于 whole-sign 代理)
house_cusps: 可选的 12 个实际宫头黄经。提供时优先用于 KP 宫头与显著星。
house_occupancy_basis: 传了宫头就必须声明判宫口径;没传宫头时记 whole_sign_proxy。
Returns:
完整KP分析结果
@@ -405,8 +407,11 @@ def calc_kp_analysis(
}
explicit_cusps = house_cusps is not None
if explicit_cusps and not house_occupancy_basis:
raise ValueError('house_occupancy_basis is required when house_cusps are provided')
if explicit_cusps and len(house_cusps) != 12:
raise ValueError('house_cusps must contain exactly 12 longitudes')
occupancy_basis = house_occupancy_basis if explicit_cusps else 'whole_sign_proxy'
# 2. 构建宫位信息(含 KP lords)。无实际宫头时保留历史 whole-sign 中点代理。
houses = []
@@ -438,6 +443,7 @@ def calc_kp_analysis(
'source': 'diliprk/VedicAstro MIT License',
'maturity_profile': kp_maturity_profile(),
'house_basis': 'explicit_cusps' if explicit_cusps else 'whole_sign_proxy',
'house_occupancy_basis': occupancy_basis,
'planets': {pname: {'kp_lords': data['kp_lords'], 'significators': planet_sig.get(pname, {})}
for pname, data in kp_planets.items()},
'houses': {h['house']: {'sign': h['sign'], 'cusp_longitude': h['cusp_longitude'], 'kp_lords': h['kp_lords'], 'significators': house_sig.get(h['house'], {})}
@@ -494,6 +500,7 @@ def build_kp_report_pack(
f"- claim_status: `{maturity.get('claim_status')}`",
f"- truth_matrix_allowed: `{maturity.get('truth_matrix_allowed')}`",
f"- closed_oracle_parity_assets: `{', '.join(closed_parity_assets.keys())}`",
f"- House occupancy basis: `{kp_analysis.get('house_occupancy_basis') or 'whole_sign_proxy'}`",
f"- planet_count: `{len(planets)}`",
f"- house_count: `{len(houses)}`",
f"- dba_period_count: `{len(periods)}`",
+1
View File
@@ -2140,6 +2140,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
'### p123 KP Chart / Cusps',
'',
f"- House basis:{_cell(kp.get('house_basis'))}",
f"- House occupancy basis:{_cell(kp.get('house_occupancy_basis'))}",
'',
'| House | Sign | Cusp longitude | KP lord chain |',
'|-------|------|----------------|---------------|',
+58
View File
@@ -0,0 +1,58 @@
"""BUG-1319: KP house occupancy is cusp-to-cusp and must be declared.
Upstream 0337b77f. Natal Bhava Chalit stays on the sandhi midpoint rule.
"""
from __future__ import annotations
import os
import sys
import pytest
SCRIPTS = os.path.join(os.path.dirname(__file__), "..", "scripts")
if SCRIPTS not in sys.path:
sys.path.insert(0, SCRIPTS)
from bhava_chalit import BhavaChalitCalculator # noqa: E402
from kp_system import build_kp_report_pack, calc_kp_analysis # noqa: E402
def test_cusp_to_cusp_differs_from_sandhi_and_default_houses_stay() -> None:
calc = BhavaChalitCalculator()
planets = {"Sun": 40.0}
sandhi = calc.get_bhava_chalit_chart(planets, 15.0, 270.0, "equal")
explicit = calc.get_bhava_chalit_chart(
planets, 15.0, 270.0, "equal", house_assignment="sandhi",
)
cusp = calc.get_bhava_chalit_chart(
planets, 15.0, 270.0, "equal", house_assignment="cusp_to_cusp",
)
assert sandhi["house_assignment"] == "sandhi"
assert explicit["planets"]["Sun"]["bhava_house"] == sandhi["planets"]["Sun"]["bhava_house"] == 2
assert cusp["house_assignment"] == "cusp_to_cusp"
assert cusp["planets"]["Sun"]["bhava_house"] == 1
assert cusp["planets"]["Sun"]["bhava_house"] != sandhi["planets"]["Sun"]["bhava_house"]
def test_unknown_house_assignment_is_rejected() -> None:
calc = BhavaChalitCalculator()
with pytest.raises(ValueError, match="house_assignment"):
calc.get_bhava_chalit_chart({"Sun": 10.0}, 15.0, 270.0, "equal", house_assignment="whole")
def test_kp_cusps_require_an_occupancy_basis_and_the_report_names_it() -> None:
planets = {"Sun": {"sign": "Taurus", "degree": 10.0, "house": 2}}
cusps = [15.0 + 30.0 * index for index in range(12)]
with pytest.raises(ValueError, match="house_occupancy_basis"):
calc_kp_analysis(planets, "Aries", house_cusps=cusps)
with pytest.raises(ValueError, match="house_occupancy_basis"):
calc_kp_analysis(planets, "Aries", house_cusps=cusps, house_occupancy_basis="")
declared = calc_kp_analysis(
planets, "Aries", house_cusps=cusps, house_occupancy_basis="cusp_to_cusp",
)
assert declared["house_occupancy_basis"] == "cusp_to_cusp"
proxy = calc_kp_analysis(planets, "Aries")
assert proxy["house_occupancy_basis"] == "whole_sign_proxy"
markdown = build_kp_report_pack(declared)["exports"]["markdown"]
assert "House occupancy basis: `cusp_to_cusp`" in markdown