From aa87c0f1270359ab84f2fc71512635c495a114f5 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Sun, 18 Oct 2026 16:41:30 +0800 Subject: [PATCH] fix(argala): keep equal occupancy unresolved until the two-level tiebreak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/BUG_HISTORY.md | 15 +++ scripts/argala.py | 148 +++++++++++++++++++-- scripts/narayana_dasha.py | 16 ++- tests/test_argala_source_boundaries.py | 32 +++++ tests/test_argala_strength_adjudication.py | 61 +++++++++ 5 files changed, 253 insertions(+), 19 deletions(-) create mode 100644 tests/test_argala_source_boundaries.py create mode 100644 tests/test_argala_strength_adjudication.py diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 1119c9de..2650c216 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -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`。 diff --git a/scripts/argala.py b/scripts/argala.py index 0f1fb771..3938e9f0 100644 --- a/scripts/argala.py +++ b/scripts/argala.py @@ -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},有轻度阻挡但仍可能有效' diff --git a/scripts/narayana_dasha.py b/scripts/narayana_dasha.py index 11be67e4..fa209be6 100644 --- a/scripts/narayana_dasha.py +++ b/scripts/narayana_dasha.py @@ -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"]) diff --git a/tests/test_argala_source_boundaries.py b/tests/test_argala_source_boundaries.py new file mode 100644 index 00000000..6f77834f --- /dev/null +++ b/tests/test_argala_source_boundaries.py @@ -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' diff --git a/tests/test_argala_strength_adjudication.py b/tests/test_argala_strength_adjudication.py new file mode 100644 index 00000000..e18ea6ac --- /dev/null +++ b/tests/test_argala_strength_adjudication.py @@ -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']