feat: Vimsopaka尊严映射修复 + 单元测试22项全绿 + 审计报告

## Vimsopaka 尊严等级映射 (Step 4.5)
- 新增 _get_dignity_level() 函数:根据行星+星座+度数判断尊严等级
- 新增 MOOLATRIKONA 常量表(含度数范围)
- 新增 PERMANENT_FRIENDS / PERMANENT_ENEMIES 关系表(BPHS标准)
- Vimsopaka 集成不再默认 NEUTRAL,正确映射 Exalted/Moolatrikona/Own/Friend/Enemy/Debilitated

## Dasha 展示修正
- 修正之前的错误修复:日期计算必须用完整年数(数学等价性)
- 第一个 MD 新增字段:balance_years(balance值)、full_years(完整值)、elapsed_at_birth
- years 字段显示 balance 值(展示友好),full_years 显示完整值(用于链式计算)

## 单元测试 (tests/)
- tests/test_dasha.py: 22 项测试全绿
  - TestDignityLevel: 10 项(Exalted/Debilitated/Own/Moolatrikona/Friend/Enemy/Neutral)
  - TestDashaCalculation: 7 项(balance展示、日期连续性、birth+remaining验证、Dasha顺序、AD、Nakshatra映射)
  - TestNakshatraMapping: 3 项(Ashwini/Revati/Jyeshtha边界)
  - TestYogaDetection: 2 项(Raja Yoga检测、无Yoga误报)

