Spaces:
Running
feat(yongshen): 用神流派分歧不再隐藏 —— 计算两法并如实呈现
Browse files问题:GenjuAnalyzer 是二选一分支 —— 正格走 _determine_yongshen_proper
(格局法),杂气/无格走 _determine_yongshen_general(扶抑法),两套从不同时跑。
于是「两派不同意」这件事根本没被计算出来,产品对每张盘只报一个用神。
9 张验证盘实测,两法结论相反 3/9,且不是随机的 ——
相反的三张全部是「身弱 + 正格(七杀格/正财格)」:格局法对七杀格取食神制杀、
对正财格取食神生财,两者都泄身;扶抑法认为身弱该扶身(印比)。
小鱼盘是关键证据:引擎独立算出 21.2% 身弱,与她本人命理档案的
「旺衰 20.98% 偏弱、取水木为喜用」高度一致 —— 而她档案的用神站扶抑法这边,
产品当时只报格局法的「食神」,方向正相反且毫无提示。
改动:
1. 新增 _compare_yongshen_schools(),把扶抑法独立算出来跟格局法摆在一起,
**不裁决谁对**。分歧本身就是判读:
格局法用神 = 命局要求你产出的形态(该走的路)
扶抑法用神 = 你的承载力缺口(先要补的底)
两者冲突 = 你要做的那件事,正在消耗你本来就不够的那部分
中和盘扶抑法本就不判方向,如实标 not_applicable,不硬凑结论。
2. 修掉夸大的标签:_determine_yongshen_proper 返回的
'判断方法': '扶抑法 + 格局法' 改为 '格局法' —— 该函数只读 favorable_gods,
一行扶抑计算都没有。
测试 6 条,逐条变异验证。其中 test_method_label_no_longer_overstates
第一版写成同义反复(if method=='格局法' 才断言),被变异 M2 抓出是死测试,已改为
按 pattern_type 断言确切值并加样本量下限。
🦊 Generated with Sisi
- genju_analyzer.py +73 -2
- test_yongshen_schools.py +135 -0
|
@@ -551,14 +551,18 @@ class GenjuAnalyzer:
|
|
| 551 |
# 仇神:生助忌神的神
|
| 552 |
仇神 = self._find_仇神(忌神)
|
| 553 |
|
| 554 |
-
|
|
|
|
|
|
|
| 555 |
'用神': 用神,
|
| 556 |
'用神说明': self._explain_yongshen(pattern_name, 用神),
|
| 557 |
'喜神': 喜神,
|
| 558 |
'忌神': 忌神,
|
| 559 |
'仇神': 仇神,
|
| 560 |
-
'判断方法': '
|
| 561 |
}
|
|
|
|
|
|
|
| 562 |
|
| 563 |
def _determine_yongshen_special(self, pattern: Dict[str, Any]) -> Dict[str, Any]:
|
| 564 |
"""特殊格局用神判断"""
|
|
@@ -606,6 +610,73 @@ class GenjuAnalyzer:
|
|
| 606 |
'日主强度': f'{strength:.1f}%',
|
| 607 |
}
|
| 608 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
def _find_仇神(self, 忌神: str) -> str:
|
| 610 |
"""找仇神(生助忌神的神)"""
|
| 611 |
# 简化实现
|
|
|
|
| 551 |
# 仇神:生助忌神的神
|
| 552 |
仇神 = self._find_仇神(忌神)
|
| 553 |
|
| 554 |
+
# 判断方法如实标注:本函数只按格局表取 favorable_gods,没有做任何扶抑计算。
|
| 555 |
+
# 原标签写「扶抑法 + 格局法」是夸大(2026-08-23 修正)。
|
| 556 |
+
result = {
|
| 557 |
'用神': 用神,
|
| 558 |
'用神说明': self._explain_yongshen(pattern_name, 用神),
|
| 559 |
'喜神': 喜神,
|
| 560 |
'忌神': 忌神,
|
| 561 |
'仇神': 仇神,
|
| 562 |
+
'判断方法': '格局法',
|
| 563 |
}
|
| 564 |
+
result['流派对照'] = self._compare_yongshen_schools(用神)
|
| 565 |
+
return result
|
| 566 |
|
| 567 |
def _determine_yongshen_special(self, pattern: Dict[str, Any]) -> Dict[str, Any]:
|
| 568 |
"""特殊格局用神判断"""
|
|
|
|
| 610 |
'日主强度': f'{strength:.1f}%',
|
| 611 |
}
|
| 612 |
|
| 613 |
+
# ------------------------------------------------------------------
|
| 614 |
+
# 用神流派对照(2026-08-23)
|
| 615 |
+
#
|
| 616 |
+
# 背景:本类原本是二选一分支——正格走 _determine_yongshen_proper(格局法),
|
| 617 |
+
# 杂气/无格走 _determine_yongshen_general(扶抑法),两套从不同时跑,
|
| 618 |
+
# 于是「两派不同意」这件事根本没被计算出来,产品只报一个结论。
|
| 619 |
+
#
|
| 620 |
+
# 9 张验证盘实测:3/9 两法结论相反,且不是随机的——
|
| 621 |
+
# 相反的三张全部是「身弱 + 正格(七杀格/正财格)」:
|
| 622 |
+
# 格局法对七杀格取食神制杀、对正财格取食神生财,两者都泄身;
|
| 623 |
+
# 而扶抑法认为身弱该扶身(印比)。小鱼盘 21.2% 身弱即属此类,
|
| 624 |
+
# 与她本人档案「旺衰 20.98% 偏弱、取水木为喜用」一致(站扶抑法这边)。
|
| 625 |
+
#
|
| 626 |
+
# 设计选择:不裁决谁对。分歧本身就是判读——
|
| 627 |
+
# 格局法用神 = 命局要求你产出的形态(该走的路)
|
| 628 |
+
# 扶抑法用神 = 你的承载力缺口(先要补的底)
|
| 629 |
+
# 两者冲突 = 你要做的那件事,正在消耗你本来就不够的那部分
|
| 630 |
+
# ------------------------------------------------------------------
|
| 631 |
+
_SUPPORT_GODS = {'正印', '偏印', '印绶', '比肩', '劫财', '比劫'}
|
| 632 |
+
_DRAIN_GODS = {'食神', '伤官', '食伤', '正财', '偏财', '财星', '正官', '七杀', '官杀'}
|
| 633 |
+
|
| 634 |
+
def _compare_yongshen_schools(self, geju_yongshen: str) -> Dict[str, Any]:
|
| 635 |
+
"""把格局法的结论跟扶抑法独立算出来的结论摆在一起,不裁决谁对。"""
|
| 636 |
+
st = self._get_canonical_strength_analysis()
|
| 637 |
+
cls, label = st.get('classification'), st.get('pattern')
|
| 638 |
+
|
| 639 |
+
if cls == 'weak':
|
| 640 |
+
fuyi, fuyi_set = '印绶 / 比劫', self._SUPPORT_GODS
|
| 641 |
+
fuyi_reason = '日主偏弱,扶抑法主张先补承载力'
|
| 642 |
+
elif cls == 'strong':
|
| 643 |
+
fuyi, fuyi_set = '食伤 / 财星 / 官杀', self._DRAIN_GODS
|
| 644 |
+
fuyi_reason = '日主偏旺,扶抑法主张克泄耗以平衡'
|
| 645 |
+
else:
|
| 646 |
+
return {
|
| 647 |
+
'agreement': 'not_applicable',
|
| 648 |
+
'strength_label': label,
|
| 649 |
+
'geju_yongshen': geju_yongshen,
|
| 650 |
+
'fuyi_yongshen': '',
|
| 651 |
+
'note': '日主中和,扶抑法不作方向判断,此处仅以格局法为准。',
|
| 652 |
+
'reading': '',
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
agree = geju_yongshen in fuyi_set
|
| 656 |
+
base = {
|
| 657 |
+
'agreement': 'agree' if agree else 'conflict',
|
| 658 |
+
'strength_label': label,
|
| 659 |
+
'support_percentage': st.get('support_percentage'),
|
| 660 |
+
'geju_yongshen': geju_yongshen,
|
| 661 |
+
'fuyi_yongshen': fuyi,
|
| 662 |
+
'fuyi_reason': fuyi_reason,
|
| 663 |
+
}
|
| 664 |
+
if agree:
|
| 665 |
+
base['note'] = '格局法与扶抑法结论一致,用神方向没有争议。'
|
| 666 |
+
base['reading'] = ''
|
| 667 |
+
else:
|
| 668 |
+
base['note'] = (f'两个流派在这张盘上不一致:格局法取「{geju_yongshen}」,'
|
| 669 |
+
f'扶抑法主张「{fuyi}」。这是真实存在的分歧,不是计算错误,'
|
| 670 |
+
f'我们不替你裁决。')
|
| 671 |
+
base['reading'] = (
|
| 672 |
+
f'这类分歧通常出现在「{label}但格局要求你输出」的命上。'
|
| 673 |
+
f'可以这样读:格局法说的「{geju_yongshen}」是命局要你交出的成果形态,'
|
| 674 |
+
f'扶抑法说的「{fuyi}」是你的承载力缺口。'
|
| 675 |
+
f'两者相冲,意味着你要做的那件事,正在消耗你本来就不够的那部分——'
|
| 676 |
+
f'不是不能做,是做之前得先把托底的结构搭起来。'
|
| 677 |
+
)
|
| 678 |
+
return base
|
| 679 |
+
|
| 680 |
def _find_仇神(self, 忌神: str) -> str:
|
| 681 |
"""找仇神(生助忌神的神)"""
|
| 682 |
# 简化实现
|
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""用神流派对照回归测试
|
| 2 |
+
|
| 3 |
+
背景:GenjuAnalyzer 原本是二选一分支——正格走 _determine_yongshen_proper(格局法),
|
| 4 |
+
杂气/无格走 _determine_yongshen_general(扶抑法),两套从不同时跑。
|
| 5 |
+
于是「两派不同意」这件事根本没被计算出来,产品对每张盘只报一个用神、不提分歧。
|
| 6 |
+
|
| 7 |
+
9 张验证盘实测:3/9 两法结论相反,且相反的三张全部是「身弱 + 正格」。
|
| 8 |
+
小鱼盘尤其关键:引擎独立算出 21.2% 身弱,与她本人命理档案的
|
| 9 |
+
「旺衰 20.98% 偏弱、取水木为喜用」一致——而她档案的用神站扶抑法这边,
|
| 10 |
+
产品当时只报格局法的「食神」,方向正相反且毫无提示。
|
| 11 |
+
|
| 12 |
+
本测试锁死:
|
| 13 |
+
1. 身弱 + 格局法取泄身十神 → 必须报 conflict,且必须给出可读的解释
|
| 14 |
+
2. 结论一致时不得伪造分歧
|
| 15 |
+
3. 中和盘扶抑法不判方向,不得硬凑一个结论
|
| 16 |
+
4. 「判断方法」不得再写成「扶抑法 + 格局法」——该函数没有做任何扶抑计算
|
| 17 |
+
5. 9 盘的分歧总数锁在 3,防止规则漂移无人察觉
|
| 18 |
+
"""
|
| 19 |
+
import sys
|
| 20 |
+
import os
|
| 21 |
+
|
| 22 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 23 |
+
|
| 24 |
+
from bazi_calculator import BaZiCalculator
|
| 25 |
+
from genju_analyzer import GenjuAnalyzer
|
| 26 |
+
|
| 27 |
+
CASES = [
|
| 28 |
+
('小鱼', '1981-10-04', '20:38', 'female'),
|
| 29 |
+
('林磊', '1983-12-13', '13:15', 'female'),
|
| 30 |
+
('Jason', '1978-10-30', '10:00', 'male'),
|
| 31 |
+
('C4', '2008-07-31', '21:00', 'female'),
|
| 32 |
+
('C5', '2012-04-09', '07:30', 'male'),
|
| 33 |
+
('C6', '2014-04-15', '16:30', 'male'),
|
| 34 |
+
('C7', '2013-05-13', '03:40', 'male'),
|
| 35 |
+
('C8', '1990-01-08', '06:30', 'male'),
|
| 36 |
+
('C9', '1980-06-07', '17:30', 'female'),
|
| 37 |
+
]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _analyze(date, time, gender):
|
| 41 |
+
chart = BaZiCalculator.calculate_bazi_complete(
|
| 42 |
+
birth_date=date, birth_time=time, gender=gender,
|
| 43 |
+
is_lunar=False, include_dayun=True, num_dayun=12,
|
| 44 |
+
)
|
| 45 |
+
return GenjuAnalyzer(chart).analyze_genju()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _compare(name):
|
| 49 |
+
d, t, g = next((c[1], c[2], c[3]) for c in CASES if c[0] == name)
|
| 50 |
+
return _analyze(d, t, g)['yongshen_analysis']
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def test_weak_body_with_draining_yongshen_reports_conflict():
|
| 54 |
+
"""小鱼:21.2% 身弱 + 七杀格取食神(泄身)→ 必须报冲突"""
|
| 55 |
+
y = _compare('小鱼')
|
| 56 |
+
cmp = y.get('流派对照')
|
| 57 |
+
assert cmp, '缺少流派对照字段'
|
| 58 |
+
assert cmp['agreement'] == 'conflict', f"应为 conflict,实际 {cmp['agreement']}"
|
| 59 |
+
assert cmp['strength_label'] == '身弱'
|
| 60 |
+
assert cmp['geju_yongshen'] == '食神'
|
| 61 |
+
assert '印' in cmp['fuyi_yongshen'] or '比' in cmp['fuyi_yongshen']
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def test_conflict_carries_a_readable_explanation():
|
| 65 |
+
"""冲突不能只丢两个词,必须给出用户能读懂的解释"""
|
| 66 |
+
cmp = _compare('小鱼')['流派对照']
|
| 67 |
+
reading = cmp.get('reading', '')
|
| 68 |
+
assert len(reading) > 60, '冲突缺少可读解释'
|
| 69 |
+
assert '承载力' in reading, '解释里没有点出承载力缺口这层意思'
|
| 70 |
+
assert '不是不能做' in reading, '解释应避免读成劝退'
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def test_agreement_does_not_fabricate_conflict():
|
| 74 |
+
"""林磊:身强 + 偏印格取七杀(泄身)→ 两法一致,不得伪造分歧"""
|
| 75 |
+
cmp = _compare('林磊')['流派对照']
|
| 76 |
+
assert cmp['agreement'] == 'agree', f"应为 agree,实际 {cmp['agreement']}"
|
| 77 |
+
assert cmp.get('reading', '') == '', '一致时不应输出冲突解释'
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def test_balanced_body_is_marked_not_applicable():
|
| 81 |
+
"""中和盘:扶抑法本来就不判方向,不得硬凑结论"""
|
| 82 |
+
cmp = _compare('C9')['流派对照']
|
| 83 |
+
assert cmp['agreement'] == 'not_applicable'
|
| 84 |
+
assert cmp['fuyi_yongshen'] == '', '中和盘不应给出扶抑法用神'
|
| 85 |
+
assert cmp.get('reading', '') == ''
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def test_method_label_no_longer_overstates():
|
| 89 |
+
"""_determine_yongshen_proper 只查格局表,没做任何扶抑计算——标签不得夸大
|
| 90 |
+
|
| 91 |
+
⚠️ 这条最初写成 `if method == '格局法': assert '扶抑' not in method`,
|
| 92 |
+
是同义反复:标签一旦改回「扶抑法 + 格局法」,if 为假、测试空过。
|
| 93 |
+
变异测试 M2 抓到了它。现改为按格局类型断言确切值。
|
| 94 |
+
"""
|
| 95 |
+
checked = 0
|
| 96 |
+
for name, d, t, g in CASES:
|
| 97 |
+
r = _analyze(d, t, g)
|
| 98 |
+
if r['primary_pattern'].get('pattern_type') != 'proper':
|
| 99 |
+
continue
|
| 100 |
+
checked += 1
|
| 101 |
+
method = r['yongshen_analysis'].get('判断方法', '')
|
| 102 |
+
assert method == '格局法', (
|
| 103 |
+
f'{name}: 正格应如实标注「格局法」,实际是「{method}」;'
|
| 104 |
+
f'该函数只读 favorable_gods,没有做任何扶抑计算'
|
| 105 |
+
)
|
| 106 |
+
assert checked >= 5, f'正格样本太少({checked}),这条测试没真正跑起来'
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def test_conflict_count_locked_at_three_of_nine():
|
| 110 |
+
"""9 盘的分歧总数锁在 3,规则漂移时必须有人知道
|
| 111 |
+
|
| 112 |
+
这不是「3 才正确」,而是「变了就得有人重新看一眼」——
|
| 113 |
+
分歧率是产品要对用户说的话,不能静默改变。
|
| 114 |
+
"""
|
| 115 |
+
counts = {'conflict': 0, 'agree': 0, 'not_applicable': 0}
|
| 116 |
+
for name, d, t, g in CASES:
|
| 117 |
+
cmp = _analyze(d, t, g)['yongshen_analysis'].get('流派对照') or {}
|
| 118 |
+
counts[cmp.get('agreement', 'not_applicable')] += 1
|
| 119 |
+
assert counts['conflict'] == 3, f"分歧数变了:{counts}"
|
| 120 |
+
assert counts['agree'] == 2, f"一致数变了:{counts}"
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
if __name__ == '__main__':
|
| 124 |
+
tests = [v for k, v in sorted(globals().items()) if k.startswith('test_')]
|
| 125 |
+
failures = []
|
| 126 |
+
for fn in tests:
|
| 127 |
+
try:
|
| 128 |
+
fn()
|
| 129 |
+
print(f' PASS {fn.__name__}')
|
| 130 |
+
except AssertionError as e:
|
| 131 |
+
failures.append(fn.__name__)
|
| 132 |
+
print(f' FAIL {fn.__name__}\n {e}')
|
| 133 |
+
print()
|
| 134 |
+
print(f'{len(failures)} failed / {len(tests)} total')
|
| 135 |
+
sys.exit(1 if failures else 0)
|