th commited on
Commit
0a908bf
·
1 Parent(s): 09b1028

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

Files changed (2) hide show
  1. genju_analyzer.py +73 -2
  2. test_yongshen_schools.py +135 -0
genju_analyzer.py CHANGED
@@ -551,14 +551,18 @@ class GenjuAnalyzer:
551
  # 仇神:生助忌神的神
552
  仇神 = self._find_仇神(忌神)
553
 
554
- return {
 
 
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
  # 简化实现
test_yongshen_schools.py ADDED
@@ -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)