## COVERAGE_AUDIT_REPORT.md
- 推送 v3.13.1 覆盖矩阵审计报告到仓库
This commit is contained in:
732642856
2026-05-04 22:53:52 +08:00
parent a9a74d339d
commit b917e029d0
4 changed files with 590 additions and 13 deletions
+254
View File
@@ -0,0 +1,254 @@
# 印度占星 Skill v3.13.1 覆盖矩阵审计报告
**生成时间**2026-04-26 01:06
**审计范围**`references/` (100个.md) + `scripts/` (20个.py) + GitHub 仓库732642856/yinduzhanxing
---
## 一、总体覆盖结论
| 维度 | 已覆盖✅ | 部分覆盖⚠️ | 未覆盖❌ | 说明 |
|------|---------|-----------|---------|------|
| 工作流引用(ai-reading-workflow-prompt.md | 39/39 | 0 | 0 | 100% |
| 引擎子命令依赖(jyotish_engine.py imports | 18/18 | 0 | 0 | 100% |
| 速查指南引用(quick-reference-guide.md | 19/21 | 0 | **2** | ⚠️ 缺口2 |
| v3.8.0v3.13.1 新增文件注册 | 14/14 | 0 | 0 | 100% |
| Python 脚本完整性 | 18/18 | 0 | **2** | ⚠️ 缺口2 |
| **综合** | **90/92** | 0 | **4** | **覆盖率 97.8%** |
---
## 二、引用矩阵(三维定位)
### 维度A:工作流引用(ai-reading-workflow-prompt.md
| 编号 | 文件名 | 状态 |
|------|--------|------|
| A01 | `planets.md` | ✅ |
| A02 | `signs-and-houses.md` | ✅ |
| A03 | `promise-assessment-templates.md` | ✅ |
| A04 | `yoga_list.md` | ✅ |
| A05 | `yoga-list-chinese.md` | ✅ |
| A06 | `yoga-strength-scoring-system.md` | ✅ |
| A07 | `argala-complete-guide.md` | ✅ |
| A08 | `retrograde-combustion-war-guide.md` | ✅ |
| A09 | `nakshatra_deities.md` | ✅ |
| A10 | `nakshatra-chinese-quick-ref.md` | ✅ |
| A11 | `shadbala-complete-methodology.md` | ✅ |
| A12 | `ashtakavarga-complete-system.md` | ✅ |
| A13 | `ketu-dual-nature-guide.md` | ✅ |
| A14 | `varga-system-quick-reference.md` | ✅ |
| A15 | `vimshottari_dasha_guide.md` | ✅ |
| A16 | `dasa-convergence-methodology.md` | ✅ |
| A17 | `transit-comprehensive-guide.md` | ✅ |
| A18 | `transit-multi-reference-guide.md` | ✅ |
| A19 | `jaimini-complete-system.md` | ✅ |
| A20 | `kp-astrology-complete-system.md` | ✅ |
| A21 | `varshaphala-annual-chart-guide.md` | ✅ |
| A22 | `tajika-yoga-complete-guide.md` | ✅ |
| A23 | `timing-prediction-template.md` | ✅ |
| A24 | `prediction-output-protocol.md` | ✅ |
| A25 | `remedies-complete-system.md` | ✅ |
| A26 | `personalized-remedies-system.md` | ✅ |
| A27 | `modern-language-guide.md` | ✅ |
| A28 | `common-misconceptions.md` | ✅ |
| A29 | `modern-life-scenarios-complete.md` | ✅ |
| A30 | `birth-time-rectification-advanced.md` | ✅ |
| A31 | `pdf-chart-reading-guide.md` | ✅ |
| A32 | `comprehensive-reading-workflow.md` | ✅ |
| A33 | `relationship-astrology-guide.md` | ✅ |
| A34 | `house-modern-mapping.md` | ✅ |
| A35 | `house-domain-planet-mapping.md` | ✅ |
| A36 | `prashna-complete-guide.md` | ✅ |
| A37 | `precision-reading-methodology.md` | ✅ |
| A38 | `deep-analysis-complete-workflow.md` | ✅ |
| A39 | `spouse-multi-layer-methodology.md` | ✅ |
> **工作流引用覆盖率:39/39 = 100%** ✅
---
### 维度B:速查指南引用(quick-reference-guide.md
| 编号 | 文件名 | 状态 | 位置 |
|------|--------|------|------|
| Q01 | `vedic-astrology-fundamentals.md` | ✅ | 场景一 |
| Q02 | `yoga-identification-guide.md` | ✅ | 场景一 |
| Q03 | `comprehensive-reading-workflow.md` | ✅ | 场景一/四 |
| Q04 | `qin_ruisheng_system.md` | ✅ | 场景一/八 |
| Q05 | `deep-analysis-complete-workflow.md` | ✅ | 场景一/七/十 |
| Q06 | `marriage-timing-validation-methodology.md` | ✅ | 场景二 |
| Q07 | `marriage-timing-comprehensive-techniques.md` | ✅ | 场景二 |
| Q08 | `spouse-multi-layer-methodology.md` | ✅ | 场景二 |
| Q09 | `navamsa-marriage-deep-analysis.md` | ✅ | 场景二 |
| Q10 | `bhrigu-pada-dasha-marriage-counting.md` | ✅ | 场景二 |
| Q11 | `pdf-data-extraction-guide.md` | ❌ | 场景四 |
| Q12 | `birth-time-rectification-guide.md` | ❌ | 场景五 |
| Q13 | `vedic-astrology-modern-practice-guide.md` | ✅ | 场景五 |
| Q14 | `single-event-inquiry-protocol.md` | ✅ | 场景六 |
| Q15 | `prashna-complete-guide.md` | ✅ | 场景六 |
| Q16 | `famous-case-library.md` | ✅ | 场景七 |
| Q17 | `celebrity-cases.md` | ✅ | 场景七 |
| Q18 | `shatabhisha-complete.md` | ✅ | 场景七 |
| Q19 | `transit-multi-reference-guide.md` | ✅ | 场景八 |
| Q20 | `navatara-kantaka-shani-guide.md` | ✅ | 场景八 |
| Q21 | `varshaphala-annual-chart-guide.md` | ✅ | 场景八 |
| Q22 | `precision-reading-methodology.md` | ✅ | 场景十 |
> **速查指南引用覆盖率:20/22 = 90.9%** ⚠️
---
### 维度C:引擎子命令与Python脚本依赖
| 子命令 | 导入的.py模块 | 文件存在 |
|--------|-------------|---------|
| chart | — | ✅ |
| dasha | — | ✅ |
| yoga | — | ✅ |
| predict | `event_prediction_model.py` | ✅ |
| varga | — | ✅ |
| celebrity | — | ✅ |
| db-stats | — | ✅ |
| transit | — | ✅ |
| shadbala | `shadbala.py` | ✅ |
| ashtakavarga | `ashtakavarga.py` | ✅ |
| memory | `hermes_memory_core.py` | ✅ |
| validate | `validate.py` | ✅ |
| audit | `validate.py`, `ashtakavarga.py`, `shadbala.py` | ✅ |
| report | `report_builder.py` | ✅ |
| varga-full | `varga.py` | ✅ |
| aspects | `aspects.py` | ✅ |
| jaimini | `jaimini.py`, `varga.py` | ✅ |
| nakshatra-adv | `nakshatra_advanced.py` | ✅ |
| argala | `argala.py` | ✅ |
| tajika | `tajika.py` | ✅ |
| synastry | `synastry.py` | ✅ |
| full-reading | 所有上述模块 | ✅ |
| prashna | `prashna.py` | ✅ |
| birth-time-rectificationquick-ref引用) | `birth_time_rectification.py` | **❌** |
| rectfyquick-ref引用) | `cmd_rectify` 子命令 | **❌** |
> **引擎依赖覆盖率:22/24 = 91.7%** ⚠️
> 注:2个缺失项均来自 quick-reference-guide.md Scene 5 的错误引用
---
## 三、未覆盖项目详细说明
### ❌ 问题1quick-reference-guide.md 引用了不存在的文件
**文件**`references/birth-time-rectification-guide.md`
**被引用位置**quick-reference-guide.md Scene 5(第147行)
**实际情况**references/ 目录下只有 `birth-time-rectification-advanced.md`,不存在 `birth-time-rectification-guide.md`
**影响**:用户按速查指南操作会找不到文件
**修复方案**:将 quick-reference-guide.md Scene 5 中两处 `birth-time-rectification-guide.md` 替换为 `birth-time-rectification-advanced.md`
---
### ❌ 问题2quick-reference-guide.md 引用了不存在的引擎子命令
**引用**`python3 scripts/jyotish_engine.py cmd_rectify ...`
**被引用位置**quick-reference-guide.md Scene 5(第153行)
**实际情况**`jyotish_engine.py``main()` 函数中不存在 `cmd_rectify` 子命令
**影响**:用户执行此命令会报错
**修复方案**:删除该命令引用,改为使用 birth-time-rectification-advanced.md 中的互动式矫正流程(AI主导,无独立引擎命令)
---
### ❌ 问题3birth_time_rectification.py 模块不存在
**引用来源**quick-reference-guide.md Scene 5
**实际情况**scripts/ 目录下无 `birth_time_rectification.py`
**影响**:如果未来要实现出生时间矫正引擎命令,无脚本可引用
**修复方案**
- 方案A(推荐):移除 quick-reference-guide.md 中对此脚本的引用,出生时间矫正完全由 AI 主导(参照 birth-time-rectification-advanced.md 的互动流程)
- 方案B:新建 `birth_time_rectification.py` 实现矫正引擎
---
### ❌ 问题4:GitHub 仓库落后本地 6 个版本
**GitHub 状态**732642856/yinduzhanxing):
- v3.7.12026-04-25
- references/: **74个** .md 文件
- scripts/: **19个** .py 文件
**本地状态**
- v3.13.12026-04-26
- references/: **100个** .md 文件
- scripts/: **20个** .py 文件
**GitHub 缺少的文件(26个.md + 1个.py**
| 文件类型 | 数量 | 说明 |
|---------|------|------|
| 婚姻专项(v3.10.0 | 4个 | spouse-multi-layer, marriage-timing-validation, marriage-timing-comprehensive, single-event-inquiry |
| 深度技法(v3.8.0 | 3个 | deep-analysis, precision-reading, badhaka |
| Prashna专项(v3.9.0 | 2个 | prashna-complete, navatara-kantaka-shani |
| Shatabhisha | 1个 | shatabhisha-complete |
| 专业发展(v3.11.0 | 6个 | advanced-techniques, marc-boney, rashi-tulya-navamsa, yogi-avayogi, pancha-pakshi, bhrigu-chakra-paddhati, classical-texts-translation |
| PDF书籍集成(v3.12.0 | 10个 | 10本PDF书籍对应的参考文件 |
| 速查指南(v3.13.1 | 1个 | quick-reference-guide.md |
| Python脚本 | 1个 | 19→20个差异) |
---
## 四、已覆盖项目分类总览(✅ 已覆盖 96项)
### 核心工作流(39项)✅
星盘基础、承诺评估、Yoga体系(含中文版)、Argala、Nakshatra(含中文速查)、Shadbala、Ashtakavarga、Ketu、分盘系统、Vimshottari Dasha(含增强计算器)、Dasa Convergence、Transit(含多参考点)、Jaimini、KP、星宿推进、Varshaphala/Tajika、应期模板、输出规范、补救济世、现代措辞
### 新增技法体系(v3.8v3.13.1)(14项)✅
深度分析工作流、精准解盘方法论、配偶多层综合分析、婚姻四技法验证、单事件问事协议、Badhaka障碍星系统、Navatara Kantaka Shani、Bhrigu Chakra Paddhati、Pancha Pakshi五鸟择时术、Marc Boney婚姻六步法、V.P. Goel Jaimini Dasha系统十种、Yogi/Ava Yogi系统、Rashi Tulya Navamsa根源冲动、实用速查指南
### 引擎计算模块(20个.py)✅
argala, ashtakavarga, aspects, dasha_analyzer, dasha_calculator, dasha_calculator_enhanced, event_prediction_model, hermes_bridge, hermes_memory_core, jaimini, jyotish_engine, nakshatra_advanced, prashna, report_builder, shadbala, synastry, tajika, validate, varga, example
### 名人案例与咨询库(10项)✅
famous-case-library, celebrity-cases, verified-cases(含8个分卷)、consultation-case-library117KB)、case-study-collection
### 合规与验证(5项)✅
birth-time-rectification-advanced(含案例库)、birth-time-rectification-cases、data-bridge-mapping、prediction-boundary-protocol、prediction-checklist
### 经典方法论(5项)✅
qin_ruisheng_system754行秦瑞生体系)、classical-texts-translation-guide、vedic-astrology-modern-practice-guide、professional-development-guide、global-astrologer-practical-methodology
### 软件与工具对比(4项)✅
software-comparison-guide、data-bridge-mapping、varga-divisional-charts-quick-reference、varga-system-quick-reference
### 其他专业文件(4项)✅
alternative-dasha-systems、condition-dasha-complete、tri-system-analysis-template、professional-development-guide
---
## 五、修复优先级
| 优先级 | 问题 | 动作 | 预计工作量 |
|--------|------|------|-----------|
| **P0** | quick-reference-guide.md引用不存在的文件/命令 | 修正两处错误引用 | 5分钟 |
| **P1** | GitHub仓库落后6个版本 | 提交v3.8v3.13.1所有更改 | 需git push |
| **P2** | birth_time_rectification.py不存在 | 确认方案A(AI主导流程)并清理引用 | 2分钟 |
---
## 六、GitHub 提交清单(待推送)
```
v3.8.0: deep-analysis-complete-workflow.md, precision-reading-methodology.md, badhaka-obstacle-planet-guide.md
v3.9.0: prashna-complete-guide.md, navatara-kantaka-shani-guide.md
v3.10.0: spouse-multi-layer-methodology.md, marriage-timing-validation-methodology.md, marriage-timing-comprehensive-techniques.md, single-event-inquiry-protocol.md
v3.11.0: advanced-techniques.md, marc-boney-marriage-six-step.md, rashi-tulya-navamsa-root-impulse.md, yogi-avayogi-system.md, pancha-pakshi-nakshatra-systems.md, bhrigu-chakra-paddhati.md, classical-texts-translation-guide.md, shadbala-interpretation-methodology.md, divisional-chart-deep-reading.md, multi-dasha-convergence-protocol.md, vp-goel-jaimini-dasha-systems.md, shasti-hayani-dasha-guide.md, raman-house-judgment-methodology.md
v3.12.0: 10本PDF书籍对应参考文件
v3.13.0: shatabhisha-complete.md
v3.13.1: quick-reference-guide.md, SKILL.md (v3.13.1), CHANGELOG.md (v3.13.1), jyotish_engine.py (v3.7.1)
SKILL.md: v3.13.0 → v3.13.1(参考文件96→100,总数修正)
```
---
*审计报告 v1.0 | 印度占星 Skill v3.13.1*
+78 -13
View File
@@ -83,6 +83,53 @@ SIGNS_CN = {'Aries': '白羊座', 'Taurus': '金牛座', 'Gemini': '双子座',
SIGN_LORDS = {'Aries': 'Mars', 'Taurus': 'Venus', 'Gemini': 'Mercury', 'Cancer': 'Moon', 'Leo': 'Sun', 'Virgo': 'Mercury', 'Libra': 'Venus', 'Scorpio': 'Mars', 'Sagittarius': 'Jupiter', 'Capricorn': 'Saturn', 'Aquarius': 'Saturn', 'Pisces': 'Jupiter'}
EXALTATION = {'Sun': 'Aries', 'Moon': 'Taurus', 'Mars': 'Capricorn', 'Mercury': 'Virgo', 'Jupiter': 'Cancer', 'Venus': 'Pisces', 'Saturn': 'Libra'}
DEBILITATION = {'Sun': 'Libra', 'Moon': 'Scorpio', 'Mars': 'Cancer', 'Mercury': 'Pisces', 'Jupiter': 'Capricorn', 'Venus': 'Virgo', 'Saturn': 'Aries'}
# Moolatrikona(三方本宫): 行星 → (星座, 度数范围起始)
MOOLATRIKONA = {'Sun': ('Leo', 0, 20), 'Moon': ('Taurus', 0, 3), 'Mars': ('Aries', 0, 12), 'Mercury': ('Virgo', 16, 20), 'Jupiter': ('Sagittarius', 0, 10), 'Venus': ('Libra', 0, 15), 'Saturn': ('Aquarius', 0, 20)}
# BPHS 永久友好关系表 (Naisargika Mitra)
PERMANENT_FRIENDS = {
'Sun': ['Moon', 'Mars', 'Jupiter'],
'Moon': ['Sun', 'Mercury'],
'Mars': ['Sun', 'Moon', 'Jupiter'],
'Mercury': ['Sun', 'Venus'],
'Jupiter': ['Sun', 'Moon', 'Mars'],
'Venus': ['Mercury', 'Saturn'],
'Saturn': ['Mercury', 'Venus'],
'Rahu': ['Venus', 'Saturn'],
'Ketu': ['Mars', 'Saturn'],
}
PERMANENT_ENEMIES = {
'Sun': ['Saturn', 'Venus'],
'Moon': [],
'Mars': ['Mercury'],
'Mercury': ['Moon'],
'Jupiter': ['Mercury', 'Venus'],
'Venus': ['Sun', 'Moon'],
'Saturn': ['Sun', 'Moon', 'Mars'],
'Rahu': ['Sun', 'Moon', 'Jupiter'],
'Ketu': ['Sun', 'Moon'],
}
def _get_dignity_level(planet, sign, deg_in_sign=None):
"""判断行星在某星座的尊严等级 (用于 Vimsopaka 映射)"""
if EXALTATION.get(planet) == sign:
return 'EXALTED'
# Moolatrikona: 需要检查度数范围
mt = MOOLATRIKONA.get(planet)
if mt and mt[0] == sign:
if deg_in_sign is not None and mt[1] <= deg_in_sign < mt[2]:
return 'MOOLATRIKONA'
if SIGN_LORDS.get(sign) == planet:
return 'OWN_SIGN'
if DEBILITATION.get(planet) == sign:
return 'DEBILITATED'
# 友好/敌对
sign_lord = SIGN_LORDS.get(sign, '')
if planet in PERMANENT_FRIENDS.get(sign_lord, []):
return 'FRIEND'
if planet in PERMANENT_ENEMIES.get(sign_lord, []):
return 'ENEMY'
return 'NEUTRAL'
PLANET_CN = {"Ketu": "南交点Ketu", "Venus": "金星Venus", "Sun": "太阳Sun", "Moon": "月亮Moon", "Mars": "火星Mars", "Rahu": "北交点Rahu", "Jupiter": "木星Jupiter", "Saturn": "土星Saturn", "Mercury": "水星Mercury"}
PLANETS_SWE = {'Sun': swe.SUN, 'Moon': swe.MOON, 'Mars': swe.MARS, 'Mercury': swe.MERCURY, 'Jupiter': swe.JUPITER, 'Venus': swe.VENUS, 'Saturn': swe.SATURN, 'Rahu': swe.MEAN_NODE} if HAS_SWE else {}
@@ -218,10 +265,10 @@ def cmd_dasha(args):
timeline = []
for i in range(9):
lord = DASHA_ORDER[(si + i) % 9]; years = DASHA_YEARS[lord]
# 第一个 MD 只取 balance(剩余年数),日期已经是 birth - elapsed
actual_years = round(remaining, 2) if i == 0 else years
end_dt = dt + timedelta(days=actual_years * 365.25)
timeline.append({"lord": lord, "lord_cn": PLANET_CN[lord], "start": dt.strftime("%Y-%m-%d"), "end": end_dt.strftime("%Y-%m-%d"), "years": actual_years, "is_current": False, "is_balance": i == 0})
end_dt = dt + timedelta(days=years * 365.25)
# 第一个 MD 的展示 years 用 balance,实际日期计算用完整年数(数学等价)
display_years = round(remaining, 2) if i == 0 else years
timeline.append({"lord": lord, "lord_cn": PLANET_CN[lord], "start": dt.strftime("%Y-%m-%d"), "end": end_dt.strftime("%Y-%m-%d"), "years": display_years, "full_years": years, "is_current": False, "is_balance": i == 0, "balance_years": round(remaining, 2) if i == 0 else None, "elapsed_at_birth": round(elapsed, 2) if i == 0 else None})
dt = end_dt
today = datetime.strptime(args.today, "%Y-%m-%d") if args.today else datetime.now()
@@ -2120,16 +2167,34 @@ def cmd_full_reading(args):
from vimsopaka_calculator import VimsopakaBalaCalculator, VargaType as VimsVargaType, DignityLevel
vims_calc = VimsopakaBalaCalculator(mode="shodasavarga")
# 将 varga_result 转换为 Vimsopaka 所需的 planet_vargas 格式
# varga_result 结构: {planet: {varga_name: {sign, sign_index, degree, ...}}}
# varga_result 结构: {varga_name: {planet: {sign, degree, ...}}}
# 需要转为: {planet: {VargaType: DignityLevel}}
dignity_map = {
'EXALTED': DignityLevel.EXALTED, 'MOOLATRIKONA': DignityLevel.MOOLATRIKONA,
'OWN_SIGN': DignityLevel.OWN_SIGN, 'FRIEND': DignityLevel.FRIEND,
'NEUTRAL': DignityLevel.NEUTRAL, 'ENEMY': DignityLevel.ENEMY,
'DEBILITATED': DignityLevel.DEBILITATED,
}
planet_vargas_input = {}
if varga_result:
for pn in planet_lons:
planet_vargas_input[pn] = {}
for vt in VimsVargaType:
try:
planet_vargas_input[pn][vt] = DignityLevel.NEUTRAL # 默认
except: pass
# 简化实现:如果 varga_result 可用,尝试映射尊严等级
for pn in planet_lons:
planet_vargas_input[pn] = {}
for vt in VimsVargaType:
# 尝试从 varga_result 中获取该行星在该分盘的 sign
varga_sign = None
varga_deg = None
if varga_result:
for vname, vdata in varga_result.items():
if isinstance(vdata, dict):
pinfo = vdata.get(pn, {})
if isinstance(pinfo, dict) and 'sign' in pinfo:
varga_sign = pinfo['sign']
varga_deg = pinfo.get('degree_in_sign', pinfo.get('degree', 0) % 30)
break
if varga_sign:
dl_key = _get_dignity_level(pn, varga_sign, varga_deg)
planet_vargas_input[pn][vt] = dignity_map.get(dl_key, DignityLevel.NEUTRAL)
else:
planet_vargas_input[pn][vt] = DignityLevel.NEUTRAL
vimsopaka_result = vims_calc.calculate_vimsopaka_bala(planet_vargas_input)
report['modules']['vimsopaka'] = vimsopaka_result
except Exception as e:
View File
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""
Jyotish Engine 单元测试
覆盖: Dasha 计算、尊严等级判定、Yoga 格局识别
运行方式:
cd yinduzhanxing
python3 -m pytest tests/ -v
# 或
python3 -m unittest discover tests/
"""
import unittest
import sys
import os
from datetime import datetime
# 确保可以导入 scripts/ 下的模块
SCRIPT_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'scripts')
sys.path.insert(0, SCRIPT_DIR)
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT_DIR)
from jyotish_engine import (
cmd_dasha, _get_dignity_level, cmd_yoga,
EXALTATION, DEBILITATION, SIGN_LORDS, MOOLATRIKONA,
DASHA_ORDER, DASHA_YEARS, NAKSHATRA_LIST, SIGNS,
)
class TestDignityLevel(unittest.TestCase):
"""测试尊严等级判定 (_get_dignity_level)"""
def test_exalted(self):
"""太阳在白羊座 = Exalted"""
self.assertEqual(_get_dignity_level('Sun', 'Aries'), 'EXALTED')
def test_debilitated(self):
"""太阳在天秤座 = Debilitated"""
self.assertEqual(_get_dignity_level('Sun', 'Libra'), 'DEBILITATED')
def test_own_sign(self):
"""太阳在狮子座 = Own Sign"""
self.assertEqual(_get_dignity_level('Sun', 'Leo'), 'OWN_SIGN')
def test_moolatrikona_in_range(self):
"""火星在白羊座 0-12° = Moolatrikona"""
self.assertEqual(_get_dignity_level('Mars', 'Aries', 5.0), 'MOOLATRIKONA')
def test_moolatrikona_out_of_range(self):
"""火星在白羊座 12°+ = Own Sign (不是 Moolatrikona)"""
self.assertEqual(_get_dignity_level('Mars', 'Aries', 15.0), 'OWN_SIGN')
def test_friend(self):
"""月亮在 Aries(火星的星座,月亮是火星的朋友)"""
# Mars 的 PERMANENT_FRIENDS 包含 Moon,所以 Moon 在 Mars 的星座是 Friend
# 但这里测的是 Moon 在 Aries: Aries lord = Mars, Moon in PERMANENT_FRIENDS['Mars'] = yes
self.assertEqual(_get_dignity_level('Moon', 'Aries'), 'FRIEND')
def test_enemy(self):
"""金星在 Aries(火星的星座,金星是火星的敌人 → 但实际看 Aries lord(Mars) 的朋友表里没有 Venus → 不是 Friend
Venus 在 PERMANENT_ENEMIES['Mars'] 吗?不在。看 Sun 的: Venus in PERMANENT_ENEMIES['Sun'].
测试:Venus 在 CancerMoon 的星座),PERMANENT_FRIENDS['Moon'] = ['Sun', 'Mercury'],不包含 Venus
PERMANENT_ENEMIES['Moon'] = [],所以 Venus 在 Cancer = NEUTRAL
"""
# 测一个明确的 Enemy: Venus 在 AriesMars 守护,Venus 不在 Mars 的 friends 也不在 enemies
# Venus 在 ScorpioMars 守护):同上
# 更好的例子: Saturn 在 LeoSun 守护,Saturn 在 PERMANENT_ENEMIES['Sun']
self.assertEqual(_get_dignity_level('Saturn', 'Leo'), 'ENEMY')
def test_neutral(self):
"""木星在 GeminiMercury 守护),Jupiter 不在 Mercury 的 friends 也不在 enemies"""
# PERMANENT_FRIENDS['Mercury'] = ['Sun', 'Venus']
# PERMANENT_ENEMIES['Mercury'] = ['Moon']
self.assertEqual(_get_dignity_level('Jupiter', 'Gemini'), 'NEUTRAL')
def test_all_exaltation_signs(self):
"""验证所有行星的入旺星座"""
expected = {
'Sun': 'Aries', 'Moon': 'Taurus', 'Mars': 'Capricorn',
'Mercury': 'Virgo', 'Jupiter': 'Cancer', 'Venus': 'Pisces', 'Saturn': 'Libra'
}
for planet, sign in expected.items():
result = _get_dignity_level(planet, sign)
self.assertEqual(result, 'EXALTED', f"{planet} in {sign} should be EXALTED, got {result}")
def test_all_debilitation_signs(self):
"""验证所有行星的落陷星座"""
expected = {
'Sun': 'Libra', 'Moon': 'Scorpio', 'Mars': 'Cancer',
'Mercury': 'Pisces', 'Jupiter': 'Capricorn', 'Venus': 'Virgo', 'Saturn': 'Aries'
}
for planet, sign in expected.items():
result = _get_dignity_level(planet, sign)
self.assertEqual(result, 'DEBILITATED', f"{planet} in {sign} should be DEBILITATED, got {result}")
class TestDashaCalculation(unittest.TestCase):
"""测试 Dasha 时间线计算"""
def _make_dasha_args(self, moon_lon, birthdate, today=None):
"""构造 cmd_dasha 参数"""
today = today or '2026-05-04'
return type('Args', (), {
'moon_lon': moon_lon,
'nakshatra': None,
'pada': None,
'birthdate': birthdate,
'today': today,
})()
def test_first_md_shows_balance(self):
"""第一个 MD 的 years 字段显示 balancefull_years 显示完整年数"""
# 月亮在 Ardra (Rahu 主宰, 18年), 月亮黄经 73° → Ardra 范围 66.67-80°
args = self._make_dasha_args(73.0, '1990-01-15')
result = cmd_dasha(args)
timeline = result.get('timeline', [])
self.assertTrue(len(timeline) > 0, "Timeline should not be empty")
first_md = timeline[0]
# Ardra 主宰 Rahu
self.assertEqual(first_md['lord'], 'Rahu')
# years (展示值) 应该 < 18balance
self.assertLess(first_md['years'], 18, f"First MD display years should be < 18, got {first_md['years']}")
# full_years 应该 = 18
self.assertEqual(first_md['full_years'], 18)
# 应该有 is_balance 和 balance_years
self.assertTrue(first_md.get('is_balance', False))
self.assertIsNotNone(first_md.get('balance_years'))
def test_timeline_date_chain_continuous(self):
"""验证时间线日期链条连续:每个 MD 的 start = 上一个 MD 的 end"""
args = self._make_dasha_args(100.0, '1990-01-15')
result = cmd_dasha(args)
timeline = result.get('timeline', [])
self.assertTrue(len(timeline) >= 2, "Need at least 2 MDs")
for i in range(1, len(timeline)):
self.assertEqual(timeline[i]['start'], timeline[i-1]['end'],
f"MD{i} start should equal MD{i-1} end: {timeline[i]['start']} != {timeline[i-1]['end']}")
def test_first_md_ends_at_birth_plus_remaining(self):
"""验证第一个 MD 的 end 日期 = birth + remaining 年(核心正确性)
原始代码的数学等价性:end = (birth - elapsed) + full_years = birth + remaining
"""
args = self._make_dasha_args(45.0, '1990-06-15', '2026-01-01')
result = cmd_dasha(args)
timeline = result.get('timeline', [])
first = timeline[0]
birth = datetime.strptime('1990-06-15', '%Y-%m-%d')
end = datetime.strptime(first['end'], '%Y-%m-%d')
# end - birth 应该 ≈ balance_years
actual_years = (end - birth).days / 365.25
balance = first.get('balance_years', first['years'])
self.assertAlmostEqual(actual_years, balance, delta=0.1,
msg=f"First MD end should be birth+{balance}y, got {actual_years:.2f}y")
def test_dasha_order_correct(self):
"""验证 Dasha 顺序遵循 Ketu→Venus→Sun→Moon→Mars→Rahu→Jupiter→Saturn→Mercury"""
# 用月亮在 Ashwini (Ketu 主宰) 的位置
args = self._make_dasha_args(5.0, '1990-01-15') # Ashwini
result = cmd_dasha(args)
timeline = result.get('timeline', [])
if len(timeline) >= 9:
actual_order = [d['lord'] for d in timeline]
self.assertEqual(actual_order, DASHA_ORDER,
f"Dasha order should be {DASHA_ORDER}, got {actual_order}")
def test_nakshatra_moon_lon_mapping(self):
"""验证月亮黄经正确映射到 Nakshatra"""
# Revati: 346.67° - 360°
args = self._make_dasha_args(350.0, '1990-01-15')
result = cmd_dasha(args)
self.assertEqual(result.get('moon_nakshatra'), 'Revati')
def test_antardasha_in_current_dasha(self):
"""验证当前 Dasha 包含 Antardasha 信息"""
args = self._make_dasha_args(100.0, '1990-01-15', '2026-05-04')
result = cmd_dasha(args)
current = result.get('current_dasha')
if current:
self.assertIn('antardasha', current, "Current dasha should have antardasha")
ad = current['antardasha']
self.assertTrue(len(ad) > 0, "Antardasha should not be empty")
def test_barack_oboma_dasha(self):
"""已知案例: Obama 月亮在 Rohini (约 35°59' → 实际 Krittika, 用 45° 测 Rohini)"""
# Krittika: 26.67° - 40°, Rohini: 40° - 53.33°
# 35.98° 在 Krittika 范围,用 45° 测 Rohini
args = self._make_dasha_args(45.0, '1961-08-04', '2024-01-01')
result = cmd_dasha(args)
self.assertEqual(result['moon_nakshatra'], 'Rohini')
# Rohini 主宰 Moon,所以第一个 MD 应该是 Moon
timeline = result.get('timeline', [])
if timeline:
self.assertEqual(timeline[0]['lord'], 'Moon')
class TestNakshatraMapping(unittest.TestCase):
"""测试 Nakshatra 映射"""
def test_ashwini_start(self):
"""0° → Ashwini"""
args = type('Args', (), {
'moon_lon': 0.5, 'nakshatra': None, 'pada': None,
'birthdate': '1990-01-01', 'today': '2026-01-01',
})()
result = cmd_dasha(args)
self.assertEqual(result['moon_nakshatra'], 'Ashwini')
def test_revati_end(self):
"""~359° → Revati"""
args = type('Args', (), {
'moon_lon': 359.0, 'nakshatra': None, 'pada': None,
'birthdate': '1990-01-01', 'today': '2026-01-01',
})()
result = cmd_dasha(args)
self.assertEqual(result['moon_nakshatra'], 'Revati')
def test_jyeshtha(self):
"""~230° → Jyeshtha (226.67° - 240°)"""
args = type('Args', (), {
'moon_lon': 230.0, 'nakshatra': None, 'pada': None,
'birthdate': '1990-01-01', 'today': '2026-01-01',
})()
result = cmd_dasha(args)
self.assertEqual(result['moon_nakshatra'], 'Jyeshtha')
class TestYogaDetection(unittest.TestCase):
"""测试 Yoga 格局识别"""
def _make_yoga_args(self, ascendant, planets_str):
return type('Args', (), {
'ascendant': ascendant,
'planets': planets_str,
})()
def test_raja_yoga_detection(self):
"""测试 Raja Yoga 检测:角宫主+三方主同宫"""
# Leo Ascendant: Kendra lords = Sun(1st), Mars(4th/Scorpio), Jupiter(7th/Pisces)
# Trikona lords = Sun(1st), Jupiter(5th/Sagittarius)
# 如果 Jupiter 和 Sun 都在某宫 → Raja Yoga
args = self._make_yoga_args('Leo', 'Sun:Leo:1,Jupiter:Leo:1,Moon:Taurus:10,Mars:Scorpio:4')
result = cmd_yoga(args)
yogas = result.get('yogas', [])
raja = [y for y in yogas if y['name'] == 'Raja Yoga']
self.assertTrue(len(raja) > 0, "Should detect Raja Yoga when Sun+Jupiter in 1st house")
def test_no_yoga_false_positive(self):
"""测试不满足条件时不产生 Yoga"""
# 行星分散,不满足任何 Yoga
args = self._make_yoga_args('Aries', 'Sun:Taurus:2,Moon:Gemini:3')
result = cmd_yoga(args)
yogas = result.get('yogas', [])
self.assertEqual(len(yogas), 0, "Should not detect any Yoga with scattered planets")
if __name__ == '__main__':
unittest.main()