fix(argala): keep equal occupancy unresolved until the two-level tiebreak
Upstream e7301feb, 86935c0a, and 4f1536f2. Ketu reverses the count, the ninth is the fifth's obstruction, and a remaining tie stays strength_comparison_required so the consultation card shows 待定. BUG-1386. Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Grok (agent loop)
Cursor
parent
6cbae4ce23
commit
aa87c0f127
@@ -36578,3 +36578,18 @@
|
||||
- 相关记录:BUG-1210。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/upstream-sync8-20261017`。上游提交 `a4d4a249`。
|
||||
|
||||
## BUG-1386 | Argala 同数直接判阻挡,第9宫被当成另一条次级
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现 / 最近更新:2026-10-17 / 2026-10-18
|
||||
- 来源:issue #80,任务书 T6。上游 `e7301feb`、`86935c0a`、`4f1536f2`。检索「Argala」「net_result」,无既有 Argala 编号。
|
||||
- 现象:参考点与阻挡宫行星数相等时,直接当成被阻挡。第9宫单独算次级 Argala。全盘结论按有行星的宫数比大小,同数和交错都会被收成支持、受阻或中性。参考星座有计都时仍按顺行数宫。
|
||||
- 触发条件:`calc_argala_for_reference` 与 `calc_argala`,以及咨询层 `build_argala` 把结果原样放进咨询卡。
|
||||
- 根因:有效条件只看 `len(planets) > len(blockers)`。次级集合把第5宫和第9宫并列。汇总用占据宫数,不用成对裁决。
|
||||
- 修复:计都在参考星座时逆行数宫。第9宫只作为第5宫的 Virodhargala。同数先走已有的 `select_narayana_seed_sign` 两级(行星数,再水星、木星或宫主的星座相位);仍平手保持 `strength_comparison_required`。交错为 `mixed`。第3宫两颗以上凶星仍是参数敏感候选,结论标 `special_argala_adjudication_required`。咨询层和咨询卡对未决结果显示「待定」。
|
||||
- 验证:`tests/test_argala.py`、`tests/test_argala_source_boundaries.py`、`tests/test_argala_strength_adjudication.py`、`tests/test_narayana_rath_rules.py` 通过。
|
||||
- 防复发:同数未决必须留在咨询卡 `verdict_label`,不得收成「无」或「被阻挡」。
|
||||
- 相关记录:无。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/upstream-sync8-20261017`。上游提交 `e7301feb`、`86935c0a`、`4f1536f2`。
|
||||
|
||||
+134
-14
@@ -7,7 +7,7 @@ Argala(门闩)行星干预模块 v1.1
|
||||
- 主Argala: 2/4/11宫
|
||||
- Virodhargala: 12/10/3宫分别阻挡2/4/11宫
|
||||
- 特殊Argala: 第3宫有2颗以上凶星
|
||||
- 次级Argala: 5/9宫
|
||||
- 次级:第5宫是 Argala,第9宫是它的 Virodhargala,不再把第9宫另算一条次级 Argala
|
||||
- Argala Rajayoga分类: Poornargala / Tripadargala / Ardhargala / Padargala
|
||||
|
||||
MIT复用来源:
|
||||
@@ -62,9 +62,71 @@ def classify_argala_rajayoga(argala_result: Dict) -> Dict:
|
||||
return {'type': 'None', 'level': 0, 'cn': '无明显Argala'}
|
||||
|
||||
|
||||
def _pair_status(planets, blockers):
|
||||
if not planets:
|
||||
return 'absent'
|
||||
if len(planets) == len(blockers):
|
||||
return 'strength_comparison_required'
|
||||
return 'effective' if len(planets) > len(blockers) else 'obstructed'
|
||||
|
||||
|
||||
def _compare_pair(planets, blockers, sign_idx, block_idx, chart, include_nodes):
|
||||
"""Equal occupancy uses the two Narayana levels already in select_narayana_seed_sign.
|
||||
|
||||
Later strength layers are not ported. A remaining tie stays unresolved.
|
||||
"""
|
||||
status = _pair_status(planets, blockers)
|
||||
if status != 'strength_comparison_required':
|
||||
return status, {'status': 'not_required', 'basis': 'occupant_count'}
|
||||
metadata = dict(
|
||||
profile='rath_argala_role_contacts_primary_lords_v1',
|
||||
source='Rath Jaimini Upadesa Sutra course, source p363',
|
||||
claim_boundary='First two levels only; traditional primary lords; later ties unresolved.',
|
||||
)
|
||||
missing = sorted((NATURAL_BENEFICS | NATURAL_MALEFICS) - set(chart))
|
||||
if include_nodes:
|
||||
missing += sorted(NODES - set(chart))
|
||||
if missing:
|
||||
return status, dict(metadata, status='blocked', reason='missing_classical_planet_signs', missing=missing)
|
||||
try:
|
||||
from scripts.narayana_dasha import select_narayana_seed_sign
|
||||
except ModuleNotFoundError:
|
||||
from narayana_dasha import select_narayana_seed_sign
|
||||
classical = NATURAL_BENEFICS | NATURAL_MALEFICS | (NODES if include_nodes else set())
|
||||
decision = select_narayana_seed_sign(
|
||||
sign_idx,
|
||||
{p: (s % 12) * 30.0 for p, s in chart.items() if p in classical},
|
||||
comparison_sign_idx=block_idx,
|
||||
)
|
||||
decision['implementation_profile'] = decision.pop('profile')
|
||||
decision.update(metadata)
|
||||
if decision['status'] == 'selected':
|
||||
status = 'effective' if decision['selected_sign_idx'] == sign_idx else 'obstructed'
|
||||
else:
|
||||
decision['reason'] = 'later_argala_sign_strength_rules_required'
|
||||
return status, decision
|
||||
|
||||
|
||||
_VERDICT_LABEL = {
|
||||
'strength_comparison_required': '待定',
|
||||
'special_argala_adjudication_required': '待定',
|
||||
'mixed': '吉凶交错',
|
||||
'supported': '支持',
|
||||
'obstructed': '受阻',
|
||||
'neutral': '中性',
|
||||
}
|
||||
|
||||
|
||||
def calc_argala_for_reference(ref_sign_idx: int, planet_sign_indices: Dict[str, int], include_nodes: bool = False) -> Dict:
|
||||
"""计算单一参考点的完整Argala。"""
|
||||
# Ketu in the reference sign reverses counting whether or not nodes count as occupants.
|
||||
direction = -1 if planet_sign_indices.get('Ketu') == ref_sign_idx else 1
|
||||
|
||||
def reference_house(offset):
|
||||
return (ref_sign_idx + direction * (offset - 1)) % 12
|
||||
|
||||
result = {
|
||||
'source_reference_packets': ['science_of_light_v1_argala_reference_points'],
|
||||
'ref_sign': SIGNS[ref_sign_idx],
|
||||
'ref_sign_idx': ref_sign_idx,
|
||||
'primary': {},
|
||||
@@ -73,15 +135,20 @@ def calc_argala_for_reference(ref_sign_idx: int, planet_sign_indices: Dict[str,
|
||||
'argala_count': 0,
|
||||
'virodhargala_count': 0,
|
||||
'net_result': 'neutral',
|
||||
'counting_direction': 'antizodiacal' if direction == -1 else 'zodiacal',
|
||||
'rule_source': 'PVR Integrated Approach §§10.5–10.7, source pages 117–118',
|
||||
}
|
||||
|
||||
for h in sorted(PRIMARY_ARGALA):
|
||||
argala_sign = _house_from(ref_sign_idx, h)
|
||||
argala_sign = reference_house(h)
|
||||
planets = _planets_in_sign(argala_sign, planet_sign_indices, include_nodes)
|
||||
block_h = VIRODHARGALA_MAP[h]
|
||||
block_sign = _house_from(ref_sign_idx, block_h)
|
||||
block_sign = reference_house(block_h)
|
||||
blockers = _planets_in_sign(block_sign, planet_sign_indices, include_nodes)
|
||||
effective = len(planets) > len(blockers)
|
||||
status, comparison = _compare_pair(
|
||||
planets, blockers, argala_sign, block_sign, planet_sign_indices, include_nodes,
|
||||
)
|
||||
effective = None if status == 'strength_comparison_required' else status == 'effective'
|
||||
result['primary'][f'H{h}'] = {
|
||||
'house_from_reference': h,
|
||||
'sign': SIGNS[argala_sign],
|
||||
@@ -91,14 +158,16 @@ def calc_argala_for_reference(ref_sign_idx: int, planet_sign_indices: Dict[str,
|
||||
'blocked_by_sign': SIGNS[block_sign],
|
||||
'blockers': blockers,
|
||||
'effective': effective,
|
||||
'effect': _argala_effect(h, planets, blockers),
|
||||
'comparison_status': status,
|
||||
'strength_comparison': comparison,
|
||||
'effect': _argala_effect(h, planets, blockers, status),
|
||||
}
|
||||
if planets:
|
||||
result['argala_count'] += 1
|
||||
if blockers:
|
||||
result['virodhargala_count'] += 1
|
||||
|
||||
h3_sign = _house_from(ref_sign_idx, 3)
|
||||
h3_sign = reference_house(3)
|
||||
h3_planets = _planets_in_sign(h3_sign, planet_sign_indices, include_nodes)
|
||||
h3_malefics = [p for p in h3_planets if p in NATURAL_MALEFICS]
|
||||
result['specific'] = {
|
||||
@@ -109,25 +178,56 @@ def calc_argala_for_reference(ref_sign_idx: int, planet_sign_indices: Dict[str,
|
||||
'malefics': h3_malefics,
|
||||
'effective': len(h3_malefics) >= 2,
|
||||
'rule': '第3宫有2颗以上天然凶星形成特殊Argala',
|
||||
'method_status': 'parameter_sensitive_two_malefic_threshold',
|
||||
'obstruction_rule': 'Once formed, special third-house Argala has no obstruction (Rath source p363). Formation threshold remains profile-dependent.',
|
||||
}
|
||||
if result['specific']['effective']:
|
||||
result['argala_count'] += 1
|
||||
|
||||
for h in sorted(SECONDARY_ARGALA):
|
||||
sign_idx = _house_from(ref_sign_idx, h)
|
||||
sign_idx = reference_house(h)
|
||||
planets = _planets_in_sign(sign_idx, planet_sign_indices, include_nodes)
|
||||
result['secondary'][f'H{h}'] = {
|
||||
'house_from_reference': h,
|
||||
'sign': SIGNS[sign_idx],
|
||||
'sign_idx': sign_idx,
|
||||
'planets': planets,
|
||||
'role': 'argala' if h == 5 else 'virodhargala',
|
||||
}
|
||||
|
||||
if result['argala_count'] > result['virodhargala_count']:
|
||||
fifth, ninth = result['secondary']['H5'], result['secondary']['H9']
|
||||
secondary_status, comparison = _compare_pair(
|
||||
fifth['planets'], ninth['planets'], fifth['sign_idx'], ninth['sign_idx'],
|
||||
planet_sign_indices, include_nodes,
|
||||
)
|
||||
fifth.update(
|
||||
blocked_by_house=9,
|
||||
blocked_by_sign=ninth['sign'],
|
||||
strength_comparison=comparison,
|
||||
blockers=ninth['planets'],
|
||||
comparison_status=secondary_status,
|
||||
effective=None if secondary_status == 'strength_comparison_required' else secondary_status == 'effective',
|
||||
)
|
||||
ninth['obstructs_house'] = 5
|
||||
|
||||
statuses = [row['comparison_status'] for row in result['primary'].values()]
|
||||
statuses.append(secondary_status)
|
||||
if 'strength_comparison_required' in statuses:
|
||||
result['net_result'] = 'strength_comparison_required'
|
||||
elif 'effective' in statuses and 'obstructed' in statuses:
|
||||
result['net_result'] = 'mixed'
|
||||
elif 'effective' in statuses:
|
||||
result['net_result'] = 'supported'
|
||||
elif result['argala_count'] < result['virodhargala_count']:
|
||||
elif 'obstructed' in statuses:
|
||||
result['net_result'] = 'obstructed'
|
||||
if result['specific']['effective']:
|
||||
result['net_result'] = 'special_argala_adjudication_required'
|
||||
result['verdict_label'] = _VERDICT_LABEL[result['net_result']]
|
||||
result['rajayoga_classification'] = classify_argala_rajayoga(result)
|
||||
result['rajayoga_classification']['method_status'] = 'legacy_occupancy_heuristic'
|
||||
result['rajayoga_classification']['claim_boundary'] = (
|
||||
'Occupancy label only; not an adjudication of obstruction or a verified Rajayoga.'
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -167,24 +267,38 @@ def _summarize_argala(houses: Dict) -> Dict:
|
||||
supported = []
|
||||
obstructed = []
|
||||
strongest = []
|
||||
unresolved = []
|
||||
mixed = []
|
||||
for house_key, data in houses.items():
|
||||
if data['net_result'] == 'supported':
|
||||
supported.append(house_key)
|
||||
elif data['net_result'] == 'obstructed':
|
||||
obstructed.append(house_key)
|
||||
level = data.get('rajayoga_classification', {}).get('level', 0)
|
||||
strongest.append((house_key, level, data.get('rajayoga_classification', {}).get('type')))
|
||||
elif data['net_result'] == 'mixed':
|
||||
mixed.append(house_key)
|
||||
elif str(data['net_result']).endswith('_required'):
|
||||
unresolved.append(house_key)
|
||||
if house_key not in unresolved:
|
||||
level = sum(row.get('effective') is True for row in data['primary'].values())
|
||||
level += int(data['secondary']['H5'].get('effective') is True)
|
||||
strongest.append((house_key, level, 'effective_pair_count'))
|
||||
strongest.sort(key=lambda x: x[1], reverse=True)
|
||||
return {
|
||||
summary = {
|
||||
'supported_houses': supported,
|
||||
'obstructed_houses': obstructed,
|
||||
'top_argala_houses': [{'house': h, 'level': lvl, 'type': typ} for h, lvl, typ in strongest[:5] if lvl > 0],
|
||||
'supported_count': len(supported),
|
||||
'obstructed_count': len(obstructed),
|
||||
'unresolved_houses': unresolved,
|
||||
'mixed_houses': mixed,
|
||||
'ranking_basis': 'effective_pair_count_not_classical_rajayoga_strength',
|
||||
}
|
||||
if unresolved:
|
||||
summary['unresolved_label'] = '待定'
|
||||
return summary
|
||||
|
||||
|
||||
def _argala_effect(house: int, planets: List[str], blockers: List[str]) -> str:
|
||||
def _argala_effect(house: int, planets: List[str], blockers: List[str], status=None) -> str:
|
||||
if not planets:
|
||||
return '无行星形成此类Argala'
|
||||
base = {
|
||||
@@ -192,7 +306,13 @@ def _argala_effect(house: int, planets: List[str], blockers: List[str]) -> str:
|
||||
4: '内在稳定、住所、母亲、教育与幸福感支持',
|
||||
11: '收益、人脉、愿望实现与社会网络支持',
|
||||
}.get(house, '一般支持')
|
||||
if blockers and len(blockers) >= len(planets):
|
||||
if blockers and len(blockers) == len(planets):
|
||||
if status == 'effective':
|
||||
return f'{base},双方数量相等,所选强弱口径判为有效'
|
||||
if status == 'obstructed':
|
||||
return f'{base},双方数量相等,所选强弱口径判为受阻'
|
||||
return f'待定:{base},双方数量相等,须比较强弱后裁决'
|
||||
if blockers and len(blockers) > len(planets):
|
||||
return f'{base},但被{len(blockers)}个Virodhargala阻挡'
|
||||
if blockers:
|
||||
return f'{base},有轻度阻挡但仍可能有效'
|
||||
|
||||
@@ -280,15 +280,21 @@ def _rasi_aspects(source_sign_idx: int, target_sign_idx: int) -> bool:
|
||||
def select_narayana_seed_sign(
|
||||
lagna_sign_idx: int,
|
||||
planet_lons: Dict[str, float],
|
||||
comparison_sign_idx: int | None = None,
|
||||
) -> Dict:
|
||||
"""Select the stronger of Lagna and seventh using the first two rule levels.
|
||||
"""Select the stronger of two signs using the first two rule levels.
|
||||
|
||||
A tie is deliberately returned as blocked: later classical strength rules are
|
||||
not yet implemented and must not be silently substituted with a preference.
|
||||
The default pair is Lagna and the seventh. Argala passes the obstructing
|
||||
sign explicitly and reuses the same two levels. A tie is deliberately
|
||||
returned as blocked: later classical strength rules are not implemented
|
||||
and must not be silently substituted with a preference.
|
||||
"""
|
||||
if not 0 <= lagna_sign_idx < 12:
|
||||
raise ValueError("lagna sign index must be in 0..11")
|
||||
candidate_indices = (lagna_sign_idx, (lagna_sign_idx + 6) % 12)
|
||||
other_sign_idx = (lagna_sign_idx + 6) % 12 if comparison_sign_idx is None else int(comparison_sign_idx)
|
||||
if not 0 <= other_sign_idx < 12 or other_sign_idx == lagna_sign_idx:
|
||||
raise ValueError("comparison sign index must be a different sign in 0..11")
|
||||
candidate_indices = (lagna_sign_idx, other_sign_idx)
|
||||
candidates = {}
|
||||
for sign_idx in candidate_indices:
|
||||
lord = SIGN_LORDS[SIGNS[sign_idx]]
|
||||
@@ -312,7 +318,7 @@ def select_narayana_seed_sign(
|
||||
}
|
||||
|
||||
lagna_candidate = candidates[lagna_sign_idx]
|
||||
seventh_sign_idx = (lagna_sign_idx + 6) % 12
|
||||
seventh_sign_idx = other_sign_idx
|
||||
seventh_candidate = candidates[seventh_sign_idx]
|
||||
lagna_score = (lagna_candidate["occupant_count"], lagna_candidate["aspect_factor_count"])
|
||||
seventh_score = (seventh_candidate["occupant_count"], seventh_candidate["aspect_factor_count"])
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from scripts.argala import calc_argala_for_reference
|
||||
|
||||
|
||||
def test_pvr_ketu_virgo_example_reverses_with_nodes_excluded():
|
||||
# PVR source p118: Virgo containing Ketu gets 2/4/11/5 from
|
||||
# Leo/Gemini/Scorpio/Taurus, even if nodes are omitted as occupants.
|
||||
result = calc_argala_for_reference(5, {
|
||||
'Ketu': 5, 'Sun': 4, 'Moon': 2, 'Mars': 7, 'Venus': 1,
|
||||
})
|
||||
assert [result['primary'][h]['sign'] for h in ('H2', 'H4', 'H11')] == ['Leo', 'Gemini', 'Scorpio']
|
||||
assert result['secondary']['H5']['sign'] == 'Taurus'
|
||||
assert result['counting_direction'] == 'antizodiacal'
|
||||
|
||||
|
||||
def test_ninth_obstructs_fifth_not_another_secondary_argala():
|
||||
result = calc_argala_for_reference(0, {'Jupiter': 4, 'Saturn': 8, 'Mars': 8})
|
||||
assert result['secondary']['H9']['role'] == 'virodhargala'
|
||||
assert result['secondary']['H5']['blockers'] == ['Saturn', 'Mars']
|
||||
assert result['secondary']['H5']['effective'] is False
|
||||
|
||||
|
||||
def test_equal_numbers_need_strength_not_automatic_obstruction():
|
||||
result = calc_argala_for_reference(0, {'Moon': 1, 'Mars': 11})
|
||||
assert result['primary']['H2']['effective'] is None
|
||||
assert result['net_result'] == 'strength_comparison_required'
|
||||
assert result['verdict_label'] == '待定'
|
||||
|
||||
|
||||
def test_occupied_house_totals_cannot_override_actual_pair():
|
||||
result = calc_argala_for_reference(0, {'Moon': 1, 'Venus': 1, 'Mars': 11})
|
||||
assert result['argala_count'] == result['virodhargala_count'] == 1
|
||||
assert result['net_result'] == 'supported'
|
||||
@@ -0,0 +1,61 @@
|
||||
from scripts.argala import calc_argala_for_reference
|
||||
from scripts.consultation_native_layers import build_argala
|
||||
|
||||
|
||||
def test_equal_occupancy_uses_rath_role_contacts():
|
||||
# Taurus: Venus in own sign + Jupiter's fixed-sign aspect from Libra.
|
||||
# Pisces: Mars occupant, no Mercury/Jupiter/lord contact.
|
||||
chart = dict(Sun=0, Moon=0, Mars=11, Mercury=0, Jupiter=6, Venus=1, Saturn=0)
|
||||
pair = calc_argala_for_reference(0, chart)['primary']['H2']
|
||||
assert pair['effective'] is True
|
||||
assert pair['strength_comparison']['status'] == 'selected'
|
||||
assert pair['strength_comparison']['selected_sign_idx'] == 1
|
||||
assert '须比较' not in pair['effect']
|
||||
|
||||
|
||||
def test_partial_chart_does_not_turn_missing_roles_into_zero():
|
||||
pair = calc_argala_for_reference(0, dict(Moon=1, Mars=11))['primary']['H2']
|
||||
assert pair['effective'] is None
|
||||
assert pair['strength_comparison']['reason'] == 'missing_classical_planet_signs'
|
||||
|
||||
|
||||
def test_still_tied_pair_stays_unresolved():
|
||||
# One occupant each, and neither sign receives a Jupiter, Mercury, or lord aspect.
|
||||
chart = dict(Sun=0, Moon=0, Mars=11, Mercury=4, Jupiter=0, Venus=1, Saturn=0)
|
||||
result = calc_argala_for_reference(0, chart)
|
||||
pair = result['primary']['H2']
|
||||
assert pair['effective'] is None
|
||||
assert pair['strength_comparison']['reason'] == 'later_argala_sign_strength_rules_required'
|
||||
assert result['net_result'] == 'strength_comparison_required'
|
||||
assert result['verdict_label'] == '待定'
|
||||
assert result['verdict_label'] not in ('无', '被阻挡')
|
||||
|
||||
|
||||
def test_unresolved_tie_reaches_consultation_layer_and_card_payload():
|
||||
chart = {
|
||||
'ascendant': {'sign': 'Aries'},
|
||||
'planets': {
|
||||
'Sun': {'sign': 'Aries'},
|
||||
'Moon': {'sign': 'Aries'},
|
||||
'Mars': {'sign': 'Pisces'},
|
||||
'Mercury': {'sign': 'Leo'},
|
||||
'Jupiter': {'sign': 'Aries'},
|
||||
'Venus': {'sign': 'Taurus'},
|
||||
'Saturn': {'sign': 'Aries'},
|
||||
},
|
||||
}
|
||||
layer = build_argala(chart)
|
||||
house = layer['houses']['house_1']
|
||||
assert layer['status'] == 'executed'
|
||||
assert house['net_result'] == 'strength_comparison_required'
|
||||
assert house['verdict_label'] == '待定'
|
||||
card = {
|
||||
'method': layer['method'],
|
||||
'summary': layer['summary'],
|
||||
'houses': layer['houses'],
|
||||
}
|
||||
assert card['summary']['unresolved_label'] == '待定'
|
||||
assert 'house_1' in card['summary']['unresolved_houses']
|
||||
assert card['houses']['house_1']['verdict_label'] == '待定'
|
||||
assert '无' not in card['houses']['house_1']['verdict_label']
|
||||
assert '被阻挡' not in card['houses']['house_1']['verdict_label']
|
||||
Reference in New Issue
Block a user