feat: 六爻算法修复 + prompt架构重构 + i18n输出规则

算法修复 (P0/P1):
- P0-1: 空亡判断改为仅从日柱计算(年月空亡标注但不断事)
- P0-2: 暗动判断重写为静爻+旺相+日冲三条件
- P1-1: 月破独立标注
- P1-2: 动不为空、旺不为空
- P1-3: 三合局判断
- P1-4: 反吟伏吟判断
- P1-5: 日辰十二长生
- P1-6: 回头生克判断

Prompt架构重构:
- 删除system_prompt中_build_env_section,不再泄露用户上下文到prompt
- 删除if is_chinese分支,_LANGUAGE_LABELS已覆盖全部语言映射
- 安全规则改为六爻专属约束,拒绝无关问题
- sign_level枚举值在所有语言版本中统一为简体中文(schema严格约束)
- _WORKER_ROLE_PLAYING始终为中文,不因ai_language切换
- _WORKER_OUTPUT_RULES按ai_language分zh-CN/zh-Hant/en三版本
- worker_rules.py独立文件管理多语言输出规则
- runner ai_language从user_context.settings.preferences提取传入prompt

清理死代码:
- 删除UserPreferences/RuntimePromptContext及辅助函数
- 删除runner中runtime_client_time参数链路
- 删除SystemAgentRuntimeConfig.extra_context
- 删除sections.py中env section marker
- 删除agent_prompt.py中AgentPromptRegistry死代码

安全规则:
- AGENTS.md添加Git Safety规则(禁止未经批准的破坏性git操作)
- opencode.json添加高危git命令审批配置

测试:
- 新增22个六爻算法单元测试(空亡/暗动/月破/三合局等)
- 重写7个prompt测试适配新签名
- 全部85个单元测试通过
This commit is contained in:
qzl
2026-04-15 16:45:57 +08:00
parent c74e3f688c
commit 9598d162dd
14 changed files with 1357 additions and 413 deletions
@@ -0,0 +1,201 @@
from __future__ import annotations
from core.divination.derivation import (
_get_kong_wang,
_resolve_liu_shou,
_wu_xing_status,
derive_divination,
)
from schemas.domain.divination import DivinationPayload, DivinationMethod, YaoType
class TestKongWangCalculation:
def test_kong_wang_jia_zi(self) -> None:
assert _get_kong_wang("甲子") == "戌亥"
def test_kong_wang_jia_xu(self) -> None:
assert _get_kong_wang("甲戌") == "申酉"
def test_kong_wang_jia_shen(self) -> None:
assert _get_kong_wang("甲申") == "午未"
def test_kong_wang_jia_wu(self) -> None:
assert _get_kong_wang("甲午") == "辰巳"
def test_kong_wang_jia_chen(self) -> None:
assert _get_kong_wang("甲辰") == "寅卯"
def test_kong_wang_jia_yin(self) -> None:
assert _get_kong_wang("甲寅") == "子丑"
class TestLiuShouCalculation:
def test_liu_shou_jia_yi(self) -> None:
assert _resolve_liu_shou("") == ("", "", "", "", "", "")
assert _resolve_liu_shou("") == ("", "", "", "", "", "")
def test_liu_shou_bing_ding(self) -> None:
assert _resolve_liu_shou("") == ("", "", "", "", "", "")
assert _resolve_liu_shou("") == ("", "", "", "", "", "")
def test_liu_shou_wu(self) -> None:
assert _resolve_liu_shou("") == ("", "", "", "", "", "")
def test_liu_shou_ji(self) -> None:
assert _resolve_liu_shou("") == ("", "", "", "", "", "")
def test_liu_shou_geng_xin(self) -> None:
assert _resolve_liu_shou("") == ("", "", "", "", "", "")
assert _resolve_liu_shou("") == ("", "", "", "", "", "")
def test_liu_shou_ren_gui(self) -> None:
assert _resolve_liu_shou("") == ("", "", "", "", "", "")
assert _resolve_liu_shou("") == ("", "", "", "", "", "")
class TestWuXingStatus:
def test_wood_wang_in_yin_mao(self) -> None:
assert _wu_xing_status("", "") == ""
assert _wu_xing_status("", "") == ""
def test_fire_wang_in_si_wu(self) -> None:
assert _wu_xing_status("", "") == ""
assert _wu_xing_status("", "") == ""
def test_metal_wang_in_shen_you(self) -> None:
assert _wu_xing_status("", "") == ""
assert _wu_xing_status("", "") == ""
def test_water_wang_in_hai_zi(self) -> None:
assert _wu_xing_status("", "") == ""
assert _wu_xing_status("", "") == ""
def test_earth_wang_in_chen_wei_chou_xu(self) -> None:
assert _wu_xing_status("", "") == ""
assert _wu_xing_status("", "") == ""
assert _wu_xing_status("", "") == ""
assert _wu_xing_status("", "") == ""
class TestKongWangFromDayOnly:
def test_kong_wang_only_from_day_gan_zhi(self) -> None:
payload = DivinationPayload(
divinationMethod=DivinationMethod.MANUAL,
questionType="测试",
question="测试空亡仅从日柱",
divinationTimeIso="2025-01-15T12:00:00+08:00",
yaoLines=[
YaoType.SHAO_YIN,
YaoType.SHAO_YIN,
YaoType.SHAO_YANG,
YaoType.SHAO_YIN,
YaoType.SHAO_YIN,
YaoType.SHAO_YANG,
],
)
result = derive_divination(payload)
assert result.ganzhi.day_kong_wang == "午未"
assert result.ganzhi.time_kong_wang == "戌亥"
kong_wang_yao = [s for s in result.special_status if "旬空" in s]
for status in kong_wang_yao:
if "" in status or "" in status:
pass
else:
assert "" not in status
assert "" not in status
class TestAnDongLogic:
def test_an_dong_wang_xiang_ri_chong(self) -> None:
payload = DivinationPayload(
divinationMethod=DivinationMethod.MANUAL,
questionType="测试",
question="测试暗动",
divinationTimeIso="2025-05-15T12:00:00+08:00",
yaoLines=[
YaoType.SHAO_YIN,
YaoType.SHAO_YIN,
YaoType.SHAO_YANG,
YaoType.SHAO_YIN,
YaoType.SHAO_YIN,
YaoType.SHAO_YANG,
],
)
result = derive_divination(payload)
an_dong_status = [s for s in result.special_status if "暗动" in s]
for status in an_dong_status:
assert "旺相" in status or "日冲" in status
def test_changing_yao_not_marked_as_kong_wang(self) -> None:
payload = DivinationPayload(
divinationMethod=DivinationMethod.MANUAL,
questionType="测试",
question="测试动爻不标空亡",
divinationTimeIso="2025-01-15T12:00:00+08:00",
yaoLines=[
YaoType.SHAO_YANG,
YaoType.SHAO_YANG,
YaoType.LAO_YANG,
YaoType.SHAO_YANG,
YaoType.SHAO_YANG,
YaoType.SHAO_YANG,
],
)
result = derive_divination(payload)
for yao in result.yao_info_list:
if yao.is_changing:
for status in result.special_status:
if f"{yao.position}" in status:
assert "旬空" not in status
class TestYuePoLogic:
def test_yue_po_independently_marked(self) -> None:
payload = DivinationPayload(
divinationMethod=DivinationMethod.MANUAL,
questionType="测试",
question="测试月破",
divinationTimeIso="2025-06-15T12:00:00+08:00",
yaoLines=[
YaoType.SHAO_YIN,
YaoType.SHAO_YIN,
YaoType.SHAO_YANG,
YaoType.SHAO_YIN,
YaoType.SHAO_YIN,
YaoType.SHAO_YANG,
],
)
result = derive_divination(payload)
yue_po_status = [s for s in result.special_status if "月破" in s]
for status in yue_po_status:
assert "月破" in status
assert "暗动" not in status
class TestWangBuWeiKong:
def test_wang_xiang_yao_not_marked_as_kong_wang(self) -> None:
payload = DivinationPayload(
divinationMethod=DivinationMethod.MANUAL,
questionType="测试",
question="测试旺不为空",
divinationTimeIso="2025-01-15T12:00:00+08:00",
yaoLines=[
YaoType.SHAO_YANG,
YaoType.SHAO_YANG,
YaoType.SHAO_YANG,
YaoType.SHAO_YANG,
YaoType.SHAO_YANG,
YaoType.SHAO_YANG,
],
)
result = derive_divination(payload)
for status in result.special_status:
if "旬空" in status:
if "" in status or "" in status:
assert "旬空" not in status or "" not in status