fix(saham): add 30 degrees when the third operand misses the B-to-A arc

Upstream ce9fed15. The exception now tests C on the forward arc from B to A, in both the value and the trace. BUG-1387.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jesse-ux
2026-10-18 16:44:43 +08:00
co-authored by Grok (agent loop) Cursor
parent aa87c0f127
commit 2f8263b6ac
5 changed files with 78 additions and 13 deletions
+15
View File
@@ -36593,3 +36593,18 @@
- 相关记录:无。
- 复发自:无。
- 修复版本:分支 `codex/upstream-sync8-20261017`。上游提交 `e7301feb`、`86935c0a`、`4f1536f2`。
## BUG-1387 | Saham 的 +30° 例外看错了操作数
- 状态:resolved
- 首次发现 / 最近更新:2026-10-17 / 2026-10-18
- 来源:issue #80,任务书 T9。上游 `ce9fed15`。检索「add_30_degrees」「Punya_Saham」,无既有编号。
- 现象:公式 Saham 在上升不落在第一操作数到第二操作数的顺行弧上时加 30°。规则文件七条公式的第三项都是上升,所以大量结果和 PVR 的判法差 30°。
- 触发条件:`_calc_formula_saham`。本仓原先没有 trace 入口。
- 根因:例外检查的是上升是否落在 A→B,PVR §28.8.1 检查的是第三操作数 C 是否落在 B→A。
- 修复:两个入口都改为 `(C - B) % 360 <= (A - B) % 360`,不满足则结果加 30°。`references/saham_rules.json` 的描述改为同一条。补上 `_formula_saham_trace`,与数值入口使用同一判法。
- 验证:`tests/test_saham_directed_operand_contract.py`。`tests/test_tajika.py` 三处断言按三栏改过:Punya 原值 85 / 新值 115;Karma 原值 40 / 新值 70;跨零点例子原值 340 / 新值 310。年运快照测试没有锁这三条公式的旧经度。
- 防复发:契约测试覆盖 C 不是上升的公式,以及 trace 与数值入口一致。
- 相关记录:无。
- 复发自:无。
- 修复版本:分支 `codex/upstream-sync8-20261017`。上游提交 `ce9fed15`。
+1 -1
View File
@@ -46,6 +46,6 @@
}
},
"exception_rules": {
"add_30_degrees": "If the Ascendant does not fall between the first and the second point in the zodiacal order, add 30 degrees (1 sign) to the final result."
"add_30_degrees": "For A - B + C, add 30 degrees when C does not lie on the forward zodiacal arc from B to A. PVR Integrated Approach section 28.8.1, source page 394."
}
}
+30 -8
View File
@@ -96,14 +96,36 @@ def _calc_formula_saham(
second = _resolve_saham_operand(formula[1], planet_lons, asc_lon, computed)
third = _resolve_saham_operand(formula[2], planet_lons, asc_lon, computed)
result = (third + (first - second)) % 360
# references/saham_rules.json: add one sign when Ascendant is not on the
# forward zodiacal arc from the first formula point to the second.
ascendant_between_points = (
first <= asc_lon <= second
if first <= second
else asc_lon >= first or asc_lon <= second
)
return result if ascendant_between_points else (result + 30.0) % 360
# PVR §28.8.1, p394: in A-B+C, C must lie on the forward arc B -> A.
third_between_points = (third - second) % 360 <= (first - second) % 360
return result if third_between_points else (result + 30.0) % 360
def _formula_saham_trace(
rule_name: str,
planet_lons: Dict[str, float],
asc_lon: float,
is_day: bool,
computed: Dict[str, float],
) -> Dict:
"""Calculate a rule-backed Saham while preserving its sensitive convention."""
rule = _SAHAM_RULES['sahams'][rule_name]
formula = rule['formula_day'] if is_day else rule['formula_night']
first = _resolve_saham_operand(formula[0], planet_lons, asc_lon, computed)
second = _resolve_saham_operand(formula[1], planet_lons, asc_lon, computed)
third = _resolve_saham_operand(formula[2], planet_lons, asc_lon, computed)
base_longitude = (third + (first - second)) % 360
third_between_points = (third - second) % 360 <= (first - second) % 360
exception_applied = not third_between_points
return {
'longitude': (base_longitude + 30.0) % 360 if exception_applied else base_longitude,
'base_longitude': base_longitude,
'rule_name': rule_name,
'formula_operands': formula,
'formula_variant': 'day' if is_day else 'night',
'zodiacal_order_exception_applied': exception_applied,
'rule_source': 'references/saham_rules.json',
}
def calc_muntha(birth_asc_idx: int, age: int) -> Dict:
@@ -0,0 +1,26 @@
import pytest
from scripts import tajika
@pytest.mark.parametrize('sun,moon,asc,expected,exception', [
(45, 120, 10, 115, True),
(45, 120, 60, 135, False),
(200, 100, 50, 310, False),
(200, 100, 150, 80, True),
])
def test_punya_matches_pvr_directed_arc(sun, moon, asc, expected, exception):
points = {'Sun': sun, 'Moon': moon}
assert tajika._calc_formula_saham('Punya_Saham', points, asc, True, {}) == expected
result = tajika._formula_saham_trace('Punya_Saham', points, asc, True, {})
assert result['longitude'] == expected
assert result['zodiacal_order_exception_applied'] is exception
def test_third_operand_is_used_even_when_not_ascendant(monkeypatch):
monkeypatch.setitem(tajika._SAHAM_RULES['sahams'], 'TestThird', {
'formula_day': ['Jupiter', 'Moon', 'Venus'],
'formula_night': ['Moon', 'Jupiter', 'Venus'],
})
points = {'Jupiter': 200., 'Moon': 50., 'Venus': 100.}
assert tajika._calc_formula_saham('TestThird', points, 60., True, {}) == 250.
assert tajika._calc_formula_saham('TestThird', points, 60., False, {}) == 340.
+6 -4
View File
@@ -285,7 +285,8 @@ class TestSahams:
def test_punya_saham_formula(self):
# Punya = Asc + (Moon - Sun), sourced from references/saham_rules.json
sun, moon, asc = 45.0, 120.0, 10.0
expected = (asc + (moon - sun)) % 360 # = 10 + 75 = 85
# 原值 85 / 新值 115 / 原因:C=Asc10 不在 B=Sun45 → A=Moon120 的顺行弧上,按 PVR p394 加 30°。
expected = 115.0
planet_lons = {'Sun': sun, 'Moon': moon, 'Mars': 90.0,
'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0,
'Saturn': 300.0, 'Rahu': 150.0, 'Ketu': 330.0}
@@ -298,14 +299,15 @@ class TestSahams:
planet_lons = {'Sun': 45.0, 'Moon': 120.0, 'Mars': 90.0,
'Mercury': 60.0, 'Jupiter': 180.0, 'Venus': 210.0,
'Saturn': 300.0, 'Rahu': 150.0, 'Ketu': 330.0}
expected = (asc + (90.0 - 60.0)) % 360
# 原值 40 / 新值 70 / 原因:C=Asc10 不在 B=Mercury60 → A=Mars90 的顺行弧上,加 30°。
expected = 70.0
result = calc_all_sahams(planet_lons, asc, datetime(1990, 6, 15, 12, 0), lat=39.9042, lon=116.4074, tz=8)
assert abs(result['karma_saham']['longitude'] - expected) < 0.01
def test_formula_saham_applies_reference_add_30_exception(self):
from tajika import _calc_formula_saham
# Asc 50 is outside the forward 100 -> 200 zodiacal arc.
# 原值 340 / 新值 310 / 原因:C=Asc50 落在 B=Sun200 → A=Moon100 的跨零点顺行弧上,不加 30°。
result = _calc_formula_saham(
'Punya_Saham',
{'Sun': 200.0, 'Moon': 100.0},
@@ -313,7 +315,7 @@ class TestSahams:
True,
{},
)
assert result == 340.0
assert result == 310.0
def test_is_faster_moon_vs_sun(self):
assert _is_faster('Moon', 'Sun')