Spaces:
Running
Running
th commited on
Commit ·
fefbb42
1
Parent(s): be3b8cb
fix(reading): add multi-chart evidence guardrails
Browse files- ai_interpreter.py +119 -28
- case_matcher.py +8 -8
- experiments/baseline_density.py +65 -6
- genju_analyzer.py +6 -6
- test_case_insights_guardrails.py +47 -0
- test_master_multi_chart.py +123 -0
- test_ten_gods_naming.py +15 -1
ai_interpreter.py
CHANGED
|
@@ -1053,31 +1053,71 @@ class AIInterpreter:
|
|
| 1053 |
has_guansha = '正官' in ten_gods or '七杀' in ten_gods
|
| 1054 |
|
| 1055 |
if has_guansha:
|
| 1056 |
-
# 推断官煞类型:正官格、七杀格、官杀混杂等
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1057 |
guansha_type = None
|
| 1058 |
if '正官' in ten_gods and '七杀' not in ten_gods:
|
| 1059 |
guansha_type = '正官格'
|
| 1060 |
elif '七杀' in ten_gods and '正官' not in ten_gods:
|
| 1061 |
guansha_type = '七杀格'
|
| 1062 |
elif '正官' in ten_gods and '七杀' in ten_gods:
|
| 1063 |
-
guansha_type = '官杀混杂'
|
| 1064 |
|
| 1065 |
if guansha_type:
|
| 1066 |
guansha_info = get_guansha_analysis(guansha_type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1067 |
if guansha_info:
|
| 1068 |
interpretation += "**【关煞分析】**\n"
|
| 1069 |
-
interpretation += f"**官煞性质:**{guansha_info.get('characteristics', '')}\n"
|
| 1070 |
-
interpretation += f"**制化方法:**{guansha_info.get('control_method', '')}\n"
|
| 1071 |
-
interpretation += f"**实战应用:**{guansha_info.get('practical_use', '')}\n\n"
|
| 1072 |
-
|
| 1073 |
-
# 🔥 3.
|
| 1074 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1075 |
disease_info = get_disease_by_wuxing(day_master_element)
|
| 1076 |
if disease_info:
|
| 1077 |
-
interpretation += "**【
|
| 1078 |
-
interpretation += f"**
|
| 1079 |
-
interpretation += f"**
|
| 1080 |
-
interpretation += f"**
|
|
|
|
|
|
|
| 1081 |
|
| 1082 |
# 如果没有任何分析结果,返回空
|
| 1083 |
#
|
|
@@ -1703,27 +1743,79 @@ class AIInterpreter:
|
|
| 1703 |
|
| 1704 |
# ========== 神煞解读 ==========
|
| 1705 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1706 |
def _interpret_auspicious_spirits(self) -> str:
|
| 1707 |
"""吉神解读"""
|
| 1708 |
if not self.shensha:
|
| 1709 |
return ''
|
| 1710 |
|
| 1711 |
-
auspicious = self.shensha.get('auspicious_gods', [])
|
| 1712 |
|
| 1713 |
if not auspicious:
|
| 1714 |
-
return '
|
| 1715 |
|
| 1716 |
interpretation = f"**吉神数量:**{len(auspicious)}个\n\n"
|
| 1717 |
|
| 1718 |
-
for god in auspicious[:
|
| 1719 |
name = god.get('name', '')
|
| 1720 |
name_en = god.get('name_en', '')
|
| 1721 |
-
|
| 1722 |
-
description = god.get('description', '')
|
| 1723 |
|
| 1724 |
interpretation += f"**{name}({name_en})**\n"
|
| 1725 |
-
interpretation += f"
|
| 1726 |
-
interpretation += f"{self.
|
|
|
|
|
|
|
|
|
|
| 1727 |
|
| 1728 |
return interpretation
|
| 1729 |
|
|
@@ -1732,27 +1824,26 @@ class AIInterpreter:
|
|
| 1732 |
if not self.shensha:
|
| 1733 |
return ''
|
| 1734 |
|
| 1735 |
-
inauspicious = self.shensha.get('inauspicious_gods', [])
|
| 1736 |
|
| 1737 |
if not inauspicious:
|
| 1738 |
return '当前可见的凶煞信号较少;仍应以现实风险和健康建议为准。'
|
| 1739 |
|
| 1740 |
interpretation = f"**凶煞数量:**{len(inauspicious)}个\n\n"
|
| 1741 |
|
| 1742 |
-
for god in inauspicious[:
|
| 1743 |
name = god.get('name', '')
|
| 1744 |
name_en = god.get('name_en', '')
|
| 1745 |
-
|
| 1746 |
-
description = god.get('description', '')
|
| 1747 |
-
remedy = god.get('remedy', '')
|
| 1748 |
|
| 1749 |
interpretation += f"**{name}({name_en})**\n"
|
| 1750 |
-
interpretation += f"
|
| 1751 |
-
interpretation += f"{self.
|
| 1752 |
-
|
| 1753 |
-
interpretation += f"化解方法:{self._phase_safe_text(remedy)}\n"
|
| 1754 |
interpretation += "\n"
|
| 1755 |
|
|
|
|
|
|
|
| 1756 |
return interpretation
|
| 1757 |
|
| 1758 |
# ========== 人生规划 ==========
|
|
|
|
| 1053 |
has_guansha = '正官' in ten_gods or '七杀' in ten_gods
|
| 1054 |
|
| 1055 |
if has_guansha:
|
| 1056 |
+
# 推断官煞类型:正官格、七杀格、官杀混杂等。
|
| 1057 |
+
# ``ditianshi_complete`` 的知识库键是“官杀混杂格”,
|
| 1058 |
+
# 且字段名是 condition/treatment/outcome;旧代码传入
|
| 1059 |
+
# “官杀混杂”并读取另一套字段,导致 6/9 张有官杀的盘
|
| 1060 |
+
# 静默没有正文。
|
| 1061 |
guansha_type = None
|
| 1062 |
if '正官' in ten_gods and '七杀' not in ten_gods:
|
| 1063 |
guansha_type = '正官格'
|
| 1064 |
elif '七杀' in ten_gods and '正官' not in ten_gods:
|
| 1065 |
guansha_type = '七杀格'
|
| 1066 |
elif '正官' in ten_gods and '七杀' in ten_gods:
|
| 1067 |
+
guansha_type = '官杀混杂格'
|
| 1068 |
|
| 1069 |
if guansha_type:
|
| 1070 |
guansha_info = get_guansha_analysis(guansha_type)
|
| 1071 |
+
# The complete classical table has specialist entries
|
| 1072 |
+
# for mixed/treated patterns, but not a bare 正官格 or
|
| 1073 |
+
# 七杀格 key. Keep those two outputs as an explicit,
|
| 1074 |
+
# non-deterministic structural prompt rather than
|
| 1075 |
+
# silently dropping the chapter.
|
| 1076 |
+
if not guansha_info:
|
| 1077 |
+
guansha_info = {
|
| 1078 |
+
'condition': (
|
| 1079 |
+
'命局出现正官信号;仍需结合月令、根气、印财食伤与日主强弱,'
|
| 1080 |
+
'不能只凭一颗官星定格。'
|
| 1081 |
+
if guansha_type == '正官格' else
|
| 1082 |
+
'命局出现七杀信号;仍需结合月令、根气、制化与日主强弱,'
|
| 1083 |
+
'不能只凭一颗七杀定吉凶。'
|
| 1084 |
+
),
|
| 1085 |
+
'treatment': (
|
| 1086 |
+
'先看官杀是否清纯有根,再看印比护身、食伤制化与岁运变化;'
|
| 1087 |
+
'把它当作观察规则/压力/责任的入口。'
|
| 1088 |
+
),
|
| 1089 |
+
'outcome': (
|
| 1090 |
+
'这是传统理论的结构提示,不是职位、财富或事件保证;'
|
| 1091 |
+
'用现实中的组织关系、责任压力与反馈核对。'
|
| 1092 |
+
),
|
| 1093 |
+
}
|
| 1094 |
if guansha_info:
|
| 1095 |
interpretation += "**【关煞分析】**\n"
|
| 1096 |
+
interpretation += f"**官煞性质:**{guansha_info.get('characteristics') or guansha_info.get('condition', '')}\n"
|
| 1097 |
+
interpretation += f"**制化方法:**{guansha_info.get('control_method') or guansha_info.get('treatment', '')}\n"
|
| 1098 |
+
interpretation += f"**实战应用:**{guansha_info.get('practical_use') or guansha_info.get('outcome', '')}\n\n"
|
| 1099 |
+
|
| 1100 |
+
# 🔥 3. 五行身心象意提示 (get_disease_by_wuxing)
|
| 1101 |
+
# BaZiCalculator stores the day-master element in English
|
| 1102 |
+
# (``wood``/``fire``/...), while the classical knowledge table is
|
| 1103 |
+
# keyed by Chinese names (``木``/``火``/...). Passing the English
|
| 1104 |
+
# value silently returned an empty section for every chart.
|
| 1105 |
+
element_names = {
|
| 1106 |
+
'wood': '木',
|
| 1107 |
+
'fire': '火',
|
| 1108 |
+
'earth': '土',
|
| 1109 |
+
'metal': '金',
|
| 1110 |
+
'water': '水',
|
| 1111 |
+
}
|
| 1112 |
+
day_master_element = element_names.get(self.day_master_element, self.day_master_element)
|
| 1113 |
disease_info = get_disease_by_wuxing(day_master_element)
|
| 1114 |
if disease_info:
|
| 1115 |
+
interpretation += "**【传统身心象意提示】**\n"
|
| 1116 |
+
interpretation += f"**传统对应部位:**{disease_info.get('organs', '')}\n"
|
| 1117 |
+
interpretation += f"**可能敏感主题:**{disease_info.get('diseases', '')}\n"
|
| 1118 |
+
interpretation += f"**命理象意解释:**{disease_info.get('causes', '')}\n"
|
| 1119 |
+
interpretation += f"**日常保养提醒:**{disease_info.get('remedies', '')}\n"
|
| 1120 |
+
interpretation += "以上是传统象意提示,不是医学诊断;如有症状请以合格医疗人员的判断为准。\n\n"
|
| 1121 |
|
| 1122 |
# 如果没有任何分析结果,返回空
|
| 1123 |
#
|
|
|
|
| 1743 |
|
| 1744 |
# ========== 神煞解读 ==========
|
| 1745 |
|
| 1746 |
+
@staticmethod
|
| 1747 |
+
def _spirit_theme(name: str, auspicious: bool) -> str:
|
| 1748 |
+
"""Return a bounded, reader-facing theme instead of raw old断语."""
|
| 1749 |
+
themes = {
|
| 1750 |
+
'天乙贵人': '支持网络、协作与困难中的转圜',
|
| 1751 |
+
'红鸾': '关系连接与表达好感的机会',
|
| 1752 |
+
'天喜': '关系连接与共同庆祝的机会',
|
| 1753 |
+
'禄神': '收入、资源管理与稳定回报',
|
| 1754 |
+
'福星': '顺势感、支持感与资源出现',
|
| 1755 |
+
'金舆': '物质资源、责任角色与生活安排',
|
| 1756 |
+
'太极贵人': '学习、钻研与独特方法',
|
| 1757 |
+
'学堂': '学习、表达与知识传递',
|
| 1758 |
+
'国印': '责任、组织角色与影响力',
|
| 1759 |
+
'羊刃': '行动强度、边界与冲动管理',
|
| 1760 |
+
'空亡': '不确定性、落空感与计划复核',
|
| 1761 |
+
'灾煞': '安全意识、意外预案与风险边界',
|
| 1762 |
+
'白虎': '高强度、工具或出行场景的安全边界',
|
| 1763 |
+
'孤辰': '独处需求、关系边界与沟通方式',
|
| 1764 |
+
'寡宿': '独处需求、亲密关系节奏与支持',
|
| 1765 |
+
'元辰': '误解、反复与信息核对',
|
| 1766 |
+
'阴阳差错': '关系沟通、期待差异与共同规则',
|
| 1767 |
+
'十恶大败': '财务边界、承诺核对与风险复盘',
|
| 1768 |
+
'魁罡': '强势表达、责任承担与柔性协作',
|
| 1769 |
+
'六厄': '身心节奏、日常照护与专业咨询边界',
|
| 1770 |
+
'勾煞': '合同、纠纷与沟通留痕',
|
| 1771 |
+
'绞煞': '牵连、边界与问题拆解',
|
| 1772 |
+
}
|
| 1773 |
+
return themes.get(name, '传统神煞名目仅作辅助提示,以现实中可观察的行为和反馈为准')
|
| 1774 |
+
|
| 1775 |
+
@staticmethod
|
| 1776 |
+
def _unique_spirits(items: Any) -> List[Dict[str, Any]]:
|
| 1777 |
+
"""按神煞名称去重,避免同一神煞在四柱重复放大。"""
|
| 1778 |
+
def strength(item: Dict[str, Any]) -> float:
|
| 1779 |
+
try:
|
| 1780 |
+
return float(item.get('strength', 0) or 0)
|
| 1781 |
+
except (TypeError, ValueError):
|
| 1782 |
+
return 0.0
|
| 1783 |
+
|
| 1784 |
+
unique: Dict[str, Dict[str, Any]] = {}
|
| 1785 |
+
for item in items or []:
|
| 1786 |
+
if not isinstance(item, dict):
|
| 1787 |
+
continue
|
| 1788 |
+
name = str(item.get('name') or '').strip()
|
| 1789 |
+
if not name:
|
| 1790 |
+
continue
|
| 1791 |
+
previous = unique.get(name)
|
| 1792 |
+
if previous is None or strength(item) > strength(previous):
|
| 1793 |
+
unique[name] = item
|
| 1794 |
+
return sorted(unique.values(), key=lambda item: (-strength(item), item.get('name', '')))
|
| 1795 |
+
|
| 1796 |
def _interpret_auspicious_spirits(self) -> str:
|
| 1797 |
"""吉神解读"""
|
| 1798 |
if not self.shensha:
|
| 1799 |
return ''
|
| 1800 |
|
| 1801 |
+
auspicious = self._unique_spirits(self.shensha.get('auspicious_gods', []))
|
| 1802 |
|
| 1803 |
if not auspicious:
|
| 1804 |
+
return '当前未见明显吉神标记;这不等于缺少机会,仍以现实中的支持、行动与反馈为准。'
|
| 1805 |
|
| 1806 |
interpretation = f"**吉神数量:**{len(auspicious)}个\n\n"
|
| 1807 |
|
| 1808 |
+
for god in auspicious[:3]:
|
| 1809 |
name = god.get('name', '')
|
| 1810 |
name_en = god.get('name_en', '')
|
| 1811 |
+
position = god.get('position_cn') or god.get('position') or '命局'
|
|
|
|
| 1812 |
|
| 1813 |
interpretation += f"**{name}({name_en})**\n"
|
| 1814 |
+
interpretation += f"出现位置:{position}\n"
|
| 1815 |
+
interpretation += f"传统主题:{self._spirit_theme(name, True)}\n"
|
| 1816 |
+
interpretation += '现实行动:记录哪些支持或资源确实出现,再决定是否保留这套做法。\n\n'
|
| 1817 |
+
|
| 1818 |
+
interpretation += '神煞只作辅助象意,不单独决定吉凶;请用现实反馈复核。\n'
|
| 1819 |
|
| 1820 |
return interpretation
|
| 1821 |
|
|
|
|
| 1824 |
if not self.shensha:
|
| 1825 |
return ''
|
| 1826 |
|
| 1827 |
+
inauspicious = self._unique_spirits(self.shensha.get('inauspicious_gods', []))
|
| 1828 |
|
| 1829 |
if not inauspicious:
|
| 1830 |
return '当前可见的凶煞信号较少;仍应以现实风险和健康建议为准。'
|
| 1831 |
|
| 1832 |
interpretation = f"**凶煞数量:**{len(inauspicious)}个\n\n"
|
| 1833 |
|
| 1834 |
+
for god in inauspicious[:3]:
|
| 1835 |
name = god.get('name', '')
|
| 1836 |
name_en = god.get('name_en', '')
|
| 1837 |
+
position = god.get('position_cn') or god.get('position') or '命局'
|
|
|
|
|
|
|
| 1838 |
|
| 1839 |
interpretation += f"**{name}({name_en})**\n"
|
| 1840 |
+
interpretation += f"出现位置:{position}\n"
|
| 1841 |
+
interpretation += f"传统主题:{self._spirit_theme(name, False)}\n"
|
| 1842 |
+
interpretation += '现实行动:把它当作一个风险核验点,保留边界与记录,必要时寻求合格专业支持。\n'
|
|
|
|
| 1843 |
interpretation += "\n"
|
| 1844 |
|
| 1845 |
+
interpretation += '神煞只作辅助象意,不单独决定吉凶;不替代医疗、法律或财务判断。\n'
|
| 1846 |
+
|
| 1847 |
return interpretation
|
| 1848 |
|
| 1849 |
# ========== 人生规划 ==========
|
case_matcher.py
CHANGED
|
@@ -129,26 +129,26 @@ FEMALE_PRINCIPLES = {
|
|
| 129 |
"夫星得用": {
|
| 130 |
"category": "marriage_good",
|
| 131 |
"conditions": ["官星清透", "财生官旺", "印护夫星"],
|
| 132 |
-
"interpretation": "婚姻
|
| 133 |
-
"advice": ["
|
| 134 |
},
|
| 135 |
"伤官见官": {
|
| 136 |
"category": "marriage_bad",
|
| 137 |
"conditions": ["伤官旺盛", "官星受克", "多合多冲"],
|
| 138 |
-
"interpretation": "
|
| 139 |
-
"advice": ["
|
| 140 |
},
|
| 141 |
"食神吐秀": {
|
| 142 |
"category": "children_good",
|
| 143 |
"conditions": ["食神有力", "子女宫吉"],
|
| 144 |
-
"interpretation": "
|
| 145 |
-
"advice": ["重视
|
| 146 |
},
|
| 147 |
"子女宫受克": {
|
| 148 |
"category": "children_bad",
|
| 149 |
"conditions": ["时柱受冲克", "食伤被制"],
|
| 150 |
-
"interpretation": "
|
| 151 |
-
"advice": ["
|
| 152 |
}
|
| 153 |
}
|
| 154 |
|
|
|
|
| 129 |
"夫星得用": {
|
| 130 |
"category": "marriage_good",
|
| 131 |
"conditions": ["官星清透", "财生官旺", "印护夫星"],
|
| 132 |
+
"interpretation": "传统象意更强调伴侣关系中的协作与共同目标;它不证明配偶的职业、财富或婚姻结果。",
|
| 133 |
+
"advice": ["把期待说清楚", "支持彼此但保持自我", "共同决策、平等相处"]
|
| 134 |
},
|
| 135 |
"伤官见官": {
|
| 136 |
"category": "marriage_bad",
|
| 137 |
"conditions": ["伤官旺盛", "官星受克", "多合多冲"],
|
| 138 |
+
"interpretation": "传统象意提示沟通、边界与权责更容易出现张力;它不等于必然分手或婚姻失败。",
|
| 139 |
+
"advice": ["把分歧具体化", "先冷静再讨论", "为关系保留共同规则"]
|
| 140 |
},
|
| 141 |
"食神吐秀": {
|
| 142 |
"category": "children_good",
|
| 143 |
"conditions": ["食神有力", "子女宫吉"],
|
| 144 |
+
"interpretation": "传统象意更适合读作照护、教养与创造性亲子互动的资源;它不能证明子女数量、健康或成就。",
|
| 145 |
+
"advice": ["重视稳定陪伴", "培养彼此独立性", "不过度替对方做决定"]
|
| 146 |
},
|
| 147 |
"子女宫受克": {
|
| 148 |
"category": "children_bad",
|
| 149 |
"conditions": ["时柱受冲克", "食伤被制"],
|
| 150 |
+
"interpretation": "传统象意提示亲子角色、照护边界或家庭节奏需要更多沟通与支持;不能据此推断生育能力或是否生育。",
|
| 151 |
+
"advice": ["提前讨论照护边界", "需要时寻求专业支持", "把精力投入可验证的家庭安排"]
|
| 152 |
}
|
| 153 |
}
|
| 154 |
|
experiments/baseline_density.py
CHANGED
|
@@ -17,10 +17,11 @@
|
|
| 17 |
1. 必须用 BaZiCalculator.calculate_bazi_complete(include_dayun=True)。
|
| 18 |
用 calculate_bazi() 拿不到大运→拿不到流年→风险年恒为 0,
|
| 19 |
会得到一份「全 0」的漂亮结果。全 0 是 harness 坏了,不是发现。
|
| 20 |
-
2.
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
3. 分母是**当前大运窗口长度**,不是一辈子。用户读的是这 12 年
|
|
|
|
| 24 |
|
| 25 |
用法
|
| 26 |
----
|
|
@@ -37,6 +38,7 @@ from dayun_analyzer import DayunAnalyzer
|
|
| 37 |
from marriage_analyzer import MarriageAnalyzer
|
| 38 |
from official_analyzer import OfficialAnalyzer
|
| 39 |
from legal_risk_analyzer import LegalRiskAnalyzer
|
|
|
|
| 40 |
|
| 41 |
CHARTS = [('小鱼','1981-10-04','20:38','female'),('林磊','1983-12-13','13:15','female'),
|
| 42 |
('Jason','1978-10-30','10:00','male'),('C4','2008-07-31','21:00','female'),
|
|
@@ -44,10 +46,37 @@ CHARTS = [('小鱼','1981-10-04','20:38','female'),('林磊','1983-12-13','13:15
|
|
| 44 |
('C7','2013-05-13','03:40','male'),('C8','1990-01-08','06:30','male'),
|
| 45 |
('C9','1980-06-07','17:30','female')]
|
| 46 |
NOW = 2026
|
|
|
|
| 47 |
|
| 48 |
def ages(lst):
|
| 49 |
return sorted({e['age'] for e in (lst or []) if isinstance(e, dict) and 'age' in e})
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
def collect(name, date, time, gender):
|
| 52 |
chart = BaZiCalculator.calculate_bazi_complete(birth_date=date, birth_time=time,
|
| 53 |
gender=gender, is_lunar=False, include_dayun=True, num_dayun=12)
|
|
@@ -66,11 +95,23 @@ def collect(name, date, time, gender):
|
|
| 66 |
'官非风险': ages(off.get('official_risk_years')),
|
| 67 |
'升迁': ages(off.get('promotion_years')),
|
| 68 |
'灾劫': ages(leg.get('disaster_years'))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
return {'name': name, 'by': int(date[:4]), 'age': age, 'win': (lo, hi),
|
| 70 |
-
'len': (hi - lo + 1) if lo else None, 'd': d
|
|
|
|
|
|
|
| 71 |
|
| 72 |
R = [collect(*c) for c in CHARTS]
|
| 73 |
-
DOM = ['婚姻风险','官非风险','升迁','灾劫']
|
| 74 |
print('='*86)
|
| 75 |
print('Q1 · 上线后的实际密度(analyze_*() 公开方法输出)')
|
| 76 |
print('='*86)
|
|
@@ -145,3 +186,21 @@ for x in DOM:
|
|
| 145 |
f'落在窗口前半段的比例 {front:.0%} (n={len(pos)})')
|
| 146 |
print()
|
| 147 |
print('若随机分布,平均位置应≈0.50、前半段比例应≈50%。')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
1. 必须用 BaZiCalculator.calculate_bazi_complete(include_dayun=True)。
|
| 18 |
用 calculate_bazi() 拿不到大运→拿不到流年→风险年恒为 0,
|
| 19 |
会得到一份「全 0」的漂亮结果。全 0 是 harness 坏了,不是发现。
|
| 20 |
+
2. 用户实际看到的是 analyze_*() 的公开输出;它会先裁到当前窗口,再按显著度
|
| 21 |
+
封顶。基线必须同时报告「窗口内候选」与「最终呈现」,否则封顶会被误读成
|
| 22 |
+
命理分辨力。
|
| 23 |
+
3. 分母是**当前大运窗口长度**,不是一辈子。用户读的是这 12 年;窗口内
|
| 24 |
+
被隐藏的候选不能悄悄算进命中率。
|
| 25 |
|
| 26 |
用法
|
| 27 |
----
|
|
|
|
| 38 |
from marriage_analyzer import MarriageAnalyzer
|
| 39 |
from official_analyzer import OfficialAnalyzer
|
| 40 |
from legal_risk_analyzer import LegalRiskAnalyzer
|
| 41 |
+
from fortune_window import filter_to_window
|
| 42 |
|
| 43 |
CHARTS = [('小鱼','1981-10-04','20:38','female'),('林磊','1983-12-13','13:15','female'),
|
| 44 |
('Jason','1978-10-30','10:00','male'),('C4','2008-07-31','21:00','female'),
|
|
|
|
| 46 |
('C7','2013-05-13','03:40','male'),('C8','1990-01-08','06:30','male'),
|
| 47 |
('C9','1980-06-07','17:30','female')]
|
| 48 |
NOW = 2026
|
| 49 |
+
DOM = ['婚姻风险','官非风险','升迁','灾劫']
|
| 50 |
|
| 51 |
def ages(lst):
|
| 52 |
return sorted({e['age'] for e in (lst or []) if isinstance(e, dict) and 'age' in e})
|
| 53 |
|
| 54 |
+
|
| 55 |
+
class _NoCapList(list):
|
| 56 |
+
"""诊断用列表:让婚姻模块跳过展示层 [:3],保留候选供对照。
|
| 57 |
+
|
| 58 |
+
MarriageAnalyzer 的「全局最多 20 条 + 当前窗口最多 3 条」逻辑目前写在
|
| 59 |
+
一个方法里,没有单独的 raw API。这个小替身只在本实验中使用,不能进入
|
| 60 |
+
生产路径;结果应理解为“模块内部全局筛选之后、窗口展示封顶之前”的候选。
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
def __len__(self):
|
| 64 |
+
actual = super().__len__()
|
| 65 |
+
return min(actual, 3)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def marriage_candidates_before_window(chart, dayun):
|
| 69 |
+
"""读取婚姻模块的窗口前候选,不改变生产方法。"""
|
| 70 |
+
import fortune_window
|
| 71 |
+
|
| 72 |
+
original_filter = fortune_window.filter_to_window
|
| 73 |
+
try:
|
| 74 |
+
fortune_window.filter_to_window = lambda entries, window, age_key='age': _NoCapList(entries or [])
|
| 75 |
+
diagnostic = MarriageAnalyzer(chart, dayun).analyze_marriage()
|
| 76 |
+
finally:
|
| 77 |
+
fortune_window.filter_to_window = original_filter
|
| 78 |
+
return (diagnostic.get('marriage_fortune_years') or {}).get('risk_years') or []
|
| 79 |
+
|
| 80 |
def collect(name, date, time, gender):
|
| 81 |
chart = BaZiCalculator.calculate_bazi_complete(birth_date=date, birth_time=time,
|
| 82 |
gender=gender, is_lunar=False, include_dayun=True, num_dayun=12)
|
|
|
|
| 95 |
'官非风险': ages(off.get('official_risk_years')),
|
| 96 |
'升迁': ages(off.get('promotion_years')),
|
| 97 |
'灾劫': ages(leg.get('disaster_years'))}
|
| 98 |
+
# 诊断层:公开输出以外,量出窗口内原本有多少候选。这里不把候选当作
|
| 99 |
+
# “预测命中”,只是为了识别 [:3] 封顶造成的密度变化。
|
| 100 |
+
off_analyzer = OfficialAnalyzer(chart, genju, dayun)
|
| 101 |
+
leg_analyzer = LegalRiskAnalyzer(chart, genju, dayun)
|
| 102 |
+
raw_entries = {
|
| 103 |
+
'婚姻风险': marriage_candidates_before_window(chart, dayun),
|
| 104 |
+
'官非风险': off_analyzer._analyze_official_risk_years(),
|
| 105 |
+
'升迁': off_analyzer._analyze_promotion_years(),
|
| 106 |
+
'灾劫': leg_analyzer._predict_disaster_years(),
|
| 107 |
+
}
|
| 108 |
+
raw_window = {x: ages(filter_to_window(raw_entries[x], (lo, hi))) for x in DOM}
|
| 109 |
return {'name': name, 'by': int(date[:4]), 'age': age, 'win': (lo, hi),
|
| 110 |
+
'len': (hi - lo + 1) if lo else None, 'd': d,
|
| 111 |
+
'raw_window': raw_window,
|
| 112 |
+
'raw_total': {x: sum(1 for _ in raw_entries[x]) for x in DOM}}
|
| 113 |
|
| 114 |
R = [collect(*c) for c in CHARTS]
|
|
|
|
| 115 |
print('='*86)
|
| 116 |
print('Q1 · 上线后的实际密度(analyze_*() 公开方法输出)')
|
| 117 |
print('='*86)
|
|
|
|
| 186 |
f'落在窗口前半段的比例 {front:.0%} (n={len(pos)})')
|
| 187 |
print()
|
| 188 |
print('若随机分布,平均位置应≈0.50、前半段比例应≈50%。')
|
| 189 |
+
|
| 190 |
+
print()
|
| 191 |
+
print('='*86)
|
| 192 |
+
print('Q5 · 当前窗口候选 vs 实际呈现(上限效应)')
|
| 193 |
+
print('='*86)
|
| 194 |
+
print('候选 = 模块已完成自身规则筛选、但尚未套当前窗口展示上限的年份;')
|
| 195 |
+
print('呈现 = 用户实际拿到的年份。隐藏不是“被验证为错误”,只是展示层封顶。')
|
| 196 |
+
print()
|
| 197 |
+
print(f"{'领域':<10}{'窗��候选':>12}{'实际呈现':>12}{'隐藏':>10}{'呈现率':>10}")
|
| 198 |
+
for x in DOM:
|
| 199 |
+
raw = sum(len(r['raw_window'][x]) for r in R)
|
| 200 |
+
shown = sum(len(r['d'][x]) for r in R)
|
| 201 |
+
hidden = raw - shown
|
| 202 |
+
ratio = shown / raw if raw else 0
|
| 203 |
+
print(f"{x:<10}{raw:>12}{shown:>12}{hidden:>10}{ratio:>9.0%}")
|
| 204 |
+
print()
|
| 205 |
+
print('解释:若“窗口候选”远高于“实际呈现”,密度下降来自产品封顶而非规则本身;')
|
| 206 |
+
print('因此在获得真实事件追踪前,逐年模块只能作为阶段提示,不能宣称准确率。')
|
genju_analyzer.py
CHANGED
|
@@ -683,14 +683,14 @@ class GenjuAnalyzer:
|
|
| 683 |
base['reading'] = ''
|
| 684 |
else:
|
| 685 |
base['note'] = (f'两个流派在这张盘上不一致:格局法取「{geju_yongshen}」,'
|
| 686 |
-
f'扶抑法主张「{fuyi}」。这是
|
| 687 |
-
f'
|
| 688 |
base['reading'] = (
|
| 689 |
f'这类分歧通常出现在「{label}但格局要求你输出」的命上。'
|
| 690 |
-
f'可以这样读:格局法说的「{geju_yongshen}」
|
| 691 |
-
f'扶抑法说的「{fuyi}」
|
| 692 |
-
f'
|
| 693 |
-
f'不是不能做,是
|
| 694 |
)
|
| 695 |
return base
|
| 696 |
|
|
|
|
| 683 |
base['reading'] = ''
|
| 684 |
else:
|
| 685 |
base['note'] = (f'两个流派在这张盘上不一致:格局法取「{geju_yongshen}」,'
|
| 686 |
+
f'扶抑法主张「{fuyi}」。这是同一命盘的两种读法,不是计算错误;'
|
| 687 |
+
f'本报告把口径并列呈现,不把分歧冒充成事实裁决。')
|
| 688 |
base['reading'] = (
|
| 689 |
f'这类分歧通常出现在「{label}但格局要求你输出」的命上。'
|
| 690 |
+
f'可以这样读:格局法说的「{geju_yongshen}」描述命局要交出的成果形态,'
|
| 691 |
+
f'扶抑法说的「{fuyi}」提示承载力与托底资源。两者相冲时,'
|
| 692 |
+
f'更稳妥的工作假设是先把托底结构搭起来,再逐步验证输出是否可持续——'
|
| 693 |
+
f'不是不能做,而是先后顺序需要用现实反馈校准。'
|
| 694 |
)
|
| 695 |
return base
|
| 696 |
|
test_case_insights_guardrails.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""六亲/子女洞察的表达边界测试。
|
| 2 |
+
|
| 3 |
+
当前数据不足以验证子女数量、健康、生育能力或“子女有出息”等结果,
|
| 4 |
+
所以经典案例层只能交付关系主题与可行动的照护建议。
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
|
| 10 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 11 |
+
|
| 12 |
+
from ai_interpreter import AIInterpreter
|
| 13 |
+
from bazi_calculator import BaZiCalculator
|
| 14 |
+
from dayun_analyzer import DayunAnalyzer
|
| 15 |
+
from genju_analyzer import GenjuAnalyzer
|
| 16 |
+
from shensha_analyzer import ShenshaAnalyzer
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
CASES = [
|
| 20 |
+
('小鱼', '1981-10-04', '20:40', 'female'),
|
| 21 |
+
('林磊', '1983-12-13', '13:15', 'female'),
|
| 22 |
+
('C4', '2008-07-31', '21:00', 'female'),
|
| 23 |
+
('C9', '1980-06-07', '17:30', 'female'),
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def test_child_and_marriage_case_copy_does_not_claim_outcomes():
|
| 28 |
+
banned = ('教育成功', '子女有出息', '生育有困难', '子女运势好')
|
| 29 |
+
for name, date, time, gender in CASES:
|
| 30 |
+
chart = BaZiCalculator.calculate_bazi_complete(
|
| 31 |
+
birth_date=date, birth_time=time, gender=gender,
|
| 32 |
+
is_lunar=False, include_dayun=True, num_dayun=12,
|
| 33 |
+
)
|
| 34 |
+
genju = GenjuAnalyzer(chart).analyze_genju()
|
| 35 |
+
dayun = DayunAnalyzer(chart, genju, current_age=2026 - int(date[:4])).analyze_all_dayun()
|
| 36 |
+
shensha = ShenshaAnalyzer(chart).analyze_all_shenshas()
|
| 37 |
+
text = AIInterpreter(chart, shensha, genju, dayun).generate_interpretation(
|
| 38 |
+
'master', 'zh-CN'
|
| 39 |
+
)['sections']['case_insights']
|
| 40 |
+
assert not any(phrase in text for phrase in banned), f'{name}: 出现未验证六亲断言'
|
| 41 |
+
if '子女' in text or '配偶' in text or '伴侣' in text:
|
| 42 |
+
assert '不能证明' in text or '不等于' in text, f'{name}: 缺少六亲边界说明'
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
if __name__ == '__main__':
|
| 46 |
+
test_child_and_marriage_case_copy_does_not_claim_outcomes()
|
| 47 |
+
print('case insight guardrails: PASS')
|
test_master_multi_chart.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""大师版跨命盘契约测试。
|
| 2 |
+
|
| 3 |
+
这组测试不判断哪一派命理“正确”,只锁产品承诺:大师层应把已实现的
|
| 4 |
+
经典模块真正交付出来,而且不能因为某个知识表的键名不一致而静默消失。
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
|
| 10 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 11 |
+
|
| 12 |
+
from ai_interpreter import AIInterpreter
|
| 13 |
+
from bazi_calculator import BaZiCalculator
|
| 14 |
+
from dayun_analyzer import DayunAnalyzer
|
| 15 |
+
from genju_analyzer import GenjuAnalyzer
|
| 16 |
+
from shensha_analyzer import ShenshaAnalyzer
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
CASES = [
|
| 20 |
+
('小鱼', '1981-10-04', '20:40', 'female'),
|
| 21 |
+
('林磊', '1983-12-13', '13:15', 'female'),
|
| 22 |
+
('Jason', '1978-10-30', '10:00', 'male'),
|
| 23 |
+
('C4', '2008-07-31', '21:00', 'female'),
|
| 24 |
+
('C5', '2012-04-09', '07:30', 'male'),
|
| 25 |
+
('C6', '2014-04-15', '16:30', 'male'),
|
| 26 |
+
('C7', '2013-05-13', '03:40', 'male'),
|
| 27 |
+
('C8', '1990-01-08', '06:30', 'male'),
|
| 28 |
+
('C9', '1980-06-07', '17:30', 'female'),
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _master(name, date, time, gender):
|
| 33 |
+
chart = BaZiCalculator.calculate_bazi_complete(
|
| 34 |
+
birth_date=date,
|
| 35 |
+
birth_time=time,
|
| 36 |
+
gender=gender,
|
| 37 |
+
is_lunar=False,
|
| 38 |
+
include_dayun=True,
|
| 39 |
+
num_dayun=12,
|
| 40 |
+
)
|
| 41 |
+
genju = GenjuAnalyzer(chart).analyze_genju()
|
| 42 |
+
dayun = DayunAnalyzer(chart, genju, current_age=2026 - int(date[:4])).analyze_all_dayun()
|
| 43 |
+
shensha = ShenshaAnalyzer(chart).analyze_all_shenshas()
|
| 44 |
+
return AIInterpreter(chart, shensha, genju, dayun).generate_interpretation('master', 'zh-CN')
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def test_master_sections_are_non_empty_across_reference_charts():
|
| 48 |
+
"""每张参考盘都应真正拿到经典深层洞察,而不是只返回标题或直接省略。"""
|
| 49 |
+
required = {
|
| 50 |
+
'personality', 'career', 'wealth', 'marriage', 'health', 'overall',
|
| 51 |
+
'tiaohou_analysis', 'shishen_analysis', 'case_insights',
|
| 52 |
+
'dayun_overview', 'life_stages', 'turning_points', 'ten_year_fortunes',
|
| 53 |
+
'life_curve_detailed', 'key_ages_analysis', 'life_planning',
|
| 54 |
+
'ditianshi_insights',
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
for name, date, time, gender in CASES:
|
| 58 |
+
result = _master(name, date, time, gender)
|
| 59 |
+
sections = result['sections']
|
| 60 |
+
missing = required - set(sections)
|
| 61 |
+
assert not missing, f'{name}: 大师版缺少章节 {sorted(missing)}'
|
| 62 |
+
empty = [key for key in required if len(str(sections.get(key, '')).strip()) < 30]
|
| 63 |
+
assert not empty, f'{name}: 大师版章节为空壳 {empty}'
|
| 64 |
+
assert result['word_count'] > 2500, f'{name}: 大师版内容异常过短'
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def test_master_health_symbolism_is_explicitly_non_diagnostic():
|
| 68 |
+
"""经典疾病篇只作为传统象意提示,不应伪装成医学诊断。"""
|
| 69 |
+
for name, date, time, gender in CASES:
|
| 70 |
+
result = _master(name, date, time, gender)
|
| 71 |
+
text = result['sections']['ditianshi_insights']
|
| 72 |
+
assert '传统身心象意提示' in text, name
|
| 73 |
+
assert '不是医学诊断' in text, name
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def test_guansha_branch_has_non_empty_evidence_when_present():
|
| 77 |
+
"""官杀归一后,6/9 参考盘应真正拿到内容而不是空标题。
|
| 78 |
+
|
| 79 |
+
The classical table uses ``condition/treatment/outcome`` while the
|
| 80 |
+
renderer exposes reader-facing labels. This catches both the old
|
| 81 |
+
``官杀混杂`` vs ``官杀混杂格`` key mismatch and empty-field regressions.
|
| 82 |
+
"""
|
| 83 |
+
seen = 0
|
| 84 |
+
for name, date, time, gender in CASES:
|
| 85 |
+
result = _master(name, date, time, gender)
|
| 86 |
+
text = result['sections']['ditianshi_insights']
|
| 87 |
+
chart = BaZiCalculator.calculate_bazi_complete(
|
| 88 |
+
birth_date=date, birth_time=time, gender=gender,
|
| 89 |
+
is_lunar=False, include_dayun=True, num_dayun=12,
|
| 90 |
+
)
|
| 91 |
+
stats = GenjuAnalyzer(chart).analyze_genju()['ten_gods_stats']
|
| 92 |
+
expected = '正官' in stats or '七杀' in stats
|
| 93 |
+
present = '**【关煞分析】**' in text
|
| 94 |
+
assert present == expected, f'{name}: 官杀存在性与章节输出不一致'
|
| 95 |
+
if present:
|
| 96 |
+
seen += 1
|
| 97 |
+
for label in ('官煞性质', '制化方法', '实战应用'):
|
| 98 |
+
line = next((ln for ln in text.splitlines() if ln.startswith(f'**{label}:**')), '')
|
| 99 |
+
assert len(line) > len(label) + 6, f'{name}: {label} 为空壳'
|
| 100 |
+
assert seen == 6, f'参考盘官杀章节数量漂移:{seen}/9'
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def test_spirit_sections_are_deduped_and_boundaried():
|
| 104 |
+
"""神煞只交付位置+主题+核验动作,不把旧断语当成结果。"""
|
| 105 |
+
banned = (
|
| 106 |
+
'子女发达', '一生富贵', '一生多灾多难', '血光、凶灾、手术',
|
| 107 |
+
'逢凶化吉', '能掌实权', '学业有成',
|
| 108 |
+
)
|
| 109 |
+
for name, date, time, gender in CASES:
|
| 110 |
+
sections = _master(name, date, time, gender)['sections']
|
| 111 |
+
for key in ('auspicious_spirits', 'inauspicious_spirits'):
|
| 112 |
+
text = sections.get(key, '')
|
| 113 |
+
assert text, f'{name}: {key} 不应为空壳'
|
| 114 |
+
assert not any(phrase in text for phrase in banned), f'{name}: {key} 泄漏绝对断语'
|
| 115 |
+
assert '神煞只作辅助象意' in text, f'{name}: {key} 缺少边界说明'
|
| 116 |
+
names = [line for line in text.splitlines() if line.startswith('**') and '(' in line]
|
| 117 |
+
assert len(names) <= 3, f'{name}: {key} 未限制展示数量'
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
if __name__ == '__main__':
|
| 121 |
+
test_master_sections_are_non_empty_across_reference_charts()
|
| 122 |
+
test_master_health_symbolism_is_explicitly_non_diagnostic()
|
| 123 |
+
print('master multi-chart contract: PASS')
|
test_ten_gods_naming.py
CHANGED
|
@@ -40,6 +40,16 @@ CASES = [
|
|
| 40 |
|
| 41 |
FULL_NAMES = {'比肩', '劫财', '食神', '伤官', '正财', '偏财', '正官', '七杀', '正印', '偏印'}
|
| 42 |
ABBREVS = {'比', '劫', '食', '伤', '财', '才', '官', '杀', '印', '枭'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
|
| 45 |
def _stats(date, time, gender):
|
|
@@ -118,9 +128,13 @@ def test_normalisation_is_a_faithful_one_to_one_rename():
|
|
| 118 |
raw[god] += 1
|
| 119 |
|
| 120 |
mapping = GenjuAnalyzer._TEN_GOD_FULL_NAME
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
expected = Counter()
|
| 122 |
for abbrev, n in raw.items():
|
| 123 |
-
expected[
|
| 124 |
|
| 125 |
actual = Counter(GenjuAnalyzer(chart).analyze_genju()['ten_gods_stats'])
|
| 126 |
assert actual == expected, f'{name}: 归一后分布对不上\n 原始 {dict(raw)}\n 期望 {dict(expected)}\n 实际 {dict(actual)}'
|
|
|
|
| 40 |
|
| 41 |
FULL_NAMES = {'比肩', '劫财', '食神', '伤官', '正财', '偏财', '正官', '七杀', '正印', '偏印'}
|
| 42 |
ABBREVS = {'比', '劫', '食', '伤', '财', '才', '官', '杀', '印', '枭'}
|
| 43 |
+
# Independent oracle for the public naming contract. Do not derive this from
|
| 44 |
+
# GenjuAnalyzer: a bijective but semantically swapped production map must fail
|
| 45 |
+
# the test rather than redefining the expected answer.
|
| 46 |
+
EXPECTED_FULL_NAMES = {
|
| 47 |
+
'比': '比肩', '劫': '劫财',
|
| 48 |
+
'食': '食神', '伤': '伤官',
|
| 49 |
+
'财': '正财', '才': '偏财',
|
| 50 |
+
'官': '正官', '杀': '七杀',
|
| 51 |
+
'印': '正印', '枭': '偏印',
|
| 52 |
+
}
|
| 53 |
|
| 54 |
|
| 55 |
def _stats(date, time, gender):
|
|
|
|
| 128 |
raw[god] += 1
|
| 129 |
|
| 130 |
mapping = GenjuAnalyzer._TEN_GOD_FULL_NAME
|
| 131 |
+
assert mapping == EXPECTED_FULL_NAMES, (
|
| 132 |
+
f'{name}: 简写到全称的语义映射发生漂移\n'
|
| 133 |
+
f' 期望 {EXPECTED_FULL_NAMES}\n 实际 {mapping}'
|
| 134 |
+
)
|
| 135 |
expected = Counter()
|
| 136 |
for abbrev, n in raw.items():
|
| 137 |
+
expected[EXPECTED_FULL_NAMES.get(abbrev, abbrev)] += n
|
| 138 |
|
| 139 |
actual = Counter(GenjuAnalyzer(chart).analyze_genju()['ten_gods_stats'])
|
| 140 |
assert actual == expected, f'{name}: 归一后分布对不上\n 原始 {dict(raw)}\n 期望 {dict(expected)}\n 实际 {dict(actual)}'
|