Merge codex/goel-jaimini-strength-20261022 (#94) into staging
Independent Staging Quality Gate / validate (push) Successful in 15m44s
Independent Staging Quality Gate / publish (push) Successful in 4m27s

# Conflicts:
#	CHANGELOG.md
#	docs/tasks/README.md
This commit is contained in:
Jesse_Chen
2026-10-22 08:53:17 +08:00
13 changed files with 1234 additions and 6 deletions
+9
View File
@@ -63,3 +63,12 @@ def test_varga_cli_d9_uses_same_navamsa_mapping_as_varga_full() -> None:
assert short["ascendant"] == full["Ascendant"]["sign"]
for planet in ["Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu"]:
assert short[planet]["sign"] == full[planet]["sign"]
def test_full_reading_exposes_goel_jaimini_strength() -> None:
report = _run_engine("full-reading", *BASE_BIRTH_ARGS)
goel = report["modules"]["jaimini"]
assert goel["goel_full_strength"]["status"] == "executed"
assert len(goel["goel_full_strength"]["rashi_strengths"]) == 12
assert set(goel["goel_special_points"]) == {"Brahma", "Maheshwara", "Rudra"}
assert goel["niryaana_shoola_strength"]["deprecated"] is True
+209
View File
@@ -0,0 +1,209 @@
"""Hand-checked synthetic positions, plus Goel's printed p15 worked table.
No private birth data; these fixtures exercise supplied-position arithmetic.
"""
from copy import deepcopy
import importlib
import importlib.util
import math
import pytest
SYNTHETIC = dict(Sun=29., Moon=58., Mars=87., Mercury=116.,
Jupiter=145., Venus=174., Saturn=203., Rahu=245., Ketu=65.)
def module():
assert importlib.util.find_spec('scripts.goel_jaimini_strength'), 'Goel producer must be published'
return importlib.import_module('scripts.goel_jaimini_strength')
def test_synthetic_five_component_totals_and_special_points():
original = deepcopy(SYNTHETIC)
result = module().build_goel_chart_evidence(SYNTHETIC, 0.)
strength = result['strength']
assert strength['atmakaraka'] == 'Sun'
assert {p: row['total'] for p, row in strength['planets'].items()} == dict(
Sun=180, Moon=135, Mars=172.5, Mercury=90, Jupiter=187.5,
Venus=86.25, Saturn=123.75)
assert {p: row['value'] for p, row in result['special_points'].items()} == dict(
Brahma='Jupiter', Maheshwara='Mars', Rudra='Mars')
assert strength['rashi_strengths'][0]['total'] == 367.5
assert strength['rashi_strengths'][6]['total'] == 225
assert SYNTHETIC == original
def test_node_policy_only_changes_sthana_and_sign_total():
m = module()
text = m.calculate_goel_strength(SYNTHETIC)
example = m.calculate_goel_strength(SYNTHETIC, node_occupancy_policy='exclude_nodes_worked_example')
assert text['planets'] == example['planets']
# Gemini has Mars+Ketu; Sagittarius has only Rahu.
assert (text['rashi_strengths'][2]['sthana'], example['rashi_strengths'][2]['sthana']) == (75, 60)
assert (text['rashi_strengths'][8]['sthana'], example['rashi_strengths'][8]['sthana']) == (60, 0)
assert text['rashi_strengths'][2]['total'] - example['rashi_strengths'][2]['total'] == 15
assert text['rashi_strengths'][8]['total'] - example['rashi_strengths'][8]['total'] == 60
def test_goel_printed_p15_numeric_example_without_birth_identity():
positions = dict(Sun=20+18/60, Moon=210+20+15/60, Mars=210+23+2/60,
Mercury=30+11+12/60, Jupiter=150+3+13/60,
Venus=330+17+28/60, Saturn=7+7/60,
Rahu=330+5+58/60, Ketu=150+5+58/60)
result = module().calculate_goel_strength(positions, node_occupancy_policy='exclude_nodes_worked_example')
assert {p: r['total'] for p, r in result['planets'].items()} == dict(
Sun=240, Moon=93.75, Mars=210, Mercury=97.5, Jupiter=101.25,
Venus=172.5, Saturn=146.25)
assert [r['total'] for r in result['rashi_strengths']] == [
412.5, 337.5, 217.5, 228.75, 330, 281.25, 247.5, 465,
281.25, 221.25, 236.25, 363.75]
def test_equal_karaka_degrees_block_all_special_point_choices():
result = module().build_goel_chart_evidence(dict(SYNTHETIC, Moon=59.), 0.)
assert result['strength']['status'] == 'blocked'
assert all(result['special_points'][p]['status'] == 'blocked'
for p in ('Brahma', 'Maheshwara', 'Rudra'))
@pytest.mark.parametrize('bad', [math.nan, math.inf, -1., 360., True, '0'])
def test_invalid_ascendant_rejected(bad):
with pytest.raises(ValueError):
module().build_goel_chart_evidence(SYNTHETIC, bad)
def test_legacy_lagna_seventh_alias_does_not_replace_pvr_second_eighth():
result = module().build_goel_chart_evidence(SYNTHETIC, 0.)
legacy = result['niryaana_shoola_strength']
assert (legacy['lagna'], legacy['seventh']) == (367.5, 225)
assert legacy['deprecated'] is True
assert legacy['comparison'] == 'lagna_vs_seventh'
from scripts.rashi_dashas_native import select_niryaana_shoola_start_sign
pvr = select_niryaana_shoola_start_sign(ascendant_longitude=0., planet_longitudes=SYNTHETIC)
assert pvr['selected_sign_idx'] == 1
assert pvr['profile'] == 'pvr_niryaana_shoola_ayur_rasi_strength_v1'
def test_brahma_ak_eighth_occupancy_overrides_lagna_selector():
result = module().build_goel_chart_evidence(dict(SYNTHETIC, Sun=239., Mercury=86.), 0.)
assert result['special_points']['Brahma']['value'] == 'Mercury'
assert result['special_points']['Brahma']['rule'] == 'eighth_lord_from_AK_occupies_eighth_from_AK'
def test_exalted_ak_eighth_lord_compares_twelfth_for_maheshwara():
result = module().build_goel_chart_evidence(dict(SYNTHETIC, Mars=297.), 0.)
assert result['special_points']['Maheshwara']['value'] == 'Jupiter'
assert result['special_points']['Maheshwara']['candidates'] == {'Mars': 150, 'Jupiter': 187.5}
def test_rudra_weaker_malefic_contact_exception():
result = module().build_goel_chart_evidence(dict(SYNTHETIC, Sun=329., Rahu=125., Ketu=305.), 0.)
planets = result['strength']['planets']
assert planets['Mars']['total'] == 127.5
assert planets['Mars']['malefic_contacts'] == []
assert planets['Venus']['total'] == 101.25
assert planets['Venus']['malefic_contacts'] == ['Mars']
assert result['special_points']['Rudra']['value'] == 'Venus'
def test_incomplete_chart_entry_preserves_blocked_points():
from scripts.jyotish_engine import _goel_jaimini_evidence
result = _goel_jaimini_evidence({}, 0.)
assert result['goel_full_strength']['status'] == 'blocked'
assert all(r['status'] == 'blocked' for r in result['goel_special_points'].values())
assert 'niryaana_shoola_strength' not in result
@pytest.mark.parametrize('bad', [dict(SYNTHETIC, Sun=math.nan),
dict(SYNTHETIC, Ketu=66.), {p: v for p, v in SYNTHETIC.items() if p != 'Saturn'}])
def test_incomplete_or_inconsistent_positions_rejected(bad):
with pytest.raises(ValueError):
module().calculate_goel_strength(bad)
def test_jaimini_special_entry_exposes_real_goel_results(monkeypatch):
from types import SimpleNamespace
from scripts import jyotish_engine as engine
chart = {'planets': {p: {'degree': v} for p, v in SYNTHETIC.items()},
'ascendant': {'degree': 0.}}
monkeypatch.setattr(engine, '_compute_chart_from_args', lambda args: (chart, 0, 2451545., 24.))
result = engine.cmd_jaimini(SimpleNamespace(mode='special', hour=12, minute=0))
assert result.get('goel_full_strength', {}).get('atmakaraka') == 'Sun'
assert result['goel_special_points']['Brahma']['value'] == 'Jupiter'
assert result['niryaana_shoola_strength']['deprecated'] is True
def test_wired_special_entry_keeps_both_node_policies(monkeypatch):
from types import SimpleNamespace
from scripts import jyotish_engine as engine
chart = {'planets': {p: {'degree': round(v, 4), 'degree_raw': v} for p, v in SYNTHETIC.items()},
'ascendant': {'degree': 0., 'lon': 0.}}
chart['planets']['Sun']['degree'] = 1.0
monkeypatch.setattr(engine, '_compute_chart_from_args', lambda args: (chart, 0, 2451545., 24.))
result = engine.cmd_jaimini(SimpleNamespace(mode='special', hour=12, minute=0))
strength = result['goel_full_strength']
assert strength['status'] == 'executed'
assert strength['node_occupancy_policy'] == 'include_nodes_text'
exclusion = strength['worked_example_node_exclusion']
assert exclusion['node_occupancy_policy'] == 'exclude_nodes_worked_example'
assert strength['atmakaraka'] == 'Sun'
assert abs(strength['planets']['Sun']['degree_in_sign'] - (SYNTHETIC['Sun'] % 30)) <= 1e-6
def test_wired_equal_degrees_block_without_atmakaraka_or_rank(monkeypatch):
from types import SimpleNamespace
from scripts import jyotish_engine as engine
positions = dict(SYNTHETIC, Moon=59.)
chart = {'planets': {p: {'degree': v} for p, v in positions.items()},
'ascendant': {'degree': 0.}}
monkeypatch.setattr(engine, '_compute_chart_from_args', lambda args: (chart, 0, 2451545., 24.))
result = engine.cmd_jaimini(SimpleNamespace(mode='special', hour=12, minute=0))
strength = result['goel_full_strength']
assert strength['status'] == 'blocked'
assert strength['reason'] == 'equal_degree_seven_karaka_rank_requires_finer_positions'
assert 'atmakaraka' not in strength
assert 'planets' not in strength
for name in ('Brahma', 'Maheshwara', 'Rudra'):
point = result['goel_special_points'][name]
assert point['status'] == 'blocked'
assert 'value' not in point
assert 'candidates' not in point
def test_full_reading_feeds_goel_degree_raw(monkeypatch):
import scripts.goel_jaimini_strength as goel
from scripts.jyotish_engine import cmd_full_reading
captured = []
real = goel.build_goel_chart_evidence
def wrapped(longitudes, ascendant):
captured.append(dict(longitudes))
return real(longitudes, ascendant)
monkeypatch.setattr(goel, 'build_goel_chart_evidence', wrapped)
report = cmd_full_reading(type('Args', (), {
'name': 'fictional',
'year': 1990, 'month': 1, 'day': 1, 'hour': 12, 'minute': 0, 'second': 0,
'lat': 39.9, 'lon': 116.4, 'tz': 8.0,
'node_mode': 'mean', 'ayanamsa': 'lahiri',
'today': '2026-10-22', 'transit_date': '2026-10-22',
'target_year': 2026, 'age': 36,
'birth_time_accuracy': 'confirmed',
})())
assert captured, 'full-reading did not call Goel'
planets = report['modules']['chart']['planets']
expected = {
name: pdata['degree_raw']
for name, pdata in planets.items()
if isinstance(pdata, dict) and 'degree_raw' in pdata
}
assert expected
for longitudes in captured:
assert set(longitudes) == set(expected)
for name, raw in expected.items():
assert abs(float(longitudes[name]) - float(raw)) <= 1e-6
strength = report['modules']['jaimini']['goel_full_strength']
assert strength['status'] == 'executed'
assert strength['node_occupancy_policy'] == 'include_nodes_text'
assert strength['worked_example_node_exclusion']['node_occupancy_policy'] == 'exclude_nodes_worked_example'
+6 -5
View File
@@ -18,11 +18,12 @@ SCRIPTS = ROOT / "scripts"
# 本地新写的静默处理仍不得出现;允许的命中只有这一处。
SILENT_EXCEPT_BUDGET = 1
UPSTREAM_SILENT_EXCEPT = "scripts/classical_astrology.py:12558"
# 原值:446
# 新值:448
# 原因:产品 2026-10-18(issue #81 选 A):上游原样文件按上游路径放在 scripts/ 根目录,
# classical_astrology.py、unified_western_reading.py 两个;本地新写的文件仍不得增加。
ROOT_PY_BUDGET = 448
# 原值:448
# 新值:452
# 原因:产品 2026-10-18 #81 决策 A 先例,issue #94:上游原样文件 goel_jaimini_strength、
# trikona_raman、sthira_brahma_goel_short_method、trikona_dasha 按上游路径放根目录;
# 本地新写的文件仍不得增加。
ROOT_PY_BUDGET = 452
def _silent_handlers() -> list[str]: