From ba97fd5980516e1ced36cb77455cc5dc726c7d18 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Fri, 2 Oct 2026 14:01:13 +0800 Subject: [PATCH] fix(marriage): counting method reports a neutral geometry index only; real Parivartana no longer crashes (BUG-1183/1184) TASK-upstream-sync4-20261002 T1 (upstream yinduzhanxing 0afa2780 for the idea; wording is ours, in Chinese, per decision 2). _interpret_marriage_count and _marriage_recommendations no longer state relationship counts, order, quality or loyalty; the Marriage-counting evidence is always neutral and carries only geometric_index (the dead D9-quality line in the adjudication is removed). The count is computed before the Parivartana override (the old order raised UnboundLocalError) and the exchange check now tests a real exchange instead of a conjunction with the dispositor. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE --- scripts/jyotish_api_server.py | 10 +- scripts/marriage_counting.py | 112 ++++++------------ tests/test_api_server_security.py | 20 ++++ tests/test_marriage_counting_geometry_only.py | 70 +++++++++++ 4 files changed, 128 insertions(+), 84 deletions(-) create mode 100644 tests/test_marriage_counting_geometry_only.py diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index 45654003..f16007ac 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -5196,13 +5196,11 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM 'Marriage-counting', 'D1/D9', marriage_counting.get('interpretation'), - 'positive' if (marriage_counting.get('marriage_count') or 0) <= 1 else 'neutral', + # BUG-1183: a geometry index, not a relationship count; never graded. + 'neutral', 'moderate', source='full_reading.modules.marriage_counting', - details={ - 'marriage_count': marriage_counting.get('marriage_count'), - 'd9_marriage_quality': marriage_counting.get('d9_marriage_quality'), - }, + details={'geometric_index': marriage_counting.get('geometric_index', marriage_counting.get('marriage_count'))}, )) vivah = full_modules.get('vivah_saham') if isinstance(full_modules, dict) else {} if isinstance(vivah, dict) and vivah.get('vivah_saham'): @@ -5819,7 +5817,6 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM seventh_sign = seventh.get('sign') or '未知' seventh_planets = seventh.get('planets_label') or '无' - d9_quality = (((counting.get('details') or {}).get('d9_marriage_quality')) or {}) timing_conclusion = str(timing.get('conclusion') or '').strip() vivah_conclusion = str(vivah.get('conclusion') or '').strip() manifestation_core = self._strip_humanized_prefix(humanized.get('manifestation_mode')) @@ -5845,7 +5842,6 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM "对象筛选这一轴,是当前婚恋判断里最不能跳过的一层。", f"严格链已经明确当前落地形式更偏{manifestation_core},也就是说重点不是立刻确认关系,而是先看谁值得继续推进。", str(counting.get('conclusion') or ''), - d9_quality.get('quality_rating') or '', "这类组合更像先识别重复的关系模式,再决定是否深入,而不是因为出现线索就直接抬升成结婚窗口。", f"时间边界:{humanized.get('time_confidence')}", ]), diff --git a/scripts/marriage_counting.py b/scripts/marriage_counting.py index fee786b8..8cad913c 100644 --- a/scripts/marriage_counting.py +++ b/scripts/marriage_counting.py @@ -3,13 +3,16 @@ Marriage Counting Method (婚姻计数法) —— Bhrigu 体系 v7.0 Jyotish Vedic Astrology Skill 来源:bhrigu-pada-dasha-marriage-counting.md -算法:D1 第7宫主在 D1 的星座(A) → D9 的星座(B) → 从A数到B = 婚姻次数 +算法:D1 第7宫主在 D1 的星座(A) → D9 的星座(B) → 从A数到B(含首尾)= 关系几何指数 + +产品口径(TASK-upstream-sync4-20261002 决策 2,BUG-1183):本模块只输出几何指数, +不输出婚姻或关系的次数、先后与质量断语。字段 marriage_count 为兼容旧消费方保留, +值与 geometric_index 相同。 v7.0 新增: - Parivartana自动重算(原为TODO) - D9 Venus/Mars/7宫主完整状态评估 - Upapada Loga整合 -- 每段关系的质量预测 """ from typing import Dict, Optional, List @@ -50,7 +53,8 @@ def marriage_counting_method( 'point_A': {'sign': int, 'sign_cn': str}, # D1 中7宫主星座 'point_B': {'sign': int, 'sign_cn': str}, # D9 中7宫主星座 'distance': int, # 从A数到B (包含A和B) - 'marriage_count': int, # 婚姻/认真关系数量 + 'geometric_index': int, # 关系几何指数(不是婚姻或关系次数) + 'marriage_count': int, # 兼容字段,同 geometric_index 'interpretation': str, # 解读 'parivartana': dict or None, # Parivartana 检查结果 'warnings': list, @@ -78,6 +82,14 @@ def marriage_counting_method( d9_lon = d9_planet_lons[d1_house7_lord] point_B = sign_of(d9_lon) + # Step 4: 计数 (从 A 到 B,包含 A 和 B)。须在 Parivartana 重算之前求出: + # 旧代码先在警告里引用 distance 再赋值,有交换时抛 UnboundLocalError(BUG-1184)。 + if point_B >= point_A: + distance = point_B - point_A + 1 + else: + # 跨越白羊座0°: 从 A 到双鱼座(11) + 从白羊座(0) 到 B + distance = (11 - point_A + 1) + (point_B + 1) + # Parivartana 检查 parivartana = None if parivartana_check and d1_houses: @@ -107,13 +119,6 @@ def marriage_counting_method( # 使用交换后的结果 distance = distance_swapped - # Step 4: 计数 (从 A 到 B,包含 A 和 B) - if point_B >= point_A: - distance = point_B - point_A + 1 - else: - # 跨越白羊座0°: 从 A 到双鱼座(11) + 从白羊座(0) 到 B - distance = (11 - point_A + 1) + (point_B + 1) - marriage_count = distance # 解读 @@ -133,6 +138,7 @@ def marriage_counting_method( 'degree': d9_lon, }, 'distance': distance, + 'geometric_index': marriage_count, 'marriage_count': marriage_count, 'interpretation': interpretation, 'parivartana': parivartana, @@ -164,10 +170,10 @@ def _check_parivartana( return {'has_parivartana': False, 'reason': f'{host} 位置未知'} host_sign = sign_of(d1_planet_lons[host]) - lord_own_sign = sign_of(d1_planet_lons.get(lord, 0)) - - # Parivartana: lord 在 host 的星座里,host 在 lord 的星座里 - if host_sign == lord_own_sign and lord_sign == sign_of(d1_planet_lons.get(host, 0)): + + # Parivartana: lord 在 host 的星座里(host 的定义已保证),host 在 lord 掌管的星座里。 + # 旧写法比较 host 与 lord 是否同星座,实际判的是合相而非互换(BUG-1184)。 + if SIGN_LORDS[host_sign] == lord: return { 'has_parivartana': True, 'planet1': lord, @@ -181,27 +187,15 @@ def _check_parivartana( def _interpret_marriage_count(count: int, A: int, B: int) -> str: - """解读婚姻计数结果""" - lines = [] - lines.append(f"婚姻/认真关系数量:{count}") - - if count == 1: - lines.append("解读:一次终身关系,忠诚度较高。") - if A == B: - lines.append(" D1与D9同星座 → 关系稳定,不易动摇。") - elif count == 2: - lines.append("解读:两段重要关系,可能再婚或长期关系更替。") - elif count == 3: - lines.append("解读:多段关系,关系模式较为复杂。") - elif count >= 4: - lines.append(f"解读:{count}段关系,关系频繁变化,需检视关系模式中的重复问题。") - - # 特殊位置解读 + """只描述几何量,不推出婚姻或关系的次数、先后与质量(BUG-1183)。""" + lines = [ + f"关系几何指数 {count}:D1 第7宫主所在星座数到它在 D9 所在星座的距离(含首尾)," + "是这个方法里的星座距离,不代表婚姻或关系的次数。" + ] if A == B: - lines.append("⭐ A=B(D1与D9同星座):内在一致,关系忠诚度高。") - if (B - A) % 12 == 6: # 对冲 - lines.append("⚠️ A与B对冲:内在矛盾,关系中的拉锯与不稳定性。") - + lines.append("D1 与 D9 两个参照点落在同一星座。") + if (B - A) % 12 == 6: + lines.append("D1 与 D9 两个参照点落在相对的星座。") return "\n".join(lines) @@ -238,7 +232,7 @@ def marriage_counting_full_analysis( **base, 'd9_marriage_quality': d9_quality, 'recommendations': recommendations, - 'note': '此法的"婚姻"指持续1年以上的认真关系,不一定是法律婚姻。', + 'note': '几何指数只是方法内的星座距离,不是婚姻或关系的计数。', } @@ -374,48 +368,12 @@ def _assess_d9_marriage_quality( def _marriage_recommendations(base: Dict, d9_quality: Dict) -> List[str]: - """生成综合建议 v7.0""" - recs = [] - count = base['marriage_count'] - - if count == 1: - recs.append("重点经营唯一关系,避免第三者介入。") - elif count == 2: - recs.append("第一次关系需认真经营;若结束,第二次关系质量需提前评估。") - # v7.0: 增加每段关系质量预测 - recs.append("第一段关系通常受D1 7宫主状态影响,第二段受D9 7宫主状态影响。") - else: - recs.append("需检视关系模式中的重复问题(依恋类型、沟通方式等)。") - recs.append("每段关系的质量递进:第一段=D1质量,后续逐渐转向D9质量。") - - # v7.0: 每段关系质量预测 - if count >= 2 and 'point_A' in base and 'point_B' in base: - a = base['point_A']['sign'] - b = base['point_B']['sign'] - # 第一段关系:从A开始的质量 - SIGN_LORDS_LIST = ['Mars','Venus','Mercury','Moon','Sun','Mercury', - 'Venus','Mars','Jupiter','Saturn','Saturn','Jupiter'] - BENEFICS = {'Jupiter', 'Venus', 'Moon', 'Mercury'} - a_lord = SIGN_LORDS_LIST[a] - if a_lord in BENEFICS: - recs.append(f"第一段关系由吉星{a_lord}主导,质量较好。") - else: - recs.append(f"第一段关系由{a_lord}主导,可能较为激烈或辛苦。") - - # D9 质量建议 - q_rating = d9_quality['quality_rating'] - if '低' in q_rating or '中下' in q_rating: - recs.append("D9显示婚姻关系有挑战 → 建议在Dasha吉期内主动经营关系。") - recs.append("强烈建议婚前/关系前进行专业占星咨询。") - elif '高' in q_rating: - recs.append("D9显示婚姻关系质量高 → 珍惜并维护现有关系。") - elif '中上' in q_rating: - recs.append("D9显示婚姻关系基础好 → 持续投入可获良好回报。") - - recs.append("建议配合 Vimshottari/Chara Dasha 确认具体结婚/分手时间。") - recs.append("注意:此法给出数量框架,具体事件需通过 Dasha + Transit 精确定位。") - - return recs + """几何指数与伴侣数量、事件断语分开(BUG-1183);不随指数大小给不同结论。""" + return [ + "几何指数需与 D1 第7宫主、它在 D9 的落点以及已记录的尊贵因素一起读,不单独下结论。", + "这个指数不指认第几位伴侣,也不说明关系的长短、好坏或是否专一。", + "涉及时间的判断须另看已算出的 Dasha 与过运,并以本人的实际经历为准。", + ] # 导出函数 diff --git a/tests/test_api_server_security.py b/tests/test_api_server_security.py index d9f899a6..a396f0a8 100644 --- a/tests/test_api_server_security.py +++ b/tests/test_api_server_security.py @@ -1815,6 +1815,26 @@ def test_thematic_report_derives_evidence_from_birth_payload() -> None: assert any(item['details'].get('derived') for item in result['themes']['career']['evidence']) +def test_marriage_counting_evidence_is_neutral_geometry_index_only() -> None: + # BUG-1183 (TASK-upstream-sync4-20261002 T1). Fictitious chart 1990-06-15 10:30 Beijing. + # 原值: marriage_count ≤ 1 记 positive,details 带 marriage_count 与 d9_marriage_quality + # 新值: 一律 neutral,details 只有 geometric_index;结论只描述几何指数 + handler = _handler() + result = handler._compute_thematic_report({ + 'theme': ['marriage'], + 'year': 1990, 'month': 6, 'day': 15, 'hour': 10, 'minute': 30, + 'lat': 39.9042, 'lon': 116.4074, 'tz': 8, + }) + rows = [item for item in result['themes']['marriage']['evidence'] if item['technique'] == 'Marriage-counting'] + assert len(rows) == 1 + row = rows[0] + assert row['sentiment'] == 'neutral' + assert set(row['details']) == {'geometric_index', 'source', 'derived'} + assert row['conclusion'].startswith(f"关系几何指数 {row['details']['geometric_index']}:") + for banned in ('再婚', '段关系', '第一段', '终身关系', '忠诚度'): + assert banned not in json.dumps(result['themes']['marriage'], ensure_ascii=False) + + def test_thematic_report_derives_relationship_strict_narrative_evidence() -> None: handler = _handler() result = handler._compute_thematic_report({ diff --git a/tests/test_marriage_counting_geometry_only.py b/tests/test_marriage_counting_geometry_only.py new file mode 100644 index 00000000..5131bfa2 --- /dev/null +++ b/tests/test_marriage_counting_geometry_only.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""BUG-1183 / BUG-1184 (TASK-upstream-sync4-20261002 T1). + +The marriage-counting method only reports a geometry index: no relationship +count, order, quality or loyalty claim, and the same advice for every index. +A real Parivartana no longer crashes the module, and a conjunction with the +dispositor is not mistaken for one. +""" + +from __future__ import annotations + +import os +import re +import sys + +SCRIPTS = os.path.join(os.path.dirname(__file__), '..', 'scripts') +if SCRIPTS not in sys.path: + sys.path.insert(0, SCRIPTS) + +from marriage_counting import marriage_counting_full_analysis # noqa: E402 + +BANNED = re.compile(r'再婚|段关系|第一段|第二段|终身关系|忠诚度|一次|两段|多段|婚姻/认真关系数量') + + +def _text(result) -> str: + return '\n'.join([ + str(result.get('interpretation') or ''), + *[str(item) for item in result.get('recommendations') or []], + str(result.get('note') or ''), + ]) + + +def test_every_index_is_described_as_geometry_without_event_claims() -> None: + advice = set() + for d9_sign in range(12): + result = marriage_counting_full_analysis('Venus', {'Venus': 15.0}, {'Venus': d9_sign * 30 + 10.0}) + index = d9_sign + 1 + assert result['geometric_index'] == index + assert result['marriage_count'] == index # compatibility field, same value + assert result['interpretation'].startswith(f'关系几何指数 {index}:') + assert '不代表婚姻或关系的次数' in result['interpretation'] + assert not BANNED.search(_text(result)), _text(result) + advice.add(tuple(result['recommendations'])) + # 原值: 指数 1 / 2 / ≥3 给三套不同建议(含「第一段关系由吉星主导,质量较好」) + # 新值: 所有指数同一套中性说明 + assert len(advice) == 1 + + +def test_same_and_opposite_sign_are_described_without_quality() -> None: + same = marriage_counting_full_analysis('Venus', {'Venus': 15.0}, {'Venus': 10.0}) + assert 'D1 与 D9 两个参照点落在同一星座。' in same['interpretation'] + opposite = marriage_counting_full_analysis('Venus', {'Venus': 15.0}, {'Venus': 190.0}) + assert 'D1 与 D9 两个参照点落在相对的星座。' in opposite['interpretation'] + + +def test_real_parivartana_does_not_crash_and_conjunction_is_not_exchange() -> None: + # Venus (7th lord) in Aries, Mars in Libra: a true exchange. + exchanged = marriage_counting_full_analysis( + 'Venus', {'Venus': 15.0, 'Mars': 185.0}, {'Venus': 160.0}, {'7': {}}) + assert 'error' not in exchanged + assert exchanged['parivartana']['has_parivartana'] is True + # Counted from Libra (the exchanged sign) to Virgo: 12. + assert exchanged['geometric_index'] == 12 + + # Venus and Mars both in Aries: a conjunction with the dispositor, not an exchange. + conjunct = marriage_counting_full_analysis( + 'Venus', {'Venus': 15.0, 'Mars': 20.0}, {'Venus': 160.0}, {'7': {}}) + assert 'error' not in conjunct + assert conjunct['parivartana']['has_parivartana'] is False + assert conjunct['geometric_index'] == 6