fix(engine): pair double-transit D1+D9 targets by identity, not name digits (BUG-1061)
Every D9 target name contains the "9" of "D9", so the digit comparison never paired D1 N宫 with D9_N宫 and, for house 9, paired D1 9宫 with any D9 planet target. Targets now carry structured metadata (layer/kind/house/planet); the cross-layer loop pairs only the same house or targets that both point at the D1 house-N lord. Target names and D1/D9/CL layer results are unchanged (D1/CL golden regenerated byte-identical); 48 samples: cross entries 31 -> 38. Evidence-card golden regenerated with the capture script. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
037cd1aa1d
commit
fb543d3849
@@ -1,5 +1,11 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-27 — 双重过运的本命 + 九分盘跨层条目按「同一宫 / 同一宫主」配对(待验收)
|
||||
|
||||
- 双重过运里「木星在本命盘、土星在九分盘(或反过来)同时激活同一件事」的跨层条目,以前按目标名字里的数字配对:九分盘名字都带「D9」的 9,于是本命第 N 宫永远配不上九分盘第 N 宫,查 9 宫时又会和不相干的九分盘目标配上(BUG-1061)。现在按目标的含义配对:本命第 N 宫配九分盘第 N 宫;第 N 宫主配它在九分盘的星座(及它恰好也是九分盘第 N 宫主时的那一项),与以前相同。
|
||||
- 影响:3 张公开盘 + 1 个虚构输入各 12 宫共 48 次核对,跨层条目 31 → 38(新增 8 条同宫条目、删掉 1 条 9 宫误配),7 处摘要句变化;本命、九分盘、月亮上升各层的命中结果不变。报告只看 7 宫,只会多出「本命 7 宫 + 九分盘 7 宫」这一类;普通对话数据卡的双重过运结论条数会随之变化。部署后最多 15 分钟内同一张盘可能仍看到旧结果(chart 缓存)。
|
||||
- Skill 版本不 bump(Skill 文本未改)。不改数据库。
|
||||
|
||||
## 2026-09-27 — 星盘资料加「性别(选填)」,婚恋按性别取夫星 / 妻星(待验收)
|
||||
|
||||
- 本人(设置 → 个人资料)和星盘档案里的每个人都可以选填性别:女 / 男 / 不填。不填不影响任何功能,注册和开场不问。说明只有一句:「用于婚恋解读里判断夫星 / 妻星,不填也能用。」档案详情显示性别,没填写「未填」。
|
||||
|
||||
+27
-9
@@ -14305,24 +14305,42 @@
|
||||
- 对报告的影响:报告里「双重过运」一节的 D9 行会变(目标命中与否、D1+D9 跨层条目数);报告摘要句本轮 48 个样本中无一变化。`frontend/tests/fixtures/report-density-fictional-reader.json` 是 09-23 的冻结快照、没有采集脚本、测试不拿它与现引擎比对,本轮未重生成;按现引擎它的「双重触发条目=2」会变成 4。
|
||||
- 部署注意:API 的 chart 缓存(`scratch/local/api_chart_cache`,15 分钟 TTL,键不含代码版本)在部署后最多 15 分钟内可能仍返回旧的 D9 结果。
|
||||
- 防复发:接入原生函数时逐个核对目标名与实际参与计算的值是否一致;D9 目标集中在一个可单测的函数里。采集数据卡 golden 用 `JYOTISH_API_CHART_CACHE_TTL_SECONDS=0 PYTHONHASHSEED=0`:chart 缓存按排序后的键存盘,热缓存会改变字典与列表顺序(值不变),09-27 的 golden 正是冷热混合顺序,已写进采集脚本说明。
|
||||
- 相关记录:BUG-1061(同函数跨层 D1+D9 判定按数字拼接比对,本轮未修)
|
||||
- 相关记录:BUG-1061(同函数跨层 D1+D9 判定按数字拼接比对,本轮未修;09-27 已另行修复)
|
||||
- 复发自:无
|
||||
- 修复版本:`f6fa367f`,随 `7a06aa00` 部署 staging(run 2976)。
|
||||
|
||||
## BUG-1061 | 双重过运跨层(D1+D9)判定按目标名里的数字比对,D9 前缀的「9」会误配也会漏配
|
||||
|
||||
- 状态:investigating(BUG-1060 审计时发现,本轮不修);产品 2026-09-27 同意修复,排在性别选填单之后
|
||||
- 首次发现 / 最近更新:2026-09-27 / 2026-09-27
|
||||
- 影响面:`scripts/jyotish_engine.py` `cmd_double_transit_pac` 「跨层 Double Transit」段;报告与数据卡的 D1+D9 结论。
|
||||
- 状态:resolved(待部署;本地回归与门禁集通过,分支 `codex/bug-1061-cross-layer-pairing-20260927` 未推送)
|
||||
- 首次发现 / 最近更新:2026-09-27 / 2026-09-27(BUG-1060 审计时发现;产品 2026-09-27 同意修复)
|
||||
- 影响面:`scripts/jyotish_engine.py` `cmd_double_transit_pac` 「跨层 Double Transit」段;报告「双重过运」一节(全读固定 `house = 7`)与普通对话数据卡的双重过运结论(`scripts/consultation_native_layers.py` 跑 1–12 宫)。D1 / D9 / CL 各层命中结果与层内 overlap 条目不受影响。
|
||||
- 现象:跨层配对用 `''.join(c for c in name if c.isdigit())` 比较 D1 与 D9 目标名。D9 目标名都带「D9」,数字串总含「9」:`D9_7宫(...)` → 「97」,永远配不上 D1 的「7宫」(本意的同宫号跨层配对从不发生);而 `house = 9` 时 D1 的「9宫(...)」→「9」会与任何 `{星}_D9(...)` / `D9_{星}(宫主)` 配上。实证:数据卡 golden 里一张公开盘 9 宫有一条 `Jupiter(D1)9宫(Cancer) + Saturn(D9)Mars_D9(Capricorn)`,这里的 `Mars_D9` 是上升主的 D9 星座目标,与 D1 第 9 宫不是同一目标,配上只因两边数字都是「9」。
|
||||
- 触发条件:任意调用都漏配同宫号;`house = 9` 时误配。
|
||||
- 根因:按目标名字符串抽数字判断「同一目标」,未排除层前缀。已核对代码与 golden 一条实例;未评估改法对报告结论数量的影响。
|
||||
- 修复:未修。改法需产品决定(例如按目标类型 + 宫号结构化比对),会改变报告和数据卡的跨层条目。
|
||||
- 验证:—
|
||||
- 防复发:跨层配对不要解析展示用的名字。
|
||||
- 根因:按展示用的目标名字符串抽数字判断「同一目标」,未排除层前缀。修复前实际生效的只有第二条规则(两边目标名都含 D1 第 N 宫主的星名,子串匹配),数字规则只产生过 9 宫误配。
|
||||
- 修复:新增 `_double_transit_target_meta`(给每个 D1 / D9 目标结构化元数据:`layer`、`kind`、`house`、`planet`;名字与目标表逐字相同,`{星}_D9` 两键重合时同名多条)与 `_double_transit_target_identities`(把元数据映射成身份集合);跨层两段循环改为「D1 与 D9 目标身份有交集才配对」,不再解析名字。目标名(字典键)、D1 / D9 / CL 各层结果、条目格式、摘要文案都不变。
|
||||
- 配对规则(逐对决定;依据 `references/marriage-timing-comprehensive-techniques.md` KN Rao「木星 / 土星关联事件宫 / 宫主 / 宫主 D9 星座」与 `strict-workflow-router.md` Double Transit 行;跨层条目回答「木星在一层、土星在另一层是否同时激活第 N 宫这件事的同一个征象点」):
|
||||
|
||||
| D1 目标(kind) | D9 目标(kind) | 决定 | 理由 |
|
||||
| --- | --- | --- | --- |
|
||||
| `N宫`(house) | `D9_N宫`(house) | **配** | 同一宫在两层;原数字规则的本意,修复前从不发生 |
|
||||
| `N宫` | 任何星体目标 | 不配 | 宫位与星体不是同一目标;修复前 `house = 9` 时误配 |
|
||||
| `{宫主}(宫主)`(house_lord) | `{宫主}_D9`(lord_d9_sign) | 配(保持) | KN Rao「宫主 / 宫主 D9 星座」,同一颗星 |
|
||||
| `{宫主}(宫主)` | `D9_{星}(宫主)`(d9_house_lord) | 仅当这颗星就是 D1 第 N 宫主时配(保持) | 同一颗星才是同一征象点;另一颗星担任的 D9 第 N 宫主不在 KN Rao 三点里。试算过「两层第 N 宫主互配」:4 组输入 × 12 宫跨层条目 31 → 59、摘要句变 14 处,属扩大技法口径,未采纳 |
|
||||
| `{上升主}(LL)` / `{对宫主}(对宫主)`(lagna_lord / opposite_lord) | 指向 D1 第 N 宫主的 D9 目标 | 仅当这颗星就是 D1 第 N 宫主时配(保持) | 与修复前子串规则等价 |
|
||||
| `{上升主}(LL)` | `{上升主}_D9`(lagna_lord_d9_sign) | 上升主不是第 N 宫主时不配(保持) | 与第 N 宫无关;试算过同星互配(连同上一行的两层宫主互配一起):同一对 `Saturn(LL) + Saturn_D9` 会在 12 个宫的调用里重复出现,跨层条目 31 → 89、摘要句变 24 处,未采纳 |
|
||||
|
||||
身份集合只有两种:`('house', N)` 与 `('event_house_lord', N)`(目标的星 = D1 第 N 宫主)。
|
||||
- 前后对照(3 张公开 AA 盘 + 报告 golden 的虚构输入,各 12 宫,`PYTHONHASHSEED=0`、chart 缓存 TTL 0):跨层条目 31 → 38(奥巴马 10 → 10、伊丽莎白·泰勒 9 → 10、乔布斯 6 → 9、虚构 6 → 9);变化只有两类:新增 8 条 D1 第 N 宫 ↔ D9 第 N 宫(泰勒 6/12 宫、乔布斯 2/6/7 宫、虚构 1/2/8 宫),删除 1 条 9 宫误配(泰勒 9 宫);所有宫主类条目 48/48 不变。`d1`、`d9`、`cl`、层内条目、`stats` 48/48 不变;摘要句 7/48 变化(6 处「❌ 无」→「⚠️ 跨层间接」,泰勒 9 宫反向)。
|
||||
- 验证:
|
||||
- 新增 `tests/test_double_transit_cross_layer_pairing.py`(8 条,已进快速门 `CORE_PYTEST_TARGETS`),三张公开 AA 盘真实引擎运行:元数据名与命令目标名逐字一致;身份规则单测;乔布斯 7 宫 `Saturn(D1)7宫(Pisces) + Jupiter(D9)D9_7宫(Cancer)` 出现;泰勒 9 宫两个命中仍在但不再配对;每条跨层条目都是同宫或同为 D1 第 N 宫主;凡同宫两层都命中必成条目(公开盘恰为 5 处);修复前的宫主类配对全部保留。在基线引擎上跑:3 failed / 4 errors(新函数不存在)/ 1 passed(快速门登记)。
|
||||
- D1 / CL golden `tests/fixtures/double_transit_d1_cl_golden.json` 按其采集命令重生成,逐字节不变。
|
||||
- 数据卡 golden `frontend/tests/fixtures/consult-evidence-card-golden.json` 用采集脚本(`JYOTISH_API_CHART_CACHE_TTL_SECONDS=0 PYTHONHASHSEED=0`)重生成,两次逐字节相同;与原文件相比只有双重过运子树变化:乔布斯结论 6 → 9(新增 2/6/7 宫同宫条目,`house_7_summary` 由「❌ 无」变「⚠️ 跨层间接」),泰勒 9 → 10(删 9 宫误配、加 6/12 宫),奥巴马不变;family / annual / timing 三路相同。
|
||||
- 对报告 / 数据卡的影响:报告只跑 7 宫,所以只会多出「D1 7 宫 + D9 7 宫」这一类条目(公开盘中乔布斯在参考日命中),9 宫误配从未进报告;报告 golden 虚构输入 7 宫条目数不变(4)。`frontend/tests/fixtures/report-density-fictional-reader.json` 仍是 09-23 冻结快照,未重生成。数据卡按 12 宫汇总,结论条数与「有双重过运的宫」会按上面的对照变化(上限 24 条未触及)。
|
||||
- 部署注意:chart 缓存(15 分钟 TTL,键不含代码版本)部署后最多 15 分钟内可能仍返回旧跨层条目。
|
||||
- 防复发:跨层配对不解析展示用名字;目标的语义放在结构化元数据里,新增目标必须同时登记 kind,`test_meta_names_are_the_command_target_names` 会拦住名字与元数据不一致。
|
||||
- 相关记录:BUG-1060
|
||||
- 复发自:无
|
||||
- 修复版本:待定
|
||||
- 修复版本:待提交(分支 `codex/bug-1061-cross-layer-pairing-20260927`)
|
||||
|
||||
## BUG-1062 | 服务角色读不到「采用日期」三列:账户保存与本人报告 worker 在自托管 PostgreSQL 上会 42501
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# PROGRESS · BUG-1061 双重过运跨层(D1+D9)配对按名字数字比对(2026-09-27)
|
||||
|
||||
- 执行方式:直接执行(产品负责人 2026-09-27 同意修复 BUG-1061,由子代理实现),无单独任务书。
|
||||
- 基线:`origin/staging` = `9aa37197`;分支 `codex/bug-1061-cross-layer-pairing-20260927`,工作树 `.worktrees/bug-1061-cross-layer-pairing-20260927`;未推送。
|
||||
- 开工预检:`python3 scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45` 退出 0,remote_visibility verified,focused tests ok。
|
||||
- 冻结计分清单(ERR-110):`scripts/jyotish_engine.py`、`scripts/run_quality_gate.py` 不在 `sealed_holdout_rerun.py` `PRODUCTION_FILES` 与 holdout v2/v3 `frozen_scoring.files` 里;未动 `scripts/jyotish_api_server.py`、`.gitea/**`、`deploy/**`、迁移。
|
||||
|
||||
## 改了什么
|
||||
|
||||
| 文件 | 改动 |
|
||||
| --- | --- |
|
||||
| `scripts/jyotish_engine.py` | 新增 `_double_transit_target_meta`(D1 / D9 目标的 layer / kind / house / planet)与 `_double_transit_target_identities`;`cmd_double_transit_pac` 跨层两段循环改为身份集合有交集才配对,删掉按名字抽数字的比较。目标名、各层结果、条目格式、摘要文案不变 |
|
||||
| `tests/test_double_transit_cross_layer_pairing.py` | 新增 8 条回归(三张公开 AA 盘真实引擎) |
|
||||
| `scripts/run_quality_gate.py` | 新测试进 `CORE_PYTEST_TARGETS` |
|
||||
| `frontend/tests/fixtures/consult-evidence-card-golden.json` | 用采集脚本重生成(未手改) |
|
||||
|
||||
配对规则逐条决定与理由见 `docs/BUG_HISTORY.md` BUG-1061。一句话:只认「同一宫」(D1 第 N 宫 ↔ D9 第 N 宫)与「同为 D1 第 N 宫主这颗星」(D1 宫主 ↔ 它的 D9 星座,以及它恰好也是 D9 第 N 宫主时的那项)。
|
||||
|
||||
试算过、未采纳的两种扩大口径(4 组输入 × 12 宫):
|
||||
|
||||
| 口径 | 跨层条目 | 摘要句变化 | 不采纳原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| 采纳:同宫 + 同为 D1 第 N 宫主 | 31 → 38 | 7 / 48 | — |
|
||||
| + 两层第 N 宫主互配(不同星) | 31 → 59 | 14 / 48 | 不是同一征象星,KN Rao 三点里没有 D9 宫主;扩大技法口径需产品另定 |
|
||||
| + 上升主与其 D9 星座互配 | 31 → 89 | 24 / 48 | 与第 N 宫无关,同一对在 12 个宫的调用里重复出现 |
|
||||
|
||||
## 前后对照(3 张公开 AA 盘 + 报告 golden 虚构输入,各 12 宫;`PYTHONHASHSEED=0`,chart 缓存 TTL 0)
|
||||
|
||||
| 输入 | 跨层条目 修复前 | 修复后 | 变化 |
|
||||
| --- | --- | --- | --- |
|
||||
| barack_obama | 10 | 10 | 无 |
|
||||
| elizabeth_taylor | 9 | 10 | +6 宫、+12 宫同宫条目;−9 宫误配 `Jupiter(D1)9宫(Cancer) + Saturn(D9)Mars_D9(Capricorn)` |
|
||||
| steve_jobs | 6 | 9 | +2 / 6 / 7 宫同宫条目 |
|
||||
| 虚构输入(报告 golden) | 6 | 9 | +1 / 2 / 8 宫同宫条目(报告跑的 7 宫不变,仍 4 条) |
|
||||
| 合计 | 31 | 38 | +8 / −1;宫主类条目全部不变 |
|
||||
|
||||
`d1`、`d9`、`cl`、层内(D1 / D9 / CL)条目、`stats` 48/48 不变;摘要句 7/48 变化(6 处「❌ 无」→「⚠️ 跨层间接」,泰勒 9 宫反向)。
|
||||
|
||||
## Golden
|
||||
|
||||
- D1 / CL golden:`JYOTISH_API_CHART_CACHE_TTL_SECONDS=0 PYTHONHASHSEED=0 python3 tests/test_double_transit_d9_targets.py --capture` 重生成,逐字节不变。
|
||||
- 数据卡 golden:`JYOTISH_API_CHART_CACHE_TTL_SECONDS=0 PYTHONHASHSEED=0 python3 scripts/research/capture_consult_evidence_card_golden.py`,两次运行逐字节相同。与原文件比,只有 `consultation_native_layers.double_transit` 子树变化(其余逐项相同):乔布斯结论 6 → 9、`house_7_summary`「❌ 无」→「⚠️ 跨层间接」;泰勒 9 → 10;奥巴马不变;family / annual / timing 三路相同。
|
||||
- `report-density-fictional-reader.json`:09-23 冻结快照、无采集脚本,本轮未重生成(报告只跑 7 宫,本轮 7 宫条目数不变)。
|
||||
|
||||
## 测试
|
||||
|
||||
| 项 | 基线 `9aa37197` | 修复后 |
|
||||
| --- | --- | --- |
|
||||
| Python 门禁集(gate-pytest-args,含 growth contract / privacy markers / validation integrity)+ `test_double_transit_d9_targets` + `test_consultation_native_layers` + `test_report_reader_main` + `test_report_density_facts`(修复后另加新文件) | 978 passed / 1 skipped | 986 passed / 1 skipped(+8 新增;同一条 skip) |
|
||||
| `tests/test_double_transit_cross_layer_pairing.py` 在基线引擎上 | 3 failed / 4 errors(新函数不存在)/ 1 passed(快速门登记) | 8 passed |
|
||||
| 前端受影响 5 个文件(evidence-card v1 / v2 / telemetry、projection native / timing) | — | 34/34 pass |
|
||||
| 前端全量(Node 22.14,`tsx --test tests/*.test.ts tests/*.test.tsx`) | 4192 / pass 4138 / fail 24 / skipped 30 / cancelled 0 | 4192 / pass 4138 / fail 24 / skipped 30 / cancelled 0 |
|
||||
|
||||
前端 24 条失败两边按名字逐条相同(含嵌套子测试比对;全部是需要 Docker / PostgreSQL 的数据库与部署套件),新增失败 0。本轮没有修改任何既有断言。前端基线的做法:同一工作树临时换回基线数据卡 golden 跑全量,再换回新 golden(前端只受这份 fixture 影响)。
|
||||
|
||||
## 未做 / 另记
|
||||
|
||||
- 部署后 chart 缓存(15 分钟 TTL,键不含代码版本)可能短暂返回旧跨层条目。
|
||||
- 环境缺口:无 Docker,数据库 / 部署套件以与基线逐条一致的失败清单代替。
|
||||
- 无新增缺陷编号。
|
||||
@@ -36,6 +36,7 @@
|
||||
| 任务书 | 进度 | 主题 | 状态 | 落点 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| —(子代理直接执行,无任务书) | `PROGRESS-bug-1060-d9-double-transit-20260927.md` | **双重过运 D9 目标错配(BUG-1060)**:D9 第 N 宫目标误用 D9 上升、D9 宫主目标误用 D1 度数,改为标签所写的 D9 星座;D1 / CL 层逐字节不变;数据卡 golden 重采集(一张盘跨层结论 9→10);另记 BUG-1061 跨层数字比对(未修) | 已验收(待部署) | `codex/bug-1060-d9-double-transit-20260927`(未推送) |
|
||||
| —(子代理直接执行,无任务书;产品 2026-09-27 授权) | `PROGRESS-bug-1061-cross-layer-pairing-20260927.md` | **双重过运跨层配对按名字数字比对(BUG-1061)**:改为结构化身份配对(D1 第 N 宫↔D9 第 N 宫;D1 第 N 宫主↔同一颗星的 D9 目标);各层结果与 D1/CL golden 逐字节不变;48 样本跨层条目 31→38;数据卡 golden 重采集(乔布斯 6→9、泰勒 9→10) | 待验收 | `codex/bug-1061-cross-layer-pairing-20260927`(未推送) |
|
||||
| `TASK-rectification-fewer-probes-card-20260926.md` | [PROGRESS](PROGRESS-rectification-fewer-probes-card-20260926.md) | **减少无效追问 + 卡片区间为主**:引导补件离线新增达标 0 → 上限 6→2、定向题问完门槛未达直接出卡;卡片区间为主标题、代表分钟副标题,第一二名差距 ≥5 个百分点才显示百分比,否则写「目前区分不开」。不放宽任何置信度。先做 | 已验收(真机清单欠) | `4e6e8d87` + 验收补改 `7203d94e`(区分不开时隐藏「最可能」),deploy-staging run 2933 已部署;回放真值 ±30/±60 19→20、宽度中位 ±1 分钟、提问 11.4→7.8;Skill 10.0.30;真机清单 `docs/testing/rectification-fewer-probes-card-20260926.md` 未走 |
|
||||
| `TASK-rectification-grounding-20260927.md` | `PROGRESS-rectification-grounding-20260927.md` | **生时校正旁白事实有据 + 去重试 + 资料瘦身**:服务端范围句被裁句裁掉(BUG-1055,复发自 588);重试按钮盲写不校验→校正里下线(1056);read-case 超限先删对话记忆(1057);点选题带经历时范围变化无人说(1058);offer/compare 剥离推断前数据、报告去重;Skill 按轮次切片、关 skills 注入 | 已验收(真机欠) | `0e0baaa7`…`2efe2258`;BUG-1055~1058;固定开销 12.0K→6.5K token、offer 100K→28.8K 字节;Skill 未 bump |
|
||||
| `TASK-rectification-telemetry-20260926.md` | [PROGRESS](PROGRESS-rectification-telemetry-20260926.md) | **匿名聚合统计**:每会话一行只存数字 / 枚举(题数分类、宽度、差距、停止原因、门槛达标、耗时、版本),管理后台只看汇总、保留 180 天;动表须 test:db。排在 fewer-probes 后 | 已验收(后台登录走查欠) | `d0bfc1fc`,门禁 run 2938 含真实 PG 测试通过、migrate 2940、deploy 2941(`e801fcf5`);只存数字/枚举、不存用户 id;后台「校正统计」页 |
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -13047,6 +13047,65 @@ def _double_transit_d9_targets(asc_deg, natal, event_house, event_lord, ll_name)
|
||||
}
|
||||
|
||||
|
||||
def _double_transit_target_meta(asc_deg, natal, event_house, event_sign, event_lord, ll_name, opposite_lord):
|
||||
"""Double Transit PAC 的 D1 / D9 目标结构化元数据(BUG-1061)。
|
||||
|
||||
返回 ``{'D1': {目标名: [meta, ...]}, 'D9': {目标名: [meta, ...]}}``,目标名与
|
||||
``cmd_double_transit_pac`` 的 D1 目标表、``_double_transit_d9_targets`` 的键逐字相同;
|
||||
同一个名字可能对应多条 meta(上升主恰是第 N 宫主时 ``{星}_D9`` 两个键重合)。
|
||||
每条 meta:``layer``、``kind``、``house``、``planet``。kind 取值:
|
||||
D1 ``house`` / ``house_lord`` / ``lagna_lord`` / ``opposite_lord``;
|
||||
D9 ``house`` / ``d9_house_lord``(D9 第 N 宫主,落在其 D9 星座)/
|
||||
``lord_d9_sign``(D1 第 N 宫主的 D9 星座)/ ``lagna_lord_d9_sign``(上升主的 D9 星座)。
|
||||
"""
|
||||
d9_asc_idx = _navamsa_idx(asc_deg)
|
||||
d9_event_si = (d9_asc_idx + event_house - 1) % 12
|
||||
d9_event_sign = SIGNS[d9_event_si]
|
||||
d9_event_lord = SIGN_LORDS[d9_event_sign]
|
||||
|
||||
def d9_sign(planet):
|
||||
return SIGNS[_navamsa_idx(natal.get(planet, {}).get('degree', 0))]
|
||||
|
||||
def meta(layer, kind, house, planet=None):
|
||||
return {'layer': layer, 'kind': kind, 'house': house, 'planet': planet}
|
||||
|
||||
layers = {'D1': {}, 'D9': {}}
|
||||
for name, row in (
|
||||
(f'{event_house}宫({event_sign})', meta('D1', 'house', event_house)),
|
||||
(f'{event_lord}(宫主)', meta('D1', 'house_lord', event_house, event_lord)),
|
||||
(f'{ll_name}(LL)', meta('D1', 'lagna_lord', 1, ll_name)),
|
||||
(f'{opposite_lord}(对宫主)', meta('D1', 'opposite_lord', 7, opposite_lord)),
|
||||
(f'D9_{event_house}宫({d9_event_sign})', meta('D9', 'house', event_house)),
|
||||
(f'D9_{d9_event_lord}(宫主)', meta('D9', 'd9_house_lord', event_house, d9_event_lord)),
|
||||
(f'{event_lord}_D9({d9_sign(event_lord)})', meta('D9', 'lord_d9_sign', event_house, event_lord)),
|
||||
(f'{ll_name}_D9({d9_sign(ll_name)})', meta('D9', 'lagna_lord_d9_sign', 1, ll_name)),
|
||||
):
|
||||
layers[row['layer']].setdefault(name, []).append(row)
|
||||
return layers
|
||||
|
||||
|
||||
def _double_transit_target_identities(metas, event_house, event_lord):
|
||||
"""跨层配对用的语义身份集合;D1 与 D9 目标的集合有交集才算「同一目标」(BUG-1061)。
|
||||
|
||||
跨层条目回答的是「木星在一层、土星在另一层,是否同时激活了第 N 宫这件事的同一个
|
||||
征象点」,按 KN Rao「事件宫 / 宫主 / 宫主 D9 星座」只认两种身份,不解析展示用的目标名:
|
||||
- ``('house', N)``:D1 第 N 宫 ↔ D9 第 N 宫(kind ``house``);
|
||||
- ``('event_house_lord', N)``:落点是 D1 第 N 宫主这颗星的目标——D1 的 ``{宫主}(宫主)``
|
||||
(及恰好是同一颗星的 ``(LL)`` / ``(对宫主)``),D9 的 ``{宫主}_D9``(宫主 D9 星座),
|
||||
以及这颗星恰好也是 D9 第 N 宫主时的 ``D9_{宫主}(宫主)``。
|
||||
不配:宫位 ↔ 星体目标;上升主 / 对宫主不是第 N 宫主时(与第 N 宫无关,且同一对会在
|
||||
12 个宫的调用里重复出现);D1 第 N 宫主 ↔ 另一颗星担任的 D9 第 N 宫主(不是同一征象星,
|
||||
KN Rao 三点里也没有 D9 宫主)。
|
||||
"""
|
||||
identities = set()
|
||||
for row in metas:
|
||||
if row['kind'] == 'house':
|
||||
identities.add(('house', row['house']))
|
||||
elif row['planet'] == event_lord:
|
||||
identities.add(('event_house_lord', event_house))
|
||||
return frozenset(identities)
|
||||
|
||||
|
||||
def cmd_double_transit_pac(args):
|
||||
"""Double Transit PAC + D9 层计算"""
|
||||
# 1. 计算本命星盘
|
||||
@@ -13194,12 +13253,16 @@ def cmd_double_transit_pac(args):
|
||||
'strength': 'strong',
|
||||
})
|
||||
|
||||
# 跨层 Double Transit
|
||||
# 跨层 Double Transit:按目标的结构化身份配对,不解析目标名(BUG-1061)
|
||||
target_meta = _double_transit_target_meta(
|
||||
asc_deg, natal, event_house, event_sign, event_lord, ll_name, opposite_lord)
|
||||
d1_ids = {name: _double_transit_target_identities(rows, event_house, event_lord)
|
||||
for name, rows in target_meta['D1'].items()}
|
||||
d9_ids = {name: _double_transit_target_identities(rows, event_house, event_lord)
|
||||
for name, rows in target_meta['D9'].items()}
|
||||
for d1t in jup_d1:
|
||||
for d9t in sat_d9:
|
||||
d1_nums = ''.join(c for c in d1t if c.isdigit())
|
||||
d9_nums = ''.join(c for c in d9t if c.isdigit())
|
||||
if d1_nums == d9_nums or (event_lord in d1t and event_lord in d9t):
|
||||
if d1_ids[d1t] & d9_ids[d9t]:
|
||||
results['double_transit'].append({
|
||||
'layer': 'D1+D9', 'target': f'Jupiter(D1){d1t} + Saturn(D9){d9t}',
|
||||
'jupiter_pac': results['d1']['jupiter'][d1t],
|
||||
@@ -13208,9 +13271,7 @@ def cmd_double_transit_pac(args):
|
||||
})
|
||||
for d1t in sat_d1:
|
||||
for d9t in jup_d9:
|
||||
d1_nums = ''.join(c for c in d1t if c.isdigit())
|
||||
d9_nums = ''.join(c for c in d9t if c.isdigit())
|
||||
if d1_nums == d9_nums or (event_lord in d1t and event_lord in d9t):
|
||||
if d1_ids[d1t] & d9_ids[d9t]:
|
||||
results['double_transit'].append({
|
||||
'layer': 'D1+D9', 'target': f'Saturn(D1){d1t} + Jupiter(D9){d9t}',
|
||||
'jupiter_pac': results['d9']['jupiter'][d9t],
|
||||
|
||||
@@ -104,6 +104,8 @@ CORE_PYTEST_TARGETS = [
|
||||
"tests/test_consultation_native_layers.py",
|
||||
# BUG-1060: Double Transit PAC D9 targets use the D9 signs their labels name.
|
||||
"tests/test_double_transit_d9_targets.py",
|
||||
# BUG-1061: D1+D9 cross-layer entries pair targets by structured identity, not name digits.
|
||||
"tests/test_double_transit_cross_layer_pairing.py",
|
||||
"tests/test_relationship_event_class_evidence.py",
|
||||
"tests/test_thematic_dasha_info_source.py",
|
||||
"tests/test_report_orchestrator_marriage_timing.py",
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
"""BUG-1061: Double Transit PAC cross-layer (D1+D9) entries pair targets by identity, not name digits.
|
||||
|
||||
Before the fix the pairing compared the digits in the target names; every D9
|
||||
name contains the "9" of "D9", so D1 ``7宫`` never met ``D9_7宫`` ("7" vs "97")
|
||||
while D1 ``9宫`` met any ``{planet}_D9`` / ``D9_{planet}(宫主)`` target.
|
||||
|
||||
Real engine runs on the three public AA charts of the evidence-card research
|
||||
(`consult_evidence_card_lib.load_public_charts`, raman ayanamsa, mean nodes,
|
||||
reference date fixed in the library). D1 / D9 / CL layer results are unchanged
|
||||
by this fix; `tests/test_double_transit_d9_targets.py` holds the D1 / CL golden.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
for path in (ROOT, ROOT / "scripts", ROOT / "scripts" / "research"):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
import jyotish_engine as engine # noqa: E402
|
||||
from consult_evidence_card_lib import load_public_charts # noqa: E402
|
||||
|
||||
HOUSES = range(1, 13)
|
||||
|
||||
|
||||
def _args(chart: dict, house: int) -> SimpleNamespace:
|
||||
body = chart["body"]
|
||||
return SimpleNamespace(
|
||||
year=body["year"], month=body["month"], day=body["day"], hour=body["hour"], minute=body["minute"],
|
||||
second=0, lat=body["lat"], lon=body["lon"], tz=float(body["tz"]), node_mode=body["node_mode"],
|
||||
ayanamsa=body["ayanamsa"], date=body["today"], house=house,
|
||||
)
|
||||
|
||||
|
||||
def _context(chart: dict, house: int) -> dict:
|
||||
natal_chart, asc_idx, _, _ = engine._compute_chart_from_args(_args(chart, house))
|
||||
ascendant = natal_chart["ascendant"]
|
||||
asc_deg = ascendant.get("lon", ascendant.get("degree", 0))
|
||||
event_sign = engine.SIGNS[(asc_idx + house - 1) % 12]
|
||||
return {
|
||||
"natal": natal_chart["planets"],
|
||||
"asc_deg": asc_deg,
|
||||
"event_sign": event_sign,
|
||||
"event_lord": engine.SIGN_LORDS[event_sign],
|
||||
"ll_name": engine.SIGN_LORDS[natal_chart["ascendant"]["sign"]],
|
||||
"opposite_lord": engine.SIGN_LORDS[engine.SIGNS[(asc_idx + 6) % 12]],
|
||||
}
|
||||
|
||||
|
||||
def _meta(ctx: dict, house: int) -> dict:
|
||||
return engine._double_transit_target_meta(
|
||||
ctx["asc_deg"], ctx["natal"], house, ctx["event_sign"], ctx["event_lord"], ctx["ll_name"], ctx["opposite_lord"],
|
||||
)
|
||||
|
||||
|
||||
def _cross(result: dict) -> list[tuple[str, str, str, str]]:
|
||||
"""(D1 planet, D1 target, D9 planet, D9 target) for every D1+D9 entry (parsed in the test only)."""
|
||||
rows = []
|
||||
for row in result["double_transit"]:
|
||||
if row["layer"] != "D1+D9":
|
||||
continue
|
||||
left, right = row["target"].split(" + ")
|
||||
d1_planet, d1_target = left.split("(D1)", 1)
|
||||
d9_planet, d9_target = right.split("(D9)", 1)
|
||||
rows.append((d1_planet, d1_target, d9_planet, d9_target))
|
||||
return rows
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def charts() -> list[dict]:
|
||||
return load_public_charts(ROOT)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def runs(charts) -> dict:
|
||||
out = {}
|
||||
for chart in charts:
|
||||
for house in HOUSES:
|
||||
ctx = _context(chart, house)
|
||||
out[(chart["id"], house)] = {
|
||||
"result": engine.cmd_double_transit_pac(_args(chart, house)),
|
||||
"ctx": ctx,
|
||||
"meta": _meta(ctx, house),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def test_quick_gate_runs_this_file() -> None:
|
||||
import run_quality_gate
|
||||
|
||||
assert "tests/test_double_transit_cross_layer_pairing.py" in run_quality_gate.CORE_PYTEST_TARGETS
|
||||
|
||||
|
||||
def test_meta_names_are_the_command_target_names(runs) -> None:
|
||||
for (chart_id, house), run in runs.items():
|
||||
ctx, meta = run["ctx"], run["meta"]
|
||||
d1_names = {
|
||||
f"{house}宫({ctx['event_sign']})", f"{ctx['event_lord']}(宫主)",
|
||||
f"{ctx['ll_name']}(LL)", f"{ctx['opposite_lord']}(对宫主)",
|
||||
}
|
||||
_, d9_targets = engine._double_transit_d9_targets(
|
||||
ctx["asc_deg"], ctx["natal"], house, ctx["event_lord"], ctx["ll_name"],
|
||||
)
|
||||
assert set(meta["D1"]) == d1_names, (chart_id, house)
|
||||
assert set(meta["D9"]) == set(d9_targets), (chart_id, house)
|
||||
result = run["result"]
|
||||
for planet in ("jupiter", "saturn"):
|
||||
assert set(result["d1"][planet]) <= set(meta["D1"]), (chart_id, house)
|
||||
assert set(result["d9"][planet]) <= set(meta["D9"]), (chart_id, house)
|
||||
|
||||
|
||||
def test_identity_rules_on_structured_meta() -> None:
|
||||
def meta(layer, kind, house, planet=None):
|
||||
return [{"layer": layer, "kind": kind, "house": house, "planet": planet}]
|
||||
|
||||
ids = engine._double_transit_target_identities
|
||||
# D1 9th house vs the lagna lord's D9 sign: the BUG-1061 false pair.
|
||||
assert not ids(meta("D1", "house", 9), 9, "Moon") & ids(meta("D9", "lagna_lord_d9_sign", 1, "Mars"), 9, "Moon")
|
||||
# D1 7th house vs D9 7th house: the pair the digit rule never made ("7" vs "97").
|
||||
assert ids(meta("D1", "house", 7), 7, "Venus") & ids(meta("D9", "house", 7), 7, "Venus")
|
||||
# D1 house-N lord vs that lord's D9 sign (KN Rao) and vs the D9 house-N lord when it is the same planet.
|
||||
lord = ids(meta("D1", "house_lord", 7, "Venus"), 7, "Venus")
|
||||
assert lord & ids(meta("D9", "lord_d9_sign", 7, "Venus"), 7, "Venus")
|
||||
assert lord & ids(meta("D9", "d9_house_lord", 7, "Venus"), 7, "Venus")
|
||||
# ... but not when another planet rules D9 house N.
|
||||
assert not lord & ids(meta("D9", "d9_house_lord", 7, "Moon"), 7, "Venus")
|
||||
# Lagna lord that is not the event lord never pairs (it would repeat in all 12 house calls).
|
||||
assert not ids(meta("D1", "lagna_lord", 1, "Mars"), 7, "Venus") & ids(meta("D9", "lagna_lord_d9_sign", 1, "Mars"), 7, "Venus")
|
||||
# A house target never pairs with a planet target.
|
||||
assert not ids(meta("D1", "house", 7), 7, "Venus") & lord
|
||||
|
||||
|
||||
def _public(charts, chart_id: str, house: int) -> tuple[dict, dict]:
|
||||
chart = next(item for item in charts if item["id"] == chart_id)
|
||||
return engine.cmd_double_transit_pac(_args(chart, house)), _context(chart, house)
|
||||
|
||||
|
||||
def test_seventh_house_pairs_d1_house_with_d9_house(charts) -> None:
|
||||
"""Public chart, house 7: Saturn on D1 7宫 and Jupiter on D9_7宫 are one cross-layer entry."""
|
||||
result, _ = _public(charts, "steve_jobs", 7)
|
||||
assert "7宫(Pisces)" in result["d1"]["saturn"]
|
||||
assert "D9_7宫(Cancer)" in result["d9"]["jupiter"]
|
||||
assert ("Saturn", "7宫(Pisces)", "Jupiter", "D9_7宫(Cancer)") in _cross(result)
|
||||
assert result["summary"].startswith("⚠️ 跨层间接")
|
||||
|
||||
|
||||
def test_ninth_house_no_longer_pairs_with_an_unrelated_d9_target(charts) -> None:
|
||||
"""Public chart, house 9: Mars_D9 is the lagna lord's D9 sign, not the 9th house."""
|
||||
result, ctx = _public(charts, "elizabeth_taylor", 9)
|
||||
assert ctx["ll_name"] == "Mars" and ctx["event_lord"] == "Moon"
|
||||
# Both hits are still there; only the pairing changed.
|
||||
assert "9宫(Cancer)" in result["d1"]["jupiter"]
|
||||
assert "Mars_D9(Capricorn)" in result["d9"]["saturn"]
|
||||
assert all(d1_target != "9宫(Cancer)" for _, d1_target, _, _ in _cross(result))
|
||||
assert all(not d9_target.startswith("Mars_D9") for _, _, _, d9_target in _cross(result))
|
||||
|
||||
|
||||
def test_every_cross_entry_joins_the_same_event_significator(runs) -> None:
|
||||
for (chart_id, house), run in runs.items():
|
||||
meta, event_lord = run["meta"], run["ctx"]["event_lord"]
|
||||
for _, d1_target, _, d9_target in _cross(run["result"]):
|
||||
d1_rows, d9_rows = meta["D1"][d1_target], meta["D9"][d9_target]
|
||||
d1_house = any(row["kind"] == "house" for row in d1_rows)
|
||||
d9_house = any(row["kind"] == "house" for row in d9_rows)
|
||||
assert d1_house == d9_house, (chart_id, house, d1_target, d9_target)
|
||||
if d1_house:
|
||||
assert d1_rows[0]["house"] == d9_rows[0]["house"] == house
|
||||
else:
|
||||
assert {row["planet"] for row in d1_rows} == {event_lord}, (chart_id, house, d1_target)
|
||||
assert event_lord in {row["planet"] for row in d9_rows}, (chart_id, house, d9_target)
|
||||
|
||||
|
||||
def test_every_same_house_hit_becomes_a_cross_entry(runs) -> None:
|
||||
same_house = set()
|
||||
for (chart_id, house), run in runs.items():
|
||||
result, meta = run["result"], run["meta"]
|
||||
cross = _cross(result)
|
||||
d1_house = next(name for name, rows in meta["D1"].items() if rows[0]["kind"] == "house")
|
||||
d9_house = next(name for name, rows in meta["D9"].items() if rows[0]["kind"] == "house")
|
||||
for d1_planet, d9_planet in (("jupiter", "saturn"), ("saturn", "jupiter")):
|
||||
if d1_house in result["d1"][d1_planet] and d9_house in result["d9"][d9_planet]:
|
||||
assert (d1_planet.title(), d1_house, d9_planet.title(), d9_house) in cross, (chart_id, house)
|
||||
same_house.add((chart_id, house))
|
||||
# On the library reference date these are the public-chart houses where both hits happen.
|
||||
assert same_house == {("elizabeth_taylor", 6), ("elizabeth_taylor", 12), ("steve_jobs", 2), ("steve_jobs", 6), ("steve_jobs", 7)}
|
||||
|
||||
|
||||
def test_event_lord_pairs_are_kept(runs) -> None:
|
||||
"""The pre-fix rule (both targets name the D1 event lord) is expressed structurally and still pairs."""
|
||||
for (chart_id, house), run in runs.items():
|
||||
result, meta, event_lord = run["result"], run["meta"], run["ctx"]["event_lord"]
|
||||
cross = _cross(result)
|
||||
for d1_planet, d9_planet in (("jupiter", "saturn"), ("saturn", "jupiter")):
|
||||
for d1_target in result["d1"][d1_planet]:
|
||||
if meta["D1"][d1_target][0]["planet"] != event_lord:
|
||||
continue
|
||||
for d9_target in result["d9"][d9_planet]:
|
||||
if event_lord in {row["planet"] for row in meta["D9"][d9_target]}:
|
||||
assert (d1_planet.title(), d1_target, d9_planet.title(), d9_target) in cross, (chart_id, house)
|
||||
Reference in New Issue
Block a user