diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c60d6c7..455515c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 印度占星 Skill 更新日志 +## 2026-09-25 — 新报告正文改为读者版 + +- 新生成的个人长报告正文改为上游读者版,不再把专业参考/审计版当正文,也不再提供那条用户入口。已经生成的报告不重算。 +- 读者版去掉结论等级规则、英文口径段、异常原文、页码和内部标识。KP 与三项过境在网页参数下能算出;年度 Muntha 使用出生上升,年主不再固定成火星。 +- 报告版本 `pl9_personal_long_report.v3`。Skill 版本不 bump:没有改 Skill 包、提示词或技法口径,只改报告正文从哪一版渲染。 +- 真人核对见 `docs/testing/report-reader-main-20260924.md`。本条尚未部署。 + ## 2026-09-25 — 0 年大运、星历切日期、星盘无资料入口(待验收) - Chara 出现 0 年大运时照实显示「0 年」,不占时间轴、不展开子运,不再把整张盘打成「算不出来」。不改引擎口径。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 5ad4e026..34276ce8 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -13598,7 +13598,8 @@ - 现象 / 触发:真实引擎虚构golden仅将LF换成CRLF,正文下载残留22段图盘JSON;新分盘组仍可提取22盘。 - 根因:report-chart-block.ts的FENCE_RE在jyotish-chart语言标记后仅匹配LF,stripReportChartBlocks共用它;旧测试验证LF剥除和CRLF字节等价,没有覆盖含真实围栏的CRLF无JSON。 - 修复:按产品D14有限例外,将语言标记及围栏尾部边界改为匹配`\\r?\\n`;不标准化整篇换行,不改SVG、投影、生成链或下载器。 -- 验证:真实虚构reader golden的LF、CRLF、混合换行均输出0段`jyotish-chart`围栏、22个SVG且`ordinaryOutputLeaks`为空;LF普通导出SHA256固定为`3c52e8bfcfcb3ce3852bd5323867a69bc47d0f9aa821a34692172e7d61c6bcb8`。定向测试已通过;尚无部署health证据。 +- 验证:真实虚构reader golden的LF、CRLF、混合换行均输出0段`jyotish-chart`围栏、22个SVG且`ordinaryOutputLeaks`为空;当时 LF 普通导出 SHA256 为 `3c52e8bfcfcb3ce3852bd5323867a69bc47d0f9aa821a34692172e7d61c6bcb8`。定向测试已通过;尚无部署health证据。 +- 2026-09-25:BUG-1026 把内部词从截断改成整行删除,上述 LF 哈希不再代表当前投影。围栏与 22 个 SVG 的合同仍在。新哈希见 BUG-1026。 - 防复发:report-chart-block与report-export-blocks定向回归覆盖真实golden CRLF/混合输入及独立LF hash锁;禁止只比较两个共同修改后的helper而声称历史输出不变。 - 相关记录:BUG-999/1003;TASK-report-chapter-export-fix-20260924.md。 - 复发自:BUG-999同输出边界的未覆盖换行路径,继承剥离器缺陷,非本轮新增回归。 @@ -13699,3 +13700,49 @@ - 相关记录:BUG-996、BUG-715 - 复发自:无 - 修复版本:本分支,待推送与部署核对 + +## BUG-1026 | 报告正文仍是审计版,投影把内部句截成半句 + +- 状态:investigating(本分支已改,未部署) +- 首次发现 / 最近更新:2026-09-24 / 2026-09-25 +- 影响面:新个人长报告正文、普通投影、旧报告下载后的可见正文。已生成的 Markdown 不回填。 +- 用户现象:正文出现「结论等级规则」、英文 boundary、「producer failed」和异常原文、`full-report://`、页码和「…所有行保持」这种半句,以及删表后留下的说明段。 +- 触发条件:生成或打开个人长报告。正文走专业参考渲染;普通投影只拦聊天键,并在 `parameter_sensitive` 处截断句子。 +- 根因:本仓最后一次引擎同步 `f2241463`(2026-09-03)早于上游读者版 `f3b5196f`(2026-09-09)。`cfcd369d` 把参考/审计版设成正文。本仓又自行加入结论等级规则、英文 boundary,以及把 error/reason 打进不可用小节。投影的 `projectProseLine` 保留内部词之前的半句,`tableIsInternal` 只删表。 +- 修复:新报告请求 `edition: "reader_main"`。读者版从上游 `origin/main` `23b9609e`(含 `f3b5196f`)整段引入 `scripts/pl9_reader_export.py`。缺省 edition 仍是参考版。参考版也去掉上述三处自加泄漏;数据不可用的节整节不输出。投影遇内部词删整行,删表时连同紧邻说明段。报告版本 `pl9_personal_long_report.v3`。旧报告不迁移、不重算。 +- 验证:虚构盘「虚构甲」1991-04-07 09:15 的真实引擎读者版 `ordinaryOutputLeaks` 为 0,并含出生资料、Ashtakavarga、标准大运、KP、Year Lord、Tajika、Muntha。定向 Python 与前端投影测试见 PROGRESS。没有登录浏览器,未声称生成过真实用户报告。 +- 防复发:读者版 golden 与 `ordinaryOutputLeaks` 不得弱化;新增长报告请求体必须带 `edition: "reader_main"`。旧 `report-density-fictional-reader.json` 继续锁旧报告。投影 sha 改动须写明原值锁的是泄漏输出。 +- 相关记录:BUG-999、BUG-1003、BUG-1009、BUG-1020、BUG-1027、BUG-1028 +- 复发自:BUG-999。当时的测试只覆盖聊天导出键(`technique_truth`、`workflow_route` 等)和一份已经泄漏的参考版 sha,没有读者版正文,也不认识 PL9 审计词;截断被 sha 锁成了正确输出。BUG-1003 只清附录,BUG-1009 只改事实表列名。 +- 修复版本:本分支,未推 staging,未部署。 +- sha 三栏:普通投影 `01cc1e03c5adca2833e014156e1162fff12e5cb336e4484c5871859cd878acd7` → `f3aeb8f4b09ba0a78af4ab5e7aee4db32d7499b8e6b1488f2925b35fd91750b9`;LF 下载 `3c52e8bfcfcb3ce3852bd5323867a69bc47d0f9aa821a34692172e7d61c6bcb8` → `b7f6947f6ae6f82e4a972be3c1f2bc50260ed0c5f029847264f40a5ee6ba7b88`。原值锁的是截断后的泄漏正文。 + +## BUG-1027 | KP 与三项 transit 因空实例字典拷贝失败 + +- 状态:investigating(本分支已改,未部署) +- 首次发现 / 最近更新:2026-09-24 / 2026-09-25 +- 影响面:网页全读路径里的 KP,以及 double transit、transit LL7L、行星聚集。CLI 的 argparse 实例不受影响。 +- 用户现象:KP Lord / Sub 节为 blocked,原文是 `'types.SimpleNamespace' object has no attribute 'year'`。 +- 触发条件:`_compute_full_reading_for_thematic` 用 `type('Args', (), {...})()` 把字段放在类上。`cmd_kp` 和 transit 执行 `SimpleNamespace(**vars(args))`,实例字典为空,随后读 `.year` 失败。 +- 根因:`vars()` 不复制类属性。上游 `origin/main` `23b9609e` 的 API 同样用这种 Args,引擎同样用 `vars()`,空结果时不把异常打进读者正文,所以缺陷一直潜伏。 +- 修复:`_copy_call_namespace` 同时读取实例字典和类属性。不改岁差、宫制或 KP 算法。API 主文件未改,类方法数不增加。 +- 验证:`tests/test_report_reader_main.py` 用同样的空实例 Args 调用 `cmd_full_reading`,KP 与三项 transit 的 status 不是 blocked,文本里没有 `has no attribute`。 +- 防复发:该类 Args 的回归留在上述测试。不要再写 `SimpleNamespace(**vars(args))`。 +- 相关记录:BUG-561、BUG-1026 +- 复发自:无。这是网页参数构造和 `vars()` 的潜伏缺陷,不是 BUG-561 的渲染分支。 +- 修复版本:本分支,未推 staging,未部署。 + +## BUG-1028 | 太阳返照漏了出生上升,年主恒为火星 + +- 状态:investigating(本分支已改,未部署) +- 首次发现 / 最近更新:2026-09-24 / 2026-09-25 +- 影响面:Muntha、Year Lord、年度章里的冲突句。 +- 用户现象:Muntha 为 blocked;Year Lord 恒为火星 / 白羊座;年度句出现「当前优先候选指向conflict between vs Capricorn」。 +- 触发条件:`solar_return_full_report` 读取 `birth_asc_sign_idx`,本仓 `calc_solar_return_chart` 的成功字典没有这个键。冲突门用整份字典比较,blocked 与正常值必判冲突;空的左侧对不上冲突正则,落到普通句。 +- 根因:`f2241463` 同步上游时漏了 `scripts/solar_return.py` 里设置 `birth_asc_sign_idx` 的那一行。上游当前 `origin/main` 在返照字典里写入该键。本仓冲突门另外比了整份字典,上游已改成只比星座和主星,但仍会把 blocked 字典算进去;本单按 D4 在一侧 `status == blocked` 时不判冲突。 +- 修复:成功的返照结果写回 `birth_asc_sign_idx`。Muntha 没有星座序号时 Year Lord 标 blocked,不再默认 0(火星)。冲突门跳过 blocked 一侧,只比较星座与主星。 +- 验证:虚构盘 1991-04-07 的 2024/2026/2028 返照都有出生上升序号,Muntha 非 blocked,Year Lord 至少两年不同。一侧 blocked 的 Muntha 不再生成 conflict between。 +- 防复发:`tests/test_report_reader_main.py` 锁住出生上升、两年不同的年主,以及 blocked 不冲突。不要在同步时只合一半太阳返照返回值。 +- 相关记录:BUG-1026;同步提交 `f2241463` +- 复发自:无。这是同步漏合,不是旧 Muntha 测试覆盖过的路径。 +- 修复版本:本分支,未推 staging,未部署。 diff --git a/docs/research/pre_work_error_ledger.md b/docs/research/pre_work_error_ledger.md index 12d581d1..d49a48cf 100644 --- a/docs/research/pre_work_error_ledger.md +++ b/docs/research/pre_work_error_ledger.md @@ -2,6 +2,12 @@ Purpose: read this file before substantial project work. It exists to stop repeat mistakes caused by multiple Codex windows, WorkBuddy mirrors, local drafts, backup folders, and partial cloud-git visibility. +## 2026-09-25 · 读者版同步预检与上游 tip + +- 工作树 `codex/report-reader-main-20260924`,基线 `514951d7`。Python 3.11.7(Anaconda)跑通 `pre_work_check.py`:远端 verified,碎片扫描与适配器诊断成功。focused tests 22 通过 / 1 失败,仍是 `.workbuddy/skills/jyotish-vedic-astrology` 镜像路径断言。没有制造镜像目录,预检整体 fail,不记为通过。 +- 任务书写上游 tip `19195f6e`。`git -C G:\Ferti\yinduzhanxing fetch origin` 后 `origin/main` 是 `23b9609e330dbee70a4725bdc222b125d9b0e39f`,且包含读者版引入提交 `f3b5196f`。读者版按这个 tip 引入,不用 `.workbuddy`。 +- 新的同步漏合见 ERR-109。 + ## 2026-09-24 · 星盘与报告组合版本推送预检 - 产品在获知CRLF合同冲突及构建/真机缺口后授权push到staging。fetch成功,HEAD与origin/staging均为5ff43b47f;写入成功仍须push与ls-remote另核。 @@ -373,3 +379,9 @@ Prevention: 不得把 `vedastro` 当作本地计算能力来源,也不得用 一次 official full snapshot 的请求清单实跑 `_official_full_snapshot_manifest` 数出来是 24 个(`events_overview` 1 + `dasha_all` 1 + `chart_core` 10 + `house_core` 12),fanout 开关 `VEDASTRO_FULL_SNAPSHOT_FANOUT_ENABLED` 的代码默认值就是 `"1"`(第 2520 行)。24 ÷ 5 个每分钟 ≈ 4.8 分钟,期间该进程所有 VedAstro 调用被同一把锁串起来;生产是 2 vCPU 单进程。参照延迟:本机单次 `api.vedastro.org` 往返 0.38 秒,快速模式(fanout 关)一次完整 snapshot 3.65 秒 `status=ok`。生产在国内 VPS,真实 RTT 未测,上述数字只是下限。 Prevention: 前台同步路径不得进入无上限排队,超预算即 fail-fast 降级并让调用方看出是限流而非「服务不可用」;排队总预算必须可配且默认不超过 `VEDASTRO_TIMEOUT_SECONDS`。不得靠调大免费层配额绕过——那是第三方的额度。判断「VedAstro 慢」之前先确认当前是不是无 key 的免费公共模式,以及 fanout 是否开着;两者都属于运行期 env 真相,不能从代码默认值推定。关联 BUG-065、BUG-161、BUG-718、BUG-720。 + +## ERR-109 | 上游读者版与太阳返照出生上升在同步时漏合 | observed 2026-09-25 + +本仓引擎同步停在 `f2241463`(2026-09-03)。上游 `f3b5196f`(2026-09-09)才引入 `reader_main`,`origin/main` 在 2026-09-25 fetch 后是 `23b9609e`,不是任务书写的 `19195f6e`。漏掉的还有 `calc_solar_return_chart` 返回值里的 `birth_asc_sign_idx`(上游 solar_return 成功字典会写这个键)。网页全读用 `type('Args', (), fields)()`,上游同样用 `SimpleNamespace(**vars(args))`,实例字典为空时 KP / transit 读不到 `year`;上游空结果不写进读者正文,所以这个缺陷在上游也潜伏。 + +Prevention: 引擎/上游同步类任务必须 fetch 后核对 `origin/main`,以实际 SHA 为准,并在进度和本台账写明与任务书 tip 的差异。读者版整段引入要带来源提交。不要把 `.workbuddy` 当主仓。同步太阳返照时核对返回键,而不只核对函数还在。关联 BUG-1026、BUG-1027、BUG-1028。 diff --git a/docs/tasks/PROGRESS-report-reader-main-20260924.md b/docs/tasks/PROGRESS-report-reader-main-20260924.md new file mode 100644 index 00000000..bc868439 --- /dev/null +++ b/docs/tasks/PROGRESS-report-reader-main-20260924.md @@ -0,0 +1,46 @@ +# PROGRESS · 报告正文换成读者版(2026-09-25) + +## 结论 + +T1–T7 已在 `codex/report-reader-main-20260924` 完成。没有推 `staging`。没有登录浏览器,没有声称生成过真实用户报告。 + +## 基线与预检 + +- 分支 `codex/report-reader-main-20260924`,开工 HEAD `514951d7`,跟踪 `origin/staging`。 +- 预检:Anaconda Python 3.11.7 运行 `scripts/pre_work_check.py --remote-timeout 8 --command-timeout 45`。远端 `verified`,碎片扫描和适配器诊断成功。focused tests 失败 1 条:`.workbuddy/skills/jyotish-vedic-astrology` 镜像路径不存在。没有制造该目录。预检整体 **fail**,不记通过。这是台账里已有的镜像断言,不是新类错误。 +- 上游:`git -C G:\Ferti\yinduzhanxing fetch origin` 成功。`origin/main` 实际是 `23b9609e330dbee70a4725bdc222b125d9b0e39f`,不是任务书的 `19195f6e`。`f3b5196f` 是其祖先。读者版按 `23b9609e` 引入。没有改上游工作区。 + +## 任务 + +| 项 | 结果 | +| --- | --- | +| T1 读者版 | 完成。`scripts/pl9_reader_export.py` 整段引入 `_render_pl9_user_markdown`、`render_pl9_parity_markdown` 及其读者版闭包。缺省 edition 仍是参考版。`pl9_ai_density` 依赖不在读者版闭包里,按让步 1 未引入。 | +| T2 生成链 | 完成。`personal-report-longform-generate.ts` 请求 `edition: "reader_main"`。版本 `pl9_personal_long_report.v3`。 | +| T3 自加泄漏 | 完成。结论等级规则、英文 boundary、不可用节的原始 error 都不再进入参考版。空数据节整节不输出。 | +| T4 KP / transit | 完成。`_copy_call_namespace` 不再依赖空的 `vars()`。虚构盘 `cmd_full_reading` 上 KP 与三项 transit 不是 blocked。 | +| T5 Muntha / Year Lord | 完成。返照结果写回 `birth_asc_sign_idx`。2024/2026/2028 的 Muntha 与 Year Lord 不恒定。一侧 blocked 不判冲突。 | +| T6 投影 | 完成。内部词删整行;删表连同紧邻说明。新增 PL9 词进入 `ordinaryOutputLeaks`。英文整段用连续 12 个英文单词,避免误伤行星名。读者版 fixture 命中为 0。 | +| T7 记录 | 完成。BUG-1026/1027/1028 仍空,已使用。1023–1025、1030 未占用。 | + +## 断言三栏 + +| 位置 | 原值 | 新值 | 原因 | +| --- | --- | --- | --- | +| `personal-report-density-retry.test.ts` 普通投影 sha | `01cc1e03c5adca2833e014156e1162fff12e5cb336e4484c5871859cd878acd7` | `f3aeb8f4b09ba0a78af4ab5e7aee4db32d7499b8e6b1488f2925b35fd91750b9` | 原 sha 锁的是截断后的泄漏正文 | +| `report-export-blocks.test.ts` LF 下载 sha | `3c52e8bfcfcb3ce3852bd5323867a69bc47d0f9aa821a34692172e7d61c6bcb8` | `b7f6947f6ae6f82e4a972be3c1f2bc50260ed0c5f029847264f40a5ee6ba7b88` | 同上,BUG-1020 当时锁的是投影前的泄漏输出 | +| `test_report_longform_gaps2.py` 空 KP | 必须出现 KP 原始表标题和 `kp_lord_sub_unavailable` | 整节不出现,且没有 `synthetic_kp_failure` | D3。BUG-561 的 blocked 标题把原始 error 打进了正文 | +| `test_report_longform_parity.py` 导航 | 必须含「结论等级规则」 | 该标题不出现 | D3 | + +## 验证 + +- Python:`pytest tests/test_report_reader_main.py tests/test_annual_tajika_pack.py tests/test_professional_report_reference_api.py tests/test_api_server_growth_contract.py tests/test_report_longform_parity.py tests/test_report_longform_gaps2.py tests/test_repo_privacy_markers.py` 通过。`jyotish_api_server.py` 增长合同仍绿(未改该文件)。 +- 前端:`tsc --noEmit` 0 错。`npm run lint` 0 error、121 warnings,均不在本单文件;没有为消 warning 改业务代码。点名测试:投影、导出哈希、读者版 fixture、`edition: "reader_main"` 通过。 +- 快速门:`scripts/run_quality_gate.py --profile quick` 退出 1。编译、JSON、能力/碎片审计、BPHS 18/18,以及门禁里的 Python pytest 名单跑完后进入 `npm test`。`npm test` 在本机失败:Skill 运行时 symlink `EPERM`,以及 Windows 把迁移 symlink 收成普通文件后的 `duplicate migration filename`。这是已知环境缺口,不是本单断言。不把快速门写成通过。没有单独再跑全量 `npm test`,也没有跑 `next build`。 +- Skill 版本不 bump。报告版本升到 v3。 + +## 没做 + +- 未推 staging,未改 workflow,未动 DNS。 +- 未做登录浏览器验收。清单在 `docs/testing/report-reader-main-20260924.md`。 +- 未引入 `pl9_ai_density` 附录。 +- 旧报告不回填。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 8d03016d..99754b06 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -174,7 +174,7 @@ | 任务书 | 进度 | 主题 | 状态 | 落点 | | --- | --- | --- | --- | --- | -| `TASK-report-reader-main-20260924.md` | — | **报告正文换上游读者版 reader_main + KP / Muntha 计算缺陷 + 投影截断(BUG-1026/1027/1028)**:正文一直是上游审计版(读者版 09-09 才出、从未同步),本仓又自加「结论等级规则」/英文段/异常原文;投影黑名单不认 PL9 词汇且截断半句、删表留说明;KP args `vars()` 拷贝为空恒失败;Muntha 漏合 `birth_asc_sign_idx` → Year Lord 恒火星。产品拍板接入读者版、旧报告不回填。引擎类需 pre_work_check;排在 BUG-1020 fix 之后 | 待领取 | — | +| `TASK-report-reader-main-20260924.md` | `PROGRESS-report-reader-main-20260924.md` | **报告正文换上游读者版 reader_main + KP / Muntha 计算缺陷 + 投影截断(BUG-1026/1027/1028)**:正文一直是上游审计版(读者版 09-09 才出、从未同步),本仓又自加「结论等级规则」/英文段/异常原文;投影黑名单不认 PL9 词汇且截断半句、删表留说明;KP args `vars()` 拷贝为空恒失败;Muntha 漏合 `birth_asc_sign_idx` → Year Lord 恒火星。产品拍板接入读者版、旧报告不回填。 | 待验收 | `codex/report-reader-main-20260924` | | `TASK-report-reader-actions-20260924.md` | — | **报告操作收敛**:列表只留「查看报告」;阅读页删「下载原始附录」「打印」,导出改图标按钮;导出对话框 ≥860 居中、窄屏仍贴底(修订 chapter-export D9/D10)。产品变更,不开 BUG 号 | 基本通过(Claude 09-24:入口收敛、图标、≥860 居中均符合;原始附录三文件因执行方权限未删、PROGRESS/清单未写 → 并入修复单) | `f125fae0`(未部署) | | `TASK-report-chapter-export-fix-20260924.md` | `PROGRESS-report-chapter-export-20260924.md` | **CRLF图盘JSON残留(BUG-1020)**:旧剥离器只匹配LF;建议D5对错误围栏残留作有限例外,D13不构成该授权。 | 代码已验收(Claude 09-24),待部署(被同批门禁红阻塞) | `fa8fef55`/`133aca49`(未部署) | | `TASK-report-chapter-export-20260924.md` | `PROGRESS-report-chapter-export-20260924.md` | **我的报告:分块导出 + 中文元信息 / 状态色 / 行内确认删除(BUG-1018/1019)**。旧整篇按钮由抽屉取代;常用 / 全部正文保持 D5 逐字节,所有组全选另附盘和核对表。后续产品裁决 D13:分盘必须带安全 SVG,有限替代原无 SVG 约束;JSON 围栏及内部字段仍禁。原生底部 dialog、760px、70%、860 两列、零额外请求、保留打印 / 原始附录。 | 已验收(Claude 09-24:同上;D13 SVG 与列表页删导出按钮均按产品口径;BUG-1018/1019 resolved;CRLF 见 fix 单) | `1420471a`(staging 已部署);真机清单欠 | diff --git a/docs/testing/report-reader-main-20260924.md b/docs/testing/report-reader-main-20260924.md new file mode 100644 index 00000000..f926ed14 --- /dev/null +++ b/docs/testing/report-reader-main-20260924.md @@ -0,0 +1,10 @@ +# 读者版正文真人核对(2026-09-25) + +没有受控登录浏览器。下面各项不能写成已经通过。请用一个明确虚构或公开的出生时间,重新生成一份报告后再看。不要把旧报告当成新正文。 + +1. 打开新报告正文。不应出现「结论等级规则」、整段英文说明、「This reference export」、`producer failed`、`Traceback`、`object has no attribute`、`full-report://`、`blocked`、`PL9 第 N 页`。 +2. 正文应能读到出生资料、力量 / Ashtakavarga、标准大运、KP、年度 Tajika。没有数据的小节应整节不出现,而不是一行报错。 +3. 看两个不同年份的年度章。Muntha 星座应不同;Year Lord 至少有两年不一样,不应每年都是火星 / 白羊座,也不应出现「conflict between vs」。 +4. KP 节应存在,不应是 `SimpleNamespace` 缺少 `year` 的失败句。 +5. 下载普通 Markdown。不应留下「所有行保持」这种半句,也不应留下孤立的「- `」。图盘仍是安全 SVG,不是 JSON 围栏。 +6. 打开一份这次改动之前生成的旧报告。正文应保持原样,只是下载投影比以前少一些内部词。不要求它变成读者版。 diff --git a/frontend/src/lib/personal-report-longform-generate.ts b/frontend/src/lib/personal-report-longform-generate.ts index c00e0f4c..f779a19f 100644 --- a/frontend/src/lib/personal-report-longform-generate.ts +++ b/frontend/src/lib/personal-report-longform-generate.ts @@ -148,7 +148,7 @@ async function fetchLongformMarkdown(input: Readonly<{ const upstream = await input.fetchImpl(`${input.apiBase.replace(/\/$/, "")}/api/professional_report_reference`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, - body: JSON.stringify({ ...input.payload, include_fact_tables: true }), + body: JSON.stringify({ ...input.payload, include_fact_tables: true, edition: "reader_main" }), cache: "no-store", signal, }); diff --git a/frontend/src/lib/report-public-projection.ts b/frontend/src/lib/report-public-projection.ts index 02336c10..fe0887a3 100644 --- a/frontend/src/lib/report-public-projection.ts +++ b/frontend/src/lib/report-public-projection.ts @@ -184,6 +184,20 @@ export const ORDINARY_OUTPUT_LEAK_PATTERNS: readonly RegExp[] = [ /<\s*object\b/i, /<\s*embed\b/i, /<\s*img\b/i, + /\bblocked\b/i, + /\bexecuted\b/i, + /\bpartial_verified\b/i, + /\bmissing_in_local\b/i, + /\bproducer\b/i, + /\bparity\b/i, + /PL9\s*第\s*\d+\s*页/, + /full-report:\/\//, + /\bpl9_[A-Za-z0-9_]+\b/i, + /\b[A-Za-z][A-Za-z0-9]*(?:_[A-Za-z0-9]+)*_unavailable\b/, + /\b[A-Za-z][A-Za-z0-9]*(?:_[A-Za-z0-9]+)*_(?:pack|packet)\b/i, + /object has no attribute/, + /\bTraceback\b/, + /(?:\b[A-Za-z]{2,}(?:'[A-Za-z]+)?\b\s+){11}\b[A-Za-z]{2,}(?:'[A-Za-z]+)?\b/, ]; export function ordinaryOutputLeaks(text: string): string[] { @@ -319,15 +333,40 @@ function limitationsFromSignals(input: ChatExportInput): string[] { return notes; } +function adjacentInternalTableIndexes(blocks: Block[]): Set { + const drop = new Set(); + blocks.forEach((block, index) => { + if (block.type !== "table" || !tableIsInternal(block.text)) return; + drop.add(index); + let cursor = index - 1; + while (cursor >= 0 && blocks[cursor]?.type === "blank") cursor -= 1; + if (cursor >= 0 && (blocks[cursor]?.type === "paragraph" || blocks[cursor]?.type === "list")) { + drop.add(cursor); + cursor -= 1; + while (cursor >= 0 && blocks[cursor]?.type === "blank") cursor -= 1; + } + if (cursor >= 0 && blocks[cursor]?.type === "heading") drop.add(cursor); + }); + return drop; +} + function analyzeOrdinaryReportMarkdown(markdown: string): Analysis { if (!markdown) return { prose: markdown, limitations: [], changed: false }; const blocks = parseBlocks(markdown); + const dropWithTable = adjacentInternalTableIndexes(blocks); const limitations = new Set(); const kept: string[] = []; let changed = false; let skipUntilLevel: number | null = null; - for (const block of blocks) { + for (let index = 0; index < blocks.length; index += 1) { + const block = blocks[index]; + if (!block) continue; + if (dropWithTable.has(index)) { + changed = true; + collectLimitations(block.raw, limitations); + continue; + } if (block.type === "heading") { const level = block.level ?? 1; if (skipUntilLevel !== null && level <= skipUntilLevel) skipUntilLevel = null; @@ -419,15 +458,11 @@ function projectProseLine(line: string): { keep: boolean; text: string; limitati if (field && isInternalKey(field[1])) { return { keep: false, text: "", limitations: notesForField(field[1], field[2] ?? "") }; } - const tokenAt = line.search(INTERNAL_KEY_TOKEN); - const limitations = tokenAt >= 0 ? notesFromText(line.slice(tokenAt)) : []; - const visible = tokenAt >= 0 ? line.slice(0, tokenAt) : line; - const cleaned = projectInline(visible); - if (!cleaned.trim()) return { keep: false, text: "", limitations }; - if (containsInternalToken(cleaned) || ordinaryOutputLeaks(cleaned).length > 0) { - return { keep: false, text: "", limitations }; + const cleaned = projectInline(line); + if (!cleaned.trim() || containsInternalToken(cleaned) || ordinaryOutputLeaks(cleaned).length > 0) { + return { keep: false, text: "", limitations: notesFromText(line) }; } - return { keep: true, text: cleaned, limitations }; + return { keep: true, text: cleaned, limitations: [] }; } /** Safety-only boundary for the separately authorized raw attachment, not prose projection. */ diff --git a/frontend/tests/fixtures/report-reader-main-fictional.json b/frontend/tests/fixtures/report-reader-main-fictional.json new file mode 100644 index 00000000..581a2aff --- /dev/null +++ b/frontend/tests/fixtures/report-reader-main-fictional.json @@ -0,0 +1,10 @@ +{ + "fixtureProvenance": { + "fictional": true, + "label": "虚构甲", + "birth": "1991-04-07 09:15 Asia/Shanghai-like offset +8, not a real person", + "edition": "reader_main", + "upstream": "23b9609e" + }, + "markdown": "# 印度占星星盘报告\n\n## 出生资料与图盘\n\n- 姓名:-\n- 出生:1991-04-07 09:15\n- 计算口径:lahiri / mean\n\n### Birth Particulars / Hindu Calendar\n\n| Field | Value |\n|-------|-------|\n| 出生日期 | 1991-04-07 |\n| 出生时间 | 09:15 |\n| 纬度 | 30.5728 |\n| 经度 | 104.0668 |\n| 时区 | UTC+8.0 |\n| 岁差 | Lahiri |\n| 岁差名称 | lahiri |\n| 岁差值 | 23.735 |\n| 交点模式 | mean |\n\n| Panchanga | Value |\n|-----------|-------|\n| overall_score | 0.5 |\n| overall_quality | 中(Mixed) |\n| 吉性_count | 3 |\n| total_elements | 6 |\n\n### Birth Chart 行星原始位置\n\n| Planet | R/C | Sign | Degree | Longitude | House | 星宿 | Pada | Speed |\n|--------|-----|------|--------|-----------|-------|-----------|------|-------|\n| 太阳 | C | 双鱼座 | 22.99° | 352.99° | 11 | 雷瓦蒂(Revati) | 2 | 0.983816 |\n| 月亮 | C | 射手座 | 20.50° | 260.50° | 8 | 前阿沙陀(Purva Ashadha) | 3 | 11.838419 |\n| 火星 | C | 双子座 | 8.40° | 68.40° | 2 | 阿尔德拉(Ardra) | 1 | 0.535139 |\n| 水星 | R | 白羊座 | 4.99° | 4.99° | 12 | 阿湿毗尼(Ashwini) | 2 | -0.251851 |\n| 木星 | C | 巨蟹座 | 9.90° | 99.90° | 3 | 普沙(Pushya) | 2 | 0.023622 |\n| 金星 | C | 白羊座 | 29.23° | 29.23° | 12 | 基利提卡(Krittika) | 1 | 1.186233 |\n| 土星 | C | 摩羯座 | 11.82° | 281.82° | 9 | 室罗伐那(Shravana) | 1 | 0.062245 |\n| 北交点 | R | 摩羯座 | 0.31° | 270.31° | 9 | 后阿沙陀(Uttara Ashadha) | 2 | -0.052944 |\n| 南交点 | R | 巨蟹座 | 0.31° | 90.31° | 3 | 普那婆苏(Punarvasu) | 4 | -0.052944 |\n\n#### Birth Chart 星主与状态字段\n\n| Planet | RL | NL | SL | SS | 状态 | SB |\n|--------|----|----|----|----|--------|----|\n| 太阳 | 木星 | 水星 | 月亮 | 水星 | 中性(中性) | 1.86 |\n| 月亮 | 木星 | 金星 | 木星 | 土星 | 入敌(敌对星座) | 1.15 |\n| 火星 | 水星 | 北交点 | 北交点 | 月亮 | 中性(中性) | 1.10 |\n| 水星 | 火星 | 南交点 | 火星 | 木星 | 入友(友好星座) | 1.08 |\n| 木星 | 月亮 | 土星 | 金星 | 水星 | 入旺(入旺) | 1.18 |\n| 金星 | 火星 | 太阳 | 北交点 | 北交点 | 入友(友好星座) | 1.45 |\n| 土星 | 土星 | 月亮 | 火星 | 太阳 | 入庙(入庙) | 1.43 |\n| 北交点 | 土星 | 太阳 | 北交点 | 水星 | 中性(中性) | - |\n| 南交点 | 月亮 | 木星 | 月亮 | 南交点 | 极敌(Great Enemy) | - |\n\n### Chandra Chart / Bhava Spashta - Sripati System\n\n| 月亮盘 | Sign | Degree | House |\n|------------|------|--------|-------|\n| 太阳 | 双鱼座 | 352.99° | 4 |\n| 月亮 | 射手座 | 260.50° | 1 |\n| 火星 | 双子座 | 68.40° | 7 |\n| 水星 | 白羊座 | 4.99° | 5 |\n| 木星 | 巨蟹座 | 99.90° | 8 |\n| 金星 | 白羊座 | 29.23° | 5 |\n| 土星 | 摩羯座 | 281.82° | 2 |\n| 北交点 | 摩羯座 | 270.31° | 2 |\n| 南交点 | 巨蟹座 | 90.31° | 8 |\n\n| Bhava Number | Bhava Arambha / House beginning | Bhava Madhya / Middle of House | Bhava Antya / House ending |\n|--------------|----------------------------------|-------------------------------|-----------------------------|\n| 1 | 20.95° | 金牛座 8.86° | 50.95° |\n| 2 | 50.95° | 双子座 3.05° | 75.15° |\n| 3 | 75.15° | 双子座 27.24° | 99.34° |\n| 4 | 99.34° | 巨蟹座 21.44° | 129.34° |\n| 5 | 129.34° | 狮子座 27.24° | 165.15° |\n| 6 | 165.15° | 天秤座 3.05° | 200.95° |\n| 7 | 200.95° | 天蝎座 8.86° | 230.95° |\n| 8 | 230.95° | 射手座 3.05° | 255.15° |\n| 9 | 255.15° | 射手座 27.24° | 279.34° |\n| 10 | 279.34° | 摩羯座 21.44° | 309.34° |\n| 11 | 309.34° | 水瓶座 27.24° | 345.15° |\n| 12 | 345.15° | 白羊座 3.05° | 20.95° |\n\n### Upagraha / Sub-Planets 点位\n\n| Point | Sign | Degree | Longitude |\n|-------|------|--------|-----------|\n| Kaala | 白羊座 | 7.99° | 7.99° |\n| Mrityu | 双子座 | 1.35° | 61.35° |\n| Artha_Praharaka | 双子座 | 22.65° | 82.65° |\n| Yama_Ghantaka | 巨蟹座 | 12.80° | 102.80° |\n| Gulika | 狮子座 | 13.02° | 133.02° |\n| Maandi | 狮子座 | 23.26° | 143.26° |\n| Dhuma | 狮子座 | 6.33° | 126.33° |\n| Vyatipata | 天蝎座 | 23.67° | 233.67° |\n| Parivesha | 金牛座 | 23.67° | 53.67° |\n| Indrachapa | 水瓶座 | 6.33° | 306.33° |\n| Upaketu | 水瓶座 | 22.99° | 322.99° |\n\n### 特殊上升与派生点\n\n| Point | Sign | Degree | House |\n|-------|------|--------|-------|\n| Bhava_上升 | 狮子座 | 11.35° | 4 |\n| Hora_上升 | 双子座 | 27.61° | 2 |\n| Ghati_上升 | 双子座 | 27.61° | 2 |\n| Arudha_上升 | 双鱼座 | 29.23° | 11 |\n| Upapada_上升 | 狮子座 | 8.40° | 4 |\n| A10_Karma_Pada | 射手座 | 11.82° | 8 |\n\n#### 敏感点与特殊落点\n\n| Point | Sign | Degree | Longitude | Lord |\n|-------|------|--------|-----------|------|\n| bhrigu_bindu | 双子座 | 25.41° | 85.41° | 水星 |\n| sarpa_drekkana / 太阳 | 双鱼座 | 22.99° | 352.99° | Not supplied by source |\n\n### 本命与分盘北印度图盘\n\n##### D1 Rashi\n\n##### 月亮盘\n\n##### Navamsha\n\n##### Bhava (Sripati)\n\n##### D5 Panchamsa\n\n##### D6 Shashthamsa\n\n##### D9 九分盘\n\n##### D10 Dasamsa\n\n##### D2 Hora\n\n##### D12 Dwadashamsa\n\n##### D16 Shodasamsa\n\n##### D20 Vimsamsa\n\n##### D27 Bhamsa\n\n##### D30 Trimsamsa\n\n##### D40 Khavedamsa\n\n##### D45 Akshavedamsa\n\n##### D60 Shashtyamsa\n\n##### D7 Saptamsa\n\n##### D11 Rudramsa\n\n#### D1 Rashi 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 太阳 | 双鱼座 | 22.99° | 352.99° | 11 |\n| 月亮 | 射手座 | 20.50° | 260.50° | 8 |\n| 火星 | 双子座 | 8.40° | 68.40° | 2 |\n| 水星 | 白羊座 | 4.99° | 4.99° | 12 |\n| 木星 | 巨蟹座 | 9.90° | 99.90° | 3 |\n| 金星 | 白羊座 | 29.23° | 29.23° | 12 |\n| 土星 | 摩羯座 | 11.82° | 281.82° | 9 |\n| 北交点 | 摩羯座 | 0.31° | 270.31° | 9 |\n| 南交点 | 巨蟹座 | 0.31° | 90.31° | 3 |\n\n#### D2 Hora 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 巨蟹座 | 17.71° | 107.71° | 1 |\n| 太阳 | 狮子座 | 15.99° | 135.99° | 2 |\n| 月亮 | 巨蟹座 | 11.00° | 101.00° | 1 |\n| 火星 | 狮子座 | 16.81° | 136.81° | 2 |\n| 水星 | 狮子座 | 9.99° | 129.99° | 2 |\n| 木星 | 巨蟹座 | 19.81° | 109.81° | 1 |\n| 金星 | 巨蟹座 | 28.45° | 118.45° | 1 |\n| 土星 | 巨蟹座 | 23.65° | 113.65° | 1 |\n| 北交点 | 巨蟹座 | 0.63° | 90.63° | 1 |\n| 南交点 | 巨蟹座 | 0.63° | 90.63° | 1 |\n\n#### D3 Drekkana 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 金牛座 | 26.57° | 56.57° | 1 |\n| 太阳 | 天蝎座 | 8.98° | 218.98° | 7 |\n| 月亮 | 狮子座 | 1.51° | 121.51° | 4 |\n| 火星 | 双子座 | 25.21° | 85.21° | 2 |\n| 水星 | 白羊座 | 14.98° | 14.98° | 12 |\n| 木星 | 巨蟹座 | 29.71° | 119.71° | 3 |\n| 金星 | 射手座 | 27.68° | 267.68° | 8 |\n| 土星 | 金牛座 | 5.47° | 35.47° | 1 |\n| 北交点 | 摩羯座 | 0.94° | 270.94° | 9 |\n| 南交点 | 巨蟹座 | 0.94° | 90.94° | 3 |\n\n#### D4 Turyamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 狮子座 | 5.42° | 125.42° | 1 |\n| 太阳 | 射手座 | 1.98° | 241.98° | 5 |\n| 月亮 | 双子座 | 22.01° | 82.01° | 11 |\n| 火星 | 处女座 | 3.61° | 153.61° | 2 |\n| 水星 | 白羊座 | 19.97° | 19.97° | 9 |\n| 木星 | 天秤座 | 9.61° | 189.61° | 3 |\n| 金星 | 摩羯座 | 26.91° | 296.91° | 6 |\n| 土星 | 白羊座 | 17.29° | 17.29° | 9 |\n| 北交点 | 摩羯座 | 1.25° | 271.25° | 6 |\n| 南交点 | 巨蟹座 | 1.25° | 91.25° | 12 |\n\n#### D5 Panchamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 水瓶座 | 14.28° | 314.28° | 1 |\n| 太阳 | 水瓶座 | 24.97° | 324.97° | 1 |\n| 月亮 | 双鱼座 | 12.51° | 342.51° | 2 |\n| 火星 | 巨蟹座 | 12.01° | 102.01° | 6 |\n| 水星 | 白羊座 | 24.97° | 24.97° | 3 |\n| 木星 | 白羊座 | 19.51° | 19.51° | 3 |\n| 金星 | 狮子座 | 26.14° | 146.14° | 7 |\n| 土星 | 天秤座 | 29.11° | 209.11° | 9 |\n| 北交点 | 处女座 | 1.56° | 151.56° | 8 |\n| 南交点 | 双鱼座 | 1.56° | 331.56° | 2 |\n\n#### D6 Shashthamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 射手座 | 23.13° | 263.13° | 1 |\n| 太阳 | 摩羯座 | 17.97° | 287.97° | 2 |\n| 月亮 | 白羊座 | 3.01° | 3.01° | 5 |\n| 火星 | 巨蟹座 | 20.42° | 110.42° | 8 |\n| 水星 | 白羊座 | 29.96° | 29.96° | 5 |\n| 木星 | 水瓶座 | 29.42° | 329.42° | 3 |\n| 金星 | 处女座 | 25.36° | 175.36° | 10 |\n| 土星 | 处女座 | 10.94° | 160.94° | 10 |\n| 北交点 | 巨蟹座 | 1.88° | 91.88° | 8 |\n| 南交点 | 摩羯座 | 1.88° | 271.88° | 2 |\n\n#### D7 Saptamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 摩羯座 | 1.99° | 271.99° | 1 |\n| 太阳 | 水瓶座 | 10.96° | 310.96° | 2 |\n| 月亮 | 白羊座 | 23.51° | 23.51° | 4 |\n| 火星 | 巨蟹座 | 28.82° | 118.82° | 7 |\n| 水星 | 金牛座 | 4.95° | 34.95° | 5 |\n| 木星 | 双鱼座 | 9.32° | 339.32° | 3 |\n| 金星 | 天秤座 | 24.59° | 204.59° | 10 |\n| 土星 | 处女座 | 22.76° | 172.76° | 9 |\n| 北交点 | 巨蟹座 | 2.19° | 92.19° | 7 |\n| 南交点 | 摩羯座 | 2.19° | 272.19° | 1 |\n\n#### D8 Ashtamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 双鱼座 | 10.84° | 340.84° | 1 |\n| 太阳 | 金牛座 | 3.96° | 33.96° | 3 |\n| 月亮 | 金牛座 | 14.02° | 44.02° | 3 |\n| 火星 | 狮子座 | 7.22° | 127.22° | 6 |\n| 水星 | 金牛座 | 9.94° | 39.94° | 3 |\n| 木星 | 金牛座 | 19.22° | 49.22° | 3 |\n| 金星 | 天蝎座 | 23.82° | 233.82° | 9 |\n| 土星 | 射手座 | 4.58° | 244.58° | 10 |\n| 北交点 | 处女座 | 2.50° | 152.50° | 7 |\n| 南交点 | 双鱼座 | 2.50° | 332.50° | 1 |\n\n#### D9 九分盘 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 双鱼座 | 19.70° | 349.70° | 1 |\n| 太阳 | 摩羯座 | 26.95° | 296.95° | 11 |\n| 月亮 | 天秤座 | 4.52° | 184.52° | 8 |\n| 火星 | 射手座 | 15.63° | 255.63° | 10 |\n| 水星 | 金牛座 | 14.94° | 44.94° | 3 |\n| 木星 | 处女座 | 29.13° | 179.13° | 7 |\n| 金星 | 射手座 | 23.05° | 263.05° | 10 |\n| 土星 | 白羊座 | 16.41° | 16.41° | 2 |\n| 北交点 | 摩羯座 | 2.82° | 272.82° | 11 |\n| 南交点 | 巨蟹座 | 2.82° | 92.82° | 5 |\n\n#### D10 Dasamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 双鱼座 | 28.55° | 358.55° | 1 |\n| 太阳 | 双子座 | 19.94° | 79.94° | 4 |\n| 月亮 | 双子座 | 25.02° | 85.02° | 4 |\n| 火星 | 狮子座 | 24.03° | 144.03° | 6 |\n| 水星 | 金牛座 | 19.93° | 49.93° | 3 |\n| 木星 | 双子座 | 9.03° | 69.03° | 4 |\n| 金星 | 摩羯座 | 22.27° | 292.27° | 11 |\n| 土星 | 射手座 | 28.23° | 268.23° | 10 |\n| 北交点 | 处女座 | 3.13° | 153.13° | 7 |\n| 南交点 | 双鱼座 | 3.13° | 333.13° | 1 |\n\n#### D11 Rudramsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 白羊座 | 7.41° | 7.41° | 1 |\n| 太阳 | 巨蟹座 | 12.94° | 102.94° | 4 |\n| 月亮 | 巨蟹座 | 15.52° | 105.52° | 4 |\n| 火星 | 处女座 | 2.43° | 152.43° | 6 |\n| 水星 | 金牛座 | 24.92° | 54.92° | 2 |\n| 木星 | 双子座 | 18.93° | 78.93° | 3 |\n| 金星 | 水瓶座 | 21.50° | 321.50° | 11 |\n| 土星 | 摩羯座 | 10.05° | 280.05° | 10 |\n| 北交点 | 处女座 | 3.44° | 153.44° | 6 |\n| 南交点 | 双鱼座 | 3.44° | 333.44° | 12 |\n\n#### D12 Dwadashamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 狮子座 | 16.26° | 136.26° | 1 |\n| 太阳 | 射手座 | 5.93° | 245.93° | 5 |\n| 月亮 | 狮子座 | 6.02° | 126.02° | 1 |\n| 火星 | 处女座 | 10.83° | 160.83° | 2 |\n| 水星 | 金牛座 | 29.92° | 59.92° | 10 |\n| 木星 | 天秤座 | 28.84° | 208.84° | 3 |\n| 金星 | 双鱼座 | 20.73° | 350.73° | 8 |\n| 土星 | 金牛座 | 21.87° | 51.87° | 10 |\n| 北交点 | 摩羯座 | 3.75° | 273.75° | 6 |\n| 南交点 | 巨蟹座 | 3.75° | 93.75° | 12 |\n\n#### D16 Shodasamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 射手座 | 21.68° | 261.68° | 1 |\n| 太阳 | 狮子座 | 7.91° | 127.91° | 9 |\n| 月亮 | 水瓶座 | 28.03° | 328.03° | 3 |\n| 火星 | 狮子座 | 14.44° | 134.44° | 9 |\n| 水星 | 双子座 | 19.89° | 79.89° | 7 |\n| 木星 | 摩羯座 | 8.45° | 278.45° | 2 |\n| 金星 | 巨蟹座 | 17.64° | 107.64° | 8 |\n| 土星 | 水瓶座 | 9.17° | 309.17° | 3 |\n| 北交点 | 狮子座 | 5.01° | 125.01° | 9 |\n| 南交点 | 狮子座 | 5.01° | 125.01° | 9 |\n\n#### D20 Vimsamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 金牛座 | 27.10° | 57.10° | 1 |\n| 太阳 | 双鱼座 | 9.89° | 339.89° | 11 |\n| 月亮 | 金牛座 | 20.04° | 50.04° | 1 |\n| 火星 | 处女座 | 18.06° | 168.06° | 5 |\n| 水星 | 巨蟹座 | 9.86° | 99.86° | 3 |\n| 木星 | 双子座 | 18.06° | 78.06° | 2 |\n| 金星 | 天蝎座 | 14.55° | 224.55° | 7 |\n| 土星 | 巨蟹座 | 26.46° | 116.46° | 3 |\n| 北交点 | 射手座 | 6.26° | 246.26° | 8 |\n| 南交点 | 射手座 | 6.26° | 246.26° | 8 |\n\n#### D24 Siddhamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 水瓶座 | 2.52° | 302.52° | 1 |\n| 太阳 | 摩羯座 | 11.87° | 281.87° | 12 |\n| 月亮 | 射手座 | 12.05° | 252.05° | 11 |\n| 火星 | 水瓶座 | 21.67° | 321.67° | 1 |\n| 水星 | 天蝎座 | 29.83° | 239.83° | 10 |\n| 木星 | 水瓶座 | 27.67° | 327.67° | 1 |\n| 金星 | 巨蟹座 | 11.46° | 101.46° | 6 |\n| 土星 | 白羊座 | 13.75° | 13.75° | 3 |\n| 北交点 | 巨蟹座 | 7.51° | 97.51° | 6 |\n| 南交点 | 巨蟹座 | 7.51° | 97.51° | 6 |\n\n#### D27 Bhamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 金牛座 | 29.09° | 59.09° | 1 |\n| 太阳 | 双子座 | 20.85° | 80.85° | 2 |\n| 月亮 | 天秤座 | 13.55° | 193.55° | 6 |\n| 火星 | 天蝎座 | 16.88° | 226.88° | 7 |\n| 水星 | 狮子座 | 14.81° | 134.81° | 4 |\n| 木星 | 双子座 | 27.38° | 87.38° | 2 |\n| 金星 | 双子座 | 9.14° | 69.14° | 2 |\n| 土星 | 狮子座 | 19.22° | 139.22° | 4 |\n| 北交点 | 天秤座 | 8.45° | 188.45° | 6 |\n| 南交点 | 天秤座 | 8.45° | 188.45° | 6 |\n\n#### D30 Trimsamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 处女座 | 25.66° | 175.66° | 1 |\n| 太阳 | 天蝎座 | 29.83° | 239.83° | 3 |\n| 月亮 | 双子座 | 15.06° | 75.06° | 10 |\n| 火星 | 水瓶座 | 12.08° | 312.08° | 6 |\n| 水星 | 白羊座 | 29.79° | 29.79° | 8 |\n| 木星 | 处女座 | 27.09° | 177.09° | 1 |\n| 金星 | 天秤座 | 6.82° | 186.82° | 2 |\n| 土星 | 处女座 | 24.68° | 174.68° | 1 |\n| 北交点 | 金牛座 | 9.39° | 39.39° | 9 |\n| 南交点 | 金牛座 | 9.39° | 39.39° | 9 |\n\n#### D40 Khavedamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 处女座 | 24.21° | 174.21° | 1 |\n| 太阳 | 白羊座 | 19.78° | 19.78° | 8 |\n| 月亮 | 巨蟹座 | 10.08° | 100.08° | 11 |\n| 火星 | 双鱼座 | 6.11° | 336.11° | 7 |\n| 水星 | 天秤座 | 19.72° | 199.72° | 2 |\n| 木星 | 天蝎座 | 6.12° | 216.12° | 3 |\n| 金星 | 双子座 | 29.10° | 89.10° | 10 |\n| 土星 | 摩羯座 | 22.91° | 292.91° | 5 |\n| 北交点 | 天秤座 | 12.52° | 192.52° | 2 |\n| 南交点 | 天秤座 | 12.52° | 192.52° | 2 |\n\n#### D45 Akshavedamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 天蝎座 | 8.48° | 218.48° | 1 |\n| 太阳 | 狮子座 | 14.75° | 134.75° | 10 |\n| 月亮 | 天秤座 | 22.59° | 202.59° | 12 |\n| 火星 | 白羊座 | 18.13° | 18.13° | 6 |\n| 水星 | 天蝎座 | 14.69° | 224.69° | 1 |\n| 木星 | 射手座 | 25.63° | 265.63° | 2 |\n| 金星 | 天蝎座 | 25.24° | 235.24° | 1 |\n| 土星 | 双鱼座 | 22.03° | 352.03° | 5 |\n| 北交点 | 天秤座 | 14.08° | 194.08° | 12 |\n| 南交点 | 天秤座 | 14.08° | 194.08° | 12 |\n\n#### D60 Shashtyamsa 数值位置表\n\n| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |\n|------|------|----------|------|----------|\n| 上升 | 天蝎座 | 21.31° | 231.31° | 1 |\n| 太阳 | 摩羯座 | 29.67° | 299.67° | 3 |\n| 月亮 | 金牛座 | 0.12° | 30.12° | 7 |\n| 火星 | 天秤座 | 24.17° | 204.17° | 12 |\n| 水星 | 摩羯座 | 29.59° | 299.59° | 3 |\n| 木星 | 双鱼座 | 24.17° | 354.17° | 5 |\n| 金星 | 水瓶座 | 13.65° | 313.65° | 4 |\n| 土星 | 摩羯座 | 19.37° | 289.37° | 3 |\n| 北交点 | 水瓶座 | 18.77° | 318.77° | 4 |\n| 南交点 | 狮子座 | 18.77° | 138.77° | 10 |\n\n## 强度、关系与 Ashtakavarga 技术页\n\n### 六维力量 / Bhava Bala 摘要\n\n- 六维力量 方法:六维力量六重力量(以 Virupa 为单位,综合 Sthana、Dig、Kala、Drik 与 BPHS Chesta)\n- 外部边界:Chesta Bala 的算法口径会影响总分,阅读时以同一口径内比较为主。\n\n| Planet | Sthana | Dig | Kala | Chesta | Naisargika | Drik | Total | Required |\n|--------|--------|-----|------|--------|------------|------|-------|----------|\n| 太阳 | 174.33 | 80.52 | 218.14 | 43.22 | 60.00 | -17.30 | 558.91 | 5.00 |\n| 月亮 | 154.58 | 10.31 | 136.61 | 58.34 | 51.43 | 1.90 | 413.17 | 6.00 |\n| 火星 | 170.28 | 14.34 | 108.38 | 32.46 | 17.14 | -11.66 | 330.94 | 5.00 |\n| 水星 | 126.66 | 71.29 | 224.90 | 23.38 | 25.71 | -18.80 | 453.14 | 7.00 |\n| 木星 | 163.37 | 39.65 | 201.33 | 40.30 | 34.29 | -18.88 | 460.06 | 6.50 |\n| 金星 | 201.14 | 87.40 | 125.26 | 34.97 | 42.86 | -13.83 | 477.80 | 5.50 |\n| 土星 | 207.11 | 80.99 | 102.57 | 22.06 | 8.57 | 7.61 | 428.91 | 5.00 |\n\n### Declination / Kranti / Speed\n\n| Planet | Degree | Declination | Kranti | Speed |\n|--------|--------|-------------|--------|-------|\n| 太阳 | 22.99° | -6.71° | - | 0.983816 |\n| 月亮 | 20.50° | 23.22° | - | 11.838419 |\n| 火星 | 8.40° | 23.97° | - | 0.535139 |\n| 水星 | 4.99° | 11.26° | - | -0.251851 |\n| 木星 | 9.90° | 19.80° | - | 0.023622 |\n| 金星 | 29.23° | 18.94° | - | 1.186233 |\n| 土星 | 11.82° | 19.32° | - | 0.062245 |\n\n### Avastha 行星状态\n\n| Planet | Bala | Jagrat | Deeptadi | Lajjitadi | Shayanadi |\n|--------|------|--------|----------|-----------|-----------|\n| 太阳 | Old | Dreaming | Shanta | Mudita | Kautuka |\n| 月亮 | Old | Dreaming | Shanta | Kshudita | Kautuka |\n| 火星 | Young | Dreaming | Shanta | Mudita | Agama |\n| 水星 | Infant | Deep Sleep | Shanta | Kshudita | Netrapani |\n| 木星 | Young | Deep Sleep | Deepta | Mudita | Nrityalipsa |\n| 金星 | Dead | Deep Sleep | Shanta | Kshudita | Netrapani |\n| 土星 | Young | Deep Sleep | Swastha | Mudita | Nidra |\n| 北交点 | Infant | Deep Sleep | Shanta | Mudita | Nidra |\n| 南交点 | Infant | Deep Sleep | Shanta | Mudita | Nrityalipsa |\n\n### Bhava Bala 十二宫力量\n\n| House | Sign | Lord | Score | 关键因素 |\n|-------|------|------|-------|----------|\n| 1 | 金牛座 | 金星 | 1.50 | 金星 in Apoklima (H12): +1.5 |\n| 2 | 双子座 | 水星 | 0.00 | 水星 in Apoklima (H12): +1.5;火星 in H2 (Malefic): -1.5 |\n| 3 | 巨蟹座 | 月亮 | 3.50 | 月亮 in Panapara (H8): +2.5;木星 in H3 (Benefic): +2 |\n| 4 | 狮子座 | 太阳 | 2.50 | 太阳 in Panapara (H11): +2.5 |\n| 5 | 处女座 | 水星 | 0.00 | 水星 in Apoklima (H12): +1.5;太阳 aspects H5 (7th, Malefic): -1.0 |\n| 6 | 天秤座 | 金星 | 3.50 | 金星 in Apoklima (H12): +1.5;水星 aspects H6 (7th, Benefic): +1.5 |\n| 7 | 天蝎座 | 火星 | 4.00 | 火星 in Panapara (H2): +2.5;木星 aspects H7 (5th, Benefic): +1.5 |\n| 8 | 射手座 | 木星 | 5.00 | 木星 in Apoklima (H3): +1.5;木星 入旺: +4 |\n| 9 | 摩羯座 | 土星 | 3.50 | 土星 in Apoklima (H9): +1.5;土星 in 入庙: +3 |\n| 10 | 水瓶座 | 土星 | 4.50 | 土星 in Apoklima (H9): +1.5;土星 in 入庙: +3 |\n| 11 | 双鱼座 | 木星 | 4.50 | 木星 in Apoklima (H3): +1.5;木星 入旺: +4 |\n| 12 | 白羊座 | 火星 | 6.50 | 火星 in Panapara (H2): +2.5;水星 in H12 (Benefic): +2 |\n\n### Planetary Friendship 矩阵源数据\n\n| Planet | Natural friends | Natural enemies | Temporary relation |\n|--------|-----------------|-----------------|--------------------|\n| 太阳 | 月亮;火星;木星 | 金星;土星 | friends: 月亮;火星;水星;金星;enemies: 木星;南交点/计都 |\n| 月亮 | 太阳;水星 | - | friends: 太阳;土星;北交点/罗睺;enemies: 火星;水星;木星;金星 |\n| 火星 | 太阳;月亮;木星 | 水星 | friends: 太阳;水星;木星;金星;enemies: 月亮;土星;北交点/罗睺 |\n| 水星 | 太阳;金星 | 月亮 | friends: 太阳;火星;木星;土星;enemies: 月亮;金星 |\n| 木星 | 太阳;月亮;火星 | 水星;金星 | friends: 火星;水星;金星;enemies: 太阳;月亮;土星;北交点/罗睺 |\n| 金星 | 水星;土星 | 太阳;月亮 | friends: 太阳;火星;木星;土星;enemies: 月亮;水星 |\n| 土星 | 水星;金星 | 太阳;月亮;火星 | friends: 太阳;月亮;水星;金星;enemies: 火星;木星;北交点/罗睺;南交点/计都 |\n| 北交点 | 金星;土星 | 太阳;月亮;木星 | friends: 太阳;月亮;水星;金星;enemies: 火星;木星;土星;南交点/计都 |\n| 南交点 | 火星;土星 | 太阳;月亮 | friends: 火星;水星;金星;enemies: 太阳;月亮;木星;土星 |\n\n### 相位矩阵\n\n| Planet 1 | Planet 2 | Aspect | Exact degree | Actual difference | Orb | Phase | Orb score |\n|----------|----------|--------|--------------|-------------------|-----|----------|----------|\n| 月亮 | 北交点 | 合相 | 0.00° | 9.81° | 9.81° | unknown_without_velocity | 1.90 |\n| 月亮 | 南交点 | opposition | 180.00° | 170.19° | 9.81° | unknown_without_velocity | 1.90 |\n| 木星 | 土星 | opposition | 180.00° | 178.08° | 1.92° | unknown_without_velocity | 78.70 |\n| 北交点 | 南交点 | opposition | 180.00° | 180.00° | 0.00° | unknown_without_velocity | 100.00 |\n\n| Planet | From house | Target house | Aspect type | Special |\n|--------|------------|--------------|-------------|---------|\n| 太阳 | 11 | 5 | 7宫相位 | False |\n| 月亮 | 8 | 2 | 7宫相位 | False |\n| 火星 | 2 | 5 | 4宫相位 | True |\n| 火星 | 2 | 8 | 7宫相位 | False |\n| 火星 | 2 | 9 | 8宫相位 | True |\n| 水星 | 12 | 6 | 7宫相位 | False |\n| 木星 | 3 | 7 | 5宫相位 | True |\n| 木星 | 3 | 9 | 7宫相位 | False |\n| 木星 | 3 | 11 | 9宫相位 | True |\n| 金星 | 12 | 6 | 7宫相位 | False |\n| 土星 | 9 | 11 | 3宫相位 | True |\n| 土星 | 9 | 3 | 7宫相位 | False |\n| 土星 | 9 | 6 | 10宫相位 | True |\n\n### Jaimini Karaka、Arudha Pada 与特殊 上升\n\n| Karaka | Planet | Degree | Domain |\n|--------|--------|--------|--------|\n| 灵魂星AK | 金星 | 29.23° | 灵魂使命、核心自我、人生最高目标 |\n| 事业星AmK | 太阳 | 22.99° | 事业方向、主要谋士、权力代理 |\n| 兄弟星BK | 月亮 | 20.50° | 兄弟姐妹、勇气、冒险精神 |\n| 母亲星MK | 土星 | 11.82° | 母亲、家庭根基、情感安全感 |\n| 子女星PK | 木星 | 9.90° | 子女、创造力、学生、智能成果 |\n| 障碍星GK | 火星 | 8.40° | 竞争对手、疾病、障碍、转化力量 |\n| 配偶星DK | 水星 | 4.99° | 配偶特质、伴侣关系、婚姻质量 |\n| 父亲星PiK | 北交点 | 0.31° | 父亲、祖先业力、传统传承 |\n\n| Point | Sign | Degree / House |\n|-------|------|----------------|\n| Arudha 上升 (AL) | 双鱼座 | 1 |\n| Dhana Pada (A2) | 水瓶座 | 2 |\n| Vikrama Pada (A3) | 金牛座 | 3 |\n| Sukha Pada (A4) | 天秤座 | 4 |\n| Mantra Pada (A5) | 天蝎座 | 5 |\n| Roga Pada (A6) | 巨蟹座 | 6 |\n| Dara Pada (A7) | 摩羯座 | 7 |\n| Mrityu Pada (A8) | 水瓶座 | 8 |\n| Dharma Pada (A9) | 天秤座 | 9 |\n| Karma Pada (A10) | 射手座 | 10 |\n| Labha Pada (A11) | 天蝎座 | 11 |\n| Upapada (UL) | 狮子座 | 12 |\n| HL | 双鱼座 | - |\n| GL | 双鱼座 | - |\n| PP | 水瓶座 | - |\n| ViL | 水瓶座 | - |\n| VL | 巨蟹座 | - |\n\n### Ashtakavarga SAV / BAV / Pinda\n\n- 方法:Ashtakavarga八分法(BPHS/PVR书例校准v2.1)\n- BAV 核对:all_valid\n\n| Sign | SAV score |\n|------|-----------|\n| 白羊座 | 26 |\n| 金牛座 | 30 |\n| 双子座 | 26 |\n| 巨蟹座 | 24 |\n| 狮子座 | 30 |\n| 处女座 | 29 |\n| 天秤座 | 29 |\n| 天蝎座 | 22 |\n| 射手座 | 27 |\n| 摩羯座 | 29 |\n| 水瓶座 | 35 |\n| 双鱼座 | 30 |\n\n### Ashtakavarga 完整宫位分数\n\n| House | Sign | SAV | BAV/承接 | Full with 上升 |\n|-------|------|-----|----------|-----------------|\n| 1 | 金牛座 | 35 | - | - |\n| 2 | 双子座 | 30 | - | - |\n| 3 | 巨蟹座 | 28 | - | - |\n| 4 | 狮子座 | 34 | - | - |\n| 5 | 处女座 | 31 | - | - |\n| 6 | 天秤座 | 33 | - | - |\n| 7 | 天蝎座 | 28 | - | - |\n| 8 | 射手座 | 30 | - | - |\n| 9 | 摩羯座 | 33 | - | - |\n| 10 | 水瓶座 | 39 | - | - |\n| 11 | 双鱼座 | 34 | - | - |\n| 12 | 白羊座 | 31 | - | - |\n\n| Planet | Total | Expected | Valid | 白羊座 | 金牛座 | 双子座 | 巨蟹座 | 狮子座 | 处女座 | 天秤座 | 天蝎座 | 射手座 | 摩羯座 | 水瓶座 | 双鱼座 |\n|--------|-------|----------|-------|-------|--------|--------|--------|-----|-------|-------|---------|-------------|-----------|----------|--------|\n| 太阳 | 48 | 48 | True | 4 | 2 | 3 | 3 | 3 | 6 | 5 | 3 | 4 | 4 | 5 | 6 |\n| 月亮 | 49 | 49 | True | 3 | 4 | 4 | 5 | 6 | 2 | 7 | 3 | 3 | 4 | 5 | 3 |\n| 火星 | 39 | 39 | True | 3 | 4 | 3 | 4 | 3 | 4 | 3 | 2 | 3 | 3 | 4 | 3 |\n| 水星 | 54 | 54 | True | 4 | 4 | 5 | 5 | 5 | 4 | 3 | 3 | 5 | 5 | 7 | 4 |\n| 木星 | 56 | 56 | True | 5 | 6 | 5 | 3 | 5 | 6 | 4 | 2 | 5 | 7 | 4 | 4 |\n| 金星 | 52 | 52 | True | 5 | 5 | 3 | 3 | 6 | 4 | 3 | 5 | 4 | 4 | 6 | 4 |\n| 土星 | 39 | 39 | True | 2 | 5 | 3 | 1 | 2 | 3 | 4 | 4 | 3 | 2 | 4 | 6 |\n\n### 二十分力量 / Pushkara / Vargottama 辅助强度\n\n| Planet | 二十分力量 | Grade | Pushkara 九分盘 | Pushkara Bhaga | Vargottama |\n|--------|-----------|-------|------------------|----------------|------------|\n| 太阳 | 10.23 | Simhasanamsa | False | False | False |\n| 月亮 | 8.96 | Paravatamsa | False | True | False |\n| 火星 | 8.45 | Paravatamsa | False | False | False |\n| 水星 | 9.09 | Paravatamsa | False | False | False |\n| 木星 | 9.77 | Paravatamsa | False | False | False |\n| 金星 | 8.91 | Paravatamsa | False | False | False |\n| 土星 | 8.71 | Paravatamsa | False | False | False |\n| 北交点 | 8.00 | Paravatamsa | False | False | True |\n| 南交点 | 6.07 | Devalokamsa | True | False | True |\n\n### Ashtakavarga Shodhya / Yoga Pinda 原始值\n\n| Planet | Rashi Pinda | Graha Pinda | Total Pinda | Bindu at own sign |\n|--------|-------------|-------------|-------------|-------------------|\n| 太阳 | 24 | 30 | 54 | 6 |\n| 月亮 | 12 | 15 | 27 | 3 |\n| 火星 | 12 | 24 | 36 | 3 |\n| 水星 | 16 | 20 | 36 | 4 |\n| 木星 | 12 | 30 | 42 | 3 |\n| 金星 | 20 | 35 | 55 | 5 |\n| 土星 | 8 | 10 | 18 | 2 |\n\n## 标准大运表格页\n\n### Vimshottari 大运\n\n- Current 大运:北交点(2023-07-06 - 2041-07-06)\n\n| 序号 | 主运 | 起始 | 结束 | 年数 |\n|------|------|------|------|------|\n| 1 | 金星 | 1980-07-05 | 2000-07-05 | 9.25 | birth_balance |\n| 2 | 太阳 | 2000-07-05 | 2006-07-06 | 6 | native |\n| 3 | 月亮 | 2006-07-06 | 2016-07-05 | 10 | native |\n| 4 | 火星 | 2016-07-05 | 2023-07-06 | 7 | native |\n| 5 | 北交点 | 2023-07-06 | 2041-07-06 | 18 | native |\n| 6 | 木星 | 2041-07-06 | 2057-07-06 | 16 | native |\n| 7 | 土星 | 2057-07-06 | 2076-07-05 | 19 | native |\n| 8 | 水星 | 2076-07-05 | 2093-07-06 | 17 | native |\n| 9 | 南交点 | 2093-07-06 | 2100-07-06 | 7 | native |\n\n| Vimshottari AD | 主运 | 子运 | 起始 | 结束 |\n|--------------|------|------|------|------|\n| 1 | 金星 | 金星 | 1980-07-05 | 1983-11-04 |\n| 2 | 金星 | 太阳 | 1983-11-04 | 1984-11-03 |\n| 3 | 金星 | 月亮 | 1984-11-03 | 1986-07-05 |\n| 4 | 金星 | 火星 | 1986-07-05 | 1987-09-04 |\n| 5 | 金星 | 北交点 | 1987-09-04 | 1990-09-04 |\n| 6 | 金星 | 木星 | 1990-09-04 | 1993-05-05 |\n| 7 | 金星 | 土星 | 1993-05-05 | 1996-07-05 |\n| 8 | 金星 | 水星 | 1996-07-05 | 1999-05-05 |\n| 9 | 金星 | 南交点 | 1999-05-05 | 2000-07-05 |\n| 10 | 太阳 | 太阳 | 2000-07-05 | 2000-10-22 |\n| 11 | 太阳 | 月亮 | 2000-10-22 | 2001-04-23 |\n| 12 | 太阳 | 火星 | 2001-04-23 | 2001-08-29 |\n| 13 | 太阳 | 北交点 | 2001-08-29 | 2002-07-23 |\n| 14 | 太阳 | 木星 | 2002-07-23 | 2003-05-12 |\n| 15 | 太阳 | 土星 | 2003-05-12 | 2004-04-23 |\n| 16 | 太阳 | 水星 | 2004-04-23 | 2005-02-27 |\n| 17 | 太阳 | 南交点 | 2005-02-27 | 2005-07-05 |\n| 18 | 太阳 | 金星 | 2005-07-05 | 2006-07-06 |\n| 19 | 月亮 | 月亮 | 2006-07-06 | 2007-05-06 |\n| 20 | 月亮 | 火星 | 2007-05-06 | 2007-12-05 |\n| 21 | 月亮 | 北交点 | 2007-12-05 | 2009-06-05 |\n| 22 | 月亮 | 木星 | 2009-06-05 | 2010-10-05 |\n| 23 | 月亮 | 土星 | 2010-10-05 | 2012-05-05 |\n| 24 | 月亮 | 水星 | 2012-05-05 | 2013-10-04 |\n| 25 | 月亮 | 南交点 | 2013-10-04 | 2014-05-05 |\n| 26 | 月亮 | 金星 | 2014-05-05 | 2016-01-04 |\n| 27 | 月亮 | 太阳 | 2016-01-04 | 2016-07-05 |\n| 28 | 火星 | 火星 | 2016-07-05 | 2016-12-01 |\n| 29 | 火星 | 北交点 | 2016-12-01 | 2017-12-19 |\n| 30 | 火星 | 木星 | 2017-12-19 | 2018-11-25 |\n| 31 | 火星 | 土星 | 2018-11-25 | 2020-01-04 |\n| 32 | 火星 | 水星 | 2020-01-04 | 2020-12-31 |\n| 33 | 火星 | 南交点 | 2020-12-31 | 2021-05-29 |\n| 34 | 火星 | 金星 | 2021-05-29 | 2022-07-30 |\n| 35 | 火星 | 太阳 | 2022-07-30 | 2022-12-04 |\n| 36 | 火星 | 月亮 | 2022-12-04 | 2023-07-06 |\n| 37 | 北交点 | 北交点 | 2023-07-06 | 2026-03-18 |\n| 38 | 北交点 | 木星 | 2026-03-18 | 2028-08-10 |\n| 39 | 北交点 | 土星 | 2028-08-10 | 2031-06-17 |\n| 40 | 北交点 | 水星 | 2031-06-17 | 2034-01-04 |\n| 41 | 北交点 | 南交点 | 2034-01-04 | 2035-01-22 |\n| 42 | 北交点 | 金星 | 2035-01-22 | 2038-01-22 |\n| 43 | 北交点 | 太阳 | 2038-01-22 | 2038-12-17 |\n| 44 | 北交点 | 月亮 | 2038-12-17 | 2040-06-17 |\n| 45 | 北交点 | 火星 | 2040-06-17 | 2041-07-06 |\n| 46 | 木星 | 木星 | 2041-07-06 | 2043-08-24 |\n| 47 | 木星 | 土星 | 2043-08-24 | 2046-03-06 |\n| 48 | 木星 | 水星 | 2046-03-06 | 2048-06-11 |\n| 49 | 木星 | 南交点 | 2048-06-11 | 2049-05-18 |\n| 50 | 木星 | 金星 | 2049-05-18 | 2052-01-17 |\n| 51 | 木星 | 太阳 | 2052-01-17 | 2052-11-04 |\n| 52 | 木星 | 月亮 | 2052-11-04 | 2054-03-06 |\n| 53 | 木星 | 火星 | 2054-03-06 | 2055-02-10 |\n| 54 | 木星 | 北交点 | 2055-02-10 | 2057-07-06 |\n| 55 | 土星 | 土星 | 2057-07-06 | 2060-07-08 |\n| 56 | 土星 | 水星 | 2060-07-08 | 2063-03-18 |\n| 57 | 土星 | 南交点 | 2063-03-18 | 2064-04-26 |\n| 58 | 土星 | 金星 | 2064-04-26 | 2067-06-26 |\n| 59 | 土星 | 太阳 | 2067-06-26 | 2068-06-07 |\n| 60 | 土星 | 月亮 | 2068-06-07 | 2070-01-07 |\n| 61 | 土星 | 火星 | 2070-01-07 | 2071-02-15 |\n| 62 | 土星 | 北交点 | 2071-02-15 | 2073-12-22 |\n| 63 | 土星 | 木星 | 2073-12-22 | 2076-07-05 |\n| 64 | 水星 | 水星 | 2076-07-05 | 2078-12-01 |\n| 65 | 水星 | 南交点 | 2078-12-01 | 2079-11-29 |\n| 66 | 水星 | 金星 | 2079-11-29 | 2082-09-29 |\n| 67 | 水星 | 太阳 | 2082-09-29 | 2083-08-05 |\n| 68 | 水星 | 月亮 | 2083-08-05 | 2085-01-04 |\n| 69 | 水星 | 火星 | 2085-01-04 | 2086-01-01 |\n| 70 | 水星 | 北交点 | 2086-01-01 | 2088-07-20 |\n| 71 | 水星 | 木星 | 2088-07-20 | 2090-10-26 |\n| 72 | 水星 | 土星 | 2090-10-26 | 2093-07-06 |\n| 73 | 南交点 | 南交点 | 2093-07-06 | 2093-12-02 |\n| 74 | 南交点 | 金星 | 2093-12-02 | 2095-02-01 |\n| 75 | 南交点 | 太阳 | 2095-02-01 | 2095-06-08 |\n| 76 | 南交点 | 月亮 | 2095-06-08 | 2096-01-07 |\n| 77 | 南交点 | 火星 | 2096-01-07 | 2096-06-05 |\n| 78 | 南交点 | 北交点 | 2096-06-05 | 2097-06-23 |\n| 79 | 南交点 | 木星 | 2097-06-23 | 2098-05-30 |\n| 80 | 南交点 | 土星 | 2098-05-30 | 2099-07-08 |\n| 81 | 南交点 | 水星 | 2099-07-08 | 2100-07-06 |\n\n| Vimshottari PD | 主运 | 子运 | 次子运 | 起始 | 结束 |\n|--------------|------|------|--------|------|------|\n| 1 | 金星 | 金星 | 金星 | 1980-07-05 | 1981-01-23 |\n| 2 | 金星 | 金星 | 太阳 | 1981-01-23 | 1981-03-25 |\n| 3 | 金星 | 金星 | 月亮 | 1981-03-25 | 1981-07-05 |\n| 4 | 金星 | 金星 | 火星 | 1981-07-05 | 1981-09-14 |\n| 5 | 金星 | 金星 | 北交点 | 1981-09-14 | 1982-03-15 |\n| 6 | 金星 | 金星 | 木星 | 1982-03-15 | 1982-08-25 |\n| 7 | 金星 | 金星 | 土星 | 1982-08-25 | 1983-03-06 |\n| 8 | 金星 | 金星 | 水星 | 1983-03-06 | 1983-08-25 |\n| 9 | 金星 | 金星 | 南交点 | 1983-08-25 | 1983-11-04 |\n| 10 | 金星 | 太阳 | 太阳 | 1983-11-04 | 1983-11-22 |\n| 11 | 金星 | 太阳 | 月亮 | 1983-11-22 | 1983-12-23 |\n| 12 | 金星 | 太阳 | 火星 | 1983-12-23 | 1984-01-13 |\n| 13 | 金星 | 太阳 | 北交点 | 1984-01-13 | 1984-03-08 |\n| 14 | 金星 | 太阳 | 木星 | 1984-03-08 | 1984-04-25 |\n| 15 | 金星 | 太阳 | 土星 | 1984-04-25 | 1984-06-22 |\n| 16 | 金星 | 太阳 | 水星 | 1984-06-22 | 1984-08-13 |\n| 17 | 金星 | 太阳 | 南交点 | 1984-08-13 | 1984-09-03 |\n| 18 | 金星 | 太阳 | 金星 | 1984-09-03 | 1984-11-03 |\n| 19 | 金星 | 月亮 | 月亮 | 1984-11-03 | 1984-12-24 |\n| 20 | 金星 | 月亮 | 火星 | 1984-12-24 | 1985-01-28 |\n| 21 | 金星 | 月亮 | 北交点 | 1985-01-28 | 1985-04-30 |\n| 22 | 金星 | 月亮 | 木星 | 1985-04-30 | 1985-07-20 |\n| 23 | 金星 | 月亮 | 土星 | 1985-07-20 | 1985-10-24 |\n| 24 | 金星 | 月亮 | 水星 | 1985-10-24 | 1986-01-19 |\n| 25 | 金星 | 月亮 | 南交点 | 1986-01-19 | 1986-02-23 |\n| 26 | 金星 | 月亮 | 金星 | 1986-02-23 | 1986-06-05 |\n| 27 | 金星 | 月亮 | 太阳 | 1986-06-05 | 1986-07-05 |\n| 28 | 金星 | 火星 | 火星 | 1986-07-05 | 1986-07-30 |\n| 29 | 金星 | 火星 | 北交点 | 1986-07-30 | 1986-10-02 |\n| 30 | 金星 | 火星 | 木星 | 1986-10-02 | 1986-11-28 |\n| 31 | 金星 | 火星 | 土星 | 1986-11-28 | 1987-02-03 |\n| 32 | 金星 | 火星 | 水星 | 1987-02-03 | 1987-04-04 |\n| 33 | 金星 | 火星 | 南交点 | 1987-04-04 | 1987-04-29 |\n| 34 | 金星 | 火星 | 金星 | 1987-04-29 | 1987-07-09 |\n| 35 | 金星 | 火星 | 太阳 | 1987-07-09 | 1987-07-31 |\n| 36 | 金星 | 火星 | 月亮 | 1987-07-31 | 1987-09-04 |\n| 37 | 金星 | 北交点 | 北交点 | 1987-09-04 | 1988-02-15 |\n| 38 | 金星 | 北交点 | 木星 | 1988-02-15 | 1988-07-11 |\n| 39 | 金星 | 北交点 | 土星 | 1988-07-11 | 1988-12-31 |\n| 40 | 金星 | 北交点 | 水星 | 1988-12-31 | 1989-06-04 |\n| 41 | 金星 | 北交点 | 南交点 | 1989-06-04 | 1989-08-07 |\n| 42 | 金星 | 北交点 | 金星 | 1989-08-07 | 1990-02-06 |\n| 43 | 金星 | 北交点 | 太阳 | 1990-02-06 | 1990-04-02 |\n| 44 | 金星 | 北交点 | 月亮 | 1990-04-02 | 1990-07-02 |\n| 45 | 金星 | 北交点 | 火星 | 1990-07-02 | 1990-09-04 |\n| 46 | 金星 | 木星 | 木星 | 1990-09-04 | 1991-01-12 |\n| 47 | 金星 | 木星 | 土星 | 1991-01-12 | 1991-06-15 |\n| 48 | 金星 | 木星 | 水星 | 1991-06-15 | 1991-10-31 |\n| 49 | 金星 | 木星 | 南交点 | 1991-10-31 | 1991-12-27 |\n| 50 | 金星 | 木星 | 金星 | 1991-12-27 | 1992-06-06 |\n| 51 | 金星 | 木星 | 太阳 | 1992-06-06 | 1992-07-25 |\n| 52 | 金星 | 木星 | 月亮 | 1992-07-25 | 1992-10-14 |\n| 53 | 金星 | 木星 | 火星 | 1992-10-14 | 1992-12-10 |\n| 54 | 金星 | 木星 | 北交点 | 1992-12-10 | 1993-05-05 |\n| 55 | 金星 | 土星 | 土星 | 1993-05-05 | 1993-11-04 |\n| 56 | 金星 | 土星 | 水星 | 1993-11-04 | 1994-04-17 |\n| 57 | 金星 | 土星 | 南交点 | 1994-04-17 | 1994-06-23 |\n| 58 | 金星 | 土星 | 金星 | 1994-06-23 | 1995-01-02 |\n| 59 | 金星 | 土星 | 太阳 | 1995-01-02 | 1995-03-01 |\n| 60 | 金星 | 土星 | 月亮 | 1995-03-01 | 1995-06-05 |\n| 61 | 金星 | 土星 | 火星 | 1995-06-05 | 1995-08-12 |\n| 62 | 金星 | 土星 | 北交点 | 1995-08-12 | 1996-02-01 |\n| 63 | 金星 | 土星 | 木星 | 1996-02-01 | 1996-07-05 |\n| 64 | 金星 | 水星 | 水星 | 1996-07-05 | 1996-11-28 |\n| 65 | 金星 | 水星 | 南交点 | 1996-11-28 | 1997-01-27 |\n| 66 | 金星 | 水星 | 金星 | 1997-01-27 | 1997-07-19 |\n| 67 | 金星 | 水星 | 太阳 | 1997-07-19 | 1997-09-09 |\n| 68 | 金星 | 水星 | 月亮 | 1997-09-09 | 1997-12-04 |\n| 69 | 金星 | 水星 | 火星 | 1997-12-04 | 1998-02-02 |\n| 70 | 金星 | 水星 | 北交点 | 1998-02-02 | 1998-07-08 |\n| 71 | 金星 | 水星 | 木星 | 1998-07-08 | 1998-11-23 |\n| 72 | 金星 | 水星 | 土星 | 1998-11-23 | 1999-05-05 |\n| 73 | 金星 | 南交点 | 南交点 | 1999-05-05 | 1999-05-30 |\n| 74 | 金星 | 南交点 | 金星 | 1999-05-30 | 1999-08-09 |\n| 75 | 金星 | 南交点 | 太阳 | 1999-08-09 | 1999-08-31 |\n| 76 | 金星 | 南交点 | 月亮 | 1999-08-31 | 1999-10-05 |\n| 77 | 金星 | 南交点 | 火星 | 1999-10-05 | 1999-10-30 |\n| 78 | 金星 | 南交点 | 北交点 | 1999-10-30 | 2000-01-02 |\n| 79 | 金星 | 南交点 | 木星 | 2000-01-02 | 2000-02-28 |\n| 80 | 金星 | 南交点 | 土星 | 2000-02-28 | 2000-05-05 |\n| 81 | 金星 | 南交点 | 水星 | 2000-05-05 | 2000-07-05 |\n| 82 | 太阳 | 太阳 | 太阳 | 2000-07-05 | 2000-07-10 |\n| 83 | 太阳 | 太阳 | 月亮 | 2000-07-10 | 2000-07-19 |\n| 84 | 太阳 | 太阳 | 火星 | 2000-07-19 | 2000-07-26 |\n| 85 | 太阳 | 太阳 | 北交点 | 2000-07-26 | 2000-08-11 |\n| 86 | 太阳 | 太阳 | 木星 | 2000-08-11 | 2000-08-26 |\n| 87 | 太阳 | 太阳 | 土星 | 2000-08-26 | 2000-09-12 |\n| 88 | 太阳 | 太阳 | 水星 | 2000-09-12 | 2000-09-27 |\n| 89 | 太阳 | 太阳 | 南交点 | 2000-09-27 | 2000-10-04 |\n| 90 | 太阳 | 太阳 | 金星 | 2000-10-04 | 2000-10-22 |\n| 91 | 太阳 | 月亮 | 月亮 | 2000-10-22 | 2000-11-06 |\n| 92 | 太阳 | 月亮 | 火星 | 2000-11-06 | 2000-11-17 |\n| 93 | 太阳 | 月亮 | 北交点 | 2000-11-17 | 2000-12-14 |\n| 94 | 太阳 | 月亮 | 木星 | 2000-12-14 | 2001-01-08 |\n| 95 | 太阳 | 月亮 | 土星 | 2001-01-08 | 2001-02-06 |\n| 96 | 太阳 | 月亮 | 水星 | 2001-02-06 | 2001-03-04 |\n| 97 | 太阳 | 月亮 | 南交点 | 2001-03-04 | 2001-03-14 |\n| 98 | 太阳 | 月亮 | 金星 | 2001-03-14 | 2001-04-14 |\n| 99 | 太阳 | 月亮 | 太阳 | 2001-04-14 | 2001-04-23 |\n| 100 | 太阳 | 火星 | 火星 | 2001-04-23 | 2001-04-30 |\n| 101 | 太阳 | 火星 | 北交点 | 2001-04-30 | 2001-05-19 |\n| 102 | 太阳 | 火星 | 木星 | 2001-05-19 | 2001-06-05 |\n| 103 | 太阳 | 火星 | 土星 | 2001-06-05 | 2001-06-26 |\n| 104 | 太阳 | 火星 | 水星 | 2001-06-26 | 2001-07-14 |\n| 105 | 太阳 | 火星 | 南交点 | 2001-07-14 | 2001-07-21 |\n| 106 | 太阳 | 火星 | 金星 | 2001-07-21 | 2001-08-12 |\n| 107 | 太阳 | 火星 | 太阳 | 2001-08-12 | 2001-08-18 |\n| 108 | 太阳 | 火星 | 月亮 | 2001-08-18 | 2001-08-29 |\n| 109 | 太阳 | 北交点 | 北交点 | 2001-08-29 | 2001-10-17 |\n| 110 | 太阳 | 北交点 | 木星 | 2001-10-17 | 2001-11-30 |\n| 111 | 太阳 | 北交点 | 土星 | 2001-11-30 | 2002-01-21 |\n| 112 | 太阳 | 北交点 | 水星 | 2002-01-21 | 2002-03-08 |\n| 113 | 太阳 | 北交点 | 南交点 | 2002-03-08 | 2002-03-28 |\n| 114 | 太阳 | 北交点 | 金星 | 2002-03-28 | 2002-05-21 |\n| 115 | 太阳 | 北交点 | 太阳 | 2002-05-21 | 2002-06-07 |\n| 116 | 太阳 | 北交点 | 月亮 | 2002-06-07 | 2002-07-04 |\n| 117 | 太阳 | 北交点 | 火星 | 2002-07-04 | 2002-07-23 |\n| 118 | 太阳 | 木星 | 木星 | 2002-07-23 | 2002-08-31 |\n| 119 | 太阳 | 木星 | 土星 | 2002-08-31 | 2002-10-17 |\n| 120 | 太阳 | 木星 | 水星 | 2002-10-17 | 2002-11-27 |\n| 121 | 太阳 | 木星 | 南交点 | 2002-11-27 | 2002-12-14 |\n| 122 | 太阳 | 木星 | 金星 | 2002-12-14 | 2003-02-01 |\n| 123 | 太阳 | 木星 | 太阳 | 2003-02-01 | 2003-02-15 |\n| 124 | 太阳 | 木星 | 月亮 | 2003-02-15 | 2003-03-12 |\n| 125 | 太阳 | 木星 | 火星 | 2003-03-12 | 2003-03-29 |\n| 126 | 太阳 | 木星 | 北交点 | 2003-03-29 | 2003-05-12 |\n| 127 | 太阳 | 土星 | 土星 | 2003-05-12 | 2003-07-06 |\n| 128 | 太阳 | 土星 | 水星 | 2003-07-06 | 2003-08-24 |\n| 129 | 太阳 | 土星 | 南交点 | 2003-08-24 | 2003-09-13 |\n| 130 | 太阳 | 土星 | 金星 | 2003-09-13 | 2003-11-10 |\n| 131 | 太阳 | 土星 | 太阳 | 2003-11-10 | 2003-11-27 |\n| 132 | 太阳 | 土星 | 月亮 | 2003-11-27 | 2003-12-26 |\n| 133 | 太阳 | 土星 | 火星 | 2003-12-26 | 2004-01-15 |\n| 134 | 太阳 | 土星 | 北交点 | 2004-01-15 | 2004-03-07 |\n| 135 | 太阳 | 土星 | 木星 | 2004-03-07 | 2004-04-23 |\n| 136 | 太阳 | 水星 | 水星 | 2004-04-23 | 2004-06-06 |\n| 137 | 太阳 | 水星 | 南交点 | 2004-06-06 | 2004-06-24 |\n| 138 | 太阳 | 水星 | 金星 | 2004-06-24 | 2004-08-15 |\n| 139 | 太阳 | 水星 | 太阳 | 2004-08-15 | 2004-08-30 |\n| 140 | 太阳 | 水星 | 月亮 | 2004-08-30 | 2004-09-25 |\n| 141 | 太阳 | 水星 | 火星 | 2004-09-25 | 2004-10-13 |\n| 142 | 太阳 | 水星 | 北交点 | 2004-10-13 | 2004-11-29 |\n| 143 | 太阳 | 水星 | 木星 | 2004-11-29 | 2005-01-09 |\n| 144 | 太阳 | 水星 | 土星 | 2005-01-09 | 2005-02-27 |\n| 145 | 太阳 | 南交点 | 南交点 | 2005-02-27 | 2005-03-07 |\n| 146 | 太阳 | 南交点 | 金星 | 2005-03-07 | 2005-03-28 |\n| 147 | 太阳 | 南交点 | 太阳 | 2005-03-28 | 2005-04-03 |\n| 148 | 太阳 | 南交点 | 月亮 | 2005-04-03 | 2005-04-14 |\n| 149 | 太阳 | 南交点 | 火星 | 2005-04-14 | 2005-04-22 |\n| 150 | 太阳 | 南交点 | 北交点 | 2005-04-22 | 2005-05-11 |\n| 151 | 太阳 | 南交点 | 木星 | 2005-05-11 | 2005-05-28 |\n| 152 | 太阳 | 南交点 | 土星 | 2005-05-28 | 2005-06-17 |\n| 153 | 太阳 | 南交点 | 水星 | 2005-06-17 | 2005-07-05 |\n| 154 | 太阳 | 金星 | 金星 | 2005-07-05 | 2005-09-04 |\n| 155 | 太阳 | 金星 | 太阳 | 2005-09-04 | 2005-09-22 |\n| 156 | 太阳 | 金星 | 月亮 | 2005-09-22 | 2005-10-23 |\n| 157 | 太阳 | 金星 | 火星 | 2005-10-23 | 2005-11-13 |\n| 158 | 太阳 | 金星 | 北交点 | 2005-11-13 | 2006-01-07 |\n| 159 | 太阳 | 金星 | 木星 | 2006-01-07 | 2006-02-25 |\n| 160 | 太阳 | 金星 | 土星 | 2006-02-25 | 2006-04-23 |\n| 161 | 太阳 | 金星 | 水星 | 2006-04-23 | 2006-06-14 |\n| 162 | 太阳 | 金星 | 南交点 | 2006-06-14 | 2006-07-06 |\n| 163 | 月亮 | 月亮 | 月亮 | 2006-07-06 | 2006-07-31 |\n| 164 | 月亮 | 月亮 | 火星 | 2006-07-31 | 2006-08-18 |\n| 165 | 月亮 | 月亮 | 北交点 | 2006-08-18 | 2006-10-02 |\n| 166 | 月亮 | 月亮 | 木星 | 2006-10-02 | 2006-11-12 |\n| 167 | 月亮 | 月亮 | 土星 | 2006-11-12 | 2006-12-30 |\n| 168 | 月亮 | 月亮 | 水星 | 2006-12-30 | 2007-02-11 |\n| 169 | 月亮 | 月亮 | 南交点 | 2007-02-11 | 2007-03-01 |\n| 170 | 月亮 | 月亮 | 金星 | 2007-03-01 | 2007-04-21 |\n| 171 | 月亮 | 月亮 | 太阳 | 2007-04-21 | 2007-05-06 |\n| 172 | 月亮 | 火星 | 火星 | 2007-05-06 | 2007-05-18 |\n| 173 | 月亮 | 火星 | 北交点 | 2007-05-18 | 2007-06-19 |\n| 174 | 月亮 | 火星 | 木星 | 2007-06-19 | 2007-07-18 |\n| 175 | 月亮 | 火星 | 土星 | 2007-07-18 | 2007-08-20 |\n| 176 | 月亮 | 火星 | 水星 | 2007-08-20 | 2007-09-20 |\n| 177 | 月亮 | 火星 | 南交点 | 2007-09-20 | 2007-10-02 |\n| 178 | 月亮 | 火星 | 金星 | 2007-10-02 | 2007-11-06 |\n| 179 | 月亮 | 火星 | 太阳 | 2007-11-06 | 2007-11-17 |\n| 180 | 月亮 | 火星 | 月亮 | 2007-11-17 | 2007-12-05 |\n| 181 | 月亮 | 北交点 | 北交点 | 2007-12-05 | 2008-02-25 |\n| 182 | 月亮 | 北交点 | 木星 | 2008-02-25 | 2008-05-08 |\n| 183 | 月亮 | 北交点 | 土星 | 2008-05-08 | 2008-08-03 |\n| 184 | 月亮 | 北交点 | 水星 | 2008-08-03 | 2008-10-19 |\n| 185 | 月亮 | 北交点 | 南交点 | 2008-10-19 | 2008-11-20 |\n| 186 | 月亮 | 北交点 | 金星 | 2008-11-20 | 2009-02-20 |\n| 187 | 月亮 | 北交点 | 太阳 | 2009-02-20 | 2009-03-19 |\n| 188 | 月亮 | 北交点 | 月亮 | 2009-03-19 | 2009-05-04 |\n| 189 | 月亮 | 北交点 | 火星 | 2009-05-04 | 2009-06-05 |\n| 190 | 月亮 | 木星 | 木星 | 2009-06-05 | 2009-08-09 |\n| 191 | 月亮 | 木星 | 土星 | 2009-08-09 | 2009-10-25 |\n| 192 | 月亮 | 木星 | 水星 | 2009-10-25 | 2010-01-02 |\n| 193 | 月亮 | 木星 | 南交点 | 2010-01-02 | 2010-01-30 |\n| 194 | 月亮 | 木星 | 金星 | 2010-01-30 | 2010-04-21 |\n| 195 | 月亮 | 木星 | 太阳 | 2010-04-21 | 2010-05-16 |\n| 196 | 月亮 | 木星 | 月亮 | 2010-05-16 | 2010-06-25 |\n| 197 | 月亮 | 木星 | 火星 | 2010-06-25 | 2010-07-24 |\n| 198 | 月亮 | 木星 | 北交点 | 2010-07-24 | 2010-10-05 |\n| 199 | 月亮 | 土星 | 土星 | 2010-10-05 | 2011-01-04 |\n| 200 | 月亮 | 土星 | 水星 | 2011-01-04 | 2011-03-27 |\n| 201 | 月亮 | 土星 | 南交点 | 2011-03-27 | 2011-04-30 |\n| 202 | 月亮 | 土星 | 金星 | 2011-04-30 | 2011-08-04 |\n| 203 | 月亮 | 土星 | 太阳 | 2011-08-04 | 2011-09-02 |\n| 204 | 月亮 | 土星 | 月亮 | 2011-09-02 | 2011-10-20 |\n| 205 | 月亮 | 土星 | 火星 | 2011-10-20 | 2011-11-23 |\n| 206 | 月亮 | 土星 | 北交点 | 2011-11-23 | 2012-02-18 |\n| 207 | 月亮 | 土星 | 木星 | 2012-02-18 | 2012-05-05 |\n| 208 | 月亮 | 水星 | 水星 | 2012-05-05 | 2012-07-17 |\n| 209 | 月亮 | 水星 | 南交点 | 2012-07-17 | 2012-08-16 |\n| 210 | 月亮 | 水星 | 金星 | 2012-08-16 | 2012-11-11 |\n| 211 | 月亮 | 水星 | 太阳 | 2012-11-11 | 2012-12-06 |\n| 212 | 月亮 | 水星 | 月亮 | 2012-12-06 | 2013-01-19 |\n| 213 | 月亮 | 水星 | 火星 | 2013-01-19 | 2013-02-18 |\n| 214 | 月亮 | 水星 | 北交点 | 2013-02-18 | 2013-05-06 |\n| 215 | 月亮 | 水星 | 木星 | 2013-05-06 | 2013-07-14 |\n| 216 | 月亮 | 水星 | 土星 | 2013-07-14 | 2013-10-04 |\n| 217 | 月亮 | 南交点 | 南交点 | 2013-10-04 | 2013-10-17 |\n| 218 | 月亮 | 南交点 | 金星 | 2013-10-17 | 2013-11-21 |\n| 219 | 月亮 | 南交点 | 太阳 | 2013-11-21 | 2013-12-02 |\n| 220 | 月亮 | 南交点 | 月亮 | 2013-12-02 | 2013-12-20 |\n| 221 | 月亮 | 南交点 | 火星 | 2013-12-20 | 2014-01-01 |\n| 222 | 月亮 | 南交点 | 北交点 | 2014-01-01 | 2014-02-02 |\n| 223 | 月亮 | 南交点 | 木星 | 2014-02-02 | 2014-03-02 |\n| 224 | 月亮 | 南交点 | 土星 | 2014-03-02 | 2014-04-05 |\n| 225 | 月亮 | 南交点 | 水星 | 2014-04-05 | 2014-05-05 |\n| 226 | 月亮 | 金星 | 金星 | 2014-05-05 | 2014-08-15 |\n| 227 | 月亮 | 金星 | 太阳 | 2014-08-15 | 2014-09-14 |\n| 228 | 月亮 | 金星 | 月亮 | 2014-09-14 | 2014-11-04 |\n| 229 | 月亮 | 金星 | 火星 | 2014-11-04 | 2014-12-09 |\n| 230 | 月亮 | 金星 | 北交点 | 2014-12-09 | 2015-03-11 |\n| 231 | 月亮 | 金星 | 木星 | 2015-03-11 | 2015-05-31 |\n| 232 | 月亮 | 金星 | 土星 | 2015-05-31 | 2015-09-04 |\n| 233 | 月亮 | 金星 | 水星 | 2015-09-04 | 2015-11-29 |\n| 234 | 月亮 | 金星 | 南交点 | 2015-11-29 | 2016-01-04 |\n| 235 | 月亮 | 太阳 | 太阳 | 2016-01-04 | 2016-01-13 |\n| 236 | 月亮 | 太阳 | 月亮 | 2016-01-13 | 2016-01-28 |\n| 237 | 月亮 | 太阳 | 火星 | 2016-01-28 | 2016-02-08 |\n| 238 | 月亮 | 太阳 | 北交点 | 2016-02-08 | 2016-03-06 |\n| 239 | 月亮 | 太阳 | 木星 | 2016-03-06 | 2016-03-31 |\n| 240 | 月亮 | 太阳 | 土星 | 2016-03-31 | 2016-04-29 |\n| 241 | 月亮 | 太阳 | 水星 | 2016-04-29 | 2016-05-24 |\n| 242 | 月亮 | 太阳 | 南交点 | 2016-05-24 | 2016-06-04 |\n| 243 | 月亮 | 太阳 | 金星 | 2016-06-04 | 2016-07-05 |\n| 244 | 火星 | 火星 | 火星 | 2016-07-05 | 2016-07-13 |\n| 245 | 火星 | 火星 | 北交点 | 2016-07-13 | 2016-08-05 |\n| 246 | 火星 | 火星 | 木星 | 2016-08-05 | 2016-08-24 |\n| 247 | 火星 | 火星 | 土星 | 2016-08-24 | 2016-09-17 |\n| 248 | 火星 | 火星 | 水星 | 2016-09-17 | 2016-10-08 |\n| 249 | 火星 | 火星 | 南交点 | 2016-10-08 | 2016-10-17 |\n| 250 | 火星 | 火星 | 金星 | 2016-10-17 | 2016-11-11 |\n| 251 | 火星 | 火星 | 太阳 | 2016-11-11 | 2016-11-18 |\n| 252 | 火星 | 火星 | 月亮 | 2016-11-18 | 2016-12-01 |\n| 253 | 火星 | 北交点 | 北交点 | 2016-12-01 | 2017-01-27 |\n| 254 | 火星 | 北交点 | 木星 | 2017-01-27 | 2017-03-19 |\n| 255 | 火星 | 北交点 | 土星 | 2017-03-19 | 2017-05-19 |\n| 256 | 火星 | 北交点 | 水星 | 2017-05-19 | 2017-07-12 |\n| 257 | 火星 | 北交点 | 南交点 | 2017-07-12 | 2017-08-04 |\n| 258 | 火星 | 北交点 | 金星 | 2017-08-04 | 2017-10-07 |\n| 259 | 火星 | 北交点 | 太阳 | 2017-10-07 | 2017-10-26 |\n| 260 | 火星 | 北交点 | 月亮 | 2017-10-26 | 2017-11-27 |\n| 261 | 火星 | 北交点 | 火星 | 2017-11-27 | 2017-12-19 |\n| 262 | 火星 | 木星 | 木星 | 2017-12-19 | 2018-02-03 |\n| 263 | 火星 | 木星 | 土星 | 2018-02-03 | 2018-03-29 |\n| 264 | 火星 | 木星 | 水星 | 2018-03-29 | 2018-05-16 |\n| 265 | 火星 | 木星 | 南交点 | 2018-05-16 | 2018-06-05 |\n| 266 | 火星 | 木星 | 金星 | 2018-06-05 | 2018-08-01 |\n| 267 | 火星 | 木星 | 太阳 | 2018-08-01 | 2018-08-18 |\n| 268 | 火星 | 木星 | 月亮 | 2018-08-18 | 2018-09-15 |\n| 269 | 火星 | 木星 | 火星 | 2018-09-15 | 2018-10-05 |\n| 270 | 火星 | 木星 | 北交点 | 2018-10-05 | 2018-11-25 |\n| 271 | 火星 | 土星 | 土星 | 2018-11-25 | 2019-01-28 |\n| 272 | 火星 | 土星 | 水星 | 2019-01-28 | 2019-03-27 |\n| 273 | 火星 | 土星 | 南交点 | 2019-03-27 | 2019-04-19 |\n| 274 | 火星 | 土星 | 金星 | 2019-04-19 | 2019-06-26 |\n| 275 | 火星 | 土星 | 太阳 | 2019-06-26 | 2019-07-16 |\n| 276 | 火星 | 土星 | 月亮 | 2019-07-16 | 2019-08-19 |\n| 277 | 火星 | 土星 | 火星 | 2019-08-19 | 2019-09-11 |\n| 278 | 火星 | 土星 | 北交点 | 2019-09-11 | 2019-11-11 |\n| 279 | 火星 | 土星 | 木星 | 2019-11-11 | 2020-01-04 |\n| 280 | 火星 | 水星 | 水星 | 2020-01-04 | 2020-02-24 |\n| 281 | 火星 | 水星 | 南交点 | 2020-02-24 | 2020-03-16 |\n| 282 | 火星 | 水星 | 金星 | 2020-03-16 | 2020-05-16 |\n| 283 | 火星 | 水星 | 太阳 | 2020-05-16 | 2020-06-03 |\n| 284 | 火星 | 水星 | 月亮 | 2020-06-03 | 2020-07-03 |\n| 285 | 火星 | 水星 | 火星 | 2020-07-03 | 2020-07-24 |\n| 286 | 火星 | 水星 | 北交点 | 2020-07-24 | 2020-09-17 |\n| 287 | 火星 | 水星 | 木星 | 2020-09-17 | 2020-11-04 |\n| 288 | 火星 | 水星 | 土星 | 2020-11-04 | 2020-12-31 |\n| 289 | 火星 | 南交点 | 南交点 | 2020-12-31 | 2021-01-09 |\n| 290 | 火星 | 南交点 | 金星 | 2021-01-09 | 2021-02-03 |\n| 291 | 火星 | 南交点 | 太阳 | 2021-02-03 | 2021-02-10 |\n| 292 | 火星 | 南交点 | 月亮 | 2021-02-10 | 2021-02-23 |\n| 293 | 火星 | 南交点 | 火星 | 2021-02-23 | 2021-03-03 |\n| 294 | 火星 | 南交点 | 北交点 | 2021-03-03 | 2021-03-26 |\n| 295 | 火星 | 南交点 | 木星 | 2021-03-26 | 2021-04-15 |\n| 296 | 火星 | 南交点 | 土星 | 2021-04-15 | 2021-05-08 |\n| 297 | 火星 | 南交点 | 水星 | 2021-05-08 | 2021-05-29 |\n| 298 | 火星 | 金星 | 金星 | 2021-05-29 | 2021-08-08 |\n| 299 | 火星 | 金星 | 太阳 | 2021-08-08 | 2021-08-30 |\n| 300 | 火星 | 金星 | 月亮 | 2021-08-30 | 2021-10-04 |\n| 301 | 火星 | 金星 | 火星 | 2021-10-04 | 2021-10-29 |\n| 302 | 火星 | 金星 | 北交点 | 2021-10-29 | 2022-01-01 |\n| 303 | 火星 | 金星 | 木星 | 2022-01-01 | 2022-02-27 |\n| 304 | 火星 | 金星 | 土星 | 2022-02-27 | 2022-05-05 |\n| 305 | 火星 | 金星 | 水星 | 2022-05-05 | 2022-07-05 |\n| 306 | 火星 | 金星 | 南交点 | 2022-07-05 | 2022-07-30 |\n| 307 | 火星 | 太阳 | 太阳 | 2022-07-30 | 2022-08-05 |\n| 308 | 火星 | 太阳 | 月亮 | 2022-08-05 | 2022-08-16 |\n| 309 | 火星 | 太阳 | 火星 | 2022-08-16 | 2022-08-23 |\n| 310 | 火星 | 太阳 | 北交点 | 2022-08-23 | 2022-09-11 |\n| 311 | 火星 | 太阳 | 木星 | 2022-09-11 | 2022-09-28 |\n| 312 | 火星 | 太阳 | 土星 | 2022-09-28 | 2022-10-19 |\n| 313 | 火星 | 太阳 | 水星 | 2022-10-19 | 2022-11-06 |\n| 314 | 火星 | 太阳 | 南交点 | 2022-11-06 | 2022-11-13 |\n| 315 | 火星 | 太阳 | 金星 | 2022-11-13 | 2022-12-04 |\n| 316 | 火星 | 月亮 | 月亮 | 2022-12-04 | 2022-12-22 |\n| 317 | 火星 | 月亮 | 火星 | 2022-12-22 | 2023-01-04 |\n| 318 | 火星 | 月亮 | 北交点 | 2023-01-04 | 2023-02-05 |\n| 319 | 火星 | 月亮 | 木星 | 2023-02-05 | 2023-03-05 |\n| 320 | 火星 | 月亮 | 土星 | 2023-03-05 | 2023-04-08 |\n| 321 | 火星 | 月亮 | 水星 | 2023-04-08 | 2023-05-08 |\n| 322 | 火星 | 月亮 | 南交点 | 2023-05-08 | 2023-05-20 |\n| 323 | 火星 | 月亮 | 金星 | 2023-05-20 | 2023-06-25 |\n| 324 | 火星 | 月亮 | 太阳 | 2023-06-25 | 2023-07-06 |\n| 325 | 北交点 | 北交点 | 北交点 | 2023-07-06 | 2023-11-30 |\n| 326 | 北交点 | 北交点 | 木星 | 2023-11-30 | 2024-04-10 |\n| 327 | 北交点 | 北交点 | 土星 | 2024-04-10 | 2024-09-13 |\n| 328 | 北交点 | 北交点 | 水星 | 2024-09-13 | 2025-01-31 |\n| 329 | 北交点 | 北交点 | 南交点 | 2025-01-31 | 2025-03-29 |\n| 330 | 北交点 | 北交点 | 金星 | 2025-03-29 | 2025-09-10 |\n| 331 | 北交点 | 北交点 | 太阳 | 2025-09-10 | 2025-10-29 |\n| 332 | 北交点 | 北交点 | 月亮 | 2025-10-29 | 2026-01-19 |\n| 333 | 北交点 | 北交点 | 火星 | 2026-01-19 | 2026-03-18 |\n| 334 | 北交点 | 木星 | 木星 | 2026-03-18 | 2026-07-13 |\n| 335 | 北交点 | 木星 | 土星 | 2026-07-13 | 2026-11-28 |\n| 336 | 北交点 | 木星 | 水星 | 2026-11-28 | 2027-04-02 |\n| 337 | 北交点 | 木星 | 南交点 | 2027-04-02 | 2027-05-23 |\n| 338 | 北交点 | 木星 | 金星 | 2027-05-23 | 2027-10-16 |\n| 339 | 北交点 | 木星 | 太阳 | 2027-10-16 | 2027-11-29 |\n| 340 | 北交点 | 木星 | 月亮 | 2027-11-29 | 2028-02-10 |\n| 341 | 北交点 | 木星 | 火星 | 2028-02-10 | 2028-04-01 |\n| 342 | 北交点 | 木星 | 北交点 | 2028-04-01 | 2028-08-10 |\n| 343 | 北交点 | 土星 | 土星 | 2028-08-10 | 2029-01-22 |\n| 344 | 北交点 | 土星 | 水星 | 2029-01-22 | 2029-06-19 |\n| 345 | 北交点 | 土星 | 南交点 | 2029-06-19 | 2029-08-18 |\n| 346 | 北交点 | 土星 | 金星 | 2029-08-18 | 2030-02-08 |\n| 347 | 北交点 | 土星 | 太阳 | 2030-02-08 | 2030-04-01 |\n| 348 | 北交点 | 土星 | 月亮 | 2030-04-01 | 2030-06-27 |\n| 349 | 北交点 | 土星 | 火星 | 2030-06-27 | 2030-08-26 |\n| 350 | 北交点 | 土星 | 北交点 | 2030-08-26 | 2031-01-30 |\n| 351 | 北交点 | 土星 | 木星 | 2031-01-30 | 2031-06-17 |\n| 352 | 北交点 | 水星 | 水星 | 2031-06-17 | 2031-10-27 |\n| 353 | 北交点 | 水星 | 南交点 | 2031-10-27 | 2031-12-21 |\n| 354 | 北交点 | 水星 | 金星 | 2031-12-21 | 2032-05-24 |\n| 355 | 北交点 | 水星 | 太阳 | 2032-05-24 | 2032-07-10 |\n| 356 | 北交点 | 水星 | 月亮 | 2032-07-10 | 2032-09-25 |\n| 357 | 北交点 | 水星 | 火星 | 2032-09-25 | 2032-11-19 |\n| 358 | 北交点 | 水星 | 北交点 | 2032-11-19 | 2033-04-07 |\n| 359 | 北交点 | 水星 | 木星 | 2033-04-07 | 2033-08-09 |\n| 360 | 北交点 | 水星 | 土星 | 2033-08-09 | 2034-01-04 |\n| 361 | 北交点 | 南交点 | 南交点 | 2034-01-04 | 2034-01-26 |\n| 362 | 北交点 | 南交点 | 金星 | 2034-01-26 | 2034-03-31 |\n| 363 | 北交点 | 南交点 | 太阳 | 2034-03-31 | 2034-04-19 |\n| 364 | 北交点 | 南交点 | 月亮 | 2034-04-19 | 2034-05-21 |\n| 365 | 北交点 | 南交点 | 火星 | 2034-05-21 | 2034-06-13 |\n| 366 | 北交点 | 南交点 | 北交点 | 2034-06-13 | 2034-08-09 |\n| 367 | 北交点 | 南交点 | 木星 | 2034-08-09 | 2034-09-29 |\n| 368 | 北交点 | 南交点 | 土星 | 2034-09-29 | 2034-11-29 |\n| 369 | 北交点 | 南交点 | 水星 | 2034-11-29 | 2035-01-22 |\n| 370 | 北交点 | 金星 | 金星 | 2035-01-22 | 2035-07-24 |\n| 371 | 北交点 | 金星 | 太阳 | 2035-07-24 | 2035-09-17 |\n| 372 | 北交点 | 金星 | 月亮 | 2035-09-17 | 2035-12-17 |\n| 373 | 北交点 | 金星 | 火星 | 2035-12-17 | 2036-02-19 |\n| 374 | 北交点 | 金星 | 北交点 | 2036-02-19 | 2036-08-02 |\n| 375 | 北交点 | 金星 | 木星 | 2036-08-02 | 2036-12-26 |\n| 376 | 北交点 | 金星 | 土星 | 2036-12-26 | 2037-06-17 |\n| 377 | 北交点 | 金星 | 水星 | 2037-06-17 | 2037-11-19 |\n| 378 | 北交点 | 金星 | 南交点 | 2037-11-19 | 2038-01-22 |\n| 379 | 北交点 | 太阳 | 太阳 | 2038-01-22 | 2038-02-08 |\n| 380 | 北交点 | 太阳 | 月亮 | 2038-02-08 | 2038-03-07 |\n| 381 | 北交点 | 太阳 | 火星 | 2038-03-07 | 2038-03-26 |\n| 382 | 北交点 | 太阳 | 北交点 | 2038-03-26 | 2038-05-15 |\n| 383 | 北交点 | 太阳 | 木星 | 2038-05-15 | 2038-06-27 |\n| 384 | 北交点 | 太阳 | 土星 | 2038-06-27 | 2038-08-19 |\n| 385 | 北交点 | 太阳 | 水星 | 2038-08-19 | 2038-10-04 |\n| 386 | 北交点 | 太阳 | 南交点 | 2038-10-04 | 2038-10-23 |\n| 387 | 北交点 | 太阳 | 金星 | 2038-10-23 | 2038-12-17 |\n| 388 | 北交点 | 月亮 | 月亮 | 2038-12-17 | 2039-02-01 |\n| 389 | 北交点 | 月亮 | 火星 | 2039-02-01 | 2039-03-05 |\n| 390 | 北交点 | 月亮 | 北交点 | 2039-03-05 | 2039-05-26 |\n| 391 | 北交点 | 月亮 | 木星 | 2039-05-26 | 2039-08-07 |\n| 392 | 北交点 | 月亮 | 土星 | 2039-08-07 | 2039-11-02 |\n| 393 | 北交点 | 月亮 | 水星 | 2039-11-02 | 2040-01-18 |\n| 394 | 北交点 | 月亮 | 南交点 | 2040-01-18 | 2040-02-19 |\n| 395 | 北交点 | 月亮 | 金星 | 2040-02-19 | 2040-05-21 |\n| 396 | 北交点 | 月亮 | 太阳 | 2040-05-21 | 2040-06-17 |\n| 397 | 北交点 | 火星 | 火星 | 2040-06-17 | 2040-07-09 |\n| 398 | 北交点 | 火星 | 北交点 | 2040-07-09 | 2040-09-05 |\n| 399 | 北交点 | 火星 | 木星 | 2040-09-05 | 2040-10-26 |\n| 400 | 北交点 | 火星 | 土星 | 2040-10-26 | 2040-12-26 |\n| 401 | 北交点 | 火星 | 水星 | 2040-12-26 | 2041-02-18 |\n| 402 | 北交点 | 火星 | 南交点 | 2041-02-18 | 2041-03-12 |\n| 403 | 北交点 | 火星 | 金星 | 2041-03-12 | 2041-05-15 |\n| 404 | 北交点 | 火星 | 太阳 | 2041-05-15 | 2041-06-04 |\n| 405 | 北交点 | 火星 | 月亮 | 2041-06-04 | 2041-07-06 |\n| 406 | 木星 | 木星 | 木星 | 2041-07-06 | 2041-10-17 |\n| 407 | 木星 | 木星 | 土星 | 2041-10-17 | 2042-02-18 |\n| 408 | 木星 | 木星 | 水星 | 2042-02-18 | 2042-06-08 |\n| 409 | 木星 | 木星 | 南交点 | 2042-06-08 | 2042-07-24 |\n| 410 | 木星 | 木星 | 金星 | 2042-07-24 | 2042-11-30 |\n| 411 | 木星 | 木星 | 太阳 | 2042-11-30 | 2043-01-08 |\n| 412 | 木星 | 木星 | 月亮 | 2043-01-08 | 2043-03-14 |\n| 413 | 木星 | 木星 | 火星 | 2043-03-14 | 2043-04-29 |\n| 414 | 木星 | 木星 | 北交点 | 2043-04-29 | 2043-08-24 |\n| 415 | 木星 | 土星 | 土星 | 2043-08-24 | 2044-01-17 |\n| 416 | 木星 | 土星 | 水星 | 2044-01-17 | 2044-05-27 |\n| 417 | 木星 | 土星 | 南交点 | 2044-05-27 | 2044-07-20 |\n| 418 | 木星 | 土星 | 金星 | 2044-07-20 | 2044-12-21 |\n| 419 | 木星 | 土星 | 太阳 | 2044-12-21 | 2045-02-06 |\n| 420 | 木星 | 土星 | 月亮 | 2045-02-06 | 2045-04-24 |\n| 421 | 木星 | 土星 | 火星 | 2045-04-24 | 2045-06-17 |\n| 422 | 木星 | 土星 | 北交点 | 2045-06-17 | 2045-11-03 |\n| 423 | 木星 | 土星 | 木星 | 2045-11-03 | 2046-03-06 |\n| 424 | 木星 | 水星 | 水星 | 2046-03-06 | 2046-07-01 |\n| 425 | 木星 | 水星 | 南交点 | 2046-07-01 | 2046-08-19 |\n| 426 | 木星 | 水星 | 金星 | 2046-08-19 | 2047-01-04 |\n| 427 | 木星 | 水星 | 太阳 | 2047-01-04 | 2047-02-14 |\n| 428 | 木星 | 水星 | 月亮 | 2047-02-14 | 2047-04-24 |\n| 429 | 木星 | 水星 | 火星 | 2047-04-24 | 2047-06-11 |\n| 430 | 木星 | 水星 | 北交点 | 2047-06-11 | 2047-10-13 |\n| 431 | 木星 | 水星 | 木星 | 2047-10-13 | 2048-02-01 |\n| 432 | 木星 | 水星 | 土星 | 2048-02-01 | 2048-06-11 |\n| 433 | 木星 | 南交点 | 南交点 | 2048-06-11 | 2048-07-01 |\n| 434 | 木星 | 南交点 | 金星 | 2048-07-01 | 2048-08-27 |\n| 435 | 木星 | 南交点 | 太阳 | 2048-08-27 | 2048-09-13 |\n| 436 | 木星 | 南交点 | 月亮 | 2048-09-13 | 2048-10-11 |\n| 437 | 木星 | 南交点 | 火星 | 2048-10-11 | 2048-10-31 |\n| 438 | 木星 | 南交点 | 北交点 | 2048-10-31 | 2048-12-21 |\n| 439 | 木星 | 南交点 | 木星 | 2048-12-21 | 2049-02-05 |\n| 440 | 木星 | 南交点 | 土星 | 2049-02-05 | 2049-03-31 |\n| 441 | 木星 | 南交点 | 水星 | 2049-03-31 | 2049-05-18 |\n| 442 | 木星 | 金星 | 金星 | 2049-05-18 | 2049-10-27 |\n| 443 | 木星 | 金星 | 太阳 | 2049-10-27 | 2049-12-15 |\n| 444 | 木星 | 金星 | 月亮 | 2049-12-15 | 2050-03-06 |\n| 445 | 木星 | 金星 | 火星 | 2050-03-06 | 2050-05-02 |\n| 446 | 木星 | 金星 | 北交点 | 2050-05-02 | 2050-09-25 |\n| 447 | 木星 | 金星 | 木星 | 2050-09-25 | 2051-02-02 |\n| 448 | 木星 | 金星 | 土星 | 2051-02-02 | 2051-07-06 |\n| 449 | 木星 | 金星 | 水星 | 2051-07-06 | 2051-11-21 |\n| 450 | 木星 | 金星 | 南交点 | 2051-11-21 | 2052-01-17 |\n| 451 | 木星 | 太阳 | 太阳 | 2052-01-17 | 2052-01-31 |\n| 452 | 木星 | 太阳 | 月亮 | 2052-01-31 | 2052-02-25 |\n| 453 | 木星 | 太阳 | 火星 | 2052-02-25 | 2052-03-13 |\n| 454 | 木星 | 太阳 | 北交点 | 2052-03-13 | 2052-04-26 |\n| 455 | 木星 | 太阳 | 木星 | 2052-04-26 | 2052-06-04 |\n| 456 | 木星 | 太阳 | 土星 | 2052-06-04 | 2052-07-20 |\n| 457 | 木星 | 太阳 | 水星 | 2052-07-20 | 2052-08-30 |\n| 458 | 木星 | 太阳 | 南交点 | 2052-08-30 | 2052-09-16 |\n| 459 | 木星 | 太阳 | 金星 | 2052-09-16 | 2052-11-04 |\n| 460 | 木星 | 月亮 | 月亮 | 2052-11-04 | 2052-12-15 |\n| 461 | 木星 | 月亮 | 火星 | 2052-12-15 | 2053-01-12 |\n| 462 | 木星 | 月亮 | 北交点 | 2053-01-12 | 2053-03-26 |\n| 463 | 木星 | 月亮 | 木星 | 2053-03-26 | 2053-05-30 |\n| 464 | 木星 | 月亮 | 土星 | 2053-05-30 | 2053-08-15 |\n| 465 | 木星 | 月亮 | 水星 | 2053-08-15 | 2053-10-23 |\n| 466 | 木星 | 月亮 | 南交点 | 2053-10-23 | 2053-11-20 |\n| 467 | 木星 | 月亮 | 金星 | 2053-11-20 | 2054-02-10 |\n| 468 | 木星 | 月亮 | 太阳 | 2054-02-10 | 2054-03-06 |\n| 469 | 木星 | 火星 | 火星 | 2054-03-06 | 2054-03-26 |\n| 470 | 木星 | 火星 | 北交点 | 2054-03-26 | 2054-05-16 |\n| 471 | 木星 | 火星 | 木星 | 2054-05-16 | 2054-06-30 |\n| 472 | 木星 | 火星 | 土星 | 2054-06-30 | 2054-08-23 |\n| 473 | 木星 | 火星 | 水星 | 2054-08-23 | 2054-10-11 |\n| 474 | 木星 | 火星 | 南交点 | 2054-10-11 | 2054-10-31 |\n| 475 | 木星 | 火星 | 金星 | 2054-10-31 | 2054-12-26 |\n| 476 | 木星 | 火星 | 太阳 | 2054-12-26 | 2055-01-12 |\n| 477 | 木星 | 火星 | 月亮 | 2055-01-12 | 2055-02-10 |\n| 478 | 木星 | 北交点 | 北交点 | 2055-02-10 | 2055-06-21 |\n| 479 | 木星 | 北交点 | 木星 | 2055-06-21 | 2055-10-16 |\n| 480 | 木星 | 北交点 | 土星 | 2055-10-16 | 2056-03-03 |\n| 481 | 木星 | 北交点 | 水星 | 2056-03-03 | 2056-07-05 |\n| 482 | 木星 | 北交点 | 南交点 | 2056-07-05 | 2056-08-25 |\n| 483 | 木星 | 北交点 | 金星 | 2056-08-25 | 2057-01-18 |\n| 484 | 木星 | 北交点 | 太阳 | 2057-01-18 | 2057-03-03 |\n| 485 | 木星 | 北交点 | 月亮 | 2057-03-03 | 2057-05-15 |\n| 486 | 木星 | 北交点 | 火星 | 2057-05-15 | 2057-07-06 |\n| 487 | 土星 | 土星 | 土星 | 2057-07-06 | 2057-12-26 |\n| 488 | 土星 | 土星 | 水星 | 2057-12-26 | 2058-05-31 |\n| 489 | 土星 | 土星 | 南交点 | 2058-05-31 | 2058-08-03 |\n| 490 | 土星 | 土星 | 金星 | 2058-08-03 | 2059-02-02 |\n| 491 | 土星 | 土星 | 太阳 | 2059-02-02 | 2059-03-29 |\n| 492 | 土星 | 土星 | 月亮 | 2059-03-29 | 2059-06-29 |\n| 493 | 土星 | 土星 | 火星 | 2059-06-29 | 2059-09-01 |\n| 494 | 土星 | 土星 | 北交点 | 2059-09-01 | 2060-02-13 |\n| 495 | 土星 | 土星 | 木星 | 2060-02-13 | 2060-07-08 |\n| 496 | 土星 | 水星 | 水星 | 2060-07-08 | 2060-11-24 |\n| 497 | 土星 | 水星 | 南交点 | 2060-11-24 | 2061-01-21 |\n| 498 | 土星 | 水星 | 金星 | 2061-01-21 | 2061-07-04 |\n| 499 | 土星 | 水星 | 太阳 | 2061-07-04 | 2061-08-22 |\n| 500 | 土星 | 水星 | 月亮 | 2061-08-22 | 2061-11-12 |\n| 501 | 土星 | 水星 | 火星 | 2061-11-12 | 2062-01-08 |\n| 502 | 土星 | 水星 | 北交点 | 2062-01-08 | 2062-06-04 |\n| 503 | 土星 | 水星 | 木星 | 2062-06-04 | 2062-10-14 |\n| 504 | 土星 | 水星 | 土星 | 2062-10-14 | 2063-03-18 |\n| 505 | 土星 | 南交点 | 南交点 | 2063-03-18 | 2063-04-11 |\n| 506 | 土星 | 南交点 | 金星 | 2063-04-11 | 2063-06-17 |\n| 507 | 土星 | 南交点 | 太阳 | 2063-06-17 | 2063-07-08 |\n| 508 | 土星 | 南交点 | 月亮 | 2063-07-08 | 2063-08-10 |\n| 509 | 土星 | 南交点 | 火星 | 2063-08-10 | 2063-09-03 |\n| 510 | 土星 | 南交点 | 北交点 | 2063-09-03 | 2063-11-03 |\n| 511 | 土星 | 南交点 | 木星 | 2063-11-03 | 2063-12-27 |\n| 512 | 土星 | 南交点 | 土星 | 2063-12-27 | 2064-02-29 |\n| 513 | 土星 | 南交点 | 水星 | 2064-02-29 | 2064-04-26 |\n| 514 | 土星 | 金星 | 金星 | 2064-04-26 | 2064-11-05 |\n| 515 | 土星 | 金星 | 太阳 | 2064-11-05 | 2065-01-02 |\n| 516 | 土星 | 金星 | 月亮 | 2065-01-02 | 2065-04-08 |\n| 517 | 土星 | 金星 | 火星 | 2065-04-08 | 2065-06-14 |\n| 518 | 土星 | 金星 | 北交点 | 2065-06-14 | 2065-12-05 |\n| 519 | 土星 | 金星 | 木星 | 2065-12-05 | 2066-05-08 |\n| 520 | 土星 | 金星 | 土星 | 2066-05-08 | 2066-11-07 |\n| 521 | 土星 | 金星 | 水星 | 2066-11-07 | 2067-04-20 |\n| 522 | 土星 | 金星 | 南交点 | 2067-04-20 | 2067-06-26 |\n| 523 | 土星 | 太阳 | 太阳 | 2067-06-26 | 2067-07-14 |\n| 524 | 土星 | 太阳 | 月亮 | 2067-07-14 | 2067-08-12 |\n| 525 | 土星 | 太阳 | 火星 | 2067-08-12 | 2067-09-01 |\n| 526 | 土星 | 太阳 | 北交点 | 2067-09-01 | 2067-10-23 |\n| 527 | 土星 | 太阳 | 木星 | 2067-10-23 | 2067-12-08 |\n| 528 | 土星 | 太阳 | 土星 | 2067-12-08 | 2068-02-01 |\n| 529 | 土星 | 太阳 | 水星 | 2068-02-01 | 2068-03-21 |\n| 530 | 土星 | 太阳 | 南交点 | 2068-03-21 | 2068-04-11 |\n| 531 | 土星 | 太阳 | 金星 | 2068-04-11 | 2068-06-07 |\n| 532 | 土星 | 月亮 | 月亮 | 2068-06-07 | 2068-07-26 |\n| 533 | 土星 | 月亮 | 火星 | 2068-07-26 | 2068-08-28 |\n| 534 | 土星 | 月亮 | 北交点 | 2068-08-28 | 2068-11-23 |\n| 535 | 土星 | 月亮 | 木星 | 2068-11-23 | 2069-02-08 |\n| 536 | 土星 | 月亮 | 土星 | 2069-02-08 | 2069-05-11 |\n| 537 | 土星 | 月亮 | 水星 | 2069-05-11 | 2069-08-01 |\n| 538 | 土星 | 月亮 | 南交点 | 2069-08-01 | 2069-09-03 |\n| 539 | 土星 | 月亮 | 金星 | 2069-09-03 | 2069-12-09 |\n| 540 | 土星 | 月亮 | 太阳 | 2069-12-09 | 2070-01-07 |\n| 541 | 土星 | 火星 | 火星 | 2070-01-07 | 2070-01-30 |\n| 542 | 土星 | 火星 | 北交点 | 2070-01-30 | 2070-04-01 |\n| 543 | 土星 | 火星 | 木星 | 2070-04-01 | 2070-05-25 |\n| 544 | 土星 | 火星 | 土星 | 2070-05-25 | 2070-07-28 |\n| 545 | 土星 | 火星 | 水星 | 2070-07-28 | 2070-09-23 |\n| 546 | 土星 | 火星 | 南交点 | 2070-09-23 | 2070-10-17 |\n| 547 | 土星 | 火星 | 金星 | 2070-10-17 | 2070-12-23 |\n| 548 | 土星 | 火星 | 太阳 | 2070-12-23 | 2071-01-13 |\n| 549 | 土星 | 火星 | 月亮 | 2071-01-13 | 2071-02-15 |\n| 550 | 土星 | 北交点 | 北交点 | 2071-02-15 | 2071-07-22 |\n| 551 | 土星 | 北交点 | 木星 | 2071-07-22 | 2071-12-07 |\n| 552 | 土星 | 北交点 | 土星 | 2071-12-07 | 2072-05-20 |\n| 553 | 土星 | 北交点 | 水星 | 2072-05-20 | 2072-10-15 |\n| 554 | 土星 | 北交点 | 南交点 | 2072-10-15 | 2072-12-14 |\n| 555 | 土星 | 北交点 | 金星 | 2072-12-14 | 2073-06-06 |\n| 556 | 土星 | 北交点 | 太阳 | 2073-06-06 | 2073-07-28 |\n| 557 | 土星 | 北交点 | 月亮 | 2073-07-28 | 2073-10-23 |\n| 558 | 土星 | 北交点 | 火星 | 2073-10-23 | 2073-12-22 |\n| 559 | 土星 | 木星 | 木星 | 2073-12-22 | 2074-04-25 |\n| 560 | 土星 | 木星 | 土星 | 2074-04-25 | 2074-09-18 |\n| 561 | 土星 | 木星 | 水星 | 2074-09-18 | 2075-01-27 |\n| 562 | 土星 | 木星 | 南交点 | 2075-01-27 | 2075-03-22 |\n| 563 | 土星 | 木星 | 金星 | 2075-03-22 | 2075-08-23 |\n| 564 | 土星 | 木星 | 太阳 | 2075-08-23 | 2075-10-09 |\n| 565 | 土星 | 木星 | 月亮 | 2075-10-09 | 2075-12-25 |\n| 566 | 土星 | 木星 | 火星 | 2075-12-25 | 2076-02-17 |\n| 567 | 土星 | 木星 | 北交点 | 2076-02-17 | 2076-07-05 |\n| 568 | 水星 | 水星 | 水星 | 2076-07-05 | 2076-11-06 |\n| 569 | 水星 | 水星 | 南交点 | 2076-11-06 | 2076-12-27 |\n| 570 | 水星 | 水星 | 金星 | 2076-12-27 | 2077-05-23 |\n| 571 | 水星 | 水星 | 太阳 | 2077-05-23 | 2077-07-06 |\n| 572 | 水星 | 水星 | 月亮 | 2077-07-06 | 2077-09-17 |\n| 573 | 水星 | 水星 | 火星 | 2077-09-17 | 2077-11-08 |\n| 574 | 水星 | 水星 | 北交点 | 2077-11-08 | 2078-03-20 |\n| 575 | 水星 | 水星 | 木星 | 2078-03-20 | 2078-07-15 |\n| 576 | 水星 | 水星 | 土星 | 2078-07-15 | 2078-12-01 |\n| 577 | 水星 | 南交点 | 南交点 | 2078-12-01 | 2078-12-22 |\n| 578 | 水星 | 南交点 | 金星 | 2078-12-22 | 2079-02-21 |\n| 579 | 水星 | 南交点 | 太阳 | 2079-02-21 | 2079-03-11 |\n| 580 | 水星 | 南交点 | 月亮 | 2079-03-11 | 2079-04-10 |\n| 581 | 水星 | 南交点 | 火星 | 2079-04-10 | 2079-05-01 |\n| 582 | 水星 | 南交点 | 北交点 | 2079-05-01 | 2079-06-25 |\n| 583 | 水星 | 南交点 | 木星 | 2079-06-25 | 2079-08-12 |\n| 584 | 水星 | 南交点 | 土星 | 2079-08-12 | 2079-10-08 |\n| 585 | 水星 | 南交点 | 水星 | 2079-10-08 | 2079-11-29 |\n| 586 | 水星 | 金星 | 金星 | 2079-11-29 | 2080-05-19 |\n| 587 | 水星 | 金星 | 太阳 | 2080-05-19 | 2080-07-10 |\n| 588 | 水星 | 金星 | 月亮 | 2080-07-10 | 2080-10-04 |\n| 589 | 水星 | 金星 | 火星 | 2080-10-04 | 2080-12-03 |\n| 590 | 水星 | 金星 | 北交点 | 2080-12-03 | 2081-05-08 |\n| 591 | 水星 | 金星 | 木星 | 2081-05-08 | 2081-09-23 |\n| 592 | 水星 | 金星 | 土星 | 2081-09-23 | 2082-03-06 |\n| 593 | 水星 | 金星 | 水星 | 2082-03-06 | 2082-07-30 |\n| 594 | 水星 | 金星 | 南交点 | 2082-07-30 | 2082-09-29 |\n| 595 | 水星 | 太阳 | 太阳 | 2082-09-29 | 2082-10-14 |\n| 596 | 水星 | 太阳 | 月亮 | 2082-10-14 | 2082-11-09 |\n| 597 | 水星 | 太阳 | 火星 | 2082-11-09 | 2082-11-27 |\n| 598 | 水星 | 太阳 | 北交点 | 2082-11-27 | 2083-01-13 |\n| 599 | 水星 | 太阳 | 木星 | 2083-01-13 | 2083-02-23 |\n| 600 | 水星 | 太阳 | 土星 | 2083-02-23 | 2083-04-13 |\n| 601 | 水星 | 太阳 | 水星 | 2083-04-13 | 2083-05-27 |\n| 602 | 水星 | 太阳 | 南交点 | 2083-05-27 | 2083-06-14 |\n| 603 | 水星 | 太阳 | 金星 | 2083-06-14 | 2083-08-05 |\n| 604 | 水星 | 月亮 | 月亮 | 2083-08-05 | 2083-09-17 |\n| 605 | 水星 | 月亮 | 火星 | 2083-09-17 | 2083-10-17 |\n| 606 | 水星 | 月亮 | 北交点 | 2083-10-17 | 2084-01-03 |\n| 607 | 水星 | 月亮 | 木星 | 2084-01-03 | 2084-03-12 |\n| 608 | 水星 | 月亮 | 土星 | 2084-03-12 | 2084-06-02 |\n| 609 | 水星 | 月亮 | 水星 | 2084-06-02 | 2084-08-14 |\n| 610 | 水星 | 月亮 | 南交点 | 2084-08-14 | 2084-09-13 |\n| 611 | 水星 | 月亮 | 金星 | 2084-09-13 | 2084-12-09 |\n| 612 | 水星 | 月亮 | 太阳 | 2084-12-09 | 2085-01-04 |\n| 613 | 水星 | 火星 | 火星 | 2085-01-04 | 2085-01-25 |\n| 614 | 水星 | 火星 | 北交点 | 2085-01-25 | 2085-03-20 |\n| 615 | 水星 | 火星 | 木星 | 2085-03-20 | 2085-05-07 |\n| 616 | 水星 | 火星 | 土星 | 2085-05-07 | 2085-07-04 |\n| 617 | 水星 | 火星 | 水星 | 2085-07-04 | 2085-08-24 |\n| 618 | 水星 | 火星 | 南交点 | 2085-08-24 | 2085-09-14 |\n| 619 | 水星 | 火星 | 金星 | 2085-09-14 | 2085-11-13 |\n| 620 | 水星 | 火星 | 太阳 | 2085-11-13 | 2085-12-02 |\n| 621 | 水星 | 火星 | 月亮 | 2085-12-02 | 2086-01-01 |\n| 622 | 水星 | 北交点 | 北交点 | 2086-01-01 | 2086-05-20 |\n| 623 | 水星 | 北交点 | 木星 | 2086-05-20 | 2086-09-22 |\n| 624 | 水星 | 北交点 | 土星 | 2086-09-22 | 2087-02-16 |\n| 625 | 水星 | 北交点 | 水星 | 2087-02-16 | 2087-06-28 |\n| 626 | 水星 | 北交点 | 南交点 | 2087-06-28 | 2087-08-21 |\n| 627 | 水星 | 北交点 | 金星 | 2087-08-21 | 2088-01-24 |\n| 628 | 水星 | 北交点 | 太阳 | 2088-01-24 | 2088-03-10 |\n| 629 | 水星 | 北交点 | 月亮 | 2088-03-10 | 2088-05-27 |\n| 630 | 水星 | 北交点 | 火星 | 2088-05-27 | 2088-07-20 |\n| 631 | 水星 | 木星 | 木星 | 2088-07-20 | 2088-11-08 |\n| 632 | 水星 | 木星 | 土星 | 2088-11-08 | 2089-03-19 |\n| 633 | 水星 | 木星 | 水星 | 2089-03-19 | 2089-07-14 |\n| 634 | 水星 | 木星 | 南交点 | 2089-07-14 | 2089-08-31 |\n| 635 | 水星 | 木星 | 金星 | 2089-08-31 | 2090-01-16 |\n| 636 | 水星 | 木星 | 太阳 | 2090-01-16 | 2090-02-27 |\n| 637 | 水星 | 木星 | 月亮 | 2090-02-27 | 2090-05-07 |\n| 638 | 水星 | 木星 | 火星 | 2090-05-07 | 2090-06-24 |\n| 639 | 水星 | 木星 | 北交点 | 2090-06-24 | 2090-10-26 |\n| 640 | 水星 | 土星 | 土星 | 2090-10-26 | 2091-03-31 |\n| 641 | 水星 | 土星 | 水星 | 2091-03-31 | 2091-08-17 |\n| 642 | 水星 | 土星 | 南交点 | 2091-08-17 | 2091-10-14 |\n| 643 | 水星 | 土星 | 金星 | 2091-10-14 | 2092-03-25 |\n| 644 | 水星 | 土星 | 太阳 | 2092-03-25 | 2092-05-14 |\n| 645 | 水星 | 土星 | 月亮 | 2092-05-14 | 2092-08-04 |\n| 646 | 水星 | 土星 | 火星 | 2092-08-04 | 2092-09-30 |\n| 647 | 水星 | 土星 | 北交点 | 2092-09-30 | 2093-02-24 |\n| 648 | 水星 | 土星 | 木星 | 2093-02-24 | 2093-07-06 |\n| 649 | 南交点 | 南交点 | 南交点 | 2093-07-06 | 2093-07-14 |\n| 650 | 南交点 | 南交点 | 金星 | 2093-07-14 | 2093-08-08 |\n| 651 | 南交点 | 南交点 | 太阳 | 2093-08-08 | 2093-08-16 |\n| 652 | 南交点 | 南交点 | 月亮 | 2093-08-16 | 2093-08-28 |\n| 653 | 南交点 | 南交点 | 火星 | 2093-08-28 | 2093-09-06 |\n| 654 | 南交点 | 南交点 | 北交点 | 2093-09-06 | 2093-09-28 |\n| 655 | 南交点 | 南交点 | 木星 | 2093-09-28 | 2093-10-18 |\n| 656 | 南交点 | 南交点 | 土星 | 2093-10-18 | 2093-11-10 |\n| 657 | 南交点 | 南交点 | 水星 | 2093-11-10 | 2093-12-02 |\n| 658 | 南交点 | 金星 | 金星 | 2093-12-02 | 2094-02-11 |\n| 659 | 南交点 | 金星 | 太阳 | 2094-02-11 | 2094-03-04 |\n| 660 | 南交点 | 金星 | 月亮 | 2094-03-04 | 2094-04-08 |\n| 661 | 南交点 | 金星 | 火星 | 2094-04-08 | 2094-05-03 |\n| 662 | 南交点 | 金星 | 北交点 | 2094-05-03 | 2094-07-06 |\n| 663 | 南交点 | 金星 | 木星 | 2094-07-06 | 2094-09-01 |\n| 664 | 南交点 | 金星 | 土星 | 2094-09-01 | 2094-11-07 |\n| 665 | 南交点 | 金星 | 水星 | 2094-11-07 | 2095-01-07 |\n| 666 | 南交点 | 金星 | 南交点 | 2095-01-07 | 2095-02-01 |\n| 667 | 南交点 | 太阳 | 太阳 | 2095-02-01 | 2095-02-07 |\n| 668 | 南交点 | 太阳 | 月亮 | 2095-02-07 | 2095-02-18 |\n| 669 | 南交点 | 太阳 | 火星 | 2095-02-18 | 2095-02-25 |\n| 670 | 南交点 | 太阳 | 北交点 | 2095-02-25 | 2095-03-16 |\n| 671 | 南交点 | 太阳 | 木星 | 2095-03-16 | 2095-04-02 |\n| 672 | 南交点 | 太阳 | 土星 | 2095-04-02 | 2095-04-23 |\n| 673 | 南交点 | 太阳 | 水星 | 2095-04-23 | 2095-05-11 |\n| 674 | 南交点 | 太阳 | 南交点 | 2095-05-11 | 2095-05-18 |\n| 675 | 南交点 | 太阳 | 金星 | 2095-05-18 | 2095-06-08 |\n| 676 | 南交点 | 月亮 | 月亮 | 2095-06-08 | 2095-06-26 |\n| 677 | 南交点 | 月亮 | 火星 | 2095-06-26 | 2095-07-09 |\n| 678 | 南交点 | 月亮 | 北交点 | 2095-07-09 | 2095-08-10 |\n| 679 | 南交点 | 月亮 | 木星 | 2095-08-10 | 2095-09-07 |\n| 680 | 南交点 | 月亮 | 土星 | 2095-09-07 | 2095-10-11 |\n| 681 | 南交点 | 月亮 | 水星 | 2095-10-11 | 2095-11-10 |\n| 682 | 南交点 | 月亮 | 南交点 | 2095-11-10 | 2095-11-22 |\n| 683 | 南交点 | 月亮 | 金星 | 2095-11-22 | 2095-12-28 |\n| 684 | 南交点 | 月亮 | 太阳 | 2095-12-28 | 2096-01-07 |\n| 685 | 南交点 | 火星 | 火星 | 2096-01-07 | 2096-01-16 |\n| 686 | 南交点 | 火星 | 北交点 | 2096-01-16 | 2096-02-07 |\n| 687 | 南交点 | 火星 | 木星 | 2096-02-07 | 2096-02-27 |\n| 688 | 南交点 | 火星 | 土星 | 2096-02-27 | 2096-03-22 |\n| 689 | 南交点 | 火星 | 水星 | 2096-03-22 | 2096-04-12 |\n| 690 | 南交点 | 火星 | 南交点 | 2096-04-12 | 2096-04-21 |\n| 691 | 南交点 | 火星 | 金星 | 2096-04-21 | 2096-05-16 |\n| 692 | 南交点 | 火星 | 太阳 | 2096-05-16 | 2096-05-23 |\n| 693 | 南交点 | 火星 | 月亮 | 2096-05-23 | 2096-06-05 |\n| 694 | 南交点 | 北交点 | 北交点 | 2096-06-05 | 2096-08-01 |\n| 695 | 南交点 | 北交点 | 木星 | 2096-08-01 | 2096-09-21 |\n| 696 | 南交点 | 北交点 | 土星 | 2096-09-21 | 2096-11-21 |\n| 697 | 南交点 | 北交点 | 水星 | 2096-11-21 | 2097-01-14 |\n| 698 | 南交点 | 北交点 | 南交点 | 2097-01-14 | 2097-02-06 |\n| 699 | 南交点 | 北交点 | 金星 | 2097-02-06 | 2097-04-10 |\n| 700 | 南交点 | 北交点 | 太阳 | 2097-04-10 | 2097-04-30 |\n| 701 | 南交点 | 北交点 | 月亮 | 2097-04-30 | 2097-06-01 |\n| 702 | 南交点 | 北交点 | 火星 | 2097-06-01 | 2097-06-23 |\n| 703 | 南交点 | 木星 | 木星 | 2097-06-23 | 2097-08-07 |\n| 704 | 南交点 | 木星 | 土星 | 2097-08-07 | 2097-09-30 |\n| 705 | 南交点 | 木星 | 水星 | 2097-09-30 | 2097-11-18 |\n| 706 | 南交点 | 木星 | 南交点 | 2097-11-18 | 2097-12-07 |\n| 707 | 南交点 | 木星 | 金星 | 2097-12-07 | 2098-02-02 |\n| 708 | 南交点 | 木星 | 太阳 | 2098-02-02 | 2098-02-19 |\n| 709 | 南交点 | 木星 | 月亮 | 2098-02-19 | 2098-03-20 |\n| 710 | 南交点 | 木星 | 火星 | 2098-03-20 | 2098-04-09 |\n| 711 | 南交点 | 木星 | 北交点 | 2098-04-09 | 2098-05-30 |\n| 712 | 南交点 | 土星 | 土星 | 2098-05-30 | 2098-08-02 |\n| 713 | 南交点 | 土星 | 水星 | 2098-08-02 | 2098-09-28 |\n| 714 | 南交点 | 土星 | 南交点 | 2098-09-28 | 2098-10-22 |\n| 715 | 南交点 | 土星 | 金星 | 2098-10-22 | 2098-12-28 |\n| 716 | 南交点 | 土星 | 太阳 | 2098-12-28 | 2099-01-17 |\n| 717 | 南交点 | 土星 | 月亮 | 2099-01-17 | 2099-02-20 |\n| 718 | 南交点 | 土星 | 火星 | 2099-02-20 | 2099-03-16 |\n| 719 | 南交点 | 土星 | 北交点 | 2099-03-16 | 2099-05-15 |\n| 720 | 南交点 | 土星 | 木星 | 2099-05-15 | 2099-07-08 |\n| 721 | 南交点 | 水星 | 水星 | 2099-07-08 | 2099-08-29 |\n| 722 | 南交点 | 水星 | 南交点 | 2099-08-29 | 2099-09-19 |\n| 723 | 南交点 | 水星 | 金星 | 2099-09-19 | 2099-11-18 |\n| 724 | 南交点 | 水星 | 太阳 | 2099-11-18 | 2099-12-06 |\n| 725 | 南交点 | 水星 | 月亮 | 2099-12-06 | 2100-01-05 |\n| 726 | 南交点 | 水星 | 火星 | 2100-01-05 | 2100-01-27 |\n| 727 | 南交点 | 水星 | 北交点 | 2100-01-27 | 2100-03-22 |\n| 728 | 南交点 | 水星 | 木星 | 2100-03-22 | 2100-05-09 |\n| 729 | 南交点 | 水星 | 土星 | 2100-05-09 | 2100-07-06 |\n\n### Yogini 大运\n\n| 序号 | 主星/星座 | 起始 | 结束 | 年数 |\n|------|-----------|------|------|------|\n| 1 | 月亮 | 1991-04-07T09:15:00 | 1993-04-06T21:15:00 | 2 |\n| 2 | 火星 | 1993-04-06T21:15:00 | 1996-04-06T15:15:00 | 3 |\n| 3 | 水星 | 1996-04-06T15:15:00 | 2000-04-06T15:15:00 | 4 |\n| 4 | 木星 | 2000-04-06T15:15:00 | 2005-04-06T21:15:00 | 5 |\n| 5 | 土星 | 2005-04-06T21:15:00 | 2011-04-07T09:15:00 | 6 |\n| 6 | 金星 | 2011-04-07T09:15:00 | 2018-04-07T03:15:00 | 7 |\n| 7 | 北交点 | 2018-04-07T03:15:00 | 2026-04-07T03:15:00 | 8 |\n| 8 | 太阳 | 2026-04-07T03:15:00 | 2027-04-07T09:15:00 | 1 |\n\n### Kala Chakra 大运\n\n| 序号 | 主星/星座 | 起始 | 结束 | 年数 |\n|------|-----------|------|------|------|\n| 1 | 南交点 | 1991-04-07T09:15:00 | 2000-04-06T15:15:00 | 9 |\n| 2 | 金星 | 2000-04-06T15:15:00 | 2010-04-07T03:15:00 | 10 |\n| 3 | 太阳 | 2010-04-07T03:15:00 | 2021-04-06T21:15:00 | 11 |\n| 4 | 月亮 | 2021-04-06T21:15:00 | 2033-04-06T21:15:00 | 12 |\n| 5 | 火星 | 2033-04-06T21:15:00 | 2046-04-07T03:15:00 | 13 |\n| 6 | 水星 | 2046-04-07T03:15:00 | 2060-04-06T15:15:00 | 14 |\n| 7 | 木星 | 2060-04-06T15:15:00 | 2075-04-07T09:15:00 | 15 |\n| 8 | 土星 | 2075-04-07T09:15:00 | 2091-04-07T09:15:00 | 16 |\n| 9 | 北交点 | 2091-04-07T09:15:00 | 2108-04-07T15:15:00 | 17 |\n| 10 | 南交点 | 2108-04-07T15:15:00 | 2126-04-08T03:15:00 | 18 |\n| 11 | 金星 | 2126-04-08T03:15:00 | 2133-04-07T21:15:00 | 7 |\n| 12 | 太阳 | 2133-04-07T21:15:00 | 2141-04-07T21:15:00 | 8 |\n\n### Tribhagi 大运\n\n| 序号 | 主星/星座 | 起始 | 结束 | 年数 |\n|------|-----------|------|------|------|\n| 1 | 金星 | 1991-04-07T09:15:00 | 1994-05-07T05:20:14 | 3.082319975537242 |\n| 2 | 太阳 | 1994-05-07T05:20:14 | 1996-05-06T17:38:33 | 2.0 |\n| 3 | 月亮 | 1996-05-06T17:38:33 | 1999-09-06T06:09:05 | 3.3333333333333335 |\n| 4 | 火星 | 1999-09-06T06:09:05 | 2002-01-05T12:30:27 | 2.3333333333333335 |\n| 5 | 北交点 | 2002-01-05T12:30:27 | 2008-01-06T01:25:24 | 6.0 |\n| 6 | 木星 | 2008-01-06T01:25:24 | 2013-05-07T02:14:15 | 5.333333333333333 |\n| 7 | 土星 | 2013-05-07T02:14:15 | 2019-09-06T09:12:15 | 6.333333333333333 |\n| 8 | 水星 | 2019-09-06T09:12:15 | 2025-05-07T04:04:09 | 5.666666666666667 |\n| 9 | 南交点 | 2025-05-07T04:04:09 | 2027-09-06T10:25:31 | 2.3333333333333335 |\n| 10 | 金星 | 2027-09-06T10:25:31 | 2034-05-07T11:26:34 | 6.666666666666667 |\n| 11 | 太阳 | 2034-05-07T11:26:34 | 2036-05-06T23:44:53 | 2.0 |\n| 12 | 月亮 | 2036-05-06T23:44:53 | 2039-09-06T12:15:25 | 3.3333333333333335 |\n| 13 | 火星 | 2039-09-06T12:15:25 | 2042-01-05T18:36:47 | 2.3333333333333335 |\n| 14 | 北交点 | 2042-01-05T18:36:47 | 2048-01-06T07:31:44 | 6.0 |\n| 15 | 木星 | 2048-01-06T07:31:44 | 2053-05-07T08:20:35 | 5.333333333333333 |\n| 16 | 土星 | 2053-05-07T08:20:35 | 2059-09-06T15:18:35 | 6.333333333333333 |\n| 17 | 水星 | 2059-09-06T15:18:35 | 2065-05-07T10:10:29 | 5.666666666666667 |\n| 18 | 南交点 | 2065-05-07T10:10:29 | 2067-09-06T16:31:51 | 2.3333333333333335 |\n| 19 | 金星 | 2067-09-06T16:31:51 | 2074-05-07T17:32:54 | 6.666666666666667 |\n| 20 | 太阳 | 2074-05-07T17:32:54 | 2076-05-07T05:51:13 | 2.0 |\n| 21 | 月亮 | 2076-05-07T05:51:13 | 2079-09-06T18:21:45 | 3.3333333333333335 |\n| 22 | 火星 | 2079-09-06T18:21:45 | 2082-01-06T00:43:07 | 2.3333333333333335 |\n| 23 | 北交点 | 2082-01-06T00:43:07 | 2088-01-06T13:38:04 | 6.0 |\n| 24 | 木星 | 2088-01-06T13:38:04 | 2093-05-07T14:26:55 | 5.333333333333333 |\n| 25 | 土星 | 2093-05-07T14:26:55 | 2099-09-06T21:24:55 | 6.333333333333333 |\n| 26 | 水星 | 2099-09-06T21:24:55 | 2105-05-08T16:16:49 | 5.666666666666667 |\n| 27 | 南交点 | 2105-05-08T16:16:49 | 2107-09-07T22:38:11 | 2.3333333333333335 |\n\n### Tribhagi 40-year 大运\n\n| 序号 | 主星/星座 | 起始 | 结束 | 年数 |\n|------|-----------|------|------|------|\n| 1 | 金星 | 1991-04-07T09:15:00 | 1994-05-07T05:20:14 | 3.082319975537242 |\n| 2 | 太阳 | 1994-05-07T05:20:14 | 1996-05-06T17:38:33 | 2.0 |\n| 3 | 月亮 | 1996-05-06T17:38:33 | 1999-09-06T06:09:05 | 3.3333333333333335 |\n| 4 | 火星 | 1999-09-06T06:09:05 | 2002-01-05T12:30:27 | 2.3333333333333335 |\n| 5 | 北交点 | 2002-01-05T12:30:27 | 2008-01-06T01:25:24 | 6.0 |\n| 6 | 木星 | 2008-01-06T01:25:24 | 2013-05-07T02:14:15 | 5.333333333333333 |\n| 7 | 土星 | 2013-05-07T02:14:15 | 2019-09-06T09:12:15 | 6.333333333333333 |\n| 8 | 水星 | 2019-09-06T09:12:15 | 2025-05-07T04:04:09 | 5.666666666666667 |\n| 9 | 南交点 | 2025-05-07T04:04:09 | 2027-09-06T10:25:31 | 2.3333333333333335 |\n\n### Shodashottari 大运\n\n| 序号 | 主星/星座 | 起始 | 结束 | 年数 |\n|------|-----------|------|------|------|\n| 1 | 南交点 | 1991-04-07T09:15:00 | 1998-03-14T12:26:47 | 6.935219944958795 |\n| 2 | 月亮 | 1998-03-14T12:26:47 | 2014-03-14T14:53:19 | 16.0 |\n| 3 | 水星 | 2014-03-14T14:53:19 | 2031-03-14T23:29:01 | 17.0 |\n| 4 | 金星 | 2031-03-14T23:29:01 | 2049-03-14T14:13:52 | 18.0 |\n| 5 | 太阳 | 2049-03-14T14:13:52 | 2060-03-14T09:54:37 | 11.0 |\n| 6 | 火星 | 2060-03-14T09:54:37 | 2072-03-14T11:44:31 | 12.0 |\n| 7 | 木星 | 2072-03-14T11:44:31 | 2085-03-14T19:43:34 | 13.0 |\n| 8 | 土星 | 2085-03-14T19:43:34 | 2099-03-15T09:51:47 | 14.0 |\n\n| AD/child | 主层 | 子层 | 起始 | 结束 |\n|----------|------|------|------|------|\n| 1 | - | 南交点 | 1991-04-07T09:15:00 | 1992-02-18T00:38:58 |\n| 2 | - | 月亮 | 1992-02-18T00:38:58 | 1992-12-30T16:02:56 |\n| 3 | - | 水星 | 1992-12-30T16:02:56 | 1993-11-12T07:26:55 |\n| 4 | - | 金星 | 1993-11-12T07:26:55 | 1994-09-24T22:50:53 |\n| 5 | - | 太阳 | 1994-09-24T22:50:53 | 1995-08-07T14:14:51 |\n| 6 | - | 火星 | 1995-08-07T14:14:51 | 1996-06-19T05:38:50 |\n| 7 | - | 木星 | 1996-06-19T05:38:50 | 1997-05-01T21:02:48 |\n| 8 | - | 土星 | 1997-05-01T21:02:48 | 1998-03-14T12:26:47 |\n| 9 | - | 月亮 | 1998-03-14T12:26:47 | 2000-03-14T00:45:06 |\n| 10 | - | 水星 | 2000-03-14T00:45:06 | 2002-03-14T13:03:25 |\n| 11 | - | 金星 | 2002-03-14T13:03:25 | 2004-03-14T01:21:44 |\n| 12 | - | 太阳 | 2004-03-14T01:21:44 | 2006-03-14T13:40:03 |\n| 13 | - | 火星 | 2006-03-14T13:40:03 | 2008-03-14T01:58:22 |\n| 14 | - | 木星 | 2008-03-14T01:58:22 | 2010-03-14T14:16:41 |\n| 15 | - | 土星 | 2010-03-14T14:16:41 | 2012-03-14T02:35:00 |\n| 16 | - | 南交点 | 2012-03-14T02:35:00 | 2014-03-14T14:53:19 |\n| 17 | - | 水星 | 2014-03-14T14:53:19 | 2016-04-28T18:57:46 |\n| 18 | - | 金星 | 2016-04-28T18:57:46 | 2018-06-13T23:02:14 |\n| 19 | - | 太阳 | 2018-06-13T23:02:14 | 2020-07-29T03:06:42 |\n| 20 | - | 火星 | 2020-07-29T03:06:42 | 2022-09-13T07:11:10 |\n| 21 | - | 木星 | 2022-09-13T07:11:10 | 2024-10-28T11:15:37 |\n| 22 | - | 土星 | 2024-10-28T11:15:37 | 2026-12-13T15:20:05 |\n| 23 | - | 南交点 | 2026-12-13T15:20:05 | 2029-01-27T19:24:33 |\n| 24 | - | 月亮 | 2029-01-27T19:24:33 | 2031-03-14T23:29:01 |\n| 25 | - | 金星 | 2031-03-14T23:29:01 | 2033-06-13T19:19:37 |\n| 26 | - | 太阳 | 2033-06-13T19:19:37 | 2035-09-13T15:10:13 |\n| 27 | - | 火星 | 2035-09-13T15:10:13 | 2037-12-13T11:00:50 |\n| 28 | - | 木星 | 2037-12-13T11:00:50 | 2040-03-14T06:51:26 |\n| 29 | - | 土星 | 2040-03-14T06:51:26 | 2042-06-14T02:42:02 |\n| 30 | - | 南交点 | 2042-06-14T02:42:02 | 2044-09-12T22:32:39 |\n| 31 | - | 月亮 | 2044-09-12T22:32:39 | 2046-12-13T18:23:15 |\n| 32 | - | 水星 | 2046-12-13T18:23:15 | 2049-03-14T14:13:52 |\n| 33 | - | 太阳 | 2049-03-14T14:13:52 | 2050-07-29T19:41:27 |\n| 34 | - | 火星 | 2050-07-29T19:41:27 | 2051-12-14T01:09:03 |\n| 35 | - | 木星 | 2051-12-14T01:09:03 | 2053-04-29T06:36:38 |\n| 36 | - | 土星 | 2053-04-29T06:36:38 | 2054-09-13T12:04:14 |\n| 37 | - | 南交点 | 2054-09-13T12:04:14 | 2056-01-28T17:31:50 |\n| 38 | - | 月亮 | 2056-01-28T17:31:50 | 2057-06-13T22:59:25 |\n| 39 | - | 水星 | 2057-06-13T22:59:25 | 2058-10-29T04:27:01 |\n| 40 | - | 金星 | 2058-10-29T04:27:01 | 2060-03-14T09:54:37 |\n| 41 | - | 火星 | 2060-03-14T09:54:37 | 2061-09-13T07:08:21 |\n| 42 | - | 木星 | 2061-09-13T07:08:21 | 2063-03-15T04:22:05 |\n| 43 | - | 土星 | 2063-03-15T04:22:05 | 2064-09-13T01:35:49 |\n| 44 | - | 南交点 | 2064-09-13T01:35:49 | 2066-03-14T22:49:34 |\n| 45 | - | 月亮 | 2066-03-14T22:49:34 | 2067-09-13T20:03:18 |\n| 46 | - | 水星 | 2067-09-13T20:03:18 | 2069-03-14T17:17:02 |\n| 47 | - | 金星 | 2069-03-14T17:17:02 | 2070-09-13T14:30:46 |\n| 48 | - | 太阳 | 2070-09-13T14:30:46 | 2072-03-14T11:44:31 |\n| 49 | - | 木星 | 2072-03-14T11:44:31 | 2073-10-29T00:44:23 |\n| 50 | - | 土星 | 2073-10-29T00:44:23 | 2075-06-14T13:44:16 |\n| 51 | - | 南交点 | 2075-06-14T13:44:16 | 2077-01-28T02:44:09 |\n| 52 | - | 月亮 | 2077-01-28T02:44:09 | 2078-09-13T15:44:02 |\n| 53 | - | 水星 | 2078-09-13T15:44:02 | 2080-04-29T04:43:55 |\n| 54 | - | 金星 | 2080-04-29T04:43:55 | 2081-12-13T17:43:48 |\n| 55 | - | 太阳 | 2081-12-13T17:43:48 | 2083-07-30T06:43:41 |\n| 56 | - | 火星 | 2083-07-30T06:43:41 | 2085-03-14T19:43:34 |\n| 57 | - | 土星 | 2085-03-14T19:43:34 | 2086-12-14T00:29:35 |\n| 58 | - | 南交点 | 2086-12-14T00:29:35 | 2088-09-13T05:15:37 |\n| 59 | - | 月亮 | 2088-09-13T05:15:37 | 2090-06-14T10:01:38 |\n| 60 | - | 水星 | 2090-06-14T10:01:38 | 2092-03-14T14:47:40 |\n| 61 | - | 金星 | 2092-03-14T14:47:40 | 2093-12-13T19:33:42 |\n| 62 | - | 太阳 | 2093-12-13T19:33:42 | 2095-09-14T00:19:43 |\n| 63 | - | 火星 | 2095-09-14T00:19:43 | 2097-06-14T05:05:45 |\n| 64 | - | 木星 | 2097-06-14T05:05:45 | 2099-03-15T09:51:47 |\n\n## 土星 与 KP 支持页\n\n### Sade Sati / Dhayya / Kantaka 土星\n\n- 月亮 sign:射手座\n- 当前阶段:none\n- 是否 active:False\n- 摘要:Sade Sati阶段框架:上升期=天蝎座,高峰期=射手座(最困难),下降期=摩羯座;需结合土星实际位置判断是否在进行中\n\n| Phase | Sign |\n|-------|------|\n| rising | 天蝎座 |\n| peak | 射手座 |\n| setting | 摩羯座 |\n\n### KP Chart / Cusps\n\n- House basis:explicit_cusps\n\n| House | Sign | Cusp longitude | KP lord chain |\n|-------|------|----------------|---------------|\n| 1 | 金牛座 | 38.9520 | RL=金星;NL=太阳;SL=金星;SSL=木星 |\n| 2 | 双子座 | 64.0900 | RL=水星;NL=火星;SL=金星;SSL=木星 |\n| 3 | 双子座 | 86.8992 | RL=水星;NL=木星;SL=金星;SSL=金星 |\n| 4 | 巨蟹座 | 111.5368 | RL=月亮;NL=水星;SL=金星;SSL=南交点 |\n| 5 | 狮子座 | 141.6932 | RL=太阳;NL=金星;SL=木星;SSL=北交点 |\n| 6 | 处女座 | 179.2564 | RL=水星;NL=火星;SL=土星;SSL=月亮 |\n| 7 | 天蝎座 | 218.9520 | RL=火星;NL=土星;SL=金星;SSL=北交点 |\n| 8 | 射手座 | 244.0900 | RL=木星;NL=南交点;SL=月亮;SSL=木星 |\n| 9 | 射手座 | 266.8992 | RL=木星;NL=太阳;SL=太阳;SSL=木星 |\n| 10 | 摩羯座 | 291.5368 | RL=土星;NL=月亮;SL=金星;SSL=北交点 |\n| 11 | 水瓶座 | 321.6932 | RL=土星;NL=木星;SL=木星;SSL=北交点 |\n| 12 | 双鱼座 | 359.2564 | RL=木星;NL=水星;SL=土星;SSL=月亮 |\n\n### KP Significators / Planet Lords\n\n| Planet | KP lord chain | A | B | C | D |\n|--------|---------------|---|---|---|---|\n| 太阳 | RL=木星;NL=水星;SL=月亮;SSL=南交点 | 12 | 12 | 2;3;6 | 5 |\n| 月亮 | RL=木星;NL=金星;SL=木星;SSL=土星 | 1 | 9 | 1 | 4 |\n| 火星 | RL=水星;NL=北交点;SL=北交点;SSL=月亮 | 9 | 2 | - | 7 |\n| 水星 | RL=火星;NL=南交点;SL=火星;SSL=土星 | 3 | 12 | - | 2;3;6 |\n| 木星 | RL=月亮;NL=土星;SL=金星;SSL=水星 | 10 | 4 | 10;11 | 8;9;12 |\n| 金星 | RL=火星;NL=太阳;SL=北交点;SSL=北交点 | 12 | 1 | 5 | 1 |\n| 土星 | RL=土星;NL=月亮;SL=北交点;SSL=北交点 | 9 | 10 | 4 | 10;11 |\n| 北交点 | RL=土星;NL=太阳;SL=北交点;SSL=南交点 | 12 | 9 | 5 | - |\n| 南交点 | RL=月亮;NL=木星;SL=月亮;SSL=金星 | 4 | 3 | 8;9;12 | - |\n\n### Natal / KP Ruling Planets\n\n| Field | Planet |\n|-------|--------|\n| day_lord | 太阳 |\n| moon_sign_lord | 木星 |\n| moon_star_lord | 金星 |\n| moon_sub_lord | 木星 |\n| ascendant_sign_lord | 金星 |\n| ascendant_star_lord | 太阳 |\n| ascendant_sub_lord | 金星 |\n\n## 年度图盘与 Tajika\n\n### Varshaphala 基础资料与年度行星表\n\n- Varshapravesha:2026-04-07 00:48:04\n- 年返上升:金牛座\n\n#### Varshesha / Year Lord\n\n| Field | Value |\n|-------|-------|\n| Varshesha | - |\n| Selection basis | - |\n| Annual ascendant sign | - |\n| Muntha sign | - |\n\n### 2027 年度 Varshaphala / Tajika 细项\n\n| 项目 | 数值 |\n|------|------|\n| Varshapravesha | - |\n| 年返上升 | 巨蟹座 |\n| Varshesha | - |\n| Muntha sign | - |\n\n### 2028 年度 Varshaphala / Tajika 细项\n\n| 项目 | 数值 |\n|------|------|\n| Varshapravesha | - |\n| 年返上升 | 天秤座 |\n| Varshesha | - |\n| Muntha sign | - |\n\n## Bhavesh、Dosha 与瑜伽 解释页\n\n### 宫主(Bhavesh)落宫解释\n\n| House | Sign | Lord | Lord placement | Lord dignity | 大运 activation |\n|-------|------|------|----------------|--------------|------------------|\n| 1 | 金牛座 | 金星 | 白羊座 H12 29.23° | 入友(友好星座) | background |\n| 2 | 双子座 | 水星 | 白羊座 H12 4.99° | 入友(友好星座) | background |\n| 3 | 巨蟹座 | 月亮 | 射手座 H8 20.50° | 入敌(敌对星座) | background |\n| 4 | 狮子座 | 太阳 | 双鱼座 H11 22.99° | 中性(中性) | background |\n| 5 | 处女座 | 水星 | 白羊座 H12 4.99° | 入友(友好星座) | background |\n| 6 | 天秤座 | 金星 | 白羊座 H12 29.23° | 入友(友好星座) | background |\n| 7 | 天蝎座 | 火星 | 双子座 H2 8.40° | 中性(中性) | background |\n| 8 | 射手座 | 木星 | 巨蟹座 H3 9.90° | 入旺(入旺) | active |\n| 9 | 摩羯座 | 土星 | 摩羯座 H9 11.82° | 入庙(入庙) | background |\n| 10 | 水瓶座 | 土星 | 摩羯座 H9 11.82° | 入庙(入庙) | background |\n| 11 | 双鱼座 | 木星 | 巨蟹座 H3 9.90° | 入旺(入旺) | active |\n| 12 | 白羊座 | 火星 | 双子座 H2 8.40° | 中性(中性) | background |\n\n### Mangala / Sade Sati / Dosha Results\n\n| Topic | Detected / phase | Key facts | Summary |\n|-------|------------------|-----------|---------|\n| Mangal / Kuja Dosha | True | mars_house=2;mars_sign=双子座;severity=中等 | 有火星婚姻煞——火星在2宫双子座,严重程度:中等 |\n| 时蛇煞 | True | rahu_house=9;ketu_house=3;type=局部 | 部分时蛇煞——5/7颗行星在轴线之间 |\n| Pitra Dosha | False | sun_house=11;sun_sign=双鱼座 | 无Pitra Dosha明显迹象 |\n| Sade Sati | False | moon_sign=射手座 | Sade Sati阶段框架:上升期=天蝎座,高峰期=射手座(最困难),下降期=摩羯座;需结合土星实际位置判断是否在进行中 |\n\n### 可用瑜伽总表\n\n- 检出的瑜伽数量:39\n- 参考层:d60;d9;panchanga\n\n#### 可用吉性瑜伽\n\n| 瑜伽 | 类别 | 强度 | 组合条件 | 作用说明 |\n|------|------|------|----------|----------|\n| 太阳后瑜伽 | 太阳类 | 中 | 行星在太阳第2宫 | 财富充裕;生活舒适;性情愉快 |\n| 月前瑜伽 | 月亮类 | 中 | 行星在月亮第2宫 | 自力更生;财富充裕;受人尊敬 |\n| 喜悦逆行格局 | 王权/事业 | 强 | 第6宫主星落入凶宫 | 因祸得福;逆境崛起;困境中成长 |\n| 王者格局(互视) | 王权/事业 | 中 | 自定义条件满足 | 权力地位;影响力 |\n| 王者格局(三方宫主星入旺) | 王权/事业 | 强 | 自定义条件满足 | 智慧卓越;命运眷顾;精神成就 |\n| 半天空格局 | 天象格局 | 中 | 自定义条件满足 | 平衡人生;中等成就 |\n| 子女格局(木星有力) | 子女/创造 | 强 | 自定义条件满足 | 子女运佳;子女智慧 |\n| 福运格局 | 吉性 | 强 | 自定义条件满足 | 命运眷顾;好运连连 |\n| 王者格局(2宫主+7宫主) | 王权/事业 | 强 | 自定义条件满足 | 财富与婚姻结合;配偶带来财富 |\n| 财富瑜伽(5/9宫主入财富宫) | 财富 | 强 | 5宫主太阳在第11宫 | 通过才华/运气致富;祖先财富 |\n| 事业瑜伽(10/11宫主合相) | karma | 强 | 土星与土星同在第9宫 | 事业成功;人脉资源强;收入与社会地位同步提升 |\n| 愿望瑜伽(11宫主强) | 愿望/收益 | 中 | 土星Saturnstrong在摩羯座 | 愿望实现;社交网络强大 |\n| 解脱瑜伽(12宫主强) | 解脱/内在 | 中 | 木星Jupiterstrong在巨蟹座 | 灵性解脱倾向;隐居/出世 |\n| 解脱瑜伽(4/8/12宫主合相) | 解脱/内在 | 强 | 南交点南交点/计都与木星同在第3宫 | 终极解脱;灵性智慧 |\n| 贵人瑜伽(9宫主入旺) | 贵人/助力 | 强 | 木星Jupiterexalted在巨蟹座 | 强大贵人运;祖先庇佑 |\n| 日后行星瑜伽(精确) | 太阳类 | 中 | 水星、金星在太阳的第2宫 | 太阳后方有行星支持;表达力与行动力增强;自我驱动力较强 |\n| 月后行星瑜伽(精确) | 月亮类 | 中 | 行星在月亮的第2宫 | 自力更生;资源积累能力;心智有后续支撑 |\n| 疾病缠身瑜伽 | 健康 | 弱 | 上升主在上升合凶宫主,或弱上升主合角/三方主 | 疾病;健康需注意;体质较弱 |\n| 常行瑜伽 | 迁移/出行 | 弱 | 上升主或月亮在变动星座 | 经常旅行;漂泊;不安于室 |\n| 真诚瑜伽 | 吉性 | 中 | 第4宫有天然吉星/强星/吉性星座,或上升主入4宫且受吉星影响 | 心地善良;真诚待人;家庭和谐 |\n| 婆罗底瑜伽 | 王权/事业 | rare | Lord of sign occupied in navamsa by 2nd/5th/11th lord, exalted and joins the 9th lord | Learned, eloquent, wealthy from knowledge |\n| 大绳瑜伽 | 天象格局 | 偶见 | 7 visible planets occupy exactly 6 distinct signs | Leader, wealthy, charitable |\n\n#### 可用混合型瑜伽\n\n| 瑜伽 | 类别 | 强度 | 组合条件 | 作用说明 |\n|------|------|------|----------|----------|\n| 水金合相格局 | 合相 | 强 | 水星与金星同在第12宫 | 艺术才华;商业头脑;审美卓越 |\n| 土罗合相格局 | 合相 | 中 | 土星与罗睺同在第9宫 | 纪律与变革;突破传统;但也可能带来阻碍 |\n| 成长格局 | 特殊组合 | 强 | 吉星在8/9/12宫 | 精神成长;灵性提升;命运眷顾 |\n| 婚姻障碍(7宫主在凶宫) | 婚恋 | 弱 | 自定义条件满足 | 婚姻挑战;配偶健康问题 |\n| 贤妻瑜伽 | 婚恋 | 中 | 7宫主或金星与木星/水星合相或互照 | 配偶品德高尚;婚姻和谐 |\n| 达摩/达姆尼瑜伽 | 特殊组合 | 待评估 | 七曜分布在六个星座 | 待补充 |\n| 房屋损失瑜伽 | 特殊组合 | 中等 | Lord of navamsa occupied by 4th lord in 12th | Loss or destruction of property/house |\n| 房屋损失瑜伽(变体) | 特殊组合 | 中等 | 自定义条件满足 | Loss or destruction of property/house |\n| 消瘦体型瑜伽 | 特殊组合 | 中等 | 上升主落在干燥星座 | 体型偏瘦,体质较敏感 |\n| 讥笑瑜伽 | 特殊组合 | 中等 | 自定义条件满足 | Ridiculed by others, mocked, subject to derision |\n| 太阳后瑜伽 | 太阳瑜伽 | 中 | 水星+金星在太阳第2宫 | 财富充裕;生活舒适;性情愉快 |\n| 月前瑜伽 | 月亮瑜伽 | 中 | 土星+北交点/罗睺在月亮第2宫 | 自力更生;财富充裕;受人尊敬 |\n\n#### 可用凶性瑜伽\n\n| 瑜伽 | 类别 | 强度 | 组合条件 | 作用说明 |\n|------|------|------|----------|----------|\n| 凶星夹击格局 | 不利组合 | 弱 | 某宫被凶星前后夹击 | 领域受限;需要额外努力;阻碍与挑战 |\n| 贫困格局(6宫主) | 不利组合 | 弱 | 自定义条件满足 | 债务问题;需要节约 |\n| 贫穷瑜伽(2宫主入6/8/12宫) | 不利组合 | 弱 | 2宫主金星在第12宫 | 财务不稳定;储蓄困难 |\n| 被亲属遗弃瑜伽 | 不利组合 | 弱 | 4宫主受凶星关联或处于弱/不利状态 | 被亲属遗弃;缺乏家庭支持;家庭关系疏远 |\n| 母亲不利瑜伽 | 不利组合 | 弱 | 月亮受凶星夹制/合相/相位,或4宫主九分盘链最终落6/8/12 | 母亲健康不佳;与母亲缘薄;母亲早逝 |\n" +} diff --git a/frontend/tests/personal-report-density-retry.test.ts b/frontend/tests/personal-report-density-retry.test.ts index 8be6c11f..88c44b42 100644 --- a/frontend/tests/personal-report-density-retry.test.ts +++ b/frontend/tests/personal-report-density-retry.test.ts @@ -15,8 +15,10 @@ const golden = JSON.parse(readFileSync(new URL("./fixtures/report-density-fictio test("density full engine reader cleanup removes embedded product contracts without losing rows or numeric fields", () => { assert.equal(golden.fixtureProvenance.fictional, true); const raw = golden.markdown as string; - // Captured by running baeec66f8's actual projector on this complete fictional engine Markdown. - assert.equal(createHash("sha256").update(projectOrdinaryReportMarkdown(raw)).digest("hex"), "01cc1e03c5adca2833e014156e1162fff12e5cb336e4484c5871859cd878acd7"); + // 原值 01cc1e03c5adca2833e014156e1162fff12e5cb336e4484c5871859cd878acd7 + // 新值 f3aeb8f4b09ba0a78af4ab5e7aee4db32d7499b8e6b1488f2925b35fd91750b9 + // 原因:原 sha 锁的是投影截断后的泄漏输出(半句与孤立标记仍在)。整行删除 PL9 内部词后正文更短。 + assert.equal(createHash("sha256").update(projectOrdinaryReportMarkdown(raw)).digest("hex"), "f3aeb8f4b09ba0a78af4ab5e7aee4db32d7499b8e6b1488f2925b35fd91750b9"); assert.match(raw, /pl9_personal_long_report\.v2/); const clean = cleanReaderAppendixMarkdown(raw); assert.doesNotMatch(clean, /pl9/i); diff --git a/frontend/tests/professional-report-reference-route.test.ts b/frontend/tests/professional-report-reference-route.test.ts index c4050108..1ed6e902 100644 --- a/frontend/tests/professional-report-reference-route.test.ts +++ b/frontend/tests/professional-report-reference-route.test.ts @@ -101,6 +101,15 @@ function executeReadyReportExport(cached: boolean) { }; } +test("new longform generation requests the reader_main edition", () => { + const generateSource = readFileSync( + new URL("../src/lib/personal-report-longform-generate.ts", import.meta.url), + "utf8", + ); + assert.match(generateSource, /edition:\s*"reader_main"/); + assert.match(generateSource, /include_fact_tables:\s*true/); +}); + test("professional reference route authenticates, checks origin, owner and ready status", () => { assert.match(routeSource, /createServerSupabaseClient/); assert.match(routeSource, /supabase\.auth\.getUser\(\)/); diff --git a/frontend/tests/report-export-blocks.test.ts b/frontend/tests/report-export-blocks.test.ts index 803890ae..e535b234 100644 --- a/frontend/tests/report-export-blocks.test.ts +++ b/frontend/tests/report-export-blocks.test.ts @@ -58,7 +58,10 @@ test("full golden body is byte identical to the retired ordinary export", () => test("LF golden ordinary export keeps its frozen output hash", () => { const output = ordinaryReportDownloadMarkdown(fixture.markdown); - assert.equal(createHash("sha256").update(output).digest("hex"), "3c52e8bfcfcb3ce3852bd5323867a69bc47d0f9aa821a34692172e7d61c6bcb8"); + // 原值 3c52e8bfcfcb3ce3852bd5323867a69bc47d0f9aa821a34692172e7d61c6bcb8 + // 新值 b7f6947f6ae6f82e4a972be3c1f2bc50260ed0c5f029847264f40a5ee6ba7b88 + // 原因:原 sha 锁的是同一份泄漏正文的普通下载。BUG-1026 起内部词整行删除,不再截成半句。 + assert.equal(createHash("sha256").update(output).digest("hex"), "b7f6947f6ae6f82e4a972be3c1f2bc50260ed0c5f029847264f40a5ee6ba7b88"); }); test("CRLF and mixed-newline golden exports remove every JSON chart fence", () => { diff --git a/frontend/tests/report-public-projection.test.ts b/frontend/tests/report-public-projection.test.ts index d75a7d4a..68d911e1 100644 --- a/frontend/tests/report-public-projection.test.ts +++ b/frontend/tests/report-public-projection.test.ts @@ -301,6 +301,70 @@ test("markdown view and chart fence contracts are not weakened", () => { assert.match(coreSource, /projectOrdinaryReportDocument/); }); +test("PL9 audit vocabulary deletes the whole line and the adjacent table caption", () => { + const source = [ + "# 读者报告", + "", + "这一句保留。", + "未完成对照,所有行保持 parameter_sensitive。", + "", + "### KP 原始表", + "", + "本节只解释下面这张表。", + "", + "| 项 | 状态 |", + "| --- | --- |", + "| annual_tajika_pack | blocked |", + "", + "表后的正文还在。", + "KP producer failed: 'types.SimpleNamespace' object has no attribute 'year'", + "Traceback (most recent call last):", + "full-report://profile/hash", + "PL9 第 42 页 fixture", + "pl9_personal_long_report.v2", + "kp_lord_sub_unavailable", + ].join("\n"); + const projected = projectOrdinaryReportMarkdown(source); + assert.match(projected, /这一句保留/); + assert.match(projected, /表后的正文还在/); + assert.doesNotMatch(projected, /所有行保持/); + assert.doesNotMatch(projected, /parameter_sensitive/); + assert.doesNotMatch(projected, /KP 原始表/); + assert.doesNotMatch(projected, /本节只解释下面这张表/); + assert.doesNotMatch(projected, /annual_tajika_pack/); + assert.doesNotMatch(projected, /^- `$/m); + assert.doesNotMatch(projected, /object has no attribute|Traceback|full-report:\/\/|PL9 第 42 页|pl9_personal_long_report|_unavailable/); + assert.deepEqual(ordinaryOutputLeaks(projected), []); +}); + +test("real reader_main fixture has no ordinary output leaks", () => { + const fixture = JSON.parse(readFileSync(new URL("./fixtures/report-reader-main-fictional.json", import.meta.url), "utf8")); + assert.equal(fixture.fixtureProvenance.fictional, true); + assert.equal(fixture.fixtureProvenance.edition, "reader_main"); + const markdown = fixture.markdown as string; + assert.equal(ordinaryOutputLeaks(markdown).length, 0, ordinaryOutputLeaks(markdown).join(", ")); + for (const chapter of ["出生资料", "Ashtakavarga", "标准大运", "KP", "Year Lord", "Tajika", "Muntha"]) { + assert.match(markdown, new RegExp(chapter)); + } + const projected = projectOrdinaryReportMarkdown(markdown); + assert.equal(ordinaryOutputLeaks(projected).length, 0); +}); + +test("twelve English words count as a paragraph leak, planet names do not", () => { + const planet = projectOrdinaryReportMarkdown("太阳与 Mars 同宫,再看 Venus。"); + assert.match(planet, /Mars/); + assert.match(planet, /Venus/); + const english = [ + "This reference export combines local calculations with available external engine", + "replays and source comparisons that stay explicitly labeled.", + ].join(" "); + const projected = projectOrdinaryReportMarkdown(`开头。\n${english}\n结尾。`); + assert.match(projected, /开头/); + assert.match(projected, /结尾/); + assert.doesNotMatch(projected, /This reference export/); + assert.deepEqual(ordinaryOutputLeaks(projected), []); +}); + function syntheticRecord(): PersonalReportRecord { return { id: "11111111-1111-4111-8111-111111111111", diff --git a/scripts/annual_tajika_pack.py b/scripts/annual_tajika_pack.py index 96ddec64..6bf25be9 100755 --- a/scripts/annual_tajika_pack.py +++ b/scripts/annual_tajika_pack.py @@ -226,12 +226,41 @@ def _first_available_field(label: str, *reports_and_key: Any) -> dict[str, Any]: return {"status": "blocked", "producer": label, "reason": f"{key}_missing", "producers_checked": producers} +def _annual_comparable_value(key: str, value: Any) -> Any: + """Compare sign and lord only. Extra dict keys are not a disagreement.""" + if not isinstance(value, dict): + return value + if key == "muntha": + return { + "muntha_sign": value.get("muntha_sign"), + "muntha_lord": value.get("muntha_lord"), + } + if key == "year_lord": + return { + "year_lord": value.get("year_lord") or value.get("lord"), + "year_lord_sign": value.get("year_lord_sign") or value.get("sign"), + } + return { + "sign": value.get("sign") or value.get("muntha_sign") or value.get("year_lord_sign"), + "lord": value.get("lord") or value.get("muntha_lord") or value.get("year_lord"), + } + + +def _field_value_is_blocked(value: Any) -> bool: + return isinstance(value, dict) and value.get("status") == "blocked" + + def _field_with_conflict_gate(key: str, solar_report: dict[str, Any], tajika_report: dict[str, Any]) -> dict[str, Any]: values = [] for producer, report in (("solar_return", solar_report), ("tajika", tajika_report)): - if isinstance(report, dict) and report.get(key) is not None and not report.get("error"): - values.append({"producer": producer, "value": report[key]}) - if len(values) >= 2 and values[0]["value"] != values[1]["value"]: + if not isinstance(report, dict) or report.get(key) is None or report.get("error"): + continue + value = report[key] + if _field_value_is_blocked(value): + continue + values.append({"producer": producer, "value": value}) + comparable_values = [_annual_comparable_value(key, item["value"]) for item in values] + if len(values) >= 2 and comparable_values[0] != comparable_values[1]: return { "status": "conflict", "field": key, diff --git a/scripts/jyotish_engine.py b/scripts/jyotish_engine.py index 5ce83773..4ecb8461 100644 --- a/scripts/jyotish_engine.py +++ b/scripts/jyotish_engine.py @@ -1975,6 +1975,29 @@ def _personal_report_source_pack_contracts( } +def _copy_call_namespace(args): + """Copy CLI args without using an empty instance ``vars()`` dict. + + ``type('Args', (), fields)()`` stores fields on the class, so ``vars(args)`` + is ``{}`` and ``SimpleNamespace(**vars(args))`` drops ``year`` and the rest. + argparse and SimpleNamespace keep fields on the instance. Upstream has the + same ``vars()`` copy. + """ + fields = {} + instance = getattr(args, "__dict__", None) + if isinstance(instance, dict): + fields.update({ + key: value for key, value in instance.items() if not str(key).startswith("_") + }) + cls = type(args) + if cls is not SimpleNamespace: + for key, value in vars(cls).items(): + if str(key).startswith("_") or callable(value): + continue + fields.setdefault(key, value) + return SimpleNamespace(**fields) + + def _attach_annual_tajika_pack(packet: dict, args) -> dict: worksheets = packet.get('worksheets') if isinstance(packet.get('worksheets'), dict) else {} timing = worksheets.get('timing_and_predictive_systems') if isinstance(worksheets.get('timing_and_predictive_systems'), dict) else None @@ -1997,7 +2020,7 @@ def _attach_annual_tajika_pack(packet: dict, args) -> dict: annual_years = {str(target_year): _annual_series_surface(timing['annual_tajika_pack'])} for year in range(target_year + 1, target_year + 3): - annual_args = SimpleNamespace(**vars(args)) + annual_args = _copy_call_namespace(args) annual_args.target_year = year try: birth_year = int(getattr(args, 'year')) @@ -2698,12 +2721,13 @@ def _native_dasha_master_families(modules: dict) -> dict: def _blocked_planned_section(heading: str, reason_code: str, detail: str = "") -> list[str]: - """Emit a planned heading with an explicit blocked reason instead of vanishing.""" - lines = [heading, '', f"`blocked` / `{reason_code}`"] - if detail: - lines.append(str(detail).strip()) - lines.append('') - return lines + """Unavailable sections are omitted entirely, including raw error text. + + Upstream returns an empty block when a producer has nothing to show. + Printing ``error`` / ``reason`` put traceback text into the reference body. + """ + del heading, reason_code, detail + return [] PLANNED_LONGFORM_SECTION_HEADINGS = ( @@ -3270,12 +3294,6 @@ def _render_finished_reading_navigation(packet: dict) -> list[str]: '4. 校时敏感层:有候选窗才读矩阵,没有则视为未做校时。', '5. 质量验收矩阵与对照覆盖表,只用来核对装配,不改写正文标签。', '', - '### 结论等级规则', - '', - '- `executed` / `partial_verified`:可阅读,但仍受参数与证据边界约束。', - '- `parameter_sensitive`:可交叉核对,不得升级为精确事件。', - '- `blocked` / `missing_in_local`:保持原标签,不得涂成可读结论。', - '', '### 专题判读协议', '', '- 事业先看 2/6/10/11 宫 KP 结构与 D10,再看三年月度支持。', @@ -3605,17 +3623,13 @@ def render_pl9_markdown(packet: dict) -> str: if not isinstance(notice, dict): return [] primary_zh = notice.get('primary_text_zh') or notice.get('text_zh') or notice.get('summary_zh') or '' - primary_en = notice.get('primary_text_en') or notice.get('text_en') or '' display_rule = notice.get('display_rule') or '' - if not any([primary_zh, primary_en, display_rule]): + if not any([primary_zh, display_rule]): return [] lines_out = ['', '## 多引擎口径说明', ''] if primary_zh: lines_out.append(str(primary_zh)) lines_out.append('') - if primary_en: - lines_out.append(str(primary_en)) - lines_out.append('') if display_rule: lines_out.append(f"显示规则:{display_rule}") lines_out.append('') @@ -13609,7 +13623,7 @@ def cmd_ashtakoot(args): # ============================================================================ def cmd_kp(args): requested_report_ayanamsa = _current_ayanamsa_name(args) - kp_args = SimpleNamespace(**vars(args)) + kp_args = _copy_call_namespace(args) kp_args.ayanamsa = 'kp' chart, asc_idx, jd, ayanamsa = _compute_chart_from_args(kp_args) @@ -15907,7 +15921,7 @@ def cmd_full_reading(args): # They are intentionally not fed into conclusion generation or timing promotion. reference_date = getattr(args, 'transit_date', None) or getattr(args, 'today', None) if reference_date: - transit_args = SimpleNamespace(**vars(args)) + transit_args = _copy_call_namespace(args) transit_args.date = reference_date transit_args.transit_date = reference_date transit_args.house = 7 @@ -17735,12 +17749,7 @@ def _professional_report_reference_boundary_notice() -> dict: '不同引擎的岁差、节点、宫制、分盘和大运变体可能产生差异;' '冲突、参数敏感或未验证项目会保留边界标签,不合并成单一确定结论。' ), - 'primary_text_en': ( - 'This reference export combines local Jyotish calculations with available external-engine ' - 'replays and source comparisons. Engine-specific settings can differ, so conflicts and ' - 'unverified variants remain explicitly labeled rather than being collapsed into one claim.' - ), - 'display_rule': '以本报告标注的 executed / blocked / parameter_sensitive 状态为准。', + 'display_rule': '冲突和未验证的项目保留边界,不合并成单一确定结论。', } diff --git a/scripts/pl9_reader_export.py b/scripts/pl9_reader_export.py new file mode 100644 index 00000000..33f2577c --- /dev/null +++ b/scripts/pl9_reader_export.py @@ -0,0 +1,2504 @@ +"""User-facing PL9 reader export. + +Copied from upstream yinduzhanxing ``origin/main`` ``23b9609e330dbee70a4725bdc222b125d9b0e39f`` +(reader edition introduced by ``f3b5196fecb23b4c71d60301bf424c697cb0e8cb``). +The task book expected tip ``19195f6e``; fetch showed ``23b9609e``, which contains that commit. +``f3b5196f..23b9609e`` reader closure lives here so ``scripts/jyotish_engine.py`` is not +rewritten in place (concession 1). Algorithm text is unchanged except ``_bind_engine_symbols``: +``render_pl9_parity_markdown`` reads ``SIGNS`` / ``SIGNS_CN`` through ``globals()``, which only +works in the engine module. + +Upstream ``_pl9_export_markdown_for_edition`` also serves ``pl9_ai_density``. That branch needs +``_render_pl9_ai_density_raw_data_markdown_en`` and the English density sanitizers, which are +outside the ``reader_main`` closure and are not ported. ``reader_main`` and the reference +default match the upstream branches. +""" + +from __future__ import annotations + +import re + +SIGNS: list[str] = [] +SIGNS_CN: dict[str, str] = {} + + +def _bind_engine_symbols() -> None: + """Point sign tables at the live engine module without importing it at load time.""" + global SIGNS, SIGNS_CN + try: + from scripts import jyotish_engine as engine + except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution + import jyotish_engine as engine + SIGNS = list(engine.SIGNS) + SIGNS_CN = dict(engine.SIGNS_CN) + + +def _reference_markdown(packet: dict) -> str: + try: + from scripts import jyotish_engine as engine + except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution + import jyotish_engine as engine + return engine.render_pl9_markdown(packet) + + +def _pl9_export_markdown_for_edition(packet: dict, edition: str) -> str: + """Dispatch reader_main vs the reference renderer. + + Upstream ``23b9609e`` ``scripts/jyotish_engine.py`` lines 13099-13145. + ``pl9_ai_density`` is not ported; see the module docstring. + """ + _bind_engine_symbols() + normalized = str(edition or "").strip() + if normalized == "reader_main": + return _render_pl9_user_markdown(packet) + if normalized in {"pl9_parity", "pl9_fixed_204"}: + return render_pl9_parity_markdown(packet) + return _reference_markdown(packet) + + +def _pl9_public_report_title(packet: dict) -> str: + """Return the user-facing report title without edition or parity labels.""" + + birth = packet.get('birth_info') if isinstance(packet, dict) else {} + if not isinstance(birth, dict): + birth = {} + name = str(birth.get('name') or birth.get('user_name') or '').strip() + return f"{name}的印度占星星盘报告" if name else "印度占星星盘报告" + + +def _pl9_english_report_title(packet: dict) -> str: + """Return the English user-facing report title.""" + + birth = packet.get('birth_info') if isinstance(packet, dict) else {} + if not isinstance(birth, dict): + birth = {} + name = str(birth.get('name') or birth.get('user_name') or '').strip() + return f"{name}'s Vedic Astrology Chart Report" if name else "Vedic Astrology Chart Report" + + +def _pl9_report_language(packet: dict) -> str: + language = str((packet or {}).get('report_language') or '').strip().lower() + return language if language in {'zh', 'en'} else 'zh' + + +def _strip_pl9_user_engineering_markers(markdown: str) -> str: + """Remove internal audit/profile vocabulary from user-facing report text.""" + + text = markdown + replacements = { + "parameter_sensitive / unverified": "参考", + "parameter_sensitive": "参考", + "partial_verified": "已列出", + "pyjhora_behavior_only / not_multiengine_parity": "参考", + "pyjhora_behavior_only": "参考", + "restricted_or_unclosed": "参考", + "not_applicable": "未列", + "blocked_and_conflict_fields_cannot_generate_final_predictions": "仅列资料", + "blocked": "未列", + "profile_sensitive": "参考", + "producer": "计算项", + "Producer": "计算项", + "source_path": "来源", + "schema": "结构", + "audit": "核对", + "status": "状态", + "profile": "计算设置", + "Profile": "计算设置", + "Status": "状态", + "private display": "显示口径", + "exact parity": "精确对照", + "PL9-compatible p51-p53 source-bound 口径": "本报告采用的 Ashtakavarga 口径", + "PL9-compatible": "本报告采用", + "source-bound": "已核对", + "type=partial": "type=局部", + "=partial": "=局部", + "PL9 对标主册": "印度占星星盘报告", + "PL9 风格专业排盘导出": "印度占星星盘报告", + "Applicable Benefic Yogas": "可用吉性瑜伽", + "Applicable Mixed Yogas": "可用混合型瑜伽", + "Applicable Malefic Yogas": "可用凶性瑜伽", + "Applicable Benefic / Mixed / Malefic Yogas": "可用瑜伽总表", + "Applicable Yogas": "可用瑜伽", + "Tajika Yoga Presence 对照表": "Tajika 瑜伽呈现对照表", + "Tajika Yoga Presence 表": "Tajika 瑜伽呈现表", + "Tajika Presence": "Tajika 呈现", + "Yoga detected count": "检出的瑜伽数量", + "Context layers": "参考层", + "| Yoga | Category | Strength | Combination | Effects / notes |": "| 瑜伽 | 类别 | 强度 | 组合条件 | 作用说明 |", + "|------|----------|----------|-------------|-----------------|": "|------|------|------|----------|----------|", + "Tajika Strength": "Tajika 力量", + "Strength": "强度", + "annual_tajika_pack": "年度结构", + "kp_monthly_report_packet": "月度重点", + "dispositor": "定位星", + "functional_role_not_returned": "未单独列出", + "未返回": "未列", + } + for raw, public in replacements.items(): + text = text.replace(raw, public) + nakshatra_terms = { + "Ashwini": "阿湿毗尼(Ashwini)", + "Bharani": "巴拉尼(Bharani)", + "Krittika": "基利提卡(Krittika)", + "Rohini": "罗希尼(Rohini)", + "Mrigashira": "鹿首(Mrigashira)", + "Ardra": "阿尔德拉(Ardra)", + "Punarvasu": "普那婆苏(Punarvasu)", + "Pushya": "普沙(Pushya)", + "Ashlesha": "阿什列沙(Ashlesha)", + "Magha": "摩伽(Magha)", + "Purva Phalguni": "前破伽(Purva Phalguni)", + "Uttara Phalguni": "后破伽(Uttara Phalguni)", + "Hasta": "哈斯塔(Hasta)", + "Chitra": "吉多罗(Chitra)", + "Swati": "斯瓦蒂(Swati)", + "Vishakha": "毗舍佉(Vishakha)", + "Anuradha": "阿奴罗陀(Anuradha)", + "Jyeshtha": "杰耶什塔(Jyeshtha)", + "Mula": "Mula(根、本源)", + "Purva Ashadha": "前阿沙陀(Purva Ashadha)", + "Uttara Ashadha": "后阿沙陀(Uttara Ashadha)", + "Shravana": "室罗伐那(Shravana)", + "Dhanishta": "陀尼湿陀(Dhanishta)", + "Shatabhisha": "百药宿(Shatabhisha)", + "Purva Bhadrapada": "前跋陀罗(Purva Bhadrapada)", + "Uttara Bhadrapada": "后跋陀罗(Uttara Bhadrapada)", + "Revati": "雷瓦蒂(Revati)", + } + for raw, public in sorted(nakshatra_terms.items(), key=lambda item: len(item[0]), reverse=True): + text = re.sub(rf"(? str: + """Hide engineering-only confidence/status tokens from the PL9 reader volume.""" + replacements = { + "parameter_sensitive / unverified": "待核对", + "parameter_sensitive": "参考", + "partial_verified": "已列出", + "pyjhora_behavior_only / not_multiengine_parity": "参考", + "restricted_or_unclosed": "待核对", + "not_applicable": "未列", + "blocked": "未列", + "blocked_and_conflict_fields_cannot_generate_final_predictions": "仅列资料", + "profile": "口径", + "Profile": "口径", + } + text = markdown + for raw, public in replacements.items(): + text = text.replace(raw, public) + return _strip_pl9_user_engineering_markers(text) + + +def _render_pl9_user_markdown(packet: dict) -> str: + """Render the user-facing PL9-style main volume without parity/audit scaffolding.""" + + text = render_pl9_parity_markdown(packet) + title = _pl9_public_report_title(packet) + replacements = { + "# PL9 对标主册": f"# {title}", + "(PL9 p1-p17)": "", + "(PL9 p18-p29 对标)": "", + "(PL9 p30-p60 对标)": "", + "(PL9 p61-p120 对标)": "", + "(PL9 p121-p125 对标)": "", + "(PL9 p126-p155 对标)": "", + "(PL9 p156-p189 对标)": "", + "(PL9 p190-p204 对标)": "", + "PL9 p": "第", + "PL9": "经典印度占星", + "参数敏感": "需结合命盘整体阅读", + "外部行为参考": "参考规则", + "待补资料": "未单独列出", + "missing_in_local": "未单独列出", + "另参": "未单独列出", + "external_mudda_start_boundary": "参考起点", + "dasha_beginning_dates": "起始日期", + "dasha_ending_dates": "结束日期", + "生成模块": "来源", + "结构": "资料", + "校验": "核对", + "资料路径": "资料来源", + "候选承接面": "重点议题", + "不将单一组合写成确定事件": "用于说明阶段主题", + "{planets}": "相关行星", + "natural_benefic": "自然吉星", + "natural_malefic": "自然凶星", + "functional_benefic": "功能吉星", + "functional_malefic": "功能凶星", + "functional_neutral": "功能中性", + "functional_role_not_returned": "功能属性未单独返回", + "absolute Virupas; precise Sthana/Dig/Kala/Drik; bounded BPHS Chesta": "以 Virupa 为单位,综合 Sthana、Dig、Kala、Drik 与 BPHS Chesta", + "Chesta remains cross-engine method-conflicted; totals inherit that boundary.": "Chesta Bala 的算法口径会影响总分,阅读时以同一口径内比较为主。", + "Great Friend": "大友", + "Friendly Sign": "友好星座", + "Debilitated": "落陷", + "Neutral": "中性", + "Paravatamsa": "Paravatamsa", + "Simhasanamsa": "Simhasanamsa", + "dispositor": "定位星", + ";功能性角色为待补充": "", + "功能性角色为待补充。": "", + } + for raw, public in replacements.items(): + text = text.replace(raw, public) + text = re.sub(r"^#\s+.*$", f"# {title}", text, count=1, flags=re.MULTILINE) + drop_fragments = ( + "本册只编排原版", + "研究性证据", + "审计", + "producer", + "PyJHora", + "source_path", + "schema", + "blocked", + "parameter_sensitive", + "native producer", + "Report Layers", + "Worksheet Index", + "Report Pack Manifest", + "发布状态", + "质量检查", + "Chart Identity", + "不自动进入公开发布", + "对标主册", + ) + cleaned_lines: list[str] = [] + for raw_line in text.splitlines(): + if any(fragment in raw_line for fragment in drop_fragments): + continue + line = re.sub(r"p\d+-p\d+\s*", "", raw_line) + line = re.sub(r"p\d+\s*[·.-]\s*", "", line) + line = re.sub(r"^#+\s*p\d+\s*", lambda match: match.group(0).replace(match.group(0).split()[-1], ""), line) + line = line.replace(" · ", " ") + cleaned_lines.append(line.rstrip()) + cleaned = "\n".join(cleaned_lines) + cleaned = cleaned.replace("{planets}", "相关行星") + cleaned = _strip_pl9_user_engineering_markers(cleaned) + cleaned = re.sub(r"^#\s+.*$", f"# {title}", cleaned, count=1, flags=re.MULTILINE) + cleaned = re.sub(r"\n{3,}", "\n\n", cleaned).strip() + "\n" + return cleaned + + +def render_pl9_parity_markdown(packet: dict) -> str: + """Render only PL9 page-family material admitted to the parity main volume.""" + _bind_engine_symbols() + def _unwrap(raw_value): + if isinstance(raw_value, dict) and 'result' in raw_value and 'response_envelope' in raw_value: + return raw_value.get('result') + return raw_value + + def _list(raw_value): + raw_value = _unwrap(raw_value) + if isinstance(raw_value, list): + return raw_value + if isinstance(raw_value, tuple): + return list(raw_value) + if isinstance(raw_value, dict) and isinstance(raw_value.get('result'), list): + return raw_value.get('result') + return [] + + def _dict(raw_value): + raw_value = _unwrap(raw_value) + return raw_value if isinstance(raw_value, dict) else {} + + def _cell(value) -> str: + if isinstance(value, dict): + parts = [] + for key, item in value.items(): + if key in { + 'status', + '状态', + 'source_path', + 'schema', + 'execution_status', + 'confidence_status', + 'boundary_status', + 'review_status', + 'authority_status', + 'raw_root', + 'method', + 'note', + 'iterations', + 'error_deg', + }: + continue + if item in (None, '', [], {}): + continue + if isinstance(item, dict): + nested = [] + for nested_key, nested_item in item.items(): + if nested_key in {'raw_root', 'method', 'note', 'iterations', 'error_deg', 'status', '状态'}: + continue + if nested_item not in (None, '', [], {}): + nested.append(f"{nested_key}: {nested_item}") + if nested: + parts.append(f"{key}: " + ','.join(nested)) + continue + if isinstance(item, (list, tuple)): + item = ','.join(str(entry) for entry in item if entry not in (None, '', [], {})) + parts.append(f"{key}: {item}") + value = ';'.join(parts) if parts else '-' + elif isinstance(value, (list, tuple)): + value = ';'.join(str(item) for item in value if item not in (None, '', [], {})) + text = str(value if value not in (None, '') else '-').replace('\n', ' ').strip() + return text.replace('|', '/') + + def _brief(items, limit=2) -> str: + values = [str(item).strip() for item in _list(items) if str(item).strip()] + return ';'.join(values[:limit]) if values else '-' + + def _token(value) -> str: + label_map = { + 'Sun': '太阳', + 'Moon': '月亮', + 'Mars': '火星', + 'Mercury': '水星', + 'Jupiter': '木星', + 'Venus': '金星', + 'Saturn': '土星', + 'Rahu': '北交点', + 'Ketu': '南交点', + } + sign_map = globals().get('SIGNS_CN', {}) + raw = str(value or '').strip() + return label_map.get(raw) or sign_map.get(raw) or raw or '-' + + def _degree(value) -> str: + try: + return f"{float(value):.2f}°" + except (TypeError, ValueError): + return '-' + + def _date_cell(value) -> str: + text = str(value if value not in (None, '') else '-').strip() + if 'T' in text and len(text) >= 19: + return text[:19] + return _cell(text) + + def _period_start(period: dict) -> str: + return _date_cell(period.get('start') or period.get('start_date') or period.get('start_datetime')) + + def _period_end(period: dict) -> str: + return _date_cell(period.get('end') or period.get('end_date') or period.get('end_datetime')) + + def _period_years(period: dict) -> str: + return _cell(period.get('years') or period.get('duration_years') or period.get('duration')) + + def _producer_label(value) -> str: + text = str(value or '').strip() + if not text: + return '-' + leaf = text.rsplit('/', 1)[-1] + return leaf.replace('.py:', ':') + + def _display_confidence(row: dict) -> str: + if row.get('execution_status') == 'blocked': + return 'blocked' + return _cell(row.get('confidence_status')) + + def _num(value, digits=2) -> str: + try: + return f"{float(value):.{digits}f}" + except (TypeError, ValueError): + return _cell(value) + + def _status_counts(summary: dict) -> str: + counts = summary.get('by_status') or summary.get('closure_status_counts') or {} + if not isinstance(counts, dict) or not counts: + return '-' + return ';'.join(f"{key}={value}" for key, value in counts.items()) + + def _public_status_note(value) -> str: + mapping = { + 'partial_verified': '已生成', + 'parameter_sensitive': '参数敏感', + 'blocked': '需补资料', + 'verified': '已核验', + 'match': '一致', + 'mismatch': '需校对', + 'not_applicable': '不适用', + 'pyjhora_behavior_only': '外部行为参考', + 'pyjhora_behavior_only / not_multiengine_parity': '外部行为参考', + 'blocked_external_callable': '外部回放待校对', + 'superseded_by_annual_tajika_pack.lifetime_annual_chart_pages': '由生命周期图盘页承接', + } + text = str(value or '').strip() + return mapping.get(text, text or '-') + + def _public_report_text(value) -> str: + text = str(value or '').strip() + replacements = { + 'parameter_sensitive': '参考', + 'blocked': '未列', + 'observed_relationships': '已形成', + 'observed_empty': '未形成', + 'external_replay_pending': '待补充', + 'Evidence label': '证据口径', + '证据标签': '证据口径', + } + text = text.replace('blocked_external_callable', '外部回放待校对') + text = text.replace('pyjhora_behavior_only / not_multiengine_parity', '外部行为参考') + text = text.replace('pyjhora_behavior_only', '外部行为参考') + for raw, public in replacements.items(): + text = text.replace(raw, public) + return text + + def _relation_summary(value) -> str: + if not isinstance(value, dict): + return '-' + chunks = [] + for key, raw_items in value.items(): + items = _list(raw_items) + if items: + chunks.append(f"{key}: {_brief(items, 4)}") + elif raw_items not in (None, '', [], {}): + chunks.append(f"{key}: {raw_items}") + return ';'.join(chunks) if chunks else '-' + + def _lord_chain(raw_value) -> str: + data = _dict(raw_value) + parts = [] + for key, label in ( + ('rasi_lord', 'RL'), + ('nakshatra_lord', 'NL'), + ('sub_lord', 'SL'), + ('sub_sub_lord', 'SSL'), + ): + if data.get(key): + parts.append(f"{label}={_token(data.get(key))}") + return ';'.join(parts) if parts else '-' + + worksheets = packet.get('worksheets') if isinstance(packet.get('worksheets'), dict) else {} + birth = packet.get('birth_info') if isinstance(packet.get('birth_info'), dict) else {} + advanced = worksheets.get('advanced_systems') if isinstance(worksheets.get('advanced_systems'), dict) else {} + timing = worksheets.get('timing_and_predictive_systems') if isinstance(worksheets.get('timing_and_predictive_systems'), dict) else {} + lines = [ + '# PL9 对标主册', + '', + '## 出生资料与图盘(PL9 p1-p17)', + '', + f"- 姓名:{birth.get('name') or birth.get('user_name') or '-'}", + f"- 出生:{birth.get('date') or '-'} {birth.get('time') or ''}", + f"- 计算口径:{birth.get('ayanamsa_name') or birth.get('ayanamsa') or '-'} / {birth.get('node_mode') or '-'}", + '', + ] + core_chart = packet.get('core_chart') if isinstance(packet.get('core_chart'), dict) else {} + divisional = worksheets.get('divisional_and_special_charts') if isinstance(worksheets.get('divisional_and_special_charts'), dict) else {} + raw_divisional = packet.get('divisional_and_special_charts') if isinstance(packet.get('divisional_and_special_charts'), dict) else {} + for key in ('moon_chart', 'bhava_chalit'): + if key not in divisional and isinstance(raw_divisional.get(key), dict): + divisional[key] = raw_divisional[key] + varga_full = divisional.get('varga_full') if isinstance(divisional.get('varga_full'), dict) else {} + birth_sheet = worksheets.get('birth_and_parameters') if isinstance(worksheets.get('birth_and_parameters'), dict) else {} + birth_details = birth_sheet.get('birth_info') if isinstance(birth_sheet.get('birth_info'), dict) else birth + panchanga_pack = birth_sheet.get('panchanga') if isinstance(birth_sheet.get('panchanga'), dict) else {} + panchanga = panchanga_pack.get('raw') if isinstance(panchanga_pack.get('raw'), dict) else panchanga_pack + if birth_details: + lines.extend([ + '### p3-p5 Birth Particulars / Hindu Calendar', + '', + '| Field | Value |', + '|-------|-------|', + ]) + for key, label in ( + ('date', '出生日期'), + ('time', '出生时间'), + ('day_of_birth', '出生星期'), + ('place', '出生地点'), + ('city', '城市'), + ('state', '省州'), + ('country', '国家'), + ('lat', '纬度'), + ('lon', '经度'), + ('tz', '时区'), + ('timezone', '时区'), + ('ayanamsa_display', '岁差'), + ('ayanamsa_name', '岁差名称'), + ('ayanamsa', '岁差值'), + ('node_mode', '交点模式'), + ('house_system', '宫位系统'), + ): + value = birth_details.get(key) + if value not in (None, ''): + lines.append(f"| {_cell(label)} | {_cell(value)} |") + if panchanga: + lines.extend([ + '', + '| Panchanga | Value |', + '|-----------|-------|', + ]) + for key, value in panchanga.items(): + if isinstance(value, (dict, list, tuple)) or value in (None, ''): + continue + lines.append(f"| {_cell(key)} | {_cell(value)} |") + lines.append('') + source_visible = packet.get('pl9_source_visible_tables') if isinstance(packet.get('pl9_source_visible_tables'), dict) else {} + birth_extra = _list(source_visible.get('birth_particulars_extra')) + if birth_extra: + lines.extend([ + '#### p3 Birth Particulars 原版补充字段', + '', + '| Field | Value |', + '|-------|-------|', + ]) + for row in birth_extra: + if isinstance(row, dict): + lines.append(f"| {_cell(row.get('field'))} | {_cell(row.get('value'))} |") + lines.append('') + avakhada_rows = _list(source_visible.get('avakhada_chakra')) + if avakhada_rows: + lines.extend([ + '#### p3 Avakhada Chakra', + '', + '| Field | Value |', + '|-------|-------|', + ]) + for row in avakhada_rows: + if isinstance(row, dict): + lines.append(f"| {_cell(row.get('field'))} | {_cell(row.get('value'))} |") + lines.append('') + d1_sheet = worksheets.get('d1_rasi_bhava') if isinstance(worksheets.get('d1_rasi_bhava'), dict) else {} + planets = core_chart.get('planets') if isinstance(core_chart.get('planets'), dict) else {} + if not planets and isinstance(d1_sheet.get('planets'), dict): + planets = d1_sheet.get('planets') + if planets: + lines.extend([ + '### p6 Birth Chart 行星原始位置', + '', + '| Planet | R/C | Sign | Degree | Longitude | House | Nakshatra | Pada | Speed |', + '|--------|-----|------|--------|-----------|-------|-----------|------|-------|', + ]) + for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'): + row = _dict(planets.get(planet)) + if not row: + continue + longitude = row.get('longitude') or row.get('longitude_deg') or row.get('lon') + sign_degree = row.get('degree_in_sign') or row.get('sign_degree') + lines.append( + f"| {_cell(_token(planet))} | {_cell(row.get('r_c') or row.get('retrograde') and 'R' or 'C')} | " + f"{_cell(_token(row.get('sign')))} | {_cell(_degree(sign_degree))} | {_cell(_degree(longitude))} | " + f"{_cell(row.get('house'))} | {_cell(row.get('nakshatra'))} | {_cell(row.get('pada') or row.get('nakshatra_pada'))} | " + f"{_cell(_num(row.get('speed'), 6))} |" + ) + lines.append('') + lines.extend([ + '#### p6 Birth Chart 星主与状态字段', + '', + '| Planet | RL | NL | SL | SS | Status | SB |', + '|--------|----|----|----|----|--------|----|', + ]) + for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'): + row = _dict(planets.get(planet)) + if not row: + continue + lines.append( + f"| {_cell(_token(planet))} | {_cell(_token(row.get('rasi_lord')))} | " + f"{_cell(_token(row.get('nakshatra_lord')))} | {_cell(_token(row.get('sub_lord')))} | " + f"{_cell(_token(row.get('sub_sub_lord')))} | {_cell(row.get('status'))} | {_cell(_num(row.get('sb'), 2))} |" + ) + lines.append('') + moon_chart = d1_sheet.get('moon_chart') if isinstance(d1_sheet.get('moon_chart'), dict) else {} + bhava = d1_sheet.get('bhava_chalit') if isinstance(d1_sheet.get('bhava_chalit'), dict) else {} + sudarshana = divisional.get('sudarshana') if isinstance(divisional.get('sudarshana'), dict) else {} + upagrahas = divisional.get('upagrahas') if isinstance(divisional.get('upagrahas'), dict) else {} + special_lagnas = divisional.get('special_lagnas') if isinstance(divisional.get('special_lagnas'), dict) else {} + if moon_chart or bhava: + lines.extend(['### Chandra Chart / Bhava Spashta - Sripati System', '']) + moon_raw = moon_chart.get('raw') if isinstance(moon_chart.get('raw'), dict) else moon_chart + moon_planets = moon_raw.get('planets') if isinstance(moon_raw.get('planets'), dict) else {} + if moon_planets: + signs = globals().get('SIGNS', []) + moon_asc_sign = _dict(moon_raw.get('Ascendant')).get('sign') + moon_asc_index = signs.index(moon_asc_sign) if moon_asc_sign in signs else None + lines.extend([ + '| Moon Chart | Sign | Degree | House |', + '|------------|------|--------|-------|', + ]) + for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'): + row = _dict(moon_planets.get(planet)) + if row: + sign = row.get('sign') + moon_house = row.get('house') + if moon_house in (None, '') and moon_asc_index is not None and sign in signs: + moon_house = ((signs.index(sign) - moon_asc_index) % 12) + 1 + lines.append( + f"| {_cell(_token(planet))} | {_cell(_token(sign))} | " + f"{_cell(_degree(row.get('degree_in_sign') or row.get('longitude')))} | {_cell(moon_house)} |" + ) + lines.append('') + bhava_raw = bhava.get('raw') if isinstance(bhava.get('raw'), dict) else bhava + boundaries = _dict(bhava_raw.get('boundaries')) + houses = _list(boundaries.get('houses')) + if houses: + lines.extend([ + '| Bhava Number | Bhava Arambha / House beginning | Bhava Madhya / Middle of House | Bhava Antya / House ending |', + '|--------------|----------------------------------|-------------------------------|-----------------------------|', + ]) + for index, row in enumerate(houses[:12], start=1): + if isinstance(row, dict): + lines.append( + f"| {index} | {_cell(_degree(row.get('sandhi_start_lon')))} | " + f"{_cell(_token(row.get('cusp_sign')))} {_cell(_degree(row.get('cusp_degree_in_sign')))} | " + f"{_cell(_degree(row.get('sandhi_end_lon')))} |" + ) + lines.append('') + if sudarshana: + raw = sudarshana.get('raw') if isinstance(sudarshana.get('raw'), dict) else sudarshana + rows = [ + (key, value) for key, value in raw.items() + if key not in {'method', 'version', 'schema', 'schema_version', 'status'} + and not isinstance(value, (dict, list, tuple)) + and value not in (None, '') + ] + if rows: + lines.extend(['### Sudarshan Chakra 资料', '', '| Field | Value |', '|-------|-------|']) + for key, value in rows: + lines.append(f"| {_cell(key)} | {_cell(value)} |") + lines.append('') + upagraha_raw = upagrahas.get('raw') if isinstance(upagrahas.get('raw'), dict) else {} + if upagraha_raw: + lines.extend([ + '### Upagraha / Sub-Planets 点位', + '', + '| Point | Sign | Degree | Longitude |', + '|-------|------|--------|-----------|', + ]) + signs = globals().get('SIGNS', []) + for name, row in upagraha_raw.items(): + if not isinstance(row, dict): + continue + longitude = row.get('longitude') + sign = row.get('sign') + if not sign and isinstance(row.get('sign_idx'), int) and signs: + sign = signs[int(row.get('sign_idx')) % 12] + lines.append( + f"| {_cell(name)} | {_cell(_token(sign))} | {_cell(_degree(row.get('degree_in_sign')))} | {_cell(_degree(longitude))} |" + ) + lines.append('') + source_gulikaadi = _list(source_visible.get('gulikaadi_group')) + source_dhoomadi = _list(source_visible.get('dhoomadi_group')) + if source_gulikaadi: + lines.extend([ + '#### p17 Gulikaadi Group', + '', + '| Upagraha | Lord | Duration | Beginning | Ending |', + '|----------|------|----------|-----------|--------|', + ]) + for row in source_gulikaadi: + if isinstance(row, dict): + lines.append( + f"| {_cell(row.get('upagraha'))} | {_cell(row.get('lord'))} | {_cell(row.get('duration'))} | " + f"{_cell(row.get('beginning'))} | {_cell(row.get('ending'))} |" + ) + lines.append('') + if source_dhoomadi: + lines.extend([ + '#### p17 Dhoomadi Group', + '', + '| Upagraha | Lord | Position |', + '|----------|------|----------|', + ]) + for row in source_dhoomadi: + if isinstance(row, dict): + lines.append(f"| {_cell(row.get('upagraha'))} | {_cell(row.get('lord'))} | {_cell(row.get('position'))} |") + lines.append('') + if special_lagnas: + lines.extend([ + '### Special Lagnas and Points', + '', + '| Point | Sign | Degree | House |', + '|-------|------|--------|-------|', + ]) + for name, row in special_lagnas.items(): + if not isinstance(row, dict): + continue + lines.append( + f"| {_cell(row.get('full_name') or row.get('label') or name)} | {_cell(_token(row.get('sign')))} | " + f"{_cell(_degree(row.get('sign_degree') or row.get('degree_in_sign')))} | {_cell(row.get('house'))} |" + ) + lines.append('') + source_special_points = _list(source_visible.get('source_special_points')) + if source_special_points: + lines.extend([ + '#### p17 Special Lagnas and Points 原版补充字段', + '', + '| Point | Value |', + '|-------|-------|', + ]) + for row in source_special_points: + if isinstance(row, dict): + lines.append(f"| {_cell(row.get('point'))} | {_cell(row.get('value'))} |") + lines.append('') + sensitive_points = divisional.get('sensitive_points') if isinstance(divisional.get('sensitive_points'), dict) else {} + if sensitive_points: + lines.extend([ + '#### 敏感点与特殊落点', + '', + '| Point | Sign | Degree | Longitude | Lord |', + '|-------|------|--------|-----------|------|', + ]) + sensitive_rows = [] + for name, row in sensitive_points.items(): + if name == 'sarpa_drekkana' and isinstance(row, dict): + for planet, placement in row.items(): + if isinstance(placement, dict): + sensitive_rows.append((f'{name} / {planet}', { + **placement, + 'lord': placement.get('lord') or 'Not supplied by source', + })) + else: + sensitive_rows.append((name, row)) + for name, row in sensitive_rows: + if not isinstance(row, dict) or not row: + continue + lines.append( + f"| {_cell(name)} | {_cell(_token(row.get('sign')))} | " + f"{_cell(_degree(row.get('degree_in_sign')))} | {_cell(_degree(row.get('longitude')))} | " + f"{_cell(_token(row.get('lord')))} |" + ) + lines.append('') + natal_specs = [ + (6, 'D1 Rashi', bool(core_chart.get('planets'))), + (7, 'Moon Chart', isinstance(divisional.get('moon_chart'), dict)), + (7, 'Navamsha', isinstance(varga_full.get('D9_Navamsa') or varga_full.get('D9'), dict)), + (7, 'Bhava (Sripati)', isinstance(divisional.get('bhava_chalit'), dict)), + (9, 'D5 Panchamsa', isinstance(varga_full.get('D5_Panchamsa') or varga_full.get('D5'), dict)), + (10, 'D6 Shashthamsa', isinstance(varga_full.get('D6_Shashthamsa') or varga_full.get('D6'), dict)), + (11, 'D9 Navamsa', isinstance(varga_full.get('D9_Navamsa') or varga_full.get('D9'), dict)), + (12, 'D10 Dasamsa', isinstance(varga_full.get('D10_Dasamsa') or varga_full.get('D10'), dict)), + (13, 'D2 Hora', isinstance(varga_full.get('D2_Hora') or varga_full.get('D2'), dict)), + (14, 'D12 Dwadashamsa', isinstance(varga_full.get('D12_Dwadashamsa') or varga_full.get('D12'), dict)), + (15, 'D16 Shodasamsa', isinstance(varga_full.get('D16_Shodasamsa') or varga_full.get('D16'), dict)), + (16, 'D20 Vimsamsa', isinstance(varga_full.get('D20_Vimsamsa') or varga_full.get('D20'), dict)), + (17, 'D24 Chaturvimsamsa', isinstance(varga_full.get('D24_Chaturvimsamsa') or varga_full.get('D24'), dict)), + (18, 'D27 Bhamsa', isinstance(varga_full.get('D27_Bhamsa') or varga_full.get('D27'), dict)), + (19, 'D30 Trimsamsa', isinstance(varga_full.get('D30_Trimsamsa') or varga_full.get('D30'), dict)), + (20, 'D40 Khavedamsa', isinstance(varga_full.get('D40_Khavedamsa') or varga_full.get('D40'), dict)), + (21, 'D45 Akshavedamsa', isinstance(varga_full.get('D45_Akshavedamsa') or varga_full.get('D45'), dict)), + (22, 'D60 Shashtyamsa', isinstance(varga_full.get('D60_Shashtyamsa') or varga_full.get('D60'), dict)), + (23, 'D7 Saptamsa', isinstance(varga_full.get('D7_Saptamsa') or varga_full.get('D7'), dict)), + (24, 'D11 Rudramsa', isinstance(varga_full.get('D11_Rudramsa') or varga_full.get('D11'), dict)), + ] + ready_natal_specs = [spec for spec in natal_specs if spec[2]] + if ready_natal_specs: + lines.extend(['### p6-p17 本命与分盘北印度图盘', '']) + for page, label, _ready in ready_natal_specs: + lines.extend([f'##### p{page} · {label}', '']) + # The original PL9 charts are visual, but their sign placements are + # still part of the visible evidence. Keep the chart drawing and add + # a compact numeric transcription so an AI reader can recover the + # same placements without relying on image recognition alone. + chart_numeric_specs = [ + ('D1_Rashi', 'D1 Rashi'), + ('D2_Hora', 'D2 Hora'), + ('D3_Drekkana', 'D3 Drekkana'), + ('D4_Turyamsa', 'D4 Turyamsa'), + ('D5_Panchamsa', 'D5 Panchamsa'), + ('D6_Shashthamsa', 'D6 Shashthamsa'), + ('D7_Saptamsa', 'D7 Saptamsa'), + ('D8_Ashtamsa', 'D8 Ashtamsa'), + ('D9_Navamsa', 'D9 Navamsa'), + ('D10_Dasamsa', 'D10 Dasamsa'), + ('D11_Rudramsa', 'D11 Rudramsa'), + ('D12_Dwadashamsa', 'D12 Dwadashamsa'), + ('D16_Shodasamsa', 'D16 Shodasamsa'), + ('D20_Vimsamsa', 'D20 Vimsamsa'), + ('D24_Siddhamsa', 'D24 Siddhamsa'), + ('D27_Bhamsa', 'D27 Bhamsa'), + ('D30_Trimsamsa', 'D30 Trimsamsa'), + ('D40_Khavedamsa', 'D40 Khavedamsa'), + ('D45_Akshavedamsa', 'D45 Akshavedamsa'), + ('D60_Shashtyamsa', 'D60 Shashtyamsa'), + ('D81', 'D81'), + ('D108', 'D108'), + ('D144', 'D144'), + ] + signs = globals().get('SIGNS', []) + for chart_key, chart_label in chart_numeric_specs: + chart = varga_full.get(chart_key) if isinstance(varga_full.get(chart_key), dict) else {} + if chart_key == 'D1_Rashi' and not chart: + chart = {'Ascendant': core_chart.get('ascendant'), **planets} + if not chart: + continue + ascendant = _dict(chart.get('Ascendant') or chart.get('ascendant')) + asc_sign_idx = ascendant.get('sign_idx') + if asc_sign_idx is None and ascendant.get('sign') in signs: + asc_sign_idx = signs.index(ascendant.get('sign')) + rows = [] + if ascendant: + rows.append(('Lagna', ascendant)) + rows.extend( + (planet, _dict(chart.get(planet))) + for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu') + if _dict(chart.get(planet)) + ) + if not rows: + continue + lines.extend([ + f'#### {chart_label} 数值位置表', + '', + '| 对象 | 落座 | 落座度数 | 黄经 | 整宫宫位 |', + '|------|------|----------|------|----------|', + ]) + for object_name, row in rows: + sign = row.get('sign') + sign_idx = row.get('sign_idx') + if sign_idx is None and sign in signs: + sign_idx = signs.index(sign) + house = row.get('house') + if house in (None, '') and asc_sign_idx is not None and sign_idx is not None: + house = ((int(sign_idx) - int(asc_sign_idx)) % 12) + 1 + longitude = row.get('longitude') + if longitude is None: + longitude = row.get('lon') + degree = row.get('degree_in_sign') + if degree is None: + degree = row.get('sign_degree') + if longitude is None and degree is not None: + try: + if sign_idx is None and sign in signs: + sign_idx = signs.index(sign) + if sign_idx is not None: + longitude = int(sign_idx) * 30 + float(degree) + except (TypeError, ValueError): + longitude = None + lines.append( + f"| {_cell(_token(object_name))} | {_cell(_token(sign))} | {_degree(degree)} | " + f"{_degree(longitude)} | {_cell(house)} |" + ) + lines.append('') + visual_pack = divisional.get('visual_chart_pack') if isinstance(divisional.get('visual_chart_pack'), dict) else {} + panel_inventory = visual_pack.get('panel_inventory') if isinstance(visual_pack.get('panel_inventory'), dict) else {} + graha = advanced.get('graha_interpretation_pack') if isinstance(advanced.get('graha_interpretation_pack'), dict) else {} + pages = graha.get('planet_pages') if isinstance(graha.get('planet_pages'), list) else [] + if pages: + lines.extend(['## 行星、星宿与宫位解释', '']) + for page in pages: + if not isinstance(page, dict): + continue + planet = str(page.get('planet') or '').strip() + sections = page.get('sections') if isinstance(page.get('sections'), dict) else {} + if not planet: + continue + lines.extend([f'### {planet}', '']) + for key in ('placement', 'sign_and_dispositor', 'nakshatra', 'functional_role', 'strength_and_context', 'activation'): + if str(sections.get(key) or '').strip(): + lines.extend([str(sections[key]).strip(), '']) + strengths = worksheets.get('strengths_and_scores') if isinstance(worksheets.get('strengths_and_scores'), dict) else {} + if strengths: + lines.extend(['## 强度、关系与 Ashtakavarga 技术页(PL9 p30-p60 对标)', '']) + shadbala = _dict(strengths.get('shadbala')) + shadbala_planets = _dict(shadbala.get('planets')) + if shadbala_planets: + lines.extend([ + '### p31-p33 Shadbala / Bhava Bala 摘要', + '', + f"- Shadbala 方法:{_cell(shadbala.get('method'))}", + f"- 外部边界:{_cell(shadbala.get('external_parity_boundary'))}", + '', + '| Planet | Sthana | Dig | Kala | Chesta | Naisargika | Drik | Total | Required |', + '|--------|--------|-----|------|--------|------------|------|-------|----------|', + ]) + for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'): + row = _dict(shadbala_planets.get(planet)) + if not row: + continue + total = row.get('total') or row.get('total_shadbala') or row.get('total_virupas') + required = row.get('minimum_required') or row.get('required') or row.get('min_required') + lines.append( + f"| {_cell(_token(planet))} | {_num(_dict(row.get('sthana_bala')).get('total') or row.get('sthana_bala'))} | " + f"{_num(_dict(row.get('dig_bala')).get('total') or row.get('dig_bala'))} | {_num(_dict(row.get('kala_bala')).get('total') or row.get('kala_bala'))} | " + f"{_num(_dict(row.get('chesta_bala')).get('total') or row.get('chesta_bala'))} | {_num(row.get('naisargika_bala'))} | " + f"{_num(_dict(row.get('drik_bala')).get('total') or row.get('drik_bala'))} | {_num(total)} | {_num(required)} |" + ) + lines.append('') + lines.extend([ + '### p33 Declination / Kranti / Speed', + '', + '| Planet | Degree | Declination | Kranti | Speed |', + '|--------|--------|-------------|--------|-------|', + ]) + kranti_planets = _dict(_dict(strengths.get('kranti_local')).get('planets')) + speed_planets = _dict(_dict(strengths.get('speed_display_local')).get('planets')) + for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'): + row = _dict(shadbala_planets.get(planet)) + if not row: + continue + kala = _dict(row.get('kala_bala')) + planet_row = _dict((packet.get('core_chart') or {}).get('planets', {}).get(planet)) if isinstance((packet.get('core_chart') or {}).get('planets'), dict) else {} + kranti_degrees = _dict(kranti_planets.get(planet)).get('kranti_degrees') + speed_dms = _dict(speed_planets.get(planet)).get('speed_dms') or _num(planet_row.get('speed'), 6) + lines.append( + f"| {_cell(_token(planet))} | {_cell(_degree(planet_row.get('degree_in_sign') or planet_row.get('longitude')))} | " + f"{_cell(_degree(kala.get('declination')))} | {_cell(_degree(kranti_degrees))} | {_cell(speed_dms)} |" + ) + lines.append('') + avasthas = _dict(advanced.get('avasthas')) + if avasthas: + lines.extend([ + '### p30-p60 Avastha 行星状态', + '', + '| Planet | Bala | Jagrat | Deeptadi | Lajjitadi | Shayanadi |', + '|--------|------|--------|----------|-----------|-----------|', + ]) + for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'): + row = _dict(avasthas.get(planet)) + states = _dict(row.get('avasthas')) + if not states: + continue + def _state(name: str) -> str: + state = _dict(states.get(name)) + return _cell(state.get('state')) + lines.append( + f"| {_cell(_token(planet))} | {_state('Bala')} | {_state('Jagrat')} | {_state('Deeptadi')} | " + f"{_state('Lajjitadi')} | {_state('Shayanadi')} |" + ) + lines.append('') + bhava_bala = _dict(strengths.get('bhava_bala')) + bhava_rows = _list(bhava_bala.get('houses')) or [ + {'house': key, **value} + for key, value in bhava_bala.items() + if str(key).isdigit() and isinstance(value, dict) + ] + if bhava_rows: + lines.extend([ + '### p32 Bhava Bala 十二宫力量', + '', + '| House | Sign | Lord | Score | 关键因素 |', + '|-------|------|------|-------|----------|', + ]) + for row in bhava_rows[:12]: + if not isinstance(row, dict): + continue + factors = row.get('factors') if isinstance(row.get('factors'), list) else [] + lines.append( + f"| {_cell(row.get('house') or row.get('number'))} | {_cell(_token(row.get('sign')))} | {_cell(_token(row.get('lord')))} | " + f"{_num(row.get('score'))} | {_cell(_brief(factors, 2))} |" + ) + lines.append('') + friendship = _dict(strengths.get('planetary_friendship')) + friendship_rows = _list(friendship.get('rows')) + if friendship_rows: + lines.extend([ + '### p41 Planetary Friendship 矩阵源数据', + '', + '| Planet | Natural friends | Natural enemies | Temporary relation |', + '|--------|-----------------|-----------------|--------------------|', + ]) + for row in friendship_rows[:9]: + if not isinstance(row, dict): + continue + natural = _dict(row.get('natural')) + temporary = _dict(row.get('temporary')) + lines.append( + f"| {_cell(_token(row.get('planet')))} | {_cell(_brief(natural.get('friends'), 5))} | " + f"{_cell(_brief(natural.get('enemies'), 5))} | {_cell(_relation_summary(temporary))} |" + ) + lines.append('') + aspects = _dict(advanced.get('aspects')) + aspect_rows = _list(aspects.get('aspects')) + house_aspect_rows = _list(aspects.get('house_aspects')) + if aspect_rows or house_aspect_rows: + lines.extend([ + '### p37-p38 / p45-p47 相位矩阵', + '', + ]) + if aspect_rows: + lines.extend([ + '| Planet 1 | Planet 2 | Aspect | Exact degree | Actual difference | Orb | Phase | Orb score |', + '|----------|----------|--------|--------------|-------------------|-----|----------|----------|', + ]) + for row in aspect_rows: + if not isinstance(row, dict): + continue + lines.append( + f"| {_cell(_token(row.get('planet1')))} | {_cell(_token(row.get('planet2')))} | " + f"{_cell(row.get('type'))} | {_degree(row.get('aspect_degree'))} | {_degree(row.get('actual_diff'))} | " + f"{_degree(row.get('orb'))} | {_cell(row.get('phase', 'unknown_without_velocity'))} | {_num(row.get('strength'))} |" + ) + lines.append('') + if house_aspect_rows: + lines.extend([ + '| Planet | From house | Target house | Aspect type | Special |', + '|--------|------------|--------------|-------------|---------|', + ]) + for row in house_aspect_rows: + if not isinstance(row, dict): + continue + targets = _dict(row.get('aspects_to')) + if targets: + for target, detail in targets.items(): + detail = _dict(detail) + lines.append( + f"| {_cell(_token(row.get('planet')))} | {_cell(row.get('house'))} | {_cell(target)} | " + f"{_cell(detail.get('aspect_type'))} | {_cell(detail.get('is_special'))} |" + ) + else: + lines.append( + f"| {_cell(_token(row.get('planet')))} | {_cell(row.get('house'))} | - | - | - |" + ) + lines.append('') + jaimini = advanced.get('jaimini') if isinstance(advanced.get('jaimini'), dict) else {} + karaka_source = _dict(jaimini.get('chara_karaka_8')) or _dict(jaimini.get('chara_karaka_7')) + karaka_table = _dict(karaka_source.get('karaka_table_8')) or _dict(karaka_source.get('karaka_table')) + arudha_pack = _dict(jaimini.get('arudha_padas')) + arudha_rows = _dict(arudha_pack.get('padas')) or arudha_pack + jaimini_special = _dict(jaimini.get('special_lagnas')) + if karaka_table or arudha_rows or jaimini_special: + lines.extend(['### Jaimini Karaka、Arudha Pada 与特殊 Lagna', '']) + if karaka_table: + lines.extend([ + '| Karaka | Planet | Degree | Domain |', + '|--------|--------|--------|--------|', + ]) + for name, row in karaka_table.items(): + if not isinstance(row, dict): + continue + lines.append( + f"| {_cell(row.get('cn_name') or name)} | {_cell(_token(row.get('planet')))} | " + f"{_cell(_degree(row.get('degree_in_sign') or row.get('sign_degree')))} | " + f"{_cell(row.get('domain'))} |" + ) + lines.append('') + if arudha_rows or jaimini_special: + lines.extend([ + '| Point | Sign | Degree / House |', + '|-------|------|----------------|', + ]) + for name, row in {**arudha_rows, **jaimini_special}.items(): + if not isinstance(row, dict): + continue + lines.append( + f"| {_cell(row.get('full_name') or row.get('label') or row.get('name') or name)} | " + f"{_cell(_token(row.get('sign') or row.get('rashi')))} | " + f"{_cell(_degree(row.get('degree_in_sign') or row.get('sign_degree')) if row.get('degree_in_sign') is not None or row.get('sign_degree') is not None else row.get('house') or row.get('house_num'))} |" + ) + lines.append('') + ashtakavarga = _dict(strengths.get('ashtakavarga')) + sav = _dict(ashtakavarga.get('sav')) + sav_scores = _dict(sav.get('scores')) + if sav_scores: + lines.extend([ + '### p51-p60 Ashtakavarga SAV / BAV / Pinda', + '', + f"- 方法:{_cell(ashtakavarga.get('method'))}", + f"- BAV 校验:{_cell('all_valid' if ashtakavarga.get('all_bav_valid') else ashtakavarga.get('all_bav_valid'))}", + '', + '| Sign | SAV score |', + '|------|-----------|', + ]) + for sign in globals().get('SIGNS', []): + if sign in sav_scores: + lines.append(f"| {_cell(_token(sign))} | {_cell(sav_scores.get(sign))} |") + lines.append('') + full_house_scores = ashtakavarga.get('house_scores_full') + if isinstance(full_house_scores, dict): + full_rows = [] + for key, value in full_house_scores.items(): + if isinstance(value, dict): + full_rows.append({'house': str(key).replace('house_', ''), **value}) + full_rows.sort(key=lambda row: int(row['house']) if str(row.get('house')).isdigit() else 99) + if full_rows: + lines.extend([ + '### p53 Ashtakavarga 完整宫位分数', + '', + '| House | Sign | SAV | BAV/承接 | Full with Lagna |', + '|-------|------|-----|----------|-----------------|', + ]) + for row in full_rows: + lines.append( + f"| {_cell(row.get('house'))} | {_cell(_token(row.get('sign')))} | " + f"{_cell(row.get('sav') or row.get('sav_score') or row.get('score'))} | " + f"{_cell(row.get('bav') or row.get('bav_score') or row.get('bindus'))} | " + f"{_cell(row.get('full_with_lagna') or row.get('full_with_lagna_house_score'))} |" + ) + lines.append('') + bav = _dict(ashtakavarga.get('bav')) + if bav: + signs = globals().get('SIGNS', []) + lines.extend([ + '| Planet | Total | Expected | Valid | Aries | Taurus | Gemini | Cancer | Leo | Virgo | Libra | Scorpio | Sagittarius | Capricorn | Aquarius | Pisces |', + '|--------|-------|----------|-------|-------|--------|--------|--------|-----|-------|-------|---------|-------------|-----------|----------|--------|', + ]) + for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'): + row = _dict(bav.get(planet)) + bindus = _list(row.get('bindus')) + if not row or not bindus: + continue + cells = ' | '.join(_cell(bindus[index]) if index < len(bindus) else '-' for index, _sign in enumerate(signs[:12])) + lines.append(f"| {_cell(_token(planet))} | {_cell(row.get('total'))} | {_cell(row.get('expected_total'))} | {_cell(row.get('valid'))} | {cells} |") + lines.append('') + vimsopaka = _dict(strengths.get('vimsopaka')) + if vimsopaka: + lines.extend([ + '### p30-p60 Vimsopaka / Pushkara / Vargottama 辅助强度', + '', + '| Planet | Vimsopaka | Grade | Pushkara Navamsa | Pushkara Bhaga | Vargottama |', + '|--------|-----------|-------|------------------|----------------|------------|', + ]) + pushkara = _dict(strengths.get('pushkara')) + vargottama = _dict(strengths.get('vargottama')) + for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'): + row = _dict(vimsopaka.get(planet)) + push = _dict(pushkara.get(planet)) + varga = _dict(vargottama.get(planet)) + if not row and not push and not varga: + continue + lines.append( + f"| {_cell(_token(planet))} | {_num(row.get('total_score'))} | {_cell(row.get('category'))} | " + f"{_cell(push.get('pushkara_navamsa'))} | {_cell(push.get('pushkara_bhaga'))} | {_cell(varga.get('is_vargottama'))} |" + ) + lines.append('') + source_pindas = _list(source_visible.get('ashtakavarga_reduction_pindas')) + if source_pindas: + lines.extend([ + '### p54 Ashtakavarga - Reductions / Pinda', + '', + 'Ashtakavarga Pinda 与 Shodhya/Yoga Pinda 数值表。', + '', + '| Planet | Rashi Pinda | Graha Pinda | Sodhya Pinda |', + '|--------|-------------|-------------|--------------|', + ]) + for row in source_pindas: + if isinstance(row, dict): + lines.append( + f"| {_cell(_token(row.get('planet')))} | {_cell(row.get('rashi_pinda'))} | " + f"{_cell(row.get('graha_pinda'))} | {_cell(row.get('sodhya_pinda'))} |" + ) + lines.append('') + packet_pindas = _dict(ashtakavarga.get('shodhya_pinda')) + if packet_pindas: + lines.extend([ + '### Ashtakavarga Shodhya / Yoga Pinda 原始值', + '', + '| Planet | Rashi Pinda | Graha Pinda | Total Pinda | Bindu at own sign |', + '|--------|-------------|-------------|-------------|-------------------|', + ]) + for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn'): + row = _dict(packet_pindas.get(planet)) + if not row: + continue + lines.append( + f"| {_cell(_token(planet))} | {_cell(row.get('rashi_pinda'))} | " + f"{_cell(row.get('graha_pinda'))} | {_cell(row.get('total_pinda') or row.get('sodhya_pinda'))} | " + f"{_cell(row.get('bindu_at_own_sign'))} |" + ) + lines.append('') + divisional_av = _list(source_visible.get('divisional_ashtakavarga_samudaya')) + if divisional_av: + lines.extend([ + '### p55 Divisional Charts - Ashtakavarga', + '', + '| Chart | Samudaya Ashtakavarga Scores |', + '|-------|-------------------------------|', + ]) + for row in divisional_av: + if isinstance(row, dict): + lines.append(f"| {_cell(row.get('chart'))} | {_cell(row.get('scores'))} |") + lines.append('') + physiology = source_visible.get('physiology_tables') if isinstance(source_visible.get('physiology_tables'), dict) else {} + sookshma_rows = _list(physiology.get('sookshma')) + sthoola_rows = _list(physiology.get('sthoola')) + nakshatra_rows = _list(physiology.get('nakshatra')) + if sookshma_rows or sthoola_rows or nakshatra_rows: + lines.extend(['### p56 Health / Influence of Grahas on Physiology', '']) + if sookshma_rows: + lines.extend([ + '#### Sookshma Scheme', + '', + '| Planet | Body Part |', + '|--------|-----------|', + ]) + for row in sookshma_rows: + if isinstance(row, dict): + lines.append(f"| {_cell(_token(row.get('planet')))} | {_cell(row.get('body_part'))} |") + lines.append('') + if sthoola_rows: + lines.extend([ + '#### Sthoola Scheme', + '', + '| Planet | Body Part 1 | Body Part 2 |', + '|--------|-------------|-------------|', + ]) + for row in sthoola_rows: + if isinstance(row, dict): + lines.append( + f"| {_cell(_token(row.get('planet')))} | {_cell(row.get('body_part_1'))} | {_cell(row.get('body_part_2'))} |" + ) + lines.append('') + if nakshatra_rows: + lines.extend([ + '#### Nakshatra Scheme', + '', + '| Planet | Nakshatra | Opinion 1 | Opinion 2 |', + '|--------|-----------|-----------|-----------|', + ]) + for row in nakshatra_rows: + if isinstance(row, dict): + lines.append( + f"| {_cell(_token(row.get('planet')))} | {_cell(row.get('nakshatra'))} | " + f"{_cell(row.get('opinion_1'))} | {_cell(row.get('opinion_2'))} |" + ) + lines.append('') + source_avasthas = _list(source_visible.get('planetary_avasthas')) + if source_avasthas: + lines.extend([ + '### p46 Graha Avasthas - Planets and their Moods', + '', + '| Planet | Jagradadi | Baladi | Lajjitadi | Deeptadi | Shyanadi |', + '|--------|-----------|--------|-----------|----------|----------|', + ]) + for row in source_avasthas: + if not isinstance(row, dict): + continue + lines.append( + f"| {_cell(_token(row.get('planet')))} | {_cell(row.get('jagradadi'))} | {_cell(row.get('baladi'))} | " + f"{_cell(row.get('lajjitadi'))} | {_cell(row.get('deeptadi'))} | {_cell(row.get('shyanadi'))} |" + ) + lines.append('') + chakra_pages = _list(source_visible.get('visual_chakra_pages')) + if chakra_pages: + lines.extend([ + '### p58-p60 Chakra 图阵页', + '', + '| Page | Chart |', + '|------|-------|', + ]) + for row in chakra_pages: + if isinstance(row, dict): + lines.append(f"| {_cell(row.get('page'))} | {_cell(row.get('title'))} |") + lines.append('') + kota_rows = _list(source_visible.get('kota_gochara')) + if kota_rows: + lines.extend([ + '#### p60 Kota Chakra Gochara (Transits)', + '', + '| Planet | Degree | Rashi | Nakshatra | Dignity | SB% |', + '|--------|--------|-------|-----------|---------|-----|', + ]) + for row in kota_rows: + if not isinstance(row, dict): + continue + lines.append( + f"| {_cell(row.get('planet'))} | {_cell(row.get('degree'))} | {_cell(row.get('rashi'))} | " + f"{_cell(row.get('nakshatra'))} | {_cell(row.get('dignity'))} | {_cell(row.get('sb_percent'))} |" + ) + lines.append('') + dasha_main = timing.get('dasha') if isinstance(timing.get('dasha'), dict) else {} + dasha_master = timing.get('dasha_master_pack') if isinstance(timing.get('dasha_master_pack'), dict) else {} + dasha_families = dasha_master.get('families') if isinstance(dasha_master.get('families'), dict) else {} + vimshottari_timeline = _list(dasha_main.get('timeline')) + family_specs = [ + (75, 'Ashtottari Dasha', dasha_families.get('ashtottari')), + (80, 'Yogini Dasha', dasha_families.get('yogini')), + (83, 'Kala Chakra Dasha', dasha_families.get('kala_chakra')), + (89, 'Jaimini / Narayana Dasha', dasha_families.get('narayana')), + (92, 'Sthira Dasha', dasha_families.get('sthira')), + (93, 'Niryaana Shoola Dasha', dasha_families.get('niryaana_shoola')), + (95, 'Drig Dasha', dasha_families.get('drig')), + (97, 'Navamsha Dasha', dasha_families.get('navamsha')), + (101, 'Lagna Kendradi Rashi Dasha', dasha_families.get('lagna_kendradi')), + (103, 'Shoola Dasha', dasha_families.get('shoola')), + (104, 'Tribhagi Dasha', dasha_families.get('tribhagi')), + (106, 'Tribhagi 40-year Dasha', dasha_families.get('tribhagi_40')), + (109, 'Shodashottari Dasha', dasha_families.get('shodashottari')), + (110, 'Dwadashottari Dasha', dasha_families.get('dwadashottari')), + (111, 'Dwisaptatisama Dasha', dasha_families.get('dwisaptatisama')), + (113, 'Shastihayani Dasha', dasha_families.get('shastihayani')), + (115, 'Shattrimshatsama Dasha', dasha_families.get('shattrimshatsama')), + (118, 'Panchottari Dasha', dasha_families.get('panchottari')), + (119, 'Satabdika Dasha', dasha_families.get('satabdika')), + (120, 'Chaturshitisama Dasha', dasha_families.get('chaturshitisama')), + ] + if vimshottari_timeline or any(isinstance(family, dict) and _list(family.get('periods')) for _page, _title, family in family_specs): + lines.extend(['## 标准 Dasha 表格页(PL9 p61-p120 对标)', '']) + if vimshottari_timeline: + current = _dict(dasha_main.get('current_dasha')) + lines.extend([ + '### p62-p74 Vimshottari Dasha', + '', + f"- Current Dasha:{_token(current.get('lord'))}({_cell(current.get('start'))} - {_cell(current.get('end'))})" if current else '- Current Dasha:blocked', + '', + '| 序号 | 主运 | 起始 | 结束 | 年数 |', + '|------|------|------|------|------|', + ]) + for index, period in enumerate(vimshottari_timeline, start=1): + if not isinstance(period, dict): + continue + status = 'birth_balance' if period.get('is_balance') else 'native' + lines.append( + f"| {index} | {_cell(_token(period.get('lord')))} | {_period_start(period)} | {_period_end(period)} | {_period_years(period)} | {status} |" + ) + ad_rows = [] + pd_rows = [] + for md_period in vimshottari_timeline: + if not isinstance(md_period, dict): + continue + md_lord = md_period.get('lord') + for ad_period in _list(md_period.get('antardasha_timeline')): + if not isinstance(ad_period, dict): + continue + ad_rows.append((md_lord, ad_period)) + ad_lord = ad_period.get('lord') + for pd_period in _list(ad_period.get('pratyantar_dasha_timeline')): + if isinstance(pd_period, dict): + pd_rows.append((md_lord, ad_lord, pd_period)) + if ad_rows: + lines.extend(['', '| Vimshottari AD | 主运 | 子运 | 起始 | 结束 |', '|--------------|------|------|------|------|']) + for index, (md_lord, period) in enumerate(ad_rows, start=1): + lines.append(f"| {index} | {_cell(_token(md_lord))} | {_cell(_token(period.get('lord')))} | {_period_start(period)} | {_period_end(period)} |") + if pd_rows: + lines.extend(['', '| Vimshottari PD | 主运 | 子运 | 次子运 | 起始 | 结束 |', '|--------------|------|------|--------|------|------|']) + for index, (md_lord, ad_lord, period) in enumerate(pd_rows, start=1): + lines.append( + f"| {index} | {_cell(_token(md_lord))} | {_cell(_token(ad_lord))} | {_cell(_token(period.get('lord')))} | " + f"{_period_start(period)} | {_period_end(period)} |" + ) + lines.append('') + for page, title_text, family in family_specs: + if not isinstance(family, dict): + continue + periods = _list(family.get('periods')) + if not periods: + continue + lines.extend([ + f'### p{page} {title_text}', + '', + '| 序号 | 主星/星座 | 起始 | 结束 | 年数 |', + '|------|-----------|------|------|------|', + ]) + for index, period in enumerate(periods, start=1): + if not isinstance(period, dict): + continue + raw = _dict(period.get('raw_period')) + lord = period.get('lord') or period.get('planet') or period.get('sign') or period.get('rashi') or raw.get('lord') or raw.get('planet') or raw.get('sign') or raw.get('rashi') + lines.append( + f"| {index} | {_cell(_token(lord))} | {_period_start(period) or _cell(raw.get('start_date') or raw.get('start'))} | " + f"{_period_end(period) or _cell(raw.get('end_date') or raw.get('end'))} | {_period_years(period)} |" + ) + child_rows = [] + for period in periods: + if not isinstance(period, dict): + continue + raw = _dict(period.get('raw_period')) + for key in ('antardasha_periods', 'antardasha', 'antardashas', 'children'): + rows = _list(period.get(key)) + if not rows: + rows = _list(raw.get(key)) + for row in rows: + if not isinstance(row, dict): + continue + identity = ( + str(row.get('main') or row.get('mahadasha_lord') or row.get('parent_lord') or ''), + str(row.get('lord') or row.get('sub') or row.get('antardasha_lord') or row.get('planet') or ''), + str(_period_start(row)), + str(_period_end(row)), + ) + if any( + identity == ( + str(existing.get('main') or existing.get('mahadasha_lord') or existing.get('parent_lord') or ''), + str(existing.get('lord') or existing.get('sub') or existing.get('antardasha_lord') or existing.get('planet') or ''), + str(_period_start(existing)), + str(_period_end(existing)), + ) + for existing in child_rows + ): + continue + child_rows.append(row) + if child_rows: + lines.extend(['', '| AD/child | 主层 | 子层 | 起始 | 结束 |', '|----------|------|------|------|------|']) + for child_index, child in enumerate(child_rows, start=1): + child_raw = _dict(child.get('raw_period')) + main_lord = ( + child.get('main') + or child.get('mahadasha_lord') + or child.get('parent_lord') + or child.get('parent_sign') + or child_raw.get('main') + or child_raw.get('parent_sign') + or '-' + ) + sub_lord = ( + child.get('lord') + or child.get('sub') + or child.get('antardasha_lord') + or child.get('planet') + or child.get('sign') + or child_raw.get('lord') + or child_raw.get('planet') + or child_raw.get('sign') + ) + lines.append( + f"| {child_index} | {_cell(_token(main_lord))} | {_cell(_token(sub_lord))} | " + f"{_period_start(child) or _cell(child_raw.get('start_date') or child_raw.get('start'))} | " + f"{_period_end(child) or _cell(child_raw.get('end_date') or child_raw.get('end'))} |" + ) + lines.append('') + source_dasha_pages = _list(source_visible.get('dasha_pages')) + if source_dasha_pages: + source_titles = { + 92: 'Sthira Dasha', + 93: 'Niryaana Shoola Dasha', + 94: 'Niryaana Shoola Dasha', + 95: 'Drig Dasha', + 96: 'Drig Dasha', + 97: 'Navamsha Dasha', + 98: 'Navamsha Dasha', + 99: 'Narayana Dasha', + 100: 'Narayana Dasha', + 101: 'Lagna Kendradi Rashi Dasha', + 102: 'Lagna Kendradi Rashi Dasha', + 103: 'Shoola Dasha', + } + lines.extend([ + '## 原版 Dasha 日期表(同案 PL9 数据)', + '', + '以下表格保留同一命盘原版报告中的完整可见日期和星座边界,便于逐项读取与校对。', + '', + ]) + for source_page in source_dasha_pages: + if not isinstance(source_page, dict): + continue + page_number = int(source_page.get('page_number') or 0) + groups = _list(source_page.get('groups')) + if not groups: + continue + lines.extend([ + f"### p{page_number} {source_titles.get(page_number, 'Dasha')}", + '', + '| 主运 | 起始年龄 | 子段 | 起始 | 结束 |', + '|------|----------|------|------|------|', + ]) + for group in groups: + if not isinstance(group, dict): + continue + group_title = group.get('title') or '' + annotations = _list(group.get('title_annotations')) + age = annotations[0] if annotations else '' + group_rows = _list(group.get('rows')) + boundaries = [ + str((_dict(row.get('cells')).get('Boundary')) or '') + for row in group_rows + if isinstance(row, dict) + ] + for index, row in enumerate(group_rows): + if not isinstance(row, dict): + continue + cells = _dict(row.get('cells')) + beginning = cells.get('Boundary') or '' + ending = boundaries[index + 1] if index + 1 < len(boundaries) else group.get('period_end') or '' + lines.append( + f"| {_cell(group_title)} | {_cell(age)} | {_cell(cells.get('Period'))} | " + f"{_cell(beginning)} | {_cell(ending)} |" + ) + lines.append('') + kp = advanced.get('kp') if isinstance(advanced.get('kp'), dict) else {} + yogas_doshas = advanced.get('yogas_doshas') if isinstance(advanced.get('yogas_doshas'), dict) else {} + if kp or yogas_doshas.get('sade_sati'): + lines.extend(['## Saturn 与 KP 支持页(PL9 p121-p125 对标)', '']) + sade_sati = _dict(yogas_doshas.get('sade_sati')) + if sade_sati: + phases = _dict(sade_sati.get('phases')) + dosha_pack = _dict(advanced.get('dosha_remedy_pack')) + saturn_status = _dict(dosha_pack.get('saturn_status')) + timeline = _dict(saturn_status.get('cycle_timeline')) + display_rows = _dict(timeline.get('pl9_display_rows')) + sadhesati_rows = _list(timeline.get('sadhesati_rows')) or _list(display_rows.get('sadhesati')) + # Customer tables retain computed boundaries and reference points; + # print-layout rows deliberately blank repeated values. + kantaka_rows = _list(timeline.get('kantaka_rows')) or _list(display_rows.get('kantaka')) + lines.extend([ + '### p121-p122 Sade Sati / Dhayya / Kantaka Saturn', + '', + f"- Moon sign:{_cell(_token(sade_sati.get('moon_sign')))}", + f"- 当前阶段:{_cell(sade_sati.get('phase') or sade_sati.get('status'))}", + f"- 是否 active:{_cell(sade_sati.get('is_active'))}", + f"- 摘要:{_cell(sade_sati.get('summary'))}", + '', + '| Phase | Sign |', + '|-------|------|', + f"| rising | {_cell(_token(phases.get('rising')))} |", + f"| peak | {_cell(_token(phases.get('peak')))} |", + f"| setting | {_cell(_token(phases.get('setting')))} |", + '', + ]) + if sadhesati_rows: + lines.extend([ + '#### p121 Results of the Dhayya of Sadhesati', + '', + '| Cycle / phase | Transit of Saturn | Beginning date | Ending date | Duration Yr-Mn-Dy | Saturn AV | Sarva AV |', + '|---------------|-------------------|----------------|-------------|-------------------|-----------|----------|', + ]) + for item in sadhesati_rows: + if not isinstance(item, dict): + continue + lines.append( + f"| {_cell(item.get('cycle_phase'))} | {_cell(item.get('transit_sign'))} | {_cell(item.get('beginning_date'))} | " + f"{_cell(item.get('ending_date'))} | {_cell(item.get('duration'))} | {_cell(item.get('saturn_av'))} | {_cell(item.get('sarva_av'))} |" + ) + lines.append('') + if kantaka_rows: + lines.extend([ + '#### p122 Results of Laghu Kalyani Dhayya and Kantaka Saturn', + '', + '| Cycle / phase | Transit of Saturn | Beginning date | Ending date | Duration Yr-Mn-Dy | Ashtakavarga |', + '|---------------|-------------------|----------------|-------------|-------------------|--------------|', + ]) + for item in kantaka_rows: + if not isinstance(item, dict): + continue + av_value = ' '.join(str(value) for value in (item.get('saturn_av'), item.get('sarva_av')) if value not in (None, '')) + lines.append( + f"| {_cell(item.get('cycle_phase'))} | {_cell(item.get('transit_sign'))} | {_cell(item.get('beginning_date'))} | " + f"{_cell(item.get('ending_date'))} | {_cell(item.get('duration'))} | {_cell(av_value)} |" + ) + lines.append('') + if kp: + lines.extend([ + '### p123 KP Chart / Cusps', + '', + f"- House basis:{_cell(kp.get('house_basis'))}", + '', + '| House | Sign | Cusp longitude | KP lord chain |', + '|-------|------|----------------|---------------|', + ]) + houses = _dict(kp.get('houses')) + for house in [str(i) for i in range(1, 13)]: + row = _dict(houses.get(house)) + if not row: + continue + lines.append( + f"| {house} | {_cell(_token(row.get('sign')))} | {_num(row.get('cusp_longitude'), 4)} | {_cell(_lord_chain(row.get('kp_lords')))} |" + ) + lines.extend(['', '### p124 KP Significators / Planet Lords', '', '| Planet | KP lord chain | A | B | C | D |', '|--------|---------------|---|---|---|---|']) + planets = _dict(kp.get('planets')) + for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'): + row = _dict(planets.get(planet)) + if not row: + continue + sig = _dict(row.get('significators')) + lines.append( + f"| {_cell(_token(planet))} | {_cell(_lord_chain(row.get('kp_lords')))} | {_cell(sig.get('A'))} | {_cell(sig.get('B'))} | {_cell(sig.get('C'))} | {_cell(sig.get('D'))} |" + ) + ruling = _dict(kp.get('ruling_planets')) + if ruling: + lines.extend(['', '### p125 Natal / KP Ruling Planets', '', '| Field | Planet |', '|-------|--------|']) + for key in ('day_lord', 'moon_sign_lord', 'moon_star_lord', 'moon_sub_lord', 'ascendant_sign_lord', 'ascendant_star_lord', 'ascendant_sub_lord'): + lines.append(f"| {_cell(key)} | {_cell(_token(ruling.get(key)))} |") + lines.append('') + kp_source_cells = _dict(source_visible.get('kp_p124_visual_cells')) + if kp_source_cells: + lines.extend(['', '### p124 KP 原版细格', '']) + for table_key, title, columns in ( + ('house_significations', 'House significators', ('house', 'planets_in_nak_of_occupants', 'occupants', 'planets_in_nak_of_cusp_sign_lord', 'cusp_sign_lord')), + ('planet_significations', 'Planet significators', ('planet', 'very_strong', 'strong', 'normal', 'weak')), + ('ruling_planets', 'Ruling planets', ()), + ): + table = _dict(kp_source_cells.get(table_key)) + rows = _list(table.get('rows')) + if not rows: + fields = _dict(table.get('fields')) + rows = [{'field': key, 'value': value} for key, value in fields.items()] + if not rows: + continue + lines.extend([f'#### {title}', '']) + if table_key == 'ruling_planets': + lines.extend(['| Field | Value |', '|-------|-------|']) + for row in rows: + lines.append(f"| {_cell(row.get('field'))} | {_cell(row.get('value'))} |") + else: + available = [key for key in columns if any(row.get(key) not in (None, '') for row in rows)] + if not available: + continue + lines.extend([ + '| ' + ' | '.join(available) + ' |', + '|' + '|'.join('---' for _ in available) + '|', + ]) + for row in rows: + lines.append('| ' + ' | '.join(_cell(row.get(key)) for key in available) + ' |') + lines.append('') + annual = timing.get('annual_tajika_pack') if isinstance(timing.get('annual_tajika_pack'), dict) else {} + if annual: + lines.extend(['## 年度图盘与 Tajika(PL9 p126-p155 对标)', '']) + annual_chart = _dict(annual.get('annual_chart')) + annual_chart_data = _dict(annual_chart.get('data')) + solar_return = _dict(annual.get('solar_return')) + solar_return_data = _dict(solar_return.get('data') or solar_return) + if solar_return_data or annual_chart_data: + lines.extend([ + '### p126-p127 Varshaphala 基础资料与年度行星表', + '', + f"- Varshapravesha:{solar_return_data.get('dt_local') or solar_return_data.get('local_time') or solar_return_data.get('date') or '-'}", + f"- 年返上升:{_token(annual_chart_data.get('asc_sign'))}", + '', + ]) + year_lord = _dict(annual.get('year_lord')) + year_lord_data = _dict(year_lord.get('data') or year_lord) + if year_lord_data: + candidates = _list(year_lord_data.get('candidates')) + lines.extend([ + '#### Varshesha / Year Lord', + '', + '| Field | Value |', + '|-------|-------|', + f"| Varshesha | {_cell(_token(year_lord_data.get('year_lord') or year_lord_data.get('lord')))} |", + f"| Selection basis | {_cell(year_lord_data.get('selection_basis'))} |", + f"| Annual ascendant sign | {_cell(_token(year_lord_data.get('annual_asc_sign')))} |", + f"| Muntha sign | {_cell(_token(year_lord_data.get('muntha_sign')))} |", + ]) + if candidates: + lines.append(f"| Candidate count | {_cell(len(candidates))} |") + lines.append('') + if candidates: + lines.extend([ + '| Candidate source | Planet | House | Sign | Panchavargiya score |', + '|------------------|--------|-------|------|---------------------|', + ]) + for row in candidates: + if not isinstance(row, dict): + continue + lines.append( + f"| {_cell(row.get('source'))} | {_cell(_token(row.get('planet')))} | {_cell(row.get('planet_house'))} | " + f"{_cell(_token(row.get('planet_sign')))} | {_cell(row.get('panchavargiya_score'))} |" + ) + lines.append('') + planet_longitudes = _dict(annual_chart_data.get('planet_longitudes')) + if planet_longitudes: + signs = globals().get('SIGNS', []) + lines.extend([ + '| Planet | Sign | Degree | Longitude |', + '|--------|------|--------|-----------|', + ]) + if annual_chart_data.get('asc_sign') is not None: + lines.append(f"| Lagna | {_cell(_token(annual_chart_data.get('asc_sign')))} | - | - |") + for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'): + longitude = planet_longitudes.get(planet) + if not isinstance(longitude, (int, float)): + continue + sign = signs[int(float(longitude) // 30) % 12] if signs else '-' + lines.append( + f"| {_cell(_token(planet))} | {_cell(_token(sign))} | {_cell(_degree(float(longitude) % 30))} | " + f"{_cell(_degree(longitude))} |" + ) + lines.append('') + current_year = _dict(annual.get('pl9_current_year_results')) + current_sections = _list(current_year.get('sections')) + if current_sections: + lines.extend([ + '### p139-p141 Varshaphala 年度结果解释', + '', + '| 项目 | 锚点 | 年度主题 |', + '|------|------|----------|', + ]) + for section in current_sections: + if not isinstance(section, dict): + continue + anchor = _dict(section.get('anchor')) + anchor_text = ', '.join( + f"{_token(key)}={_token(value)}" + for key, value in anchor.items() + if key != 'field_status' and value not in (None, '') + ) + lines.append( + f"| {_cell(section.get('title'))} | {_cell(anchor_text)} | " + f"{_cell(_brief(section.get('themes'), 3))} |" + ) + lines.append('') + annual_dasha = _dict(annual.get('pl9_annual_dasha_results')) + dasha_rows = _list(annual_dasha.get('rows')) + if dasha_rows: + lines.extend([ + '### p144-p145 Annual Dasha 结果解释', + '', + '| 阶段 | 主星 | 年盘宫位 | 日期边界 | 主题 |', + '|------|------|----------|----------|------|', + ]) + for row in dasha_rows: + if not isinstance(row, dict): + continue + boundary = f"{row.get('start')} - {row.get('end')}" if row.get('start') and row.get('end') else row.get('start') or '-' + lines.append( + f"| {_cell(row.get('order'))} | {_cell(_token(row.get('lord')))} | {_cell(row.get('annual_whole_sign_house'))} | " + f"{_cell(boundary)} | {_cell(_brief(row.get('themes'), 2))} |" + ) + lines.append('') + normalized = _dict(annual.get('normalized_tables')) + mudda_rows = _list(normalized.get('mudda_rows')) + if mudda_rows: + lines.extend([ + '### p132-p135 Mudda / Patyayini 年度 Dasha 表', + '', + '| balance profile | 主星 | 开始 | 结束 | 边界口径 |', + '|-----------------|------|------|------|----------|', + ]) + for row in mudda_rows: + if not isinstance(row, dict): + continue + lines.append( + f"| {_cell(row.get('balance_profile'))} | {_cell(_token(row.get('lord')))} | " + f"{_cell(row.get('start'))} | {_cell(row.get('end'))} | {_public_status_note(row.get('boundary_status'))} |" + ) + lines.append('') + patyayini_rows = _list(normalized.get('patyayini_rows')) + if patyayini_rows: + lines.extend([ + '### p134 Patyayini / Patyamsha ending-date 表', + '', + '| 主层 | 子层 | 边界 | 边界语义 |', + '|------|------|------|----------|', + ]) + for row in patyayini_rows: + if not isinstance(row, dict): + continue + lines.append( + f"| {_cell(_token(row.get('main')))} | {_cell(_token(row.get('sub')))} | " + f"{_cell(row.get('boundary'))} | {_public_status_note(row.get('boundary_status'))} |" + ) + lines.append('') + tajika_rows = _list(normalized.get('tajika_yoga_rows')) or _list(normalized.get('pyjhora_tajika_yoga_rows')) + if tajika_rows: + lines.extend([ + '### p136-p138 Tajika Yoga Presence 对照表', + '', + '| Yoga | 结果 | 形成行星 | 相位运动(入相/分离) |', + '|------|------|----------|----------------------|', + ]) + for row in tajika_rows[:32]: + if not isinstance(row, dict): + continue + forming = row.get('forming_planets') or row.get('planets') or row.get('raw_relationships') + lines.append( + f"| {_cell(row.get('yoga') or row.get('name'))} | {_cell(_public_report_text(row.get('observation_status') or row.get('condition_status') or row.get('present')))} | " + f"{_cell(forming)} | {_cell({'applying': '入相', 'separating': '分离'}.get(str(row.get('motion') or row.get('aspect_motion') or '-'), row.get('motion') or row.get('aspect_motion') or '-'))} |" + ) + lines.append('') + saham_rows = _list(normalized.get('saham_rows')) + if saham_rows: + lines.extend([ + '### p130 Sahams 点位表', + '', + '| Saham | 星座 | 星座内度数 |', + '|-------|------|------------|', + ]) + for row in saham_rows[:48]: + if not isinstance(row, dict): + continue + lines.append( + f"| {_cell(row.get('name') or row.get('saham'))} | {_cell(_token(row.get('sign')))} | " + f"{_cell(_degree(row.get('degree_in_sign')))} |" + ) + lines.append('') + public_replay = _dict(annual.get('public_panchavargiya_replay')) + replay_coverage = _dict(public_replay.get('coverage')) + if public_replay: + lines.extend([ + '### 公开 Panchavargiya 数值复核边界', + '', + '| 项目 | 结果 |', + '|------|------|', + f"| 公开复核状态 | {_cell(public_replay.get('status'))} |", + f"| 覆盖行星 | {_cell(replay_coverage.get('planet_count'))} |", + f"| 已匹配分量 | {_cell(replay_coverage.get('exact_component_match_count'))} / {_cell(replay_coverage.get('component_count'))} |", + f"| 已匹配总分 | {_cell(replay_coverage.get('exact_total_match_count'))} / {_cell(replay_coverage.get('planet_count'))} |", + f"| 可直接用于公式升级 | {_cell(public_replay.get('promotion_allowed'))} |", + '', + ]) + annual_extra_specs = ( + ('annual_anchor_rows', 'p126 年度锚点', ('name', 'label', 'value', 'sign', 'degree', 'house')), + ('annual_planet_rows', 'p127 年度行星资料', ('planet', 'sign', 'degree', 'longitude', 'nakshatra', 'pada')), + ('true_solar_return_rows', 'True Solar Return', ('field', 'value', 'date', 'time', 'dt_local')), + ('panchadhikari_rows', 'Panchadhikari', ('planet', 'role', 'value', 'sign', 'house')), + ('tripataki_placement_rows', 'Tripataki Placement', ('planet', 'sign', 'house', 'degree', 'placement')), + ('tripataki_vedha_rows', 'Tripataki Vedha', ('planet', 'target', 'sign', 'house', 'result')), + ('current_year_result_rows', 'p139-p141 年度结果', ('title', 'value', 'theme', 'summary', 'anchor')), + ('tajika_presence_rows', 'Tajika Presence', ('yoga', 'name', 'present', 'forming_planets')), + ('tajika_condition_status_rows', 'Tajika Condition', ('yoga', 'name', 'condition', 'present')), + ('tajika_forming_pair_rows', 'Tajika Forming Pair', ('yoga', 'name', 'planet_a', 'planet_b', 'forming_planets')), + ('tajika_strength_rows', 'Tajika Strength', ('planet', 'yoga', 'strength', 'value')), + ('raman_saham_rows', 'Raman Sahams', ('name', 'saham', 'sign', 'degree_in_sign', 'degree')), + ) + for table_key, title, preferred_keys in annual_extra_specs: + rows = _list(normalized.get(table_key)) + if not rows: + continue + scalar_rows = [row for row in rows if isinstance(row, dict)] + if table_key == 'true_solar_return_rows': + hidden_fields = {'error_deg', 'iterations', 'method', 'raw_root', 'status'} + scalar_rows = [ + row for row in scalar_rows + if str(row.get('field') or '').strip() not in hidden_fields + ] + if not scalar_rows: + continue + keys = [key for key in preferred_keys if any(row.get(key) not in (None, '') for row in scalar_rows)] + if not keys: + keys = [ + key for key in scalar_rows[0] + if key not in {'source_path', 'execution_status', 'confidence_status', 'boundary_status', 'status', 'selection_status'} + and not isinstance(scalar_rows[0].get(key), (dict, list, tuple)) + ][:8] + if not keys: + continue + lines.extend([ + f'### {title}', + '', + '| ' + ' | '.join(_cell(key) for key in keys) + ' |', + '|' + '|'.join('---' for _key in keys) + '|', + ]) + for row in scalar_rows: + lines.append('| ' + ' | '.join(_cell(row.get(key)) for key in keys) + ' |') + lines.append('') + annual_special = _dict(annual.get('annual_special_points')) + annual_special_data = _dict(annual_special.get('data')) + if annual_special_data: + lines.extend([ + '### 年度特殊点', + '', + '| Point | Sign | Degree | Longitude | Lord |', + '|-------|------|--------|-----------|------|', + ]) + for name, row in annual_special_data.items(): + if not isinstance(row, dict): + continue + lines.append( + f"| {_cell(row.get('name') or name)} | {_cell(_token(row.get('sign')))} | " + f"{_cell(_degree(row.get('degree_in_sign')))} | {_cell(_degree(row.get('longitude')))} | " + f"{_cell(_token(row.get('lord')))} |" + ) + lines.append('') + tripataki = _dict(annual.get('tripataki_chakra')) + tripataki_placements = _list(tripataki.get('placements')) + tripataki_vedha = _list(tripataki.get('vedha_rows')) + if tripataki_placements: + lines.extend([ + '### Tripataki Chakra 年盘落点', + '', + '| Body | Sign | Degree | Longitude |', + '|------|------|--------|-----------|', + ]) + for row in tripataki_placements: + if not isinstance(row, dict): + continue + lines.append( + f"| {_cell(row.get('body'))} | {_cell(_token(row.get('sign')))} | " + f"{_cell(_degree(row.get('degree_in_sign')))} | {_cell(_degree(row.get('longitude')))} |" + ) + lines.append('') + if tripataki_vedha: + lines.extend([ + '| Target | Vedha sources |', + '|--------|---------------|', + ]) + for row in tripataki_vedha: + if not isinstance(row, dict): + continue + sources = row.get('sources') if isinstance(row.get('sources'), list) else [] + lines.append(f"| {_cell(row.get('target'))} | {_cell('、'.join(map(str, sources)))} |") + lines.append('') + monthly_pack = _dict(annual.get('monthly_chart_pack')) + monthly_rows = _list(monthly_pack.get('rows')) + if monthly_rows: + lines.extend([ + '### 年度月返照图盘数据', + '', + '| 月份 | 本地时刻 | 年盘上升 | 上升度数 | 行星黄经(度) |', + '|------|----------|----------|----------|-----------------|', + ]) + for row in monthly_rows: + if not isinstance(row, dict): + continue + asc = _dict(row.get('ascendant')) + longitudes = _dict(row.get('planet_longitudes')) + longitude_text = ';'.join( + f"{_token(planet)} {_degree(value)}" + for planet, value in longitudes.items() + if isinstance(value, (int, float)) + ) + lines.append( + f"| {_cell(row.get('month_index'))} | {_cell(row.get('local_datetime'))} | " + f"{_cell(_token(asc.get('sign')))} | {_cell(_degree(asc.get('degree_in_sign') or asc.get('degree')))} | " + f"{_cell(longitude_text)} |" + ) + lines.append('') + lifetime_pack = _dict(annual.get('lifetime_annual_chart_pages')) + lifetime_pages = _list(lifetime_pack.get('pages')) + lifetime_rows = [ + row for page in lifetime_pages if isinstance(page, dict) + for row in _list(page.get('rows')) if isinstance(row, dict) + ] + if lifetime_rows: + lines.extend([ + '### 生命周期年度返照图盘数据', + '', + '| 年龄 | 年份 | 年返时刻 | 上升 | 上升度数 | 行星黄经(度) |', + '|------|------|----------|------|----------|-----------------|', + ]) + for row in lifetime_rows: + asc = _dict(row.get('ascendant')) + longitudes = _dict(row.get('planet_longitudes')) + longitude_text = ';'.join( + f"{_token(planet)} {_degree(value)}" + for planet, value in longitudes.items() + if isinstance(value, (int, float)) + ) + lines.append( + f"| {_cell(row.get('completed_age'))} | {_cell(row.get('target_year'))} | " + f"{_cell(row.get('local_datetime'))} | {_cell(_token(asc.get('sign')))} | " + f"{_cell(_degree(asc.get('degree_in_sign') or asc.get('degree')))} | {_cell(longitude_text)} |" + ) + lines.append('') + annual_series = _dict(annual.get('annual_chart_series_pack')) + for series_key, title in ( + ('birth_place_eight_year_overview', '出生地八年年度总览'), + ('local_eight_year_overview', '现居地八年年度总览'), + ): + series = _dict(annual_series.get(series_key)) + series_rows = _list(series.get('rows')) + if not series_rows: + continue + lines.extend([ + f'### {title}', + '', + '| 年份 | 年龄 | 年返时刻 | 年盘上升 |', + '|------|------|----------|----------|', + ]) + for row in series_rows: + if not isinstance(row, dict): + continue + lines.append( + f"| {_cell(row.get('target_year'))} | {_cell(row.get('age'))} | " + f"{_cell(row.get('solar_return_local') or row.get('local_datetime'))} | " + f"{_cell(_token(row.get('annual_ascendant')))} |" + ) + lines.append('') + annual_visual = annual.get('pl9_annual_visual_panels') if isinstance(annual.get('pl9_annual_visual_panels'), dict) else {} + panels = annual_visual.get('panels') if isinstance(annual_visual.get('panels'), list) else [] + if panels: + lines.extend(['### p139-p155 年度北印度图盘', '']) + for panel in panels: + if not isinstance(panel, dict): + continue + lines.append(f"##### p{panel.get('pl9_page')} · {panel.get('chart_label') or panel.get('panel_id')}") + lines.append('') + annual_profile = _dict(annual.get('profile')) + primary_target_year = str(annual_profile.get('target_year') or '') if annual_profile else '' + annual_series = _dict(timing.get('annual_tajika_series')) + annual_series_years = _dict(annual_series.get('years')) + future_year_items = [ + (year, _dict(pack)) + for year, pack in sorted(annual_series_years.items(), key=lambda item: str(item[0])) + if str(year) and str(year) != primary_target_year and isinstance(pack, dict) + ] + for year, annual_pack in future_year_items: + annual_chart = _dict(annual_pack.get('annual_chart')) + annual_chart_data = _dict(annual_chart.get('data')) + solar_return = _dict(annual_pack.get('solar_return')) + solar_return_data = _dict(solar_return.get('data') or solar_return) + lines.extend([f'### {year} 年度 Varshaphala / Tajika 细项', '']) + if solar_return_data or annual_chart_data: + lines.extend([ + '| 项目 | 数值 |', + '|------|------|', + f"| Varshapravesha | {_cell(solar_return_data.get('dt_local') or solar_return_data.get('local_time') or solar_return_data.get('date') or '-')} |", + f"| 年返上升 | {_cell(_token(annual_chart_data.get('asc_sign')))} |", + ]) + year_lord = _dict(annual_pack.get('year_lord')) + year_lord_data = _dict(year_lord.get('data') or year_lord) + if year_lord_data: + lines.extend([ + f"| Varshesha | {_cell(_token(year_lord_data.get('year_lord') or year_lord_data.get('lord')))} |", + f"| Muntha sign | {_cell(_token(year_lord_data.get('muntha_sign')))} |", + ]) + lines.append('') + planet_longitudes = _dict(annual_chart_data.get('planet_longitudes')) + if planet_longitudes: + signs = globals().get('SIGNS', []) + lines.extend([ + '#### 年度行星资料', + '', + '| 行星 | 星座 | 落座度数 | 黄经 |', + '|------|------|----------|------|', + ]) + if annual_chart_data.get('asc_sign') is not None: + lines.append(f"| Lagna | {_cell(_token(annual_chart_data.get('asc_sign')))} | - | - |") + for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'): + longitude = planet_longitudes.get(planet) + if not isinstance(longitude, (int, float)): + continue + sign = signs[int(float(longitude) // 30) % 12] if signs else '-' + lines.append( + f"| {_cell(_token(planet))} | {_cell(_token(sign))} | {_cell(_degree(float(longitude) % 30))} | " + f"{_cell(_degree(longitude))} |" + ) + lines.append('') + annual_dasha = _dict(annual_pack.get('pl9_annual_dasha_results')) + dasha_rows = _list(annual_dasha.get('rows')) + if dasha_rows: + lines.extend([ + '#### Annual Dasha 阶段表', + '', + '| 阶段 | 主星 | 年盘宫位 | 日期边界 | 主题 |', + '|------|------|----------|----------|------|', + ]) + for row in dasha_rows: + if not isinstance(row, dict): + continue + boundary = f"{row.get('start')} - {row.get('end')}" if row.get('start') and row.get('end') else row.get('start') or '-' + lines.append( + f"| {_cell(row.get('order'))} | {_cell(_token(row.get('lord')))} | {_cell(row.get('annual_whole_sign_house'))} | " + f"{_cell(boundary)} | {_cell(_brief(row.get('themes'), 2))} |" + ) + lines.append('') + normalized = _dict(annual_pack.get('normalized_tables')) + future_specs = ( + ('mudda_rows', 'Mudda / Patyayini 年度 Dasha 表', ('balance_profile', 'lord', 'start', 'end', 'boundary_status')), + ('patyayini_rows', 'Patyayini / Patyamsha ending-date 表', ('main', 'sub', 'boundary', 'boundary_status')), + ('saham_rows', 'Sahams 点位表', ('name', 'saham', 'sign', 'degree_in_sign')), + ('tajika_yoga_rows', 'Tajika Yoga Presence 表', ('yoga', 'name', 'present', 'forming_planets')), + ('pyjhora_tajika_yoga_rows', 'Tajika Yoga Presence 表', ('yoga', 'name', 'present', 'forming_planets')), + ('annual_planet_rows', '年度行星扩展字段', ('planet', 'sign', 'degree', 'longitude', 'nakshatra', 'pada')), + ('annual_anchor_rows', '年度锚点', ('name', 'label', 'value', 'sign', 'degree', 'house')), + ) + rendered_titles = set() + for table_key, title, preferred_keys in future_specs: + rows = [row for row in _list(normalized.get(table_key)) if isinstance(row, dict)] + if not rows: + continue + if title in rendered_titles and table_key == 'pyjhora_tajika_yoga_rows': + continue + rendered_titles.add(title) + keys = [key for key in preferred_keys if any(row.get(key) not in (None, '') for row in rows)] + if not keys: + keys = [ + key for key in rows[0] + if key not in {'source_path', 'execution_status', 'confidence_status', 'boundary_status', 'status', 'selection_status'} + and not isinstance(rows[0].get(key), (dict, list, tuple)) + ][:6] + if not keys: + continue + lines.extend([ + f'#### {title}', + '', + '| ' + ' | '.join(_cell(key) for key in keys) + ' |', + '|' + '|'.join('---' for _key in keys) + '|', + ]) + for row in rows: + lines.append('| ' + ' | '.join(_cell(row.get(key)) for key in keys) + ' |') + lines.append('') + special = _dict(annual_pack.get('annual_special_points')) + special_data = _dict(special.get('data')) + if special_data: + lines.extend([ + '#### 年度特殊点', + '', + '| 点位 | 星座 | 落座度数 | 黄经 | 守护星 |', + '|------|------|----------|------|--------|', + ]) + for name, row in special_data.items(): + if not isinstance(row, dict): + continue + lines.append( + f"| {_cell(row.get('name') or name)} | {_cell(_token(row.get('sign')))} | " + f"{_cell(_degree(row.get('degree_in_sign')))} | {_cell(_degree(row.get('longitude')))} | " + f"{_cell(_token(row.get('lord')))} |" + ) + lines.append('') + monthly_pack = _dict(annual_pack.get('monthly_chart_pack')) + monthly_rows = _list(monthly_pack.get('rows')) + if monthly_rows: + lines.extend([ + '#### 年度月返照图盘数据', + '', + '| 月份 | 本地时刻 | 年盘上升 | 上升度数 |', + '|------|----------|----------|----------|', + ]) + for row in monthly_rows: + if not isinstance(row, dict): + continue + asc = _dict(row.get('ascendant')) + lines.append( + f"| {_cell(row.get('month_index'))} | {_cell(row.get('local_datetime'))} | " + f"{_cell(_token(asc.get('sign')))} | {_cell(_degree(asc.get('degree_in_sign') or asc.get('degree')))} |" + ) + lines.append('') + d1_sheet = worksheets.get('d1_rasi_bhava') if isinstance(worksheets.get('d1_rasi_bhava'), dict) else {} + d1_houses = _dict(d1_sheet.get('houses')) or _dict(core_chart.get('houses')) + d1_planets = _dict(d1_sheet.get('planets')) or _dict(core_chart.get('planets')) + advanced_yoga = advanced.get('yoga') if isinstance(advanced.get('yoga'), dict) else {} + detected_yogas = _list(advanced_yoga.get('detected_yogas')) or _list(advanced_yoga.get('yogas')) + dosha_pack = _dict(advanced.get('dosha_remedy_pack')) + bhavesh_pack = _dict(advanced.get('bhavesh_interpretation_pack')) + if d1_houses or yogas_doshas or detected_yogas or bhavesh_pack or dosha_pack: + lines.extend(['## Bhavesh、Dosha 与 Yoga 解释页(PL9 p156-p189 对标)', '']) + if d1_houses and d1_planets: + lines.extend([ + '### p156-p162 宫主(Bhavesh)落宫解释', + '', + '| House | Sign | Lord | Lord placement | Lord dignity | Dasha activation |', + '|-------|------|------|----------------|--------------|------------------|', + ]) + active_lords = set() + current_dasha = _dict(timing.get('dasha')).get('current_dasha') if isinstance(timing.get('dasha'), dict) else {} + if isinstance(current_dasha, dict) and current_dasha.get('lord'): + active_lords.add(str(current_dasha.get('lord'))) + for ad in _list(current_dasha.get('antardasha_timeline')): + if isinstance(ad, dict) and ad.get('is_current') and ad.get('lord'): + active_lords.add(str(ad.get('lord'))) + for index in range(1, 13): + house = _dict(d1_houses.get(f'house_{index}') or d1_houses.get(str(index))) + lord = house.get('lord') + lord_row = _dict(d1_planets.get(str(lord))) + placement = '-' + if lord_row: + placement = f"{_token(lord_row.get('sign'))} H{lord_row.get('house') or '-'} {_degree(lord_row.get('degree_in_sign'))}" + lines.append( + f"| {index} | {_cell(_token(house.get('cusp_sign') or house.get('sign')))} | {_cell(_token(lord))} | " + f"{_cell(placement)} | {_cell(lord_row.get('status') or lord_row.get('dignity'))} | {_cell('active' if str(lord) in active_lords else 'background')} |" + ) + lines.append('') + bhavesh_pages = _list(bhavesh_pack.get('pages')) + if bhavesh_pages: + lines.extend([ + '### Bhavesh 宫主逐宫解释', + '', + ]) + for row in bhavesh_pages: + if not isinstance(row, dict): + continue + sections = _dict(row.get('sections')) + title = f"第{row.get('house')}宫主:{_token(row.get('lord'))}" + lines.extend([f'#### {title}', '']) + for key in ('house_context', 'lord_placement', 'result', 'context', 'activation'): + value = str(sections.get(key) or '').strip() + if value: + lines.extend([_public_report_text(value), '']) + if yogas_doshas: + lines.extend([ + '### p163-p171 Mangala / Sade Sati / Dosha Results', + '', + '| Topic | Detected / phase | Key facts | Summary |', + '|-------|------------------|-----------|---------|', + ]) + for key, label in ( + ('mangal_dosha', 'Mangal / Kuja Dosha'), + ('kaal_sarp_dosha', 'Kaal Sarp Dosha'), + ('pitra_dosha', 'Pitra Dosha'), + ('sade_sati', 'Sade Sati'), + ): + row = _dict(yogas_doshas.get(key)) + if not row: + continue + detected = row.get('has_dosha') + if detected is None: + detected = row.get('is_active') if row.get('is_active') is not None else row.get('phase') + fact_bits = [] + for fact_key in ('mars_house', 'mars_sign', 'sun_house', 'sun_sign', 'rahu_house', 'ketu_house', 'moon_sign', 'severity', 'type'): + if row.get(fact_key) not in (None, ''): + fact_bits.append(f"{fact_key}={_token(row.get(fact_key))}") + lines.append( + f"| {_cell(label)} | {_cell(detected)} | {_cell(';'.join(map(str, fact_bits)) or '-')} | {_cell(row.get('summary'))} |" + ) + lines.append('') + dosha_rows = _list(dosha_pack.get('dosha_rows')) + if dosha_rows: + lines.extend([ + '### Dosha 逐项事实与解释', + '', + ]) + for row in dosha_rows: + if not isinstance(row, dict): + continue + title = str(row.get('title') or row.get('id') or '').strip() + sections = _dict(row.get('sections')) + if not title: + continue + lines.extend([f'#### {title}', '']) + for key in ('detected', 'consideration', 'facts', 'result', 'cancellation'): + value = str(sections.get(key) or '').strip() + if value: + lines.extend([_public_report_text(value), '']) + practices = _dict(dosha_pack.get('supportive_practices')) + recommendations = _list(practices.get('recommendations')) + if recommendations: + lines.extend([ + '### 传统辅助建议', + '', + '| 对象 | 建议 | 宝石/次数 |', + '|------|------|-----------|', + ]) + for row in recommendations: + if not isinstance(row, dict): + continue + actions = row.get('actions') if isinstance(row.get('actions'), list) else [] + advice = row.get('note') or ';'.join(str(item) for item in actions if item) + lines.append( + f"| {_cell(_token(row.get('planet') or row.get('dosha')))} | " + f"{_cell(_public_report_text(advice))} | " + f"{_cell(row.get('gem') or row.get('repetitions') or ('No gem or repetition count supplied' if _pl9_report_language(packet) == 'en' else '源数据未提供宝石或次数'))} |" + ) + lines.append('') + if detected_yogas: + lines.extend([ + '### p172-p189 Applicable Benefic / Mixed / Malefic Yogas', + '', + f"- Yoga detected count:{len(detected_yogas)}", + f"- Context layers:{_cell(_brief(advanced_yoga.get('context_layers'), 6))}", + '', + ]) + def _yoga_group_label(row: dict) -> str: + category = str(row.get('category') or row.get('subtype') or '').lower() + name = str(row.get('name') or row.get('name_cn') or row.get('type') or '').lower() + if category in {'durbhaga', 'ari'} or 'daridra' in name or 'arishta' in name: + return 'Applicable Malefic Yogas' + if category in {'conjunction', 'special', 'kalatra', 'solar_yoga', 'lunar_yoga'} or 'dosha' in name: + return 'Applicable Mixed Yogas' + return 'Applicable Benefic Yogas' + + grouped_yogas: dict[str, list[dict]] = { + 'Applicable Benefic Yogas': [], + 'Applicable Mixed Yogas': [], + 'Applicable Malefic Yogas': [], + } + for row in detected_yogas: + if isinstance(row, dict): + grouped_yogas[_yoga_group_label(row)].append(row) + for group_title, group_rows in grouped_yogas.items(): + if not group_rows: + continue + lines.extend([ + f'#### {group_title}', + '', + '| Yoga | Category | Strength | Combination | Effects / notes |', + '|------|----------|----------|-------------|-----------------|', + ]) + for row in group_rows: + effects = row.get('effects') if isinstance(row.get('effects'), list) else [] + lines.append( + f"| {_cell(row.get('name_cn') or row.get('name') or row.get('type'))} | {_cell(row.get('category') or row.get('subtype'))} | " + f"{_cell(row.get('strength'))} | {_cell(row.get('combination') or row.get('source_reference') or row.get('reason'))} | " + f"{_cell(_public_report_text(_brief(effects, 3) or row.get('interpretation') or row.get('summary')))} |" + ) + lines.append('') + supportive_raw = dosha_pack.get('supportive_practices') + supportive_practices = _list(supportive_raw) + if isinstance(supportive_raw, dict): + supportive_practices = _list(supportive_raw.get('recommendations')) + if supportive_practices: + lines.extend([ + '### Remedies / Supportive Practices', + '', + '| Topic | Practice | Gem / count | Notes |', + '|-------|----------|-------------|-------|', + ]) + for row in supportive_practices: + if not isinstance(row, dict): + continue + actions = row.get('actions') if isinstance(row.get('actions'), list) else [] + practice = row.get('practice') or row.get('name') or row.get('remedy') or row.get('mantra') or _brief(actions, 3) + count_or_gem = row.get('gem') or row.get('repetitions') or ('No gem or repetition count supplied' if _pl9_report_language(packet) == 'en' else '源数据未提供宝石或次数') + lines.append( + f"| {_cell(row.get('topic') or row.get('title') or row.get('id') or row.get('planet') or row.get('dosha'))} | " + f"{_cell(practice)} | {_cell(count_or_gem)} | " + f"{_cell(_public_report_text(row.get('notes') or row.get('summary') or row.get('description')))} |" + ) + lines.append('') + dasha = timing.get('dasha_interpretation_pack') if isinstance(timing.get('dasha_interpretation_pack'), dict) else {} + dasha_pages = dasha.get('page_series') if isinstance(dasha.get('page_series'), list) else [] + if dasha_pages: + lines.extend(['## 大运解释', '']) + for page in dasha_pages: + if not isinstance(page, dict): + continue + title = str(page.get('title') or '').strip() + sections = page.get('sections') if isinstance(page.get('sections'), dict) else {} + if not title: + continue + lines.extend([f'### {title}', '']) + for key in ('general', 'natal_context', 'domain_context', 'current_subperiod'): + if str(sections.get(key) or '').strip(): + lines.extend([_public_report_text(sections[key]), '']) + # The source-bound pack already contains the ordered MD/AD/PD + # narrative blocks. Keep them in the main export; the compact + # section summary alone drops the original report's useful density. + seen_blocks = { + str(sections.get(key) or '').strip() + for key in ('general', 'natal_context', 'domain_context', 'current_subperiod') + if str(sections.get(key) or '').strip() + } + for block in page.get('flow_blocks') or []: + if not isinstance(block, dict): + continue + block_title = str(block.get('heading') or block.get('title') or '').strip() + block_sections = block.get('sections') + if block_title and block_title != title: + lines.extend([f'#### {block_title}', '']) + if not isinstance(block_sections, list): + continue + for item in block_sections: + if not isinstance(item, (list, tuple)) or len(item) < 2: + continue + value = str(item[1] or '').strip() + if not value or value in seen_blocks: + continue + seen_blocks.add(value) + lines.extend([_public_report_text(value), '']) + return _sanitize_pl9_public_markdown('\n'.join(lines)) diff --git a/scripts/professional_report_reference.py b/scripts/professional_report_reference.py index 7fed9916..046d81d9 100644 --- a/scripts/professional_report_reference.py +++ b/scripts/professional_report_reference.py @@ -26,6 +26,24 @@ class ProfessionalReportReferenceInputError(ValueError): """The professional-reference request is outside the public contract.""" +READER_MAIN_EDITION = "reader_main" +REFERENCE_EDITION = "reference" +REPORT_VERSION_READER = "pl9_personal_long_report.v3" + + +def _normalize_edition(value: Any) -> str: + if value is None or value == "": + return REFERENCE_EDITION + if not isinstance(value, str): + raise ProfessionalReportReferenceInputError("edition must be a string") + normalized = value.strip() + if normalized in {"", REFERENCE_EDITION, "professional_reference"}: + return REFERENCE_EDITION + if normalized == READER_MAIN_EDITION: + return READER_MAIN_EDITION + raise ProfessionalReportReferenceInputError("edition must be reference or reader_main") + + def _normalize_format(value: Any) -> str: if value is None: return "json" @@ -100,6 +118,7 @@ def build_professional_report_reference(handler, body: dict[str, Any], *, engine if not isinstance(body, dict): raise ProfessionalReportReferenceInputError("request body must be an object") output_format = _normalize_format(body.get("format")) + edition = _normalize_edition(body.get("edition")) packs = _normalize_packs(body.get("packs")) birth = handler._high_rigor_birth_payload(body) @@ -114,15 +133,29 @@ def build_professional_report_reference(handler, body: dict[str, Any], *, engine except ValueError as exc: raise ProfessionalReportReferenceInputError(str(exc)) from exc + if edition == READER_MAIN_EDITION and isinstance(packet, dict): + packet = dict(packet) + packet["report_version"] = REPORT_VERSION_READER if output_format == "markdown": + if edition == READER_MAIN_EDITION: + try: + from scripts.pl9_reader_export import _pl9_export_markdown_for_edition + except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution path + from pl9_reader_export import _pl9_export_markdown_for_edition + markdown = _pl9_export_markdown_for_edition(packet, READER_MAIN_EDITION) + else: + markdown = resolved_engine.render_pl9_markdown(packet) return { "format": "markdown", - "markdown": resolved_engine.render_pl9_markdown(packet), + "edition": edition, + "report_version": REPORT_VERSION_READER if edition == READER_MAIN_EDITION else packet.get("report_version"), + "markdown": markdown, **({"fact_table_packet": report_density_packet(packet), "reader_dasha_applicability": clean_reader_appendix_markdown(render_dasha_applicability(packet))} if body.get("include_fact_tables") is True else {}), } return { "format": "json", + "edition": edition, "report": packet, } diff --git a/scripts/shared_full_report_authority.py b/scripts/shared_full_report_authority.py index 76d992e2..3283cdd4 100755 --- a/scripts/shared_full_report_authority.py +++ b/scripts/shared_full_report_authority.py @@ -15,7 +15,7 @@ from typing import Any AUTHORITY_SCHEMA_VERSION = "jyotish.shared_full_report_authority.v1" REPORT_SCHEMA_VERSION = "expert_report_output.v2" -DEFAULT_REPORT_VERSION = "pl9_personal_long_report.v2" +DEFAULT_REPORT_VERSION = "pl9_personal_long_report.v3" PL9_EXPORT_SCHEMA = "pl9_style_professional_export_v1" QUALITY_GATE_SCHEMA = "jyotish.full_report_quality_gate.v1" diff --git a/scripts/solar_return.py b/scripts/solar_return.py index e2c1da70..63cc77b8 100644 --- a/scripts/solar_return.py +++ b/scripts/solar_return.py @@ -401,6 +401,7 @@ def calc_solar_return_chart( 'houses': houses, 'ascendant': ascendant, 'asc_sign_idx': sr_asc_idx, + 'birth_asc_sign_idx': birth_asc_idx, 'target_year': target_year, 'birth_year': birth_year, 'age': target_year - birth_year, @@ -505,14 +506,20 @@ def solar_return_full_report( # 3. Year Lord(= Muntha 守护星) try: muntha_data = result.get('muntha', {}) - muntha_sign_val = muntha_data.get('muntha_sign_idx', muntha_sign if 'muntha_sign' in dir() else 0) - year_lord_sign = muntha_sign_val - year_lord = SIGN_LORDS[SIGNS[year_lord_sign]] - result['year_lord'] = { - 'year_lord': year_lord, - 'year_lord_sign_idx': year_lord_sign, - 'year_lord_sign': SIGNS[year_lord_sign], - } + muntha_sign_val = muntha_data.get('muntha_sign_idx') if isinstance(muntha_data, dict) else None + if not isinstance(muntha_sign_val, int) or isinstance(muntha_sign_val, bool): + result['year_lord'] = { + 'status': 'blocked', + 'reason': 'muntha_sign_required_for_year_lord', + } + else: + year_lord_sign = muntha_sign_val % 12 + year_lord = SIGN_LORDS[SIGNS[year_lord_sign]] + result['year_lord'] = { + 'year_lord': year_lord, + 'year_lord_sign_idx': year_lord_sign, + 'year_lord_sign': SIGNS[year_lord_sign], + } except Exception as e: result['year_lord'] = {'error': str(e)} diff --git a/tests/test_report_longform_gaps2.py b/tests/test_report_longform_gaps2.py index b47607ad..49e7c3e2 100644 --- a/tests/test_report_longform_gaps2.py +++ b/tests/test_report_longform_gaps2.py @@ -17,22 +17,36 @@ from scripts.timing_precision_contract import build_timing_precision_contract from tests.test_report_longform_parity import BEIJING, _producer_packet -def test_kp_error_pack_emits_blocked_heading_instead_of_vanishing() -> None: +def test_kp_error_pack_omits_the_section_instead_of_printing_the_error() -> None: + """原值:空 KP 仍输出原始表标题和 kp_lord_sub_unavailable。 + 新值:整节不输出,synthetic_kp_failure / producer failed 也不出现。 + 原因:D3。BUG-561 用 blocked 标题代替消失,但标题里打印了原始 error。 + """ markdown = render_pl9_markdown(_producer_packet({ "advanced_systems": {"kp": {"error": "synthetic_kp_failure", "status": "blocked"}}, })) - assert "### KP Lord / Sub 原始表(本地计算)" in markdown - assert "kp_lord_sub_unavailable" in markdown - assert "#### KP 事业结构小表" in markdown - assert "#### KP 事业宫位核对(2 / 6 / 10 / 11 宫)" in markdown - assert "#### KP 财务结构小表" in markdown - assert "#### KP 关系结构小表" in markdown + assert "### KP Lord / Sub 原始表(本地计算)" not in markdown + assert "kp_lord_sub_unavailable" not in markdown + assert "synthetic_kp_failure" not in markdown + assert "producer failed" not in markdown + assert "#### KP 事业结构小表" not in markdown + assert "#### KP 事业宫位核对(2 / 6 / 10 / 11 宫)" not in markdown + assert "#### KP 财务结构小表" not in markdown + assert "#### KP 关系结构小表" not in markdown -def test_planned_headings_are_present_or_blocked_on_empty_packet() -> None: +def test_empty_packet_omits_unavailable_planned_sections() -> None: + """原值:PLANNED_LONGFORM_SECTION_HEADINGS 在空包里必须逐条出现。 + 新值:没有数据的占位节不出现;仍有正文的审计节可以留下。 + 原因:D3,数据不可用的节整节不输出,且不得带原始 error。 + """ markdown = render_pl9_markdown(_producer_packet({})) - missing = [heading for heading in PLANNED_LONGFORM_SECTION_HEADINGS if heading not in markdown] - assert missing == [], f"planned headings vanished: {missing}" + omitted = [heading for heading in PLANNED_LONGFORM_SECTION_HEADINGS if heading not in markdown] + assert "### KP Lord / Sub 原始表(本地计算)" in omitted + assert "### Profile-aware Benchmark Boundary" in omitted + assert "producer failed" not in markdown + assert "结论等级规则" not in markdown + assert "This reference export" not in markdown def test_module_execution_audit_survives_raw_strip() -> None: diff --git a/tests/test_report_longform_parity.py b/tests/test_report_longform_parity.py index c84431db..299cf4d9 100644 --- a/tests/test_report_longform_parity.py +++ b/tests/test_report_longform_parity.py @@ -64,12 +64,12 @@ def test_reading_navigation_emits_required_headings() -> None: for heading in ( "## 成品阅读导航", "### 先读什么", - "### 结论等级规则", "### 专题判读协议", "### 质量验收矩阵", "### 对照覆盖表", ): assert heading in markdown + assert "### 结论等级规则" not in markdown assert "| 三年年度展开 | present |" in markdown assert "| KP 三年流月支持 | present |" in markdown diff --git a/tests/test_report_reader_main.py b/tests/test_report_reader_main.py new file mode 100644 index 00000000..cc2fa6c8 --- /dev/null +++ b/tests/test_report_reader_main.py @@ -0,0 +1,220 @@ +"""Reader-main export and the KP / Muntha defects behind BUG-1026/1027/1028.""" + +from __future__ import annotations + +import re + +import pytest + +from scripts.annual_tajika_pack import _field_brief, _field_with_conflict_gate +from scripts.jyotish_engine import ( + _copy_call_namespace, + _professional_report_reference_boundary_notice, + cmd_full_reading, + render_pl9_markdown, +) +from scripts.pl9_reader_export import _pl9_export_markdown_for_edition +from scripts.professional_report_reference import ( + REPORT_VERSION_READER, + build_professional_report_reference, +) +from scripts.shared_full_report_authority import DEFAULT_REPORT_VERSION +from scripts.solar_return import calc_solar_return_chart, solar_return_full_report + +# Clearly fictional. Not a user, not a published celebrity case. +FICTIONAL = { + "name": "虚构甲", + "year": 1991, + "month": 4, + "day": 7, + "hour": 9, + "minute": 15, + "second": 0, + "lat": 30.5728, + "lon": 104.0668, + "tz": 8.0, + "node_mode": "mean", + "ayanamsa": "lahiri", + "today": "2026-04-07", + "transit_date": "2026-04-07", + "target_year": 2026, + "age": 35, + "birth_time_accuracy": "confirmed", +} + +LEAK_PATTERNS = ( + re.compile(r"\bblocked\b", re.I), + re.compile(r"\bexecuted\b", re.I), + re.compile(r"\bpartial_verified\b", re.I), + re.compile(r"\bmissing_in_local\b", re.I), + re.compile(r"\bproducer\b", re.I), + re.compile(r"\bparity\b", re.I), + re.compile(r"PL9\s*第\s*\d+\s*页"), + re.compile(r"full-report://"), + re.compile(r"\bpl9_[A-Za-z0-9_]+\b", re.I), + re.compile(r"\b[A-Za-z][A-Za-z0-9]*_unavailable\b"), + re.compile(r"\b[A-Za-z][A-Za-z0-9]*_(?:pack|packet)\b", re.I), + re.compile(r"object has no attribute"), + re.compile(r"\bTraceback\b"), + re.compile(r"结论等级规则"), + re.compile(r"This reference export"), + re.compile(r"producer failed"), + re.compile(r"PyJHora"), + re.compile(r"\bparameter_sensitive\b"), + re.compile(r"\bschema\b", re.I), + re.compile(r"\bsource_path\b"), + re.compile(r"\{planets\}"), +) + +CORE_CHAPTERS = ( + "出生资料", + "Ashtakavarga", + "标准大运", + "KP", + "Year Lord", + "Tajika", + "Muntha", +) + + +def _class_args(**overrides): + fields = {**FICTIONAL, **overrides} + return type("Args", (), fields)() + + +def _leak_hits(markdown: str) -> list[str]: + return [pattern.pattern for pattern in LEAK_PATTERNS if pattern.search(markdown)] + + +@pytest.fixture(scope="module") +def full_reading(): + return cmd_full_reading(_class_args()) + + +def test_default_edition_stays_reference_and_reader_requests_v3() -> None: + assert DEFAULT_REPORT_VERSION == "pl9_personal_long_report.v3" + assert REPORT_VERSION_READER == "pl9_personal_long_report.v3" + + class Handler: + def _high_rigor_birth_payload(self, body): + return dict(FICTIONAL) + + def _compute_full_reading_for_thematic(self, body): + return {"modules": {"chart": {}}} + + class Engine: + def build_professional_report_reference_packet(self, full_reading, args, packs): + return {"schema": "pl9_style_professional_export_v1", "selected_report_pack_ids": packs} + + def render_pl9_markdown(self, packet): + return "reference-body" + + handler = Handler() + engine = Engine() + default = build_professional_report_reference(handler, {"format": "markdown"}, engine=engine) + assert default["edition"] == "reference" + assert default["markdown"] == "reference-body" + reader = build_professional_report_reference( + handler, + {"format": "markdown", "edition": "reader_main"}, + engine=engine, + ) + assert reader["edition"] == "reader_main" + assert reader["report_version"] == "pl9_personal_long_report.v3" + + +def test_reference_body_drops_local_leak_phrases() -> None: + packet = { + "schema": "pl9_style_professional_export_v1", + "birth_info": {"name": "虚构甲"}, + "reader_engine_boundary_notice": _professional_report_reference_boundary_notice(), + "worksheets": {}, + } + markdown = render_pl9_markdown(packet) + assert "结论等级规则" not in markdown + assert "This reference export" not in markdown + assert "producer failed" not in markdown + assert "多引擎口径说明" in markdown + + +def test_class_args_copy_keeps_year_for_kp_and_transit(full_reading) -> None: + args = _class_args() + copied = _copy_call_namespace(args) + assert copied.year == 1991 + assert vars(args) == {} + modules = full_reading["modules"] + for name in ("kp", "double_transit_pac", "transit_ll7l", "planetary_congregation"): + module = modules.get(name) + assert isinstance(module, dict), name + blob = repr(module) + assert "has no attribute" not in blob + assert module.get("status") != "blocked", (name, module.get("status"), module.get("reason")) + assert "error" not in module or not str(module.get("error", "")).startswith("KP producer failed") + + +def test_reader_main_hides_audit_vocabulary(full_reading) -> None: + from scripts.jyotish_engine import build_professional_report_reference_packet + + packet = build_professional_report_reference_packet(full_reading, _class_args(), ["full"]) + reference = _pl9_export_markdown_for_edition(packet, "reference") + reader = _pl9_export_markdown_for_edition(packet, "reader_main") + assert "结论等级规则" not in reference + assert "This reference export" not in reference + hits = _leak_hits(reader) + assert hits == [], hits + for chapter in CORE_CHAPTERS: + assert chapter in reader, chapter + assert reader.strip().startswith("#") + + +def test_solar_return_restores_birth_asc_and_year_lord_moves() -> None: + lords = set() + signs = set() + for year in (2024, 2026, 2028): + chart = calc_solar_return_chart( + FICTIONAL["year"], FICTIONAL["month"], FICTIONAL["day"], + FICTIONAL["hour"], FICTIONAL["minute"], + FICTIONAL["lat"], FICTIONAL["lon"], FICTIONAL["tz"], + year, + ayanamsa_name="lahiri", + ) + assert "error" not in chart, chart.get("error") + assert isinstance(chart.get("birth_asc_sign_idx"), int) + report = solar_return_full_report( + FICTIONAL["year"], FICTIONAL["month"], FICTIONAL["day"], + FICTIONAL["hour"], FICTIONAL["minute"], + FICTIONAL["lat"], FICTIONAL["lon"], FICTIONAL["tz"], + year, + ayanamsa_name="lahiri", + ) + muntha = report["muntha"] + assert muntha.get("status") != "blocked", muntha + assert isinstance(muntha.get("muntha_sign_idx"), int) + year_lord = report["year_lord"] + assert year_lord.get("status") != "blocked", year_lord + lords.add(year_lord["year_lord"]) + signs.add(muntha["muntha_sign"]) + assert len(signs) >= 2 + assert len(lords) >= 2 + + +def test_blocked_side_does_not_invent_a_conflict() -> None: + gated = _field_with_conflict_gate( + "muntha", + {"muntha": {"status": "blocked", "reason": "natal_ascendant_required_for_muntha"}}, + {"muntha": {"muntha_sign": "Capricorn", "muntha_lord": "Saturn", "note": "extra"}}, + ) + assert gated["status"] == "partial_verified" + assert "conflict" not in _field_brief("Muntha", gated) + same_sign = _field_with_conflict_gate( + "year_lord", + {"year_lord": {"year_lord": "Mars", "year_lord_sign": "Aries", "trace": "left"}}, + {"year_lord": {"year_lord": "Mars", "year_lord_sign": "Aries", "trace": "right"}}, + ) + assert same_sign["status"] == "partial_verified" + disagree = _field_with_conflict_gate( + "year_lord", + {"year_lord": {"year_lord": "Mars", "year_lord_sign": "Aries"}}, + {"year_lord": {"year_lord": "Venus", "year_lord_sign": "Taurus"}}, + ) + assert disagree["status"] == "conflict"