merge origin/staging into rect scoring perf (issue #57)
Docs-only conflicts (CHANGELOG, BUG_HISTORY 1318-1322 vs 1323-1325/1345) resolved by keeping both; agent-loop rule A. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LutgzyJqNhPpZy1nFsJtN3
This commit is contained in:
@@ -4,6 +4,13 @@
|
||||
|
||||
- 校正每轮重算更快(2 小时窗口约快一倍以上),结果不变。一次要算的分钟数超过 241 会直接拒绝,不再占着计算空位排盘。Skill 版本不 bump(BUG-1323~1325、BUG-1345)。
|
||||
|
||||
## 2026-10-14 — 婚恋评级改成证据档位,KP 判宫和大运年长对齐(待合入 staging)
|
||||
|
||||
- 婚恋结构指数不再写成婚姻稳定、离婚、再婚或建议婚前咨询。中文报告用中文档位,英文报告用英文档位。配偶星分数按 0 到 1 显示,不再写成百分制。
|
||||
- KP 按宫头到下一宫头判宫,报告写明口径。本命宫位表的判宫不变。
|
||||
- 接口里兜底大运的一年改成和推运脚本一样的 365.25 天。
|
||||
- 星历路径只核查,没改。Skill 版本不变(BUG-1318~1321;BUG-1322 仍在核查)。
|
||||
|
||||
## 2026-10-13 — 明说要做生时校正时,第一步就能打开(待合入 staging)
|
||||
|
||||
- 新对话第一句就说「帮我做生时校正 / 生时纠正 / 核对出生时间」,会直接打开校正,只留一句过渡,不先写一段解读。
|
||||
|
||||
@@ -17720,6 +17720,81 @@
|
||||
- 复发自:BUG-1306。
|
||||
- 修复版本:分支 `codex/rectification-step0-entry-20261013`,尚未合入 staging。
|
||||
|
||||
## BUG-1318 | 婚恋评级仍写成稳定或婚前咨询,配偶星分数被写成百分制
|
||||
|
||||
- 状态:fixed-pending-verify
|
||||
- 首次发现 / 最近更新:2026-10-14 / 2026-10-14
|
||||
- 来源:issue #56。上游 `f891a2c1`。任务书 `docs/tasks/TASK-upstream-sync7-20261014.md` T1。
|
||||
- 现象:D9 综合评级仍写「婚姻稳定」「强烈建议婚前咨询」。这份文字进全读导出包,模型能读到。配偶星证据行把 0 到 1 的指数写成「婚姻质量 x/100」。
|
||||
- 触发条件:算出婚恋几何指数,或关系时机证据带出配偶星。
|
||||
- 根因:同步第六轮按标题筛提交时,漏了 10-02 到 10-03 的这条措辞修正。分数行把 0 到 1 当成了百分制。
|
||||
- 修复:五档改成证据档位,并写明这是结构指数,不预测婚姻次数、稳定、离婚、伴侣好坏,也不提示婚前咨询。中文报告用中文档位,英文报告用英文档位。证据行改成「婚姻质量指数 x(0到1)」,不乘 100。
|
||||
- 验证:`tests/test_d9_evidence_bands.py` 3 通过。公开盘中英文各一条快照。配偶星行不再出现 `/100`。
|
||||
- 防复发:五档文字不得含「稳定、离婚、再婚、婚前咨询、几段」;快照锁住公开盘的两句。
|
||||
- 相关记录:BUG-1183。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/upstream-sync7-20261014`,尚未合入 staging。
|
||||
|
||||
## BUG-1319 | KP 落宫沿用本命中点判宫,传了宫头也不声明口径
|
||||
|
||||
- 状态:fixed-pending-verify
|
||||
- 首次发现 / 最近更新:2026-10-14 / 2026-10-14
|
||||
- 来源:issue #56。上游 `0337b77f`。任务书 T2。
|
||||
- 现象:KP 用的行星落宫和本命 Bhava Chalit 表一样,按相邻宫头的中点判宫。宫头传进 KP 分析时,结果不写明判宫口径。
|
||||
- 触发条件:打开 KP。本命 Bhava Chalit 表不走这条。
|
||||
- 根因:同步第六轮漏了夹在上游隐私提交里的判宫修正。本命表和 KP 共用了中点函数。
|
||||
- 修复:KP 改成宫头到下一宫头。本命表默认仍是中点,宫位数字不变。传了宫头却不声明口径就拒绝;没传宫头记整宫代理。KP 报告写出判宫口径。
|
||||
- 验证:`tests/test_kp_cusp_house_assignment.py` 3 通过。同一颗星在两种口径下落入不同宫;默认中点与改前一致。
|
||||
- 防复发:未知口径直接报错;报告正文必须出现 House occupancy basis。
|
||||
- 相关记录:BUG-1318。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/upstream-sync7-20261014`,尚未合入 staging。
|
||||
|
||||
## BUG-1320 | 瑜伽角宫主和三方主的顺序随进程哈希变化
|
||||
|
||||
- 状态:fixed-pending-verify
|
||||
- 首次发现 / 最近更新:2026-10-14 / 2026-10-14
|
||||
- 来源:issue #56。上游 `0337b77f`。任务书 T3。
|
||||
- 现象:同一张盘的角宫主、三方主列表用了集合去重。集合顺序会跟着进程哈希变,瑜伽参与者顺序不稳定。
|
||||
- 触发条件:规则表达式读取这两份宫主列表。
|
||||
- 根因:去重用了不保证顺序的集合。同步第六轮漏了上游把它们改成排序的那一处。
|
||||
- 修复:两处都改成先去重再按名字排序。
|
||||
- 验证:`tests/test_yoga_lord_order_stable.py` 1 通过。哈希种子 0 和 1 两个进程算出的参与者顺序相同。
|
||||
- 防复发:两个子进程的名单必须逐项相同,并且等于排好序的名单。
|
||||
- 相关记录:BUG-1319。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/upstream-sync7-20261014`,尚未合入 staging。
|
||||
|
||||
## BUG-1321 | 接口里的大运年长和推运脚本差了一点,起止会对不齐
|
||||
|
||||
- 状态:fixed-pending-verify
|
||||
- 首次发现 / 最近更新:2026-10-14 / 2026-10-14
|
||||
- 来源:issue #56。上游 `4d543209`。任务书 T4。
|
||||
- 现象:接口的兜底大运和「进入本运几年」用 365.25636 天为一年。同一条时间线的推运脚本用 365.25 天。年限一长,日期会差一天。
|
||||
- 触发条件:走兜底大运时间线,或计算进入当前大运的年数。
|
||||
- 根因:两处常量没有跟着推运脚本改。同步第六轮漏了上游 10-03 的这一处。
|
||||
- 修复:这两处改成 365.25,与推运脚本相同。其它大运系统的年长不动。
|
||||
- 验证:`tests/test_dasha_year_length_alignment.py` 2 通过。同一大运起止相差 0 天。158 年的旧常量会落到下一天,新常量不会。
|
||||
- 防复发:兜底时间线必须和推运脚本的起止是同一天;进入本运的年数按 365.25 除。
|
||||
- 相关记录:BUG-1320。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/upstream-sync7-20261014`,尚未合入 staging。
|
||||
|
||||
## BUG-1322 | 星历路径被设成空字符串后,计算退回低精度星历
|
||||
|
||||
- 状态:investigating
|
||||
- 首次发现 / 最近更新:2026-10-14 / 2026-10-14
|
||||
- 来源:issue #56。上游 `e1909724` 点出的同类问题。任务书 T5,本轮只核查、不改代码。
|
||||
- 现象:排盘和过境入口都把星历路径设成空字符串。本机随后计算太阳位置时,返回标记含低精度星历位,不含星历文件位。
|
||||
- 触发条件:调用排盘或过境计算。本轮没有改这条路径。
|
||||
- 根因:尚未在生产镜像里核实。本机仓库里的星历文件只在参考资料目录,不在计算进程会去找的路径上。空路径是进程全局的,后一次调用会盖掉前面设过的路径。
|
||||
- 修复:未改。建议另开一单:镜像里放一份星历文件,路径显式设上,并断言返回标记不再是低精度星历。
|
||||
- 验证:本机一次探针。生产镜像未打开,不能当作已确认。
|
||||
- 防复发:未做。等修复单再加断言。
|
||||
- 相关记录:BUG-1321。
|
||||
- 复发自:无。
|
||||
- 修复版本:未修复。
|
||||
|
||||
## BUG-1323 | 校正引导窗口按候选两两比较,窗口一长就平方变慢
|
||||
|
||||
- 状态:fixed-pending-verify(尚未合入 staging)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# PROGRESS · 上游同步第七轮(2026-10-14)
|
||||
|
||||
- 分支:`codex/upstream-sync7-20261014`
|
||||
- 基线:任务书写的是 `77057a81`。开工时 `origin/staging` 已到 `99ef1bbd`,本分支从这里建。
|
||||
- 结论:T1 到 T4 已做完并推这个分支。T5 只核查,代码没改。星历在这台电脑上会退回低精度,生产镜像没打开,记 BUG-1322,状态是核查中。
|
||||
|
||||
## 做了什么
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| T1 婚恋断语 | 五档改成证据档位,中文给中文报告,英文给英文报告。写明不预测婚姻次数、稳定、离婚、伴侣好坏,也不提示婚前咨询。配偶星那一行改成 0 到 1 的指数,不再写 `/100`。BUG-1318,上游 `f891a2c1`。 |
|
||||
| T2 KP 判宫 | KP 按宫头到下一宫头。本命宫位表仍按中点,默认宫位不变。传了宫头却不说口径就拒绝;没传记整宫代理。报告多一行判宫口径。BUG-1319,上游 `0337b77f`。 |
|
||||
| T3 瑜伽排序 | 角宫主、三方主先去重再按名字排序。BUG-1320,上游 `0337b77f`。 |
|
||||
| T4 大运年长 | 接口里两处 365.25636 改成 365.25,和推运脚本同一天。别的大运系统没动。BUG-1321,上游 `4d543209`。 |
|
||||
| T5 星历 | 见下。不改代码。BUG-1322,核查中。 |
|
||||
|
||||
## 检查
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 婚恋档位 | `pytest tests/test_d9_evidence_bands.py` | 3 通过 |
|
||||
| KP 判宫 | `pytest tests/test_kp_cusp_house_assignment.py` | 3 通过 |
|
||||
| 本命宫位表 | `pytest tests/test_bhava_chalit.py` | 通过(与上面一批共 20 通过、0 失败) |
|
||||
| 几何指数旧测试 | `pytest tests/test_marriage_counting_geometry_only.py` | 通过(同上) |
|
||||
| 瑜伽顺序 | `pytest tests/test_yoga_lord_order_stable.py` | 1 通过 |
|
||||
| 大运年长 | `pytest tests/test_dasha_year_length_alignment.py` | 2 通过 |
|
||||
| 接口增长合同 | `pytest tests/test_api_server_growth_contract.py` | 通过 |
|
||||
| KP 报告标题回归 | `pytest tests/test_report_longform_parity.py` 里 KP 那一条 | 通过 |
|
||||
| 快速门、前端全量 | 未跑 | 按执行分工,留给验收方 |
|
||||
| 数据库 | 未跑 | 没动表 |
|
||||
|
||||
## T5 星历核查
|
||||
|
||||
本机把路径设成空字符串后,算太阳位置的返回标记带低精度星历,不带星历文件。仓库里的星历文件在参考资料目录,计算时用不到。排盘和过境两处都设成同一个空路径,所以这两处彼此一致,但都会退回低精度。路径是整个进程共用的,谁后设谁生效。
|
||||
|
||||
生产镜像这台电脑打不开,不能说线上也是这样。健康检查只看星历库能不能导入,看不出用的是哪一套。建议另开一单再改。
|
||||
|
||||
## 偏离
|
||||
|
||||
- 关系时机里还有一处用 65 和 45 去比这个 0 到 1 的指数。任务书只允许改配偶星那一行文字,所以没动。它现在加不上那一档分数。建议另开小单。
|
||||
- 生时校正打分文件没有改。
|
||||
@@ -331,7 +331,7 @@
|
||||
| `TASK-rectification-probe-supply-research-20260913.md` | — | 研究单:六题后引擎在剩余候选上再出带年月题的四种放宽规则,20 例公开 AA 数据离线量收益,有收益才立实现单 | 已合入(离线测量,不改线上) | `b063c668` |
|
||||
| `TASK-rectification-refresh-r3-r4-20260913.md` | `PROGRESS-rectification-refresh-r3-r4-20260913.md` | 刷新阶段 R3(MIN_BOUNDARY_DAYS 45→30)+ R4(pratyantar 与 D9/D10 上升 Narayana);首轮出题不变。即使放宽仍有 0 题例子,定向补事另线保留(BUG-664/665) | 已实现 `ab1ade59`,已部署;2026-09-13 验收通过(实现单由执行方自拟,无产品决策记录段;收益口径见研究文档) | `codex/rectification-refresh-r3-r4-20260913` |
|
||||
| `TASK-upstream-sync6-20261007.md` | [PROGRESS-upstream-sync6-20261007.md](PROGRESS-upstream-sync6-20261007.md) | **上游同步第六轮(`23be1807`)**:10-05 五个计算修正一行未合——D5/D6/D8/D11 映射(乔布斯盘四张分盘星座全不同)、Vimsopaka 权重、Yogini 顺序与出生余额、Panchadhikari 宫位参照、Narayana 并列、特殊上升、出生秒数、Neecha Bhanga 两条件、首段大运子运、岁差被覆盖;逐项对照原书核实;最先做 | 已验收(门禁补修 `2df6ee72`,已部署 staging) | [PROGRESS-upstream-sync6-20261007.md](PROGRESS-upstream-sync6-20261007.md) |
|
||||
| `TASK-upstream-sync7-20261014.md` | — | **上游同步第七轮(BUG-1318 起)**:婚恋评级断语改证据档位、DK「婚姻质量 X/100」改对;KP 宫头到宫头判宫;瑜伽宫主排序去重;api_server 大运年长 365.25;星历路径只核查。不动校正打分。Grok 执行(agent 循环) | 待执行 | — |
|
||||
| `TASK-upstream-sync7-20261014.md` | [PROGRESS-upstream-sync7-20261014.md](PROGRESS-upstream-sync7-20261014.md) | **上游同步第七轮(BUG-1318 起)**:婚恋评级断语改证据档位、DK「婚姻质量 X/100」改对;KP 宫头到宫头判宫;瑜伽宫主排序去重;api_server 大运年长 365.25;星历路径只核查。不动校正打分。Grok 执行(agent 循环) | 待验收 | `codex/upstream-sync7-20261014`(BUG-1318~1321 已改;BUG-1322 星历路径核查中,未改代码) |
|
||||
| `TASK-beta-main-path-20261014.md` | — | **内测主路径**:BUG-1063 他人报告 worker 读档案(逐列只读授权 + test:db);对话与报告「AI 生成,仅供参考」标识与免责;BUG-1080/283 状态;状态板按事实清账;冒烟清单 `docs/testing/beta-smoke-20261014.md`。与发布单并行 | 待执行 | — |
|
||||
| `TASK-release-path-production-20261014.md` | — | **发布通路 + 生产首发**:产品 10-14 授权改生产工作流(发布 SHA = 最近门禁成功提交 + 其后纯文档);63 个迁移清单与演练脚本、首发 runbook;生产拒绝测试验证码。Grok 不 dispatch 生产工作流 | 待执行 | — |
|
||||
| `TASK-beta-safety-net-20261014.md` | — | **内测安全网**:每日备份 + 恢复演练、探活告警、本地错误汇总、日志轮转、按 requestId 查日志。**等发布通路单合入、#58 合入后开工** | 待前置 | — |
|
||||
|
||||
+23
-2
@@ -272,6 +272,20 @@ class BhavaChalitCalculator:
|
||||
# fallback: 最近的宫
|
||||
return 1
|
||||
|
||||
def _planet_bhava_from_cusps(self, planet_lon: float, cusps: List[float]) -> int:
|
||||
"""KP 宫头到宫头:宫 i(0 起)的范围是 cusp[i] → cusp[(i+1)%12]。
|
||||
|
||||
落在下一宫头上的点归下一宫。本命 Bhava Chalit 表不用这条。
|
||||
"""
|
||||
for i in range(12):
|
||||
start = cusps[i]
|
||||
end = cusps[(i + 1) % 12]
|
||||
arc = _angular_dist(start, end)
|
||||
pos = _angular_dist(start, planet_lon)
|
||||
if arc > 0 and pos < arc:
|
||||
return i + 1
|
||||
return 1
|
||||
|
||||
def _planet_rashi_house(self, planet_lon: float, asc_lon: float) -> int:
|
||||
"""整宫制宫位 (Rashi house)。"""
|
||||
p_si = _sign_idx(planet_lon)
|
||||
@@ -330,7 +344,8 @@ class BhavaChalitCalculator:
|
||||
asc_lon: float, mc_lon: float,
|
||||
house_system: str = 'sripati',
|
||||
jd: float = None, lat: float = None,
|
||||
lon: float = None) -> Dict:
|
||||
lon: float = None,
|
||||
house_assignment: str = 'sandhi') -> Dict:
|
||||
"""根据 Bhava 边界重新分配行星宫位。
|
||||
|
||||
Parameters
|
||||
@@ -346,13 +361,18 @@ class BhavaChalitCalculator:
|
||||
dict with keys:
|
||||
house_system, boundaries, planets
|
||||
"""
|
||||
if house_assignment not in ('sandhi', 'cusp_to_cusp'):
|
||||
raise ValueError("house_assignment must be 'sandhi' or 'cusp_to_cusp'")
|
||||
cusps = self.calculate_cusps(asc_lon, mc_lon, house_system,
|
||||
jd, lat, lon)
|
||||
sandhis = self.calculate_sandhis(cusps)
|
||||
|
||||
planets = {}
|
||||
for pname, plon in planet_lons.items():
|
||||
bhava = self._planet_bhava(plon, sandhis)
|
||||
if house_assignment == 'cusp_to_cusp':
|
||||
bhava = self._planet_bhava_from_cusps(plon, cusps)
|
||||
else:
|
||||
bhava = self._planet_bhava(plon, sandhis)
|
||||
rashi = self._planet_rashi_house(plon, asc_lon)
|
||||
si = _sign_idx(plon)
|
||||
deg_in_sign = plon - si * 30
|
||||
@@ -380,6 +400,7 @@ class BhavaChalitCalculator:
|
||||
return {
|
||||
'house_system': house_system,
|
||||
'house_system_desc': self.HOUSE_SYSTEMS.get(house_system, ''),
|
||||
'house_assignment': house_assignment,
|
||||
'ascendant_lon': round(asc_lon, 4),
|
||||
'mc_lon': round(mc_lon, 4),
|
||||
'planets': planets,
|
||||
|
||||
@@ -6378,7 +6378,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
|
||||
periods = []
|
||||
current = birth_dt
|
||||
for lord in DASHA_ORDER:
|
||||
end_date = current + timedelta(days=period_years * 365.25636)
|
||||
end_date = current + timedelta(days=period_years * 365.25)
|
||||
periods.append({
|
||||
'lord': lord,
|
||||
'years': period_years,
|
||||
@@ -7398,7 +7398,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
|
||||
current_idx, current_md = analyzer.find_current(timeline, today)
|
||||
antardashas = analyzer.build_antardasha(current_md)
|
||||
current_ad = analyzer.find_current_sub(antardashas, today)
|
||||
years_into_md = max(0.0, (today - current_md['start']).days / 365.25636)
|
||||
years_into_md = max(0.0, (today - current_md['start']).days / 365.25)
|
||||
five_levels = enhanced.calculate_five_level_dasha(current_md['lord'], years_into_md)
|
||||
formatted_timeline = [self._format_datetime_period(period) for period in timeline]
|
||||
formatted_antardashas = [self._format_datetime_period(period) for period in antardashas]
|
||||
@@ -7551,7 +7551,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM
|
||||
evidence.append({
|
||||
'label': 'Darakaraka (DK)',
|
||||
'value': f"{dk_planet} · H{darakaraka.get('dk_house', '-')}",
|
||||
'note': f"{darakaraka.get('core_profile') or '配偶象征星'};婚姻质量 {darakaraka.get('marriage_quality_score', '-')}/100",
|
||||
'note': f"{darakaraka.get('core_profile') or '配偶象征星'};婚姻质量指数 {darakaraka.get('marriage_quality_score', '-')}(0到1)",
|
||||
})
|
||||
except Exception as exc:
|
||||
darakaraka = {'error': str(exc), 'dk_planet': dk_planet}
|
||||
|
||||
@@ -13963,6 +13963,7 @@ def cmd_kp(args):
|
||||
)
|
||||
bhava_chart = kp_house_calculator.get_bhava_chalit_chart(
|
||||
planet_lons, float(asc_lon), 0.0, 'placidus', jd, args.lat, args.lon,
|
||||
house_assignment='cusp_to_cusp',
|
||||
)
|
||||
bhava_planets = bhava_chart.get('planets') if isinstance(bhava_chart.get('planets'), dict) else {}
|
||||
|
||||
@@ -13977,7 +13978,12 @@ def cmd_kp(args):
|
||||
'house': bhava_row.get('bhava_house', pdata.get('house', 1)),
|
||||
}
|
||||
|
||||
result = calc_kp_analysis(planet_positions, asc_sign, house_cusps=boundaries.get('cusps'))
|
||||
result = calc_kp_analysis(
|
||||
planet_positions,
|
||||
asc_sign,
|
||||
house_cusps=boundaries.get('cusps'),
|
||||
house_occupancy_basis='cusp_to_cusp',
|
||||
)
|
||||
moon_lon = planets.get('Moon', {}).get('degree_raw', planets.get('Moon', {}).get('degree', 0.0)) if isinstance(planets.get('Moon'), dict) else 0.0
|
||||
try:
|
||||
birth_moment = datetime(int(args.year), int(args.month), int(args.day), int(args.hour), int(getattr(args, 'minute', 0) or 0))
|
||||
|
||||
@@ -371,6 +371,7 @@ def calc_kp_analysis(
|
||||
planet_positions: Dict,
|
||||
asc_sign: str = 'Aries',
|
||||
house_cusps: Optional[List[float]] = None,
|
||||
house_occupancy_basis: Optional[str] = None,
|
||||
) -> Dict:
|
||||
"""
|
||||
完整KP分析(基于 diliprk/VedicAstro MIT 算法)。
|
||||
@@ -379,6 +380,7 @@ def calc_kp_analysis(
|
||||
planet_positions: 行星位置 {planet: {'sign': str, 'degree': float, 'house': int}}
|
||||
asc_sign: 上升星座名称(无显式宫头时用于 whole-sign 代理)
|
||||
house_cusps: 可选的 12 个实际宫头黄经。提供时优先用于 KP 宫头与显著星。
|
||||
house_occupancy_basis: 传了宫头就必须声明判宫口径;没传宫头时记 whole_sign_proxy。
|
||||
|
||||
Returns:
|
||||
完整KP分析结果
|
||||
@@ -405,8 +407,11 @@ def calc_kp_analysis(
|
||||
}
|
||||
|
||||
explicit_cusps = house_cusps is not None
|
||||
if explicit_cusps and not house_occupancy_basis:
|
||||
raise ValueError('house_occupancy_basis is required when house_cusps are provided')
|
||||
if explicit_cusps and len(house_cusps) != 12:
|
||||
raise ValueError('house_cusps must contain exactly 12 longitudes')
|
||||
occupancy_basis = house_occupancy_basis if explicit_cusps else 'whole_sign_proxy'
|
||||
|
||||
# 2. 构建宫位信息(含 KP lords)。无实际宫头时保留历史 whole-sign 中点代理。
|
||||
houses = []
|
||||
@@ -438,6 +443,7 @@ def calc_kp_analysis(
|
||||
'source': 'diliprk/VedicAstro MIT License',
|
||||
'maturity_profile': kp_maturity_profile(),
|
||||
'house_basis': 'explicit_cusps' if explicit_cusps else 'whole_sign_proxy',
|
||||
'house_occupancy_basis': occupancy_basis,
|
||||
'planets': {pname: {'kp_lords': data['kp_lords'], 'significators': planet_sig.get(pname, {})}
|
||||
for pname, data in kp_planets.items()},
|
||||
'houses': {h['house']: {'sign': h['sign'], 'cusp_longitude': h['cusp_longitude'], 'kp_lords': h['kp_lords'], 'significators': house_sig.get(h['house'], {})}
|
||||
@@ -494,6 +500,7 @@ def build_kp_report_pack(
|
||||
f"- claim_status: `{maturity.get('claim_status')}`",
|
||||
f"- truth_matrix_allowed: `{maturity.get('truth_matrix_allowed')}`",
|
||||
f"- closed_oracle_parity_assets: `{', '.join(closed_parity_assets.keys())}`",
|
||||
f"- House occupancy basis: `{kp_analysis.get('house_occupancy_basis') or 'whole_sign_proxy'}`",
|
||||
f"- planet_count: `{len(planets)}`",
|
||||
f"- house_count: `{len(houses)}`",
|
||||
f"- dba_period_count: `{len(periods)}`",
|
||||
|
||||
@@ -21,6 +21,23 @@ from typing import Dict, Optional, List
|
||||
SIGN_CN = ['白羊座','金牛座','双子座','巨蟹座','狮子座','处女座',
|
||||
'天秤座','天蝎座','射手座','摩羯座','水瓶座','双鱼座']
|
||||
|
||||
# D9 结构证据档位(BUG-1318)。中文给报告正文,英文键与英文短句给英文版。
|
||||
# 不写婚姻稳定、离婚、再婚、段数或婚前咨询。
|
||||
D9_CLAIM_BOUNDARY = (
|
||||
'D9 结构证据指数,不预测婚姻次数、稳定、离婚、伴侣好坏,也不提示需要婚前咨询'
|
||||
)
|
||||
D9_CLAIM_BOUNDARY_EN = (
|
||||
'D9 structural evidence index. It does not predict marriage count, stability, '
|
||||
'divorce, or partner quality, and it does not suggest premarital counseling.'
|
||||
)
|
||||
D9_EVIDENCE_BANDS = (
|
||||
('supportive_evidence_stronger', '支持证据较强', 'Stronger supportive evidence'),
|
||||
('supportive_evidence_mixed_positive', '支持证据偏正', 'Mixed supportive evidence'),
|
||||
('mixed_evidence', '证据混杂', 'Mixed evidence'),
|
||||
('pressure_evidence_mixed', '压力证据偏杂', 'Mixed pressure evidence'),
|
||||
('pressure_evidence_stronger', '压力证据较强', 'Stronger pressure evidence'),
|
||||
)
|
||||
|
||||
|
||||
def sign_of(lon: float) -> int:
|
||||
"""从黄道经度计算星座序号 (0-11)"""
|
||||
@@ -348,25 +365,42 @@ def _assess_d9_marriage_quality(
|
||||
quality_points -= 1
|
||||
factors.append("D9 Ketu在7宫 → 关系中的分离倾向")
|
||||
|
||||
# 综合评级
|
||||
# 证据档位,不是婚姻结局(BUG-1318,上游 f891a2c1)。
|
||||
if quality_points >= 4:
|
||||
rating = "高(关系质量好,伴侣支持强,婚姻稳定)"
|
||||
band = D9_EVIDENCE_BANDS[0]
|
||||
elif quality_points >= 1:
|
||||
rating = "中上(关系质量尚可,需经营但基础好)"
|
||||
band = D9_EVIDENCE_BANDS[1]
|
||||
elif quality_points >= 0:
|
||||
rating = "中(关系质量一般,需用心经营)"
|
||||
band = D9_EVIDENCE_BANDS[2]
|
||||
elif quality_points >= -2:
|
||||
rating = "中下(关系有挑战,需提前沟通和调整期望)"
|
||||
band = D9_EVIDENCE_BANDS[3]
|
||||
else:
|
||||
rating = "低(关系质量有重大挑战,强烈建议婚前咨询)"
|
||||
band = D9_EVIDENCE_BANDS[4]
|
||||
|
||||
return {
|
||||
'quality_rating': rating,
|
||||
'quality_band': band[0],
|
||||
'quality_rating': band[1],
|
||||
'quality_rating_en': band[2],
|
||||
'quality_points': quality_points,
|
||||
'factors': factors,
|
||||
'claim_boundary': D9_CLAIM_BOUNDARY,
|
||||
'claim_boundary_en': D9_CLAIM_BOUNDARY_EN,
|
||||
}
|
||||
|
||||
|
||||
def render_d9_evidence_report(quality: Dict, language: str = 'zh') -> str:
|
||||
"""中文报告用证据档位短句;英文版用英文短句。两边都带同一条边界。"""
|
||||
if not isinstance(quality, dict):
|
||||
quality = {}
|
||||
if language == 'en':
|
||||
band = quality.get('quality_rating_en') or quality.get('quality_band') or ''
|
||||
boundary = quality.get('claim_boundary_en') or D9_CLAIM_BOUNDARY_EN
|
||||
return f"D9 evidence band: {band}\n{boundary}"
|
||||
band = quality.get('quality_rating') or ''
|
||||
boundary = quality.get('claim_boundary') or D9_CLAIM_BOUNDARY
|
||||
return f"D9 证据档位:{band}\n{boundary}"
|
||||
|
||||
|
||||
def _marriage_recommendations(base: Dict, d9_quality: Dict) -> List[str]:
|
||||
"""几何指数与伴侣数量、事件断语分开(BUG-1183);不随指数大小给不同结论。"""
|
||||
return [
|
||||
|
||||
@@ -2140,6 +2140,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
'### p123 KP Chart / Cusps',
|
||||
'',
|
||||
f"- House basis:{_cell(kp.get('house_basis'))}",
|
||||
f"- House occupancy basis:{_cell(kp.get('house_occupancy_basis'))}",
|
||||
'',
|
||||
'| House | Sign | Cusp longitude | KP lord chain |',
|
||||
'|-------|------|----------------|---------------|',
|
||||
|
||||
@@ -1687,11 +1687,13 @@ class YogaEngine:
|
||||
# 辅助:获取角宫主星列表
|
||||
def _kendra_lords_list():
|
||||
ai = SIGNS.index(ctx.ascendant) if ctx.ascendant in SIGNS else 0
|
||||
return list(set([SIGN_LORDS[SIGNS[(ai + h - 1) % 12]] for h in [1, 4, 7, 10]]))
|
||||
# set 顺序随进程哈希变化;排序后同一张盘的角宫主参与者顺序才稳定。
|
||||
return sorted(set([SIGN_LORDS[SIGNS[(ai + h - 1) % 12]] for h in [1, 4, 7, 10]]))
|
||||
# 辅助:获取三方宫主星列表
|
||||
def _trikona_lords_list():
|
||||
ai = SIGNS.index(ctx.ascendant) if ctx.ascendant in SIGNS else 0
|
||||
return list(set([SIGN_LORDS[SIGNS[(ai + h - 1) % 12]] for h in [1, 5, 9]]))
|
||||
# set 顺序随进程哈希变化;排序后同一张盘的三方主参与者顺序才稳定。
|
||||
return sorted(set([SIGN_LORDS[SIGNS[(ai + h - 1) % 12]] for h in [1, 5, 9]]))
|
||||
|
||||
safe_globals = {
|
||||
# 安全内置函数:只开放规则表达式需要的纯函数,避免 custom 规则因 all/any/len/set 缺失而静默失效。
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"""BUG-1318: D9 marriage rating is an evidence band, not an outcome claim.
|
||||
|
||||
Upstream f891a2c1. The five bands must not say the relationship is stable,
|
||||
ending, repeated, or in need of premarital counseling. The DK evidence line
|
||||
keeps the 0-1 index and does not print a fake /100 score.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = os.path.join(os.path.dirname(__file__), "..")
|
||||
SCRIPTS = os.path.join(ROOT, "scripts")
|
||||
if SCRIPTS not in sys.path:
|
||||
sys.path.insert(0, SCRIPTS)
|
||||
|
||||
from jyotish_engine import compute_chart_data # noqa: E402
|
||||
|
||||
|
||||
def _api_handler():
|
||||
"""Reuse the existing unbound handler helper. Do not add another forgery site."""
|
||||
path = os.path.join(os.path.dirname(__file__), "test_api_server_security.py")
|
||||
spec = importlib.util.spec_from_file_location("_api_security_helpers", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module._handler()
|
||||
from marriage_counting import ( # noqa: E402
|
||||
D9_CLAIM_BOUNDARY,
|
||||
D9_EVIDENCE_BANDS,
|
||||
_assess_d9_marriage_quality,
|
||||
marriage_counting_full_analysis,
|
||||
render_d9_evidence_report,
|
||||
)
|
||||
from varga import calc_varga # noqa: E402
|
||||
|
||||
BANNED = re.compile(r"稳定|离婚|再婚|婚前咨询|几段")
|
||||
|
||||
|
||||
def _quality(points_chart: dict) -> dict:
|
||||
return _assess_d9_marriage_quality(points_chart, None)
|
||||
|
||||
|
||||
def test_five_bands_have_no_outcome_claims_and_keep_the_boundary() -> None:
|
||||
samples = {
|
||||
"supportive_evidence_stronger": {"Venus": 331.0, "Mars": 271.0, "Jupiter": 91.0},
|
||||
"supportive_evidence_mixed_positive": {"Venus": 31.0},
|
||||
"mixed_evidence": {"Venus": 10.0},
|
||||
"pressure_evidence_mixed": {"Venus": 151.0},
|
||||
"pressure_evidence_stronger": {"Venus": 151.0, "Mars": 91.0, "Jupiter": 271.0},
|
||||
}
|
||||
seen = []
|
||||
for key, chart in samples.items():
|
||||
quality = _quality(chart)
|
||||
assert quality["quality_band"] == key
|
||||
assert quality["claim_boundary"] == D9_CLAIM_BOUNDARY
|
||||
assert quality["claim_boundary_en"]
|
||||
assert not BANNED.search(quality["quality_rating"])
|
||||
assert not BANNED.search(quality["quality_rating_en"])
|
||||
seen.append(quality["quality_band"])
|
||||
assert seen == [band[0] for band in D9_EVIDENCE_BANDS]
|
||||
|
||||
|
||||
def test_public_chart_report_snapshots_in_both_languages() -> None:
|
||||
cases = json.loads(
|
||||
Path(ROOT, "references/real_case_calibration/minute_rectification_development_v1.json").read_text(encoding="utf-8")
|
||||
)["cases"]
|
||||
birth = next(case for case in cases if case["case_id"] == "steve_jobs_1955_development")["birth"]
|
||||
assert birth["source"]["rodden_rating"] == "AA"
|
||||
year, month, day = (int(part) for part in birth["date"].split("-"))
|
||||
hour, minute = (int(part) for part in birth["time"].split(":"))
|
||||
chart, *_ = compute_chart_data(
|
||||
year, month, day, hour, minute,
|
||||
birth["latitude"], birth["longitude"], birth["timezone_offset"],
|
||||
ayanamsa_name="raman",
|
||||
)
|
||||
longitudes = {name: row["degree_raw"] for name, row in chart["planets"].items()}
|
||||
d9 = {}
|
||||
for name, lon in longitudes.items():
|
||||
divisional = calc_varga(lon, 9)
|
||||
d9[name] = divisional["sign_idx"] * 30 + divisional["degree_in_sign"]
|
||||
result = marriage_counting_full_analysis(chart["houses"]["house_7"]["lord"], longitudes, d9)
|
||||
quality = result["d9_marriage_quality"]
|
||||
assert quality["quality_band"] == "mixed_evidence"
|
||||
assert quality["quality_points"] == 0
|
||||
chinese = render_d9_evidence_report(quality, "zh")
|
||||
english = render_d9_evidence_report(quality, "en")
|
||||
assert chinese == (
|
||||
"D9 证据档位:证据混杂\n"
|
||||
"D9 结构证据指数,不预测婚姻次数、稳定、离婚、伴侣好坏,也不提示需要婚前咨询"
|
||||
)
|
||||
assert english == (
|
||||
"D9 evidence band: Mixed evidence\n"
|
||||
"D9 structural evidence index. It does not predict marriage count, stability, "
|
||||
"divorce, or partner quality, and it does not suggest premarital counseling."
|
||||
)
|
||||
assert "/100" not in chinese
|
||||
assert "/100" not in english
|
||||
|
||||
|
||||
def test_dk_evidence_line_prints_the_zero_to_one_index() -> None:
|
||||
cases = json.loads(
|
||||
Path(ROOT, "references/real_case_calibration/minute_rectification_development_v1.json").read_text(encoding="utf-8")
|
||||
)["cases"]
|
||||
birth = next(case for case in cases if case["case_id"] == "steve_jobs_1955_development")["birth"]
|
||||
year, month, day = (int(part) for part in birth["date"].split("-"))
|
||||
hour, minute = (int(part) for part in birth["time"].split(":"))
|
||||
chart, asc_idx, *_ = compute_chart_data(
|
||||
year, month, day, hour, minute,
|
||||
birth["latitude"], birth["longitude"], birth["timezone_offset"],
|
||||
ayanamsa_name="raman",
|
||||
)
|
||||
longitudes = {name: row["degree_raw"] for name, row in chart["planets"].items()}
|
||||
handler = _api_handler()
|
||||
evidence = handler._compute_relationship_timing_evidence(
|
||||
{},
|
||||
chart["planets"],
|
||||
longitudes,
|
||||
asc_idx,
|
||||
chart["ascendant"]["sign"],
|
||||
)
|
||||
notes = [row["note"] for row in evidence["evidence"] if row.get("label") == "Darakaraka (DK)"]
|
||||
assert len(notes) == 1
|
||||
assert "/100" not in notes[0]
|
||||
assert "婚姻质量指数" in notes[0]
|
||||
assert "(0到1)" in notes[0]
|
||||
score = evidence["darakaraka"]["marriage_quality_score"]
|
||||
assert isinstance(score, float)
|
||||
assert 0.0 <= score <= 1.0
|
||||
assert str(score) in notes[0]
|
||||
@@ -0,0 +1,62 @@
|
||||
"""BUG-1321: the API fallback dasha year matches dasha_analyzer's 365.25 days.
|
||||
|
||||
Upstream 4d543209. The two 365.25636 constants in the API server made the
|
||||
fallback timeline and the years-into-mahadasha figure drift from the analyzer.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
SCRIPTS = os.path.join(os.path.dirname(__file__), "..", "scripts")
|
||||
if SCRIPTS not in sys.path:
|
||||
sys.path.insert(0, SCRIPTS)
|
||||
|
||||
from dasha_analyzer import build_dasha_timeline, lon_to_nakshatra # noqa: E402
|
||||
|
||||
|
||||
def _api_handler():
|
||||
"""Reuse the existing unbound handler helper. Do not add another forgery site."""
|
||||
path = os.path.join(os.path.dirname(__file__), "test_api_server_security.py")
|
||||
spec = importlib.util.spec_from_file_location("_api_security_helpers_dasha", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module._handler()
|
||||
|
||||
|
||||
def test_fallback_timeline_matches_analyzer_to_the_day() -> None:
|
||||
birth = datetime(2000, 1, 1)
|
||||
nak_info, progress, _pada = lon_to_nakshatra(0.0)
|
||||
timeline, _elapsed, _remaining, _lord = build_dasha_timeline("2000-01-01", nak_info, progress)
|
||||
handler = _api_handler()
|
||||
years = timeline[0]["years"]
|
||||
periods = handler._fallback_dasha_periods(birth, "generic", {"years": years * 9})
|
||||
assert periods[0]["lord"] == timeline[0]["lord"]
|
||||
assert periods[0]["start"] == timeline[0]["start"].strftime("%Y-%m-%d")
|
||||
assert periods[0]["end"] == timeline[0]["end"].strftime("%Y-%m-%d")
|
||||
fallback_days = (
|
||||
datetime.strptime(periods[0]["end"], "%Y-%m-%d")
|
||||
- datetime.strptime(periods[0]["start"], "%Y-%m-%d")
|
||||
).days
|
||||
analyzer_days = (timeline[0]["end"].date() - timeline[0]["start"].date()).days
|
||||
assert fallback_days - analyzer_days == 0
|
||||
|
||||
long_periods = handler._fallback_dasha_periods(birth, "generic", {"years": 1422})
|
||||
expected = (birth + timedelta(days=158 * 365.25)).strftime("%Y-%m-%d")
|
||||
stale = (birth + timedelta(days=158 * 365.25636)).strftime("%Y-%m-%d")
|
||||
assert expected != stale
|
||||
assert long_periods[0]["end"] == expected
|
||||
|
||||
|
||||
def test_years_into_mahadasha_uses_the_analyzer_year() -> None:
|
||||
birth = datetime(2000, 1, 1)
|
||||
today = datetime(2003, 1, 1)
|
||||
handler = _api_handler()
|
||||
layer = handler._compute_vimshottari_analysis_layer(birth, 0.0, current_date="2003-01-01")
|
||||
days = (today - birth).days
|
||||
years_into = layer["five_levels"]["mahadasha"]["years_into_period"]
|
||||
assert years_into == days / 365.25
|
||||
assert years_into != days / 365.25636
|
||||
@@ -0,0 +1,58 @@
|
||||
"""BUG-1319: KP house occupancy is cusp-to-cusp and must be declared.
|
||||
|
||||
Upstream 0337b77f. Natal Bhava Chalit stays on the sandhi midpoint rule.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
SCRIPTS = os.path.join(os.path.dirname(__file__), "..", "scripts")
|
||||
if SCRIPTS not in sys.path:
|
||||
sys.path.insert(0, SCRIPTS)
|
||||
|
||||
from bhava_chalit import BhavaChalitCalculator # noqa: E402
|
||||
from kp_system import build_kp_report_pack, calc_kp_analysis # noqa: E402
|
||||
|
||||
|
||||
def test_cusp_to_cusp_differs_from_sandhi_and_default_houses_stay() -> None:
|
||||
calc = BhavaChalitCalculator()
|
||||
planets = {"Sun": 40.0}
|
||||
sandhi = calc.get_bhava_chalit_chart(planets, 15.0, 270.0, "equal")
|
||||
explicit = calc.get_bhava_chalit_chart(
|
||||
planets, 15.0, 270.0, "equal", house_assignment="sandhi",
|
||||
)
|
||||
cusp = calc.get_bhava_chalit_chart(
|
||||
planets, 15.0, 270.0, "equal", house_assignment="cusp_to_cusp",
|
||||
)
|
||||
assert sandhi["house_assignment"] == "sandhi"
|
||||
assert explicit["planets"]["Sun"]["bhava_house"] == sandhi["planets"]["Sun"]["bhava_house"] == 2
|
||||
assert cusp["house_assignment"] == "cusp_to_cusp"
|
||||
assert cusp["planets"]["Sun"]["bhava_house"] == 1
|
||||
assert cusp["planets"]["Sun"]["bhava_house"] != sandhi["planets"]["Sun"]["bhava_house"]
|
||||
|
||||
|
||||
def test_unknown_house_assignment_is_rejected() -> None:
|
||||
calc = BhavaChalitCalculator()
|
||||
with pytest.raises(ValueError, match="house_assignment"):
|
||||
calc.get_bhava_chalit_chart({"Sun": 10.0}, 15.0, 270.0, "equal", house_assignment="whole")
|
||||
|
||||
|
||||
def test_kp_cusps_require_an_occupancy_basis_and_the_report_names_it() -> None:
|
||||
planets = {"Sun": {"sign": "Taurus", "degree": 10.0, "house": 2}}
|
||||
cusps = [15.0 + 30.0 * index for index in range(12)]
|
||||
with pytest.raises(ValueError, match="house_occupancy_basis"):
|
||||
calc_kp_analysis(planets, "Aries", house_cusps=cusps)
|
||||
with pytest.raises(ValueError, match="house_occupancy_basis"):
|
||||
calc_kp_analysis(planets, "Aries", house_cusps=cusps, house_occupancy_basis="")
|
||||
declared = calc_kp_analysis(
|
||||
planets, "Aries", house_cusps=cusps, house_occupancy_basis="cusp_to_cusp",
|
||||
)
|
||||
assert declared["house_occupancy_basis"] == "cusp_to_cusp"
|
||||
proxy = calc_kp_analysis(planets, "Aries")
|
||||
assert proxy["house_occupancy_basis"] == "whole_sign_proxy"
|
||||
markdown = build_kp_report_pack(declared)["exports"]["markdown"]
|
||||
assert "House occupancy basis: `cusp_to_cusp`" in markdown
|
||||
@@ -0,0 +1,72 @@
|
||||
"""BUG-1320: kendra and trikona lord lists do not follow PYTHONHASHSEED.
|
||||
|
||||
Upstream 0337b77f. Two processes with different hash seeds must list the
|
||||
same yoga participants in the same order.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
_PROBE = r"""
|
||||
import json
|
||||
import sys
|
||||
sys.path.insert(0, "scripts")
|
||||
from yoga_engine import YogaContext, YogaEngine, detect_yogas
|
||||
|
||||
planets = {
|
||||
"Sun": {"sign": "Leo", "house": 5, "degree": 10},
|
||||
"Moon": {"sign": "Cancer", "house": 4, "degree": 5},
|
||||
"Mars": {"sign": "Aries", "house": 1, "degree": 8},
|
||||
"Mercury": {"sign": "Virgo", "house": 6, "degree": 12},
|
||||
"Jupiter": {"sign": "Sagittarius", "house": 9, "degree": 15},
|
||||
"Venus": {"sign": "Libra", "house": 7, "degree": 3},
|
||||
"Saturn": {"sign": "Capricorn", "house": 10, "degree": 20},
|
||||
"Rahu": {"sign": "Taurus", "house": 2, "degree": 9},
|
||||
"Ketu": {"sign": "Scorpio", "house": 8, "degree": 9},
|
||||
}
|
||||
rules = "references/yoga_rules.json"
|
||||
engine = YogaEngine(rules)
|
||||
ctx = YogaContext(planets, "Aries")
|
||||
namespace = engine._build_custom_exec_globals(ctx, {}, {})
|
||||
rows = detect_yogas(planets, "Aries", rules)
|
||||
print(json.dumps({
|
||||
"kendra": namespace["kendra_lords_list"](),
|
||||
"trikona": namespace["trikona_lords_list"](),
|
||||
"participants": [
|
||||
[row.get("rule_id"), str(row.get("combination"))]
|
||||
for row in rows
|
||||
],
|
||||
}, ensure_ascii=False))
|
||||
"""
|
||||
|
||||
|
||||
def _run(seed: str) -> dict:
|
||||
env = os.environ.copy()
|
||||
env["PYTHONHASHSEED"] = seed
|
||||
env["PYTHONIOENCODING"] = "utf-8"
|
||||
completed = subprocess.run(
|
||||
[sys.executable, "-c", _PROBE],
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def test_yoga_participants_keep_the_same_order_across_hash_seeds() -> None:
|
||||
first = _run("0")
|
||||
second = _run("1")
|
||||
assert first == second
|
||||
assert first["kendra"] == sorted(first["kendra"])
|
||||
assert first["trikona"] == sorted(first["trikona"])
|
||||
assert first["kendra"] == ["Mars", "Moon", "Saturn", "Venus"]
|
||||
assert first["trikona"] == ["Jupiter", "Mars", "Sun"]
|
||||
Reference in New Issue
Block a user