fix: feed current dasha into web marriage path and keep chart fences on failure (BUG-609, BUG-610, BUG-611)
Consultation was passing the birth mahadasha into event-class splits, so marriage hits stayed empty and timing still said 缔结. Read live MD/AD from dasha_sub_periods, derive activation copy from the split, and isolate fence failures so one bad varga cannot 500 the whole report. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-09 — 婚恋激活句按心动 / 成对 / 领证出,网页咨询接上当前大小运
|
||||
|
||||
网页咨询路径现在用当前大运/小运喂给婚恋三层,不再拿出生时大运空转。没有领证命中时,激活句只写观察、并标明不等于领证;没有事件类命中时不再写「关键期」。小运缺失时大运名不再拼 `Unknown`。Skill 版本不变。
|
||||
|
||||
|
||||
## 2026-09-09 — 婚恋分心动 / 成对 / 领证三层作答;长报告时间系统表加十套条件大运
|
||||
|
||||
婚恋问题不再把 Venus / Jupiter / Moon 大运写成一句「婚恋机会」。心动、成对、领证分开说:心动只是观察,不能说成领证;法律婚只认大运/小运命中,不认更细的一层。长报告「时间系统证据状态」表在原来五套后面加上十套条件大运,适用的标 `parameter_sensitive`,不适用的标 `not_applicable`。Skill 版本 6.9.16。
|
||||
|
||||
@@ -9445,3 +9445,52 @@
|
||||
- 相关记录:`TASK-upstream-sync2-20260909.md`;上游 09-04 婚恋触发审计
|
||||
- 复发自:无(上游审计结论在本仓补登记)
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-609 | 网页咨询婚恋路径喂的是出生大运,事件类 hits 恒空
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-09
|
||||
- 最近更新:2026-09-09
|
||||
- 影响面:`/api/consultation_workflow` 婚恋主题、`_thematic_dasha_info`、`report_orchestrator` 婚恋 `timing`
|
||||
- 用户现象:问结婚时机时,心动 / 成对 / 领证拆分没有大运命中;时间标签可能出现 `Unknown`,并仍写出「缔结或调整伴侣关系的关键期」。
|
||||
- 触发条件:网页咨询走复用 chart 模块的婚恋主题,chart 模块 dasha 没有 `vimshottari_analysis`。
|
||||
- 根因:`_thematic_dasha_info` 只从 full-reading 的 `vimshottari_analysis` 取当前 MD/AD。咨询路径 09-06 起复用 chart 模块后这个字段不存在,于是只剩出生时 `current_md`。BUG-608 把 `dasha_info` 接上后,断链第一次变成空 hits。激活句也不看 `event_class_split`。
|
||||
- 修复:当前 MD/AD/PD 改从 `chart.modules.dasha_sub_periods.current` 读取,缺了再回退 `vimshottari_analysis`;婚恋 `activation_description` 按事件类出句;AD 缺失时 `dasha_period` 只写 MD。
|
||||
- 验证:`tests/test_thematic_dasha_info_source.py`、`tests/test_report_orchestrator_marriage_timing.py`、`tests/test_relationship_event_class_evidence.py` 咨询形黄金件。
|
||||
- 防复发:上述测试列入 `CORE_PYTEST_TARGETS`。咨询契约只增不改。感情分析模块逻辑未改,只修喂数。
|
||||
- 相关记录:BUG-608、BUG-555、`TASK-upstream-sync2-fix-20260909.md`
|
||||
- 复发自:无(BUG-608 接入后的喂数断链)
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-610 | 分盘围栏构造失败会让整份长报告 500
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-09
|
||||
- 最近更新:2026-09-09
|
||||
- 影响面:`render_pl9_markdown`、`/api/reports/<id>/professional-reference`、`pl9-export`
|
||||
- 用户现象:某一张分盘上升星座名不在英文十二星座内时,专业参考 Markdown 与导出一起失败。
|
||||
- 触发条件:分盘 `Ascendant.sign` 缺失或为中文星座名,围栏 `build_chart_block` / `_sign_name` 抛 `ValueError`。
|
||||
- 根因:南印度 SVG 在 `try` 内,失败只写「图盘生成失败」;紧接着的围栏构造在 `try` 外,一张分盘出错就中断整份报告。
|
||||
- 修复:围栏失败只丢掉该张围栏、保留 SVG,并打一条不含用户串的 warning。
|
||||
- 验证:`tests/test_report_chart_block.py` 中文上升名用例:Markdown 仍含 `<svg`,该处无 `jyotish-chart` 围栏。
|
||||
- 防复发:该文件已在 `CORE_PYTEST_TARGETS`。失败路径不加围栏,也不再打断报告。
|
||||
- 相关记录:BUG-607、`TASK-upstream-sync2-fix-20260909.md`
|
||||
- 复发自:无(BUG-607 围栏成功路径未覆盖失败分支)
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-611 | `/api/health` 的 version 写死旧 Skill 号
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-09
|
||||
- 最近更新:2026-09-09
|
||||
- 影响面:`GET /api/health`、chart 成功/回退响应里的 `version`
|
||||
- 用户现象:健康检查报的 Skill 版本与 `jyotish_vedic.__version__` 不一致。
|
||||
- 触发条件:访问 `/api/health` 或读取 chart 响应 `version`。
|
||||
- 根因:版本字符串散落三处硬编码,没有从 `jyotish_vedic.__version__` 读。
|
||||
- 修复:三处改为读取 `__version__`;回退路径保留 `-fallback` 后缀。
|
||||
- 验证:`tests/test_api_server_security.py::test_health_endpoint_exposes_runtime_accuracy_metadata`。
|
||||
- 防复发:健康检查断言 `version == jyotish_api_server.__version__`。
|
||||
- 相关记录:`TASK-upstream-sync2-fix-20260909.md`
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# PROGRESS · 上游同步二修复单(网页婚恋 dasha_info / 围栏 / 版本号,2026-09-09)
|
||||
|
||||
- 工作树:`.worktrees/upstream-sync2-fix-20260909`
|
||||
- 分支:`codex/upstream-sync2-fix-20260909`(track `origin/staging`)
|
||||
- 基线:`origin/staging` @ `ee6173e8`(含任务书 `26ef220f`)
|
||||
- BUG:**BUG-609 / 610 / 611**
|
||||
- 未改 `.gitea/workflows/**`,不提升 `main`,未 push
|
||||
|
||||
## 做了什么
|
||||
|
||||
| 任务 | 状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 1 喂正确 MD/AD | 完成 | `_thematic_dasha_info` 先读 `dasha_sub_periods.current`,再回退 `vimshottari_analysis`;咨询路径把 `full_modules` 并进 chart。当前 AD 补进 timeline,避免 `Saturn-Unknown` |
|
||||
| 2 婚恋 activation 句 | 完成 | `report_orchestrator` 按 `event_class_split` 出句;legal 才写「缔结」;romantic/formation 写「不等于领证」;无命中写「本期无婚恋事件类命中」。AD 缺失只写 MD |
|
||||
| 3 围栏失败不打断 | 完成 | `_render_south_chart` 围栏 `try/except`,失败返回 SVG;warning 只带 chart id |
|
||||
| 4 版本字符串 | 完成 | health / chart / fallback 三处读 `jyotish_vedic.__version__` |
|
||||
| 5 记录 | 完成 | BUG-609/610/611、CHANGELOG、本文件、走查清单补 MD/AD 一致 |
|
||||
|
||||
`scripts/jyotish_api_server.py` `wc -l` = 11313(基线 11300,净增 13,上限 15)。前端未改。
|
||||
|
||||
## 测试
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| `tests/test_thematic_dasha_info_source.py` | 绿 |
|
||||
| `tests/test_report_orchestrator_marriage_timing.py` | 绿 |
|
||||
| `tests/test_relationship_event_class_evidence.py` | 绿(含 1990 虚构咨询形黄金件) |
|
||||
| `tests/test_report_chart_block.py` | 绿(含中文上升名保留 SVG) |
|
||||
| `tests/test_api_server_security.py::test_health_endpoint_exposes_runtime_accuracy_metadata` | 绿 |
|
||||
| `tests/test_api_server_growth_contract.py` | 绿 |
|
||||
| `tests/test_consultation_workflow_domains.py` | 绿 |
|
||||
| in-process 咨询形 thematic `theme=marriage` | `Romantic-activation` hits `5L_dasha`,`Relationship-formation` hits `7L_dasha`;`dasha_period=Saturn-Jupiter`;activation 为「不等于领证」 |
|
||||
|
||||
虚构盘说明:Leo 上升,5L=Jupiter,7L=Saturn,月亮在 7 宫;当前 Saturn MD / Jupiter AD,所以 5L 与 7L 都会命中。未换出生。
|
||||
|
||||
## 偏离
|
||||
|
||||
没有砍让步项。时间轴原先只有 MD 行、AD 为 `Unknown`;为满足「不再含 Unknown」且与当前小运一致,把 `dasha_sub_periods.current` 的 AD 填回对应 MD 行。没有从 `dasha.periods` 手算日期。
|
||||
@@ -151,7 +151,7 @@
|
||||
| `TASK-upstream-sync-20260903.md` | `PROGRESS-upstream-sync-20260903.md`、`PROGRESS-upstream-capabilities-20260903.md` | 上游快照推进到 `a6f47abd`、引擎三方合并、health 三窗 / VedAstro REST 桥 / 专业报告导出 / 新字段接聊天与报告 / 校正解冻三项 | 已验收 | 0/1/2/4/6:`45d13258`、`f2241463`、`734d2059`;3/5/7:`c2f23131`~`6063c1f7`(BUG-514)。staging 已部署 `6063c1f7`。修复单 `4ee79210` 已验收 |
|
||||
| `TASK-upstream-sync-fix-20260903.md` | `PROGRESS-upstream-sync-fix-20260903.md` | 非原生主题恢复 `degraded`、finance Yogi/D11/confidence cap、模板注册表引用、VedAstro/MCP finance 路由与 quick CORE 扩列 | 已验收 | `4ee79210`(BUG-515);staging health 已到该 SHA;十主题 e2e 合同全对,Python 红 90→69 无新增 |
|
||||
| `TASK-upstream-sync2-20260909.md` | `PROGRESS-upstream-sync2-20260909.md` | 上游活跃分支 `b9a0ef8f`(未进 main):婚恋三层触发模型(punarphoo / event_class_split 进证据链)+ 十套条件大运接 full reading 与长报告时间系统表;快照锁推进;Skill 6.9.16;不取 shadbala profile / PL9 用户版 / 校正泄漏项 | 已验收(04ad3325):定向 176 通过、quick 门 708/1 env 失败、pl9 时间系统表 5+10 行;1 P1(网页咨询路径 `dasha_info` 无 MD/AD,事件类拆分空转)→ 修复单 `TASK-upstream-sync2-fix-20260909.md` | 分支 `codex/upstream-sync2-20260909`(BUG-608) |
|
||||
| `TASK-upstream-sync2-fix-20260909.md` | — | 修复单:`_thematic_dasha_info` 改读 `dasha_sub_periods.current`、婚恋 activation 句按事件类出、`_render_south_chart` 围栏失败不打断报告、health 版本读 `__version__` | 待领取 | 分支 `codex/upstream-sync2-fix-20260909`(BUG-609~611) |
|
||||
| `TASK-upstream-sync2-fix-20260909.md` | `PROGRESS-upstream-sync2-fix-20260909.md` | 修复单:`_thematic_dasha_info` 改读 `dasha_sub_periods.current`、婚恋 activation 句按事件类出、`_render_south_chart` 围栏失败不打断报告、health 版本读 `__version__` | 待验收 | 分支 `codex/upstream-sync2-fix-20260909`(BUG-609~611) |
|
||||
|
||||
## 命名与归档
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 婚恋三层与条件大运:真实环境走查清单(2026-09-09,BUG-608)
|
||||
|
||||
自动化替代证据见 `tests/test_punarphoo_observation.py`、`tests/test_relationship_event_class_evidence.py`、`tests/test_full_reading_conditional_dashas.py`。下面每一条都需要一位有真实账号的人在 staging 上对照。
|
||||
自动化替代证据见 `tests/test_punarphoo_observation.py`、`tests/test_relationship_event_class_evidence.py`、`tests/test_full_reading_conditional_dashas.py`、`tests/test_thematic_dasha_info_source.py`、`tests/test_report_orchestrator_marriage_timing.py`。下面每一条都需要一位有真实账号的人在 staging 上对照。
|
||||
|
||||
前置:staging 已部署含本轮改动的 SHA;账号点数足以问一句婚恋并生成一份个人长报告。
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
- [ ] 回答把心动、成对、领证分开说,不会合成一句「婚恋机会」。
|
||||
- [ ] 心动层如果出现,语气是观察,没有写成「会结婚」或「领证窗口」。
|
||||
- [ ] 没有给出细到某一个月的更细一层(PD)日期当结婚月。
|
||||
- [ ] 回答里的大运名与页面时间轴当前 MD/AD 一致,且不出现 `Unknown`。
|
||||
|
||||
## 2. 长报告时间系统表
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ if REPO_ROOT not in sys.path:
|
||||
if SCRIPTS_DIR not in sys.path:
|
||||
sys.path.insert(0, SCRIPTS_DIR)
|
||||
|
||||
from jyotish_vedic import __version__
|
||||
|
||||
try:
|
||||
from scripts.local_env import load_local_env
|
||||
from scripts.rectification_policy import (
|
||||
@@ -3399,7 +3401,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
swisseph_version = None
|
||||
self._json({
|
||||
'status': 'ok',
|
||||
'version': '6.9.14',
|
||||
'version': __version__,
|
||||
'git_commit': os.environ.get('GITHUB_SHA') or 'unknown',
|
||||
'swisseph_available': swisseph_available,
|
||||
'swisseph_version': swisseph_version,
|
||||
@@ -5254,6 +5256,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
dasha_timeline=dasha_timeline,
|
||||
yogas=raw.get('yogas') if isinstance(raw.get('yogas'), list) else [],
|
||||
ashtakavarga=self._safe_dict(raw.get('ashtakavarga')),
|
||||
event_class_split=self._safe_dict(raw.get('event_class_split')),
|
||||
)
|
||||
|
||||
def _safe_dict(self, value):
|
||||
@@ -5287,8 +5290,15 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
'start': start_year,
|
||||
'end': end_year,
|
||||
})
|
||||
live = ((raw.get('modules') or {}).get('dasha_sub_periods') or {}).get('current') or {}
|
||||
md, ad = (live.get('mahadasha') or {}).get('lord'), (live.get('antardasha') or {}).get('lord')
|
||||
for row in timeline:
|
||||
if md and ad and row.get('mahadasha') == md and row.get('antardasha') in ('Unknown', '', None):
|
||||
row['antardasha'] = ad
|
||||
break
|
||||
return timeline
|
||||
|
||||
|
||||
def _thematic_period_lord(self, value):
|
||||
if isinstance(value, str) and value.strip():
|
||||
return value.strip()
|
||||
@@ -5428,7 +5438,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
ashtakavarga = full_modules.get('ashtakavarga') or collect('ashtakavarga', lambda: self._compute_ashtakavarga(payload))
|
||||
relationship = collect('relationship', lambda: self._compute_relationship({
|
||||
**payload,
|
||||
'dasha_info': self._thematic_dasha_info(chart, dasha),
|
||||
'dasha_info': self._thematic_dasha_info({**chart, 'modules': {**(chart.get('modules') or {}), **full_modules}}, dasha),
|
||||
}))
|
||||
career = collect('career', lambda: self._compute_career(payload))
|
||||
jaimini = full_modules.get('jaimini') or collect('jaimini', lambda: self._compute_jaimini({**payload, 'mode': 'all'}))
|
||||
@@ -5441,6 +5451,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
'yogas': chart.get('yogas') or [],
|
||||
'ashtakavarga': ashtakavarga or {},
|
||||
'modules': full_modules,
|
||||
'event_class_split': (relationship.get('event_class_split') if isinstance(relationship, dict) else {}) or {},
|
||||
}
|
||||
if yogas and isinstance(yogas.get('result'), dict):
|
||||
enriched['yogas'] = list(enriched['yogas']) + (yogas['result'].get('extended_yogas') or [])
|
||||
@@ -6553,17 +6564,19 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
return ''
|
||||
|
||||
def _thematic_dasha_info(self, chart, dasha):
|
||||
current = {}
|
||||
if isinstance(chart.get('dasha'), dict):
|
||||
current.update(chart['dasha'])
|
||||
current = dict(chart['dasha']) if isinstance(chart.get('dasha'), dict) else {}
|
||||
modules = chart.get('modules') if isinstance(chart.get('modules'), dict) else {}
|
||||
live = modules.get('dasha_sub_periods') if isinstance(modules.get('dasha_sub_periods'), dict) else {}
|
||||
live = live.get('current') if isinstance(live.get('current'), dict) else {}
|
||||
analysis = dasha.get('vimshottari_analysis') if isinstance(dasha, dict) else None
|
||||
if isinstance(analysis, dict):
|
||||
md = ((analysis.get('current') or {}).get('mahadasha') or {}).get('lord')
|
||||
ad = ((analysis.get('current') or {}).get('antardasha') or {}).get('lord')
|
||||
if md:
|
||||
current['maha_dasha'] = md
|
||||
if ad:
|
||||
current['antar_dasha'] = ad
|
||||
analysis = (analysis.get('current') if isinstance(analysis, dict) else None) or {}
|
||||
for source in (analysis, live):
|
||||
if not isinstance(source, dict):
|
||||
continue
|
||||
for src, dest in (('mahadasha', 'maha_dasha'), ('antardasha', 'antar_dasha'), ('pratyantardasha', 'pratyantar_dasha')):
|
||||
lord = (source.get(src) or {}).get('lord') if isinstance(source.get(src), dict) else None
|
||||
if lord:
|
||||
current[dest] = lord
|
||||
return current
|
||||
|
||||
def _parse_birth_datetime(self, body):
|
||||
@@ -6997,7 +7010,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
sign = house.get('cusp_sign', SIGNS[(asc_sign_idx + h - 1) % 12])
|
||||
houses[h] = {'sign': sign, 'sign_idx': SIGNS.index(sign)}
|
||||
result = {
|
||||
'success': True, 'version': '6.9.15',
|
||||
'success': True, 'version': __version__,
|
||||
'birth': {
|
||||
'date': f'{year}-{month:02d}-{day:02d}',
|
||||
'time': self._format_birth_time(hour, minute, second),
|
||||
@@ -7117,7 +7130,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler):
|
||||
special_lagnas = jaimini.calc_special_lagnas(asc_sign_idx, int(hour), minute + second / 60.0)
|
||||
|
||||
result = {
|
||||
'success': True, 'version': '6.9.15-fallback',
|
||||
'success': True, 'version': f'{__version__}-fallback',
|
||||
'warning': 'Swiss Ephemeris未安装,使用简化计算',
|
||||
'birth': {
|
||||
'date': f'{year}-{month:02d}-{day:02d}',
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
import csv
|
||||
@@ -4246,13 +4247,17 @@ def render_pl9_markdown(packet: dict) -> str:
|
||||
for name in PLANET_ORDER
|
||||
for row in [natal_planets.get(name)]
|
||||
}
|
||||
block = build_chart_block(
|
||||
resolve_chart_id(chart_id, title),
|
||||
title,
|
||||
positions if isinstance(positions, dict) else {},
|
||||
ascendant_row if isinstance(ascendant_row, dict) else {},
|
||||
retrograde_by_planet,
|
||||
)
|
||||
try:
|
||||
block = build_chart_block(
|
||||
resolve_chart_id(chart_id, title),
|
||||
title,
|
||||
positions if isinstance(positions, dict) else {},
|
||||
ascendant_row if isinstance(ascendant_row, dict) else {},
|
||||
retrograde_by_planet,
|
||||
)
|
||||
except Exception:
|
||||
logging.warning('report chart fence failed id=%s', chart_id or 'unknown')
|
||||
return svg
|
||||
return f"{svg}\n\n{block}"
|
||||
|
||||
def _d1_chart_svg() -> str | None:
|
||||
|
||||
@@ -241,6 +241,7 @@ class BirthChartData:
|
||||
dasha_timeline: List[Dict[str, Any]] = field(default_factory=list)
|
||||
yogas: List[Dict[str, Any]] = field(default_factory=list)
|
||||
ashtakavarga: Dict[str, Any] = field(default_factory=dict)
|
||||
event_class_split: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -685,9 +686,15 @@ class TimingAnchorBuilder:
|
||||
|
||||
# 根据主题生成激活描述
|
||||
activation = self._activation_text(theme, current)
|
||||
md = str(current.get("mahadasha") or "").strip()
|
||||
ad = str(current.get("antardasha") or "").strip()
|
||||
if ad and ad != "Unknown":
|
||||
dasha_period = f"{md}-{ad}" if md else ad
|
||||
else:
|
||||
dasha_period = md or "Unknown"
|
||||
|
||||
return TimingAnchor(
|
||||
dasha_period=current.get("mahadasha", "Unknown") + "-" + current.get("antardasha", "Unknown"),
|
||||
dasha_period=dasha_period,
|
||||
start_year=current.get("start", now),
|
||||
end_year=current.get("end", now + 3),
|
||||
activation_description=activation,
|
||||
@@ -695,10 +702,11 @@ class TimingAnchorBuilder:
|
||||
)
|
||||
|
||||
def _activation_text(self, theme: ThemeName, period: Dict[str, Any]) -> str:
|
||||
if theme == ThemeName.MARRIAGE:
|
||||
return self._marriage_activation_text()
|
||||
md = period.get("mahadasha", "")
|
||||
ad = period.get("antardasha", "")
|
||||
theme_desc = {
|
||||
ThemeName.MARRIAGE: "婚姻相关宫位与 karaka 将被激活,是缔结或调整伴侣关系的关键期",
|
||||
ThemeName.CAREER: "事业宫位能量被引动,可能出现职位变动、项目突破或行业转换的契机",
|
||||
ThemeName.WEALTH: "财富宫位与 Dhana Yoga 被触发,收入结构可能发生变化",
|
||||
ThemeName.HEALTH: "健康宫位能量显现,是关注身体信号、调整作息的重要时期",
|
||||
@@ -706,6 +714,20 @@ class TimingAnchorBuilder:
|
||||
}
|
||||
return f"{md} 主运配合 {ad} 副运,{theme_desc.get(theme, '相关能量将被激活')}"
|
||||
|
||||
def _marriage_activation_text(self) -> str:
|
||||
split = self.chart.event_class_split if isinstance(getattr(self.chart, "event_class_split", None), dict) else {}
|
||||
legal = split.get("legal_marriage") if isinstance(split.get("legal_marriage"), dict) else {}
|
||||
romantic = split.get("romantic_activation") if isinstance(split.get("romantic_activation"), dict) else {}
|
||||
formation = split.get("relationship_formation") if isinstance(split.get("relationship_formation"), dict) else {}
|
||||
legal_hits = legal.get("hits") if isinstance(legal.get("hits"), list) else []
|
||||
romantic_hits = romantic.get("hits") if isinstance(romantic.get("hits"), list) else []
|
||||
formation_hits = formation.get("hits") if isinstance(formation.get("hits"), list) else []
|
||||
if legal_hits:
|
||||
return "婚姻相关宫位与 karaka 将被激活,是缔结或调整伴侣关系的关键期"
|
||||
if romantic_hits or formation_hits:
|
||||
return "感情被激活的观察期,不等于领证"
|
||||
return "本期无婚恋事件类命中"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# 主题化解盘编排器(主控类)
|
||||
|
||||
@@ -91,6 +91,8 @@ CORE_PYTEST_TARGETS = [
|
||||
"tests/test_mcp_strict_workflow_relationship.py",
|
||||
"tests/test_punarphoo_observation.py",
|
||||
"tests/test_relationship_event_class_evidence.py",
|
||||
"tests/test_thematic_dasha_info_source.py",
|
||||
"tests/test_report_orchestrator_marriage_timing.py",
|
||||
"tests/test_full_reading_conditional_dashas.py",
|
||||
"tests/test_capability_evidence_pool.py",
|
||||
"tests/test_readme_badges.py",
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
{
|
||||
"note": "Fictional 1990-06-15 10:30 Beijing; Leo lagna; 5L=Jupiter, 7L=Saturn, Moon in 7H. Current MD Saturn / AD Jupiter. Trimmed from consultation-shaped chart modules.",
|
||||
"birth": {
|
||||
"year": 1990,
|
||||
"month": 6,
|
||||
"day": 15,
|
||||
"hour": 10,
|
||||
"minute": 30,
|
||||
"lat": 39.9042,
|
||||
"lon": 116.4074,
|
||||
"tz": 8,
|
||||
"today": "2026-09-09"
|
||||
},
|
||||
"chart": {
|
||||
"ascendant": {
|
||||
"sign": "Leo",
|
||||
"sign_idx": 4,
|
||||
"lon": 131.4482,
|
||||
"degree": 11.45,
|
||||
"degree_in_sign": 11.45
|
||||
},
|
||||
"planets": {
|
||||
"Jupiter": {
|
||||
"sign": "Gemini",
|
||||
"house": 11,
|
||||
"lon": 83.52630829337346,
|
||||
"degree": 23.5263,
|
||||
"degree_in_sign": 23.5263,
|
||||
"sign_idx": 2,
|
||||
"longitude": 83.52630829337346
|
||||
},
|
||||
"Ketu": {
|
||||
"sign": "Cancer",
|
||||
"house": 12,
|
||||
"lon": 107.4414312800169,
|
||||
"degree": 17.4414,
|
||||
"degree_in_sign": 17.4414,
|
||||
"sign_idx": 3,
|
||||
"longitude": 107.4414312800169
|
||||
},
|
||||
"Mars": {
|
||||
"sign": "Pisces",
|
||||
"house": 8,
|
||||
"lon": 348.4792230303051,
|
||||
"degree": 18.4792,
|
||||
"degree_in_sign": 18.4792,
|
||||
"sign_idx": 11,
|
||||
"longitude": 348.4792230303051
|
||||
},
|
||||
"Mercury": {
|
||||
"sign": "Taurus",
|
||||
"house": 10,
|
||||
"lon": 42.73765469811987,
|
||||
"degree": 12.7377,
|
||||
"degree_in_sign": 12.7377,
|
||||
"sign_idx": 1,
|
||||
"longitude": 42.73765469811987
|
||||
},
|
||||
"Moon": {
|
||||
"sign": "Aquarius",
|
||||
"house": 7,
|
||||
"lon": 317.8190269192091,
|
||||
"degree": 17.819,
|
||||
"degree_in_sign": 17.819,
|
||||
"sign_idx": 10,
|
||||
"longitude": 317.8190269192091
|
||||
},
|
||||
"Rahu": {
|
||||
"sign": "Capricorn",
|
||||
"house": 6,
|
||||
"lon": 287.4414312800169,
|
||||
"degree": 17.4414,
|
||||
"degree_in_sign": 17.4414,
|
||||
"sign_idx": 9,
|
||||
"longitude": 287.4414312800169
|
||||
},
|
||||
"Saturn": {
|
||||
"sign": "Capricorn",
|
||||
"house": 6,
|
||||
"lon": 271.7776735771559,
|
||||
"degree": 1.7777,
|
||||
"degree_in_sign": 1.7777,
|
||||
"sign_idx": 9,
|
||||
"longitude": 271.7776735771559
|
||||
},
|
||||
"Sun": {
|
||||
"sign": "Gemini",
|
||||
"house": 11,
|
||||
"lon": 61.47407825034283,
|
||||
"degree": 1.4741,
|
||||
"degree_in_sign": 1.4741,
|
||||
"sign_idx": 2,
|
||||
"longitude": 61.47407825034283
|
||||
},
|
||||
"Venus": {
|
||||
"sign": "Aries",
|
||||
"house": 9,
|
||||
"lon": 26.034770273615777,
|
||||
"degree": 26.0348,
|
||||
"degree_in_sign": 26.0348,
|
||||
"sign_idx": 0,
|
||||
"longitude": 26.034770273615777
|
||||
}
|
||||
},
|
||||
"modules": {
|
||||
"dasha": {
|
||||
"current_md": "Rahu",
|
||||
"start_date": "1975-05-25",
|
||||
"remaining_years": 2.94,
|
||||
"total_years": 18,
|
||||
"birth_balance": {
|
||||
"elapsed_years": 15.06,
|
||||
"lord": "Rahu",
|
||||
"remaining_years": 2.94
|
||||
},
|
||||
"periods": [
|
||||
{
|
||||
"lord": "Rahu",
|
||||
"start": "1975-05-25",
|
||||
"end": "1993-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Jupiter",
|
||||
"start": "1993-05-25",
|
||||
"end": "2009-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Saturn",
|
||||
"start": "2009-05-25",
|
||||
"end": "2028-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Mercury",
|
||||
"start": "2028-05-25",
|
||||
"end": "2045-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Ketu",
|
||||
"start": "2045-05-25",
|
||||
"end": "2052-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Venus",
|
||||
"start": "2052-05-25",
|
||||
"end": "2072-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Sun",
|
||||
"start": "2072-05-25",
|
||||
"end": "2078-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Moon",
|
||||
"start": "2078-05-25",
|
||||
"end": "2088-05-25"
|
||||
},
|
||||
{
|
||||
"lord": "Mars",
|
||||
"start": "2088-05-25",
|
||||
"end": "2095-05-25"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dasha_sub_periods": {
|
||||
"status": "ready",
|
||||
"current": {
|
||||
"mahadasha": {
|
||||
"lord": "Saturn",
|
||||
"start": "2009-05-25",
|
||||
"end": "2028-05-25"
|
||||
},
|
||||
"antardasha": {
|
||||
"lord": "Jupiter",
|
||||
"start": "2025-11-11",
|
||||
"end": "2028-05-25"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"thematic_report": {
|
||||
"themes": {
|
||||
"marriage": {
|
||||
"timing": {
|
||||
"dasha_period": "Saturn-Jupiter",
|
||||
"start_year": 2009,
|
||||
"end_year": 2028,
|
||||
"activation_description": "感情被激活的观察期,不等于领证",
|
||||
"is_current": true
|
||||
},
|
||||
"evidence": [
|
||||
{
|
||||
"technique": "D1-7th-house",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"technique": "Spouse-status-yoga",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"technique": "DK-UL-Dasha timing",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"technique": "Romantic-activation",
|
||||
"details": {
|
||||
"hits": [
|
||||
"5L_dasha"
|
||||
],
|
||||
"natal_structure": [
|
||||
"7H_Moon"
|
||||
],
|
||||
"event_class": "romantic_activation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"technique": "Relationship-formation",
|
||||
"details": {
|
||||
"hits": [
|
||||
"7L_dasha"
|
||||
],
|
||||
"event_class": "relationship_formation"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,6 +214,7 @@ def test_health_endpoint_exposes_runtime_accuracy_metadata() -> None:
|
||||
assert handler.status_code == 200
|
||||
payload = handler.payload()
|
||||
assert payload['status'] == 'ok'
|
||||
assert payload['version'] == jyotish_api_server.__version__
|
||||
assert payload['ayanamsa_default'] == 'raman'
|
||||
assert 'swisseph_available' in payload
|
||||
assert 'swisseph_version' in payload
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
from jyotish_api_server import JyotishAPIHandler # noqa: E402
|
||||
from relationship_analysis import analyze_relationship # noqa: E402
|
||||
from relationship_event_class_evidence import build_event_class_evidence # noqa: E402
|
||||
|
||||
@@ -63,3 +65,36 @@ def test_pratyantar_venus_does_not_emit_legal_marriage_item() -> None:
|
||||
assert "Venus_dasha" not in split["legal_marriage"]["hits"]
|
||||
items = build_event_class_evidence(relationship, _theme_evidence)
|
||||
assert all(item["technique"] != "Legal-marriage" for item in items)
|
||||
|
||||
|
||||
def test_consultation_shaped_1990_chart_feeds_nonempty_event_class_hits() -> None:
|
||||
golden = json.loads(
|
||||
(ROOT / "tests/golden/upstream_sync2/consultation_marriage_1990_fictional.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
)
|
||||
chart = golden["chart"]
|
||||
handler = JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||
dasha = (chart.get("modules") or {}).get("dasha") or {}
|
||||
dasha_info = handler._thematic_dasha_info(chart, dasha)
|
||||
assert dasha_info["maha_dasha"] == "Saturn"
|
||||
assert dasha_info["antar_dasha"] == "Jupiter"
|
||||
timing = (((golden.get("thematic_report") or {}).get("themes") or {}).get("marriage") or {}).get("timing") or {}
|
||||
assert "Unknown" not in str(timing.get("dasha_period") or "")
|
||||
assert timing.get("dasha_period") == "Saturn-Jupiter"
|
||||
relationship = analyze_relationship(
|
||||
chart["planets"],
|
||||
asc_sign=chart["ascendant"]["sign"],
|
||||
dasha_info=dasha_info,
|
||||
)
|
||||
items = handler._derived_marriage_evidence(
|
||||
chart,
|
||||
{"relationship": relationship, "full_modules": chart.get("modules") or {}},
|
||||
)
|
||||
by_name = {item["technique"]: item for item in items}
|
||||
nonempty = [
|
||||
name
|
||||
for name in ("Romantic-activation", "Relationship-formation")
|
||||
if (by_name.get(name) or {}).get("details", {}).get("hits")
|
||||
]
|
||||
assert nonempty, {name: (by_name.get(name) or {}).get("details") for name in by_name}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from types import SimpleNamespace
|
||||
@@ -195,3 +196,14 @@ def test_chart_block_suite_is_on_the_quick_quality_gate() -> None:
|
||||
from scripts.run_quality_gate import CORE_PYTEST_TARGETS
|
||||
|
||||
assert "tests/test_report_chart_block.py" in CORE_PYTEST_TARGETS
|
||||
|
||||
|
||||
def test_chinese_ascendant_sign_keeps_svg_and_drops_fence() -> None:
|
||||
packet = copy.deepcopy(_golden()["packet"])
|
||||
packet["core_chart"]["ascendant"]["sign"] = "白羊"
|
||||
markdown = render_pl9_markdown(packet)
|
||||
start = markdown.index("#### D1 — Rashi Chart(本命盘)")
|
||||
end = markdown.index("#### D9 — Navamsa(婚盘)")
|
||||
section = markdown[start:end]
|
||||
assert "<svg" in section
|
||||
assert "```jyotish-chart" not in section
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Marriage timing copy follows event_class_split; missing AD is not spelled Unknown."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from scripts.report_orchestrator import BirthChartData, ThemeName, TimingAnchorBuilder
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ORCHESTRATOR = ROOT / "scripts" / "report_orchestrator.py"
|
||||
|
||||
|
||||
def _builder(split: dict, *, antardasha: str = "Unknown") -> TimingAnchorBuilder:
|
||||
chart = BirthChartData(
|
||||
dasha_timeline=[
|
||||
{"mahadasha": "Saturn", "antardasha": antardasha, "start": 2025, "end": 2028},
|
||||
],
|
||||
event_class_split=split,
|
||||
)
|
||||
return TimingAnchorBuilder(chart)
|
||||
|
||||
|
||||
def test_romantic_only_says_not_legal_marriage() -> None:
|
||||
timing = _builder({"romantic_activation": {"hits": ["5L_dasha"]}}).build_for_theme(ThemeName.MARRIAGE)
|
||||
assert timing is not None
|
||||
assert "不等于领证" in timing.activation_description
|
||||
assert "缔结" not in timing.activation_description
|
||||
assert "关键期" not in timing.activation_description
|
||||
|
||||
|
||||
def test_legal_md_ad_hit_may_say_solemnize() -> None:
|
||||
timing = _builder(
|
||||
{"legal_marriage": {"hits": ["Venus_dasha"]}},
|
||||
antardasha="Venus",
|
||||
).build_for_theme(ThemeName.MARRIAGE)
|
||||
assert timing is not None
|
||||
assert "缔结" in timing.activation_description
|
||||
assert timing.dasha_period == "Saturn-Venus"
|
||||
|
||||
|
||||
def test_no_hits_omits_key_period_and_solemnize() -> None:
|
||||
timing = _builder({}).build_for_theme(ThemeName.MARRIAGE)
|
||||
assert timing is not None
|
||||
assert "关键期" not in timing.activation_description
|
||||
assert "缔结" not in timing.activation_description
|
||||
assert timing.activation_description == "本期无婚恋事件类命中"
|
||||
assert "Unknown" not in timing.dasha_period
|
||||
assert timing.dasha_period == "Saturn"
|
||||
|
||||
|
||||
def test_legal_phrase_only_lives_on_the_legal_branch() -> None:
|
||||
source = ORCHESTRATOR.read_text(encoding="utf-8")
|
||||
assert source.count("缔结或调整伴侣关系的关键期") == 1
|
||||
legal_fn = source[source.index("def _marriage_activation_text") :]
|
||||
legal_fn = legal_fn[: legal_fn.index("class ThematicReportOrchestrator")]
|
||||
assert "if legal_hits:" in legal_fn
|
||||
before_legal, after_legal = legal_fn.split("if legal_hits:", 1)
|
||||
assert "缔结或调整伴侣关系的关键期" not in before_legal
|
||||
assert "缔结或调整伴侣关系的关键期" in after_legal.split("return", 1)[1].split("\n", 1)[0]
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Current MD/AD for thematic reports come from dasha_sub_periods, not birth MD."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from scripts.jyotish_api_server import JyotishAPIHandler
|
||||
|
||||
|
||||
def _handler() -> JyotishAPIHandler:
|
||||
return JyotishAPIHandler.__new__(JyotishAPIHandler)
|
||||
|
||||
|
||||
def test_prefers_dasha_sub_periods_current_over_birth_md() -> None:
|
||||
info = _handler()._thematic_dasha_info(
|
||||
{
|
||||
"dasha": {"current_md": "Rahu"},
|
||||
"modules": {
|
||||
"dasha_sub_periods": {
|
||||
"current": {
|
||||
"mahadasha": {"lord": "Saturn"},
|
||||
"antardasha": {"lord": "Jupiter"},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{"current_md": "Rahu", "periods": []},
|
||||
)
|
||||
assert info["maha_dasha"] == "Saturn"
|
||||
assert info["antar_dasha"] == "Jupiter"
|
||||
|
||||
|
||||
def test_falls_back_to_vimshottari_analysis_when_sub_periods_missing() -> None:
|
||||
info = _handler()._thematic_dasha_info(
|
||||
{"dasha": {"current_md": "Rahu"}},
|
||||
{
|
||||
"vimshottari_analysis": {
|
||||
"current": {
|
||||
"mahadasha": {"lord": "Venus"},
|
||||
"antardasha": {"lord": "Moon"},
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
assert info["maha_dasha"] == "Venus"
|
||||
assert info["antar_dasha"] == "Moon"
|
||||
|
||||
|
||||
def test_omits_maha_dasha_when_neither_source_has_current_lords() -> None:
|
||||
info = _handler()._thematic_dasha_info({"dasha": {"current_md": "Rahu"}}, {"periods": []})
|
||||
assert "maha_dasha" not in info
|
||||
assert "antar_dasha" not in info
|
||||
|
||||
|
||||
def test_timeline_fills_current_antardasha_from_sub_periods() -> None:
|
||||
timeline = _handler()._normalize_thematic_dasha_timeline(
|
||||
{
|
||||
"periods": [
|
||||
{"lord": "Saturn", "start": "2009-05-25", "end": "2028-05-25"},
|
||||
],
|
||||
"modules": {
|
||||
"dasha_sub_periods": {
|
||||
"current": {
|
||||
"mahadasha": {"lord": "Saturn"},
|
||||
"antardasha": {"lord": "Jupiter"},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
assert timeline[0]["mahadasha"] == "Saturn"
|
||||
assert timeline[0]["antardasha"] == "Jupiter"
|
||||
|
||||
|
||||
def test_source_tests_are_on_the_quick_quality_gate() -> None:
|
||||
from scripts.run_quality_gate import CORE_PYTEST_TARGETS
|
||||
|
||||
assert "tests/test_thematic_dasha_info_source.py" in CORE_PYTEST_TARGETS
|
||||
assert "tests/test_report_orchestrator_marriage_timing.py" in CORE_PYTEST_TARGETS
|
||||
Reference in New Issue
Block a user