From b8eef758712618e3e03e6f39e5ebc7b9b50e55b4 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Thu, 8 Oct 2026 19:08:44 +0800 Subject: [PATCH] fix(report): project annual sections and thicken dasha reading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 完整数据版第二轮。年运章写出资料包里已有的节,没有数据的不新算。大运解释补回可能段落。瑜伽作用不再断言寿命。术语替换不进标识符。罗睺和计都不写守护第未列出宫。未合入 staging。 --- CHANGELOG.md | 8 + docs/BUG_HISTORY.md | 96 ++++ ...PROGRESS-report-full-data-fix2-20261008.md | 112 +++++ docs/tasks/README.md | 2 +- .../testing/report-full-data-fix2-20261008.md | 21 + scripts/annual_tajika_pack.py | 43 +- scripts/pl9_full_data_export.py | 44 +- scripts/pl9_interpretation_packs.py | 336 ++++++++++++- scripts/pl9_language_terms.py | 82 +++- scripts/pl9_reader_export.py | 446 +++++++++++++----- scripts/solar_return.py | 11 + scripts/tajika_mudda.py | 24 +- tests/test_report_full_data_fix.py | 76 +++ tests/test_year_lord_blocked_no_fallback.py | 12 +- 14 files changed, 1148 insertions(+), 165 deletions(-) create mode 100644 docs/tasks/PROGRESS-report-full-data-fix2-20261008.md create mode 100644 docs/testing/report-full-data-fix2-20261008.md diff --git a/CHANGELOG.md b/CHANGELOG.md index d14253c5..8a315911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ - 行星、上升和宫头以前用含章动的视位置,减去不含章动的平岁差,全体偏一个很小的角度。现在与 Swiss Ephemeris 的恒星黄道使用同一套章动(BUG-1275)。 - 大运日期可能因此前后移动 2–3 天。同一份生时校正输入的分数会变。算法身份从 scoring-11 升到 scoring-12。以前做好的校正记录仍能打开,只读,并可以按新算法再比一次。Skill 版本不变。 +## 2026-10-08 — 完整数据版补上年运细项和大运读法(未上线) + +- 年运章写出资料包里已经有的太阳返照、年主依据、年度锚点、年度行星、Tajika 对照和年度结果。Patyayini、Raman Sahams、Tripataki、月返照、八年总览、一生年运没有新算;Patyayini 仍写「没有实现」。 +- 大运解释补上「这段时间可能突出的主题」「条件好时可能」「受压时可能表现为」「怎么细读」。不写疾病诊断、死亡、寿命和「一定会」。 +- 瑜伽表里长寿类格局的名称还在,作用改成体质和恢复力的参考,不再把寿命当断语(BUG-1277)。 +- 罗睺、计都写成不守护宫位(BUG-1279)。术语替换不再把英文复数 s 留在汉字后面,也不改字段名(BUG-1278)。 +- 英文四张公开或虚构盘的汉字数仍是 0。生成按钮文案没改。不改数据库。Skill 版本不变。未合入,未上线。 + ## 2026-10-08 — 完整数据版补上解释,英文可以生成(未上线) - 个人报告的中文补上大运、行星、宫主、Dosha 和瑜伽的解释,开篇「重点」写当前三级大运和事业、财富、婚恋三宫的宫主。Patyayini 仍没有实现。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index d9f24abd..dbb670be 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -17182,3 +17182,99 @@ - 修复版本:本分支 `codex/ephemeris-nutation-20261008`。未推送,未部署。 - Claude 验收(2026-10-08,Linux,屏蔽 PyJHora):乔布斯、奥巴马、虚构盘 × Lahiri / Raman / KP,七星与 `FLG_SIDEREAL` 最大差 5.7e-14°。v5 九格:先验头名 −1.3 / −1.3 / −1.3,六题回放 +2.6 / +1.3 / +6.5,线上区间 0 / −5.2 / 0(百分点,±10 / ±30 / ±60);产品 2026-10-08 接受。 - 验收补修(Claude,产品授权直接执行):执行方未跑 Python 全量,留下 8 条旧期望(全量 62→70,快速门红)。三栏:`test_user_invocation_acceptance_contract` 子运起止 2014-04-27/2016-09-20、2023-07-13/2026-09-11、2026-09-11/2027-08-24 → 2014-04-25/2016-09-18、2023-07-11/2026-09-09、2026-09-09/2027-08-22(月亮含章动后边界提前约 2 天;2023-07-11 与独立 `FLG_SIDEREAL` 计算一致),连带 `test_one_command_user_invocation_acceptance_check`、`test_skill_release_clean_trial`;`test_solar_return_frame` 本命太阳与求解框架之差 > 10″ → < 1″(旧值正是本 Bug 造成的框架差);`test_shadbala_precise_context` 月亮 Kala 存档差值 +52.18 → +52.17(夹具里的恒星黄道经度按旧平岁差框架采集,配真岁差后 Ayana 26.09 → 26.08);`references/cross_project_contract/fixture_manifest.v1.json` 本仓兼容哈希 `25704246…` → `f1ea2b20…`(上游仓自存另一份哈希,不受影响);`tests/fixtures/double_transit_d1_cl_golden.json` 与 `references/oracle/shadbala_dig_source_of_truth_audit_2026_07_22.json` 用真实引擎重生成(快照只有经度与上升平移约 0.005°,Dig 值不变)。 + +## BUG-1276 | 年主无法确定时,英文测试仍要求保留中文原句 + +- 状态:resolved(本分支 `codex/report-full-data-edition-20261007`,未合入 staging,未上线) +- 首次发现 / 最近更新:2026-10-08 / 2026-10-08 +- 来源:`TASK-report-full-data-fix2-20261008` 的 F1。Claude 验收时 Python 全量比基线多 1 条失败。 +- 影响面:`tests/test_year_lord_blocked_no_fallback.py`。英文报告正文不再包中文。 +- 现象:`test_report_shows_the_blocked_sentence_in_both_editions` 仍要求英文行含「年主暂无法确定」和 `[source text retained:`。 +- 触发条件:英文汉字清零之后再跑这条旧断言。 +- 根因:BUG-1272 清掉英文汉字之后,这条断言要保留的中文已经不该出现。英文清理器还会丢掉含有英文单词 blocked 的行。 +- 修复:英文改为断言 `Year lord cannot be determined yet`,并断言不再含那句中文、也不含 source text retained。注释写了原值 / 新值 / 原因。中文行仍要求「年主暂无法确定(年盘缺太阳或月亮位置)」。没有把汉字塞回英文。 +- 验证:该文件与 `tests/test_report_full_data_fix.py` 本机一并重跑。Linux 全量未跑,不声称失败名单 ⊆ 62。 +- 防复发:同一条测试。 +- 相关记录:BUG-1272。 +- 复发自:无。 +- 修复版本:本分支,未合入 staging。 + +## BUG-1277 | 瑜伽表「作用」列把长寿写成断语 + +- 状态:resolved(本分支 `codex/report-full-data-edition-20261007`,未合入 staging,未上线) +- 首次发现 / 最近更新:2026-10-08 / 2026-10-08 +- 来源:`TASK-report-full-data-fix2-20261008` 的 F2。 +- 影响面:完整数据版中英瑜伽表的作用列。格局名称不动。 +- 现象:中文例「长寿格局 | 寿命/健康 | 强 | … | 长寿;健康良好」。英文名称「Ayur Yoga (Strong Longevity)」,作用列写 Longevity。 +- 触发条件:检出 Ayur 一类格局并生成完整数据版。 +- 根因:作用列沿用规则里的寿命说法。红线要求全文不把寿命当断语。名称可以留。 +- 修复:作用列改为「传统上与体质和恢复力有关,仅作参考」;英文 `Traditionally associated with constitution and recovery. Reference only.`。类别 `ayur` 显示为体质参考。没有改 `references/yoga_rules.json`。 +- 验证:`test_yoga_effect_does_not_assert_lifespan`。乔布斯盘中文去掉格局名之后,不再含「长寿」「寿命长」「短寿」「寿命」。 +- 防复发:同一条测试。名称「长寿格局」和 Ayur Yoga (Strong Longevity) 允许留下。 +- 相关记录:BUG-1272。 +- 复发自:无。 +- 修复版本:本分支,未合入 staging。 + +## BUG-1278 | 术语替换改坏字段名,并在汉字后留下英文复数 s + +- 状态:resolved(本分支 `codex/report-full-data-edition-20261007`,未合入 staging,未上线) +- 首次发现 / 最近更新:2026-10-08 / 2026-10-08 +- 来源:`TASK-report-full-data-fix2-20261008` 的 F3。 +- 影响面:完整数据版中文清理。 +- 现象:「功能吉星s」「功能凶星s」「功能中性s」「角宫s」「宫条件s」。字段名变成 `strict_功能吉星_malefic_v2`、`bphs_ch34_general_with_星座_exceptions_v2`。 +- 触发条件:生成完整数据版中文,清理器按短词替换。 +- 根因:替换作用到了标识符和反引号。英文复数 s 留在已经被换成汉字的词后面。 +- 修复:自然语言才替换。字段名、标识符、反引号和表格机器键不替换。复数 s 留在英文词上。`house condition` 先于单独的 house 替换。 +- 验证:`test_term_replacement_does_not_enter_identifiers`。样例中文不得匹配 `[一-鿿]+s`,标识符不得是下划线贴着汉字。乔布斯盘该复数模式计数为 0。附录里更早留下的 `Arudha_上升` 一类标签没有在本轮改全局 Lagna 替换。 +- 防复发:同一条测试。 +- 相关记录:BUG-1274。 +- 复发自:无。 +- 修复版本:本分支,未合入 staging。 + +## BUG-1279 | 罗睺和计都写成「守护第未列出宫」 + +- 状态:resolved(本分支 `codex/report-full-data-edition-20261007`,未合入 staging,未上线) +- 首次发现 / 最近更新:2026-10-08 / 2026-10-08 +- 来源:`TASK-report-full-data-fix2-20261008` 的 F4。验收时该句在中文约 163 次。 +- 影响面:大运解释里罗睺、计都以及守护宫列表为空的句子。 +- 现象:「守护第未列出宫」。 +- 触发条件:大运解释要写某颗星守护哪些宫,列表为空。罗睺、计都本来不守护宫位。 +- 根因:空列表被填成「未列出」,再接进「守护第…宫」。 +- 修复:罗睺、计都写「不守护宫位」,英文 "rules no house"。其他空列表写「守护宫未单独列出」。没有做全局删除「未列出」。 +- 验证:`test_rahu_ketu_do_not_rule_an_unlisted_house`。乔布斯盘中文该句计数为 0。 +- 防复发:同一条测试。 +- 相关记录:BUG-1272。 +- 复发自:无。 +- 修复版本:本分支,未合入 staging。 + +## BUG-1280 | 年运章没有写出资料包里已经有的节 + +- 状态:resolved(本分支 `codex/report-full-data-edition-20261007`,未合入 staging,未上线) +- 首次发现 / 最近更新:2026-10-08 / 2026-10-08 +- 来源:`TASK-report-full-data-fix2-20261008` 的 F5。对照上游 `23be1807` 的年运标题,不是该仓库当前工作树。 +- 影响面:完整数据版「年度图盘与 Tajika」。 +- 现象:本分支当时约 6 节、约 4 千字。中文没有「真实太阳返照」。资料包里已有的年主依据、年度锚点、年度行星、Tajika 分项和年度结果没有进正文。 +- 触发条件:生成完整数据版。 +- 根因:阅读层没有把 `annual_tajika_pack` 里已有的表投影成正文。另一部分上游节的字段本仓没有,不该新算。 +- 修复:有数据的节写入正文,结果解释用「可能」。Patyayini 用户可见文字是「没有实现」/ "not implemented",不用英文单词 blocked。不新算 Patyayini、Raman Sahams、Tripataki、月返照、八年总览、生命周期年度返照。普通 Sahams 不冒充 Raman。Mudda 仍是主期加裁进本年内的子运,加上短的「可能」读法;没有日级行,也没有第二套余额口径。 +- 验证:乔布斯公开盘本机中文年运章 23753 字、18 个三级标题,含真实太阳返照、Panchadhikari、年度锚点、年度行星资料、Tajika 呈现 / Condition / Forming Pair / 力量、年度北印度图盘、Varshaphala 年度结果解释、年度大运结果解释。Mudda 段 14039 字。英文四张盘汉字数为 0。上游约 29 节、约 11.8 万字是任务书数字,本机没有重跑上游导出。 +- 防复发:进度记录里按上游每个年运三级标题列了已有或缺少的字段名。 +- 相关记录:BUG-1272、BUG-1275(章动不在本分支)。 +- 复发自:无。 +- 修复版本:本分支,未合入 staging。 + +## BUG-1281 | 大运解释只有结构,没有上游那种段落厚度 + +- 状态:resolved(本分支 `codex/report-full-data-edition-20261007`,未合入 staging,未上线) +- 首次发现 / 最近更新:2026-10-08 / 2026-10-08 +- 来源:`TASK-report-full-data-fix2-20261008` 的 F6。产品决定把传统段落补回,改成「可能」。 +- 影响面:完整数据版「大运解释」。决策 4 的七种大运不加入。 +- 现象:四级标题约 734,和上游 743 接近,但该章当时约 8.5 万字,上游任务书约 20.2 万字。 +- 触发条件:生成完整数据版中文。 +- 根因:每段大多只剩一句,没有「可能突出的主题 / 条件好时 / 受压时 / 怎么细读」。 +- 修复:`build_dasha_interpretation_pack(dasha_master_pack, profile=None)` 补上这四段,用「可能」。不写疾病诊断、死亡、寿命、必然、一定会。不移植 PyJHora replay,不新算 sade sati,不加入 sthira、drig、shoola、navamsha、niryaana_shoola、lagna_kendradi、chara。 +- 验证:乔布斯盘该章 295707 字,约为任务书上游 202000 字的 146%,四级标题仍是 734。该章「死亡」计数为 0。红线测试仍过。没有把 80% 降到 70%。 +- 防复发:解释包保留这四段。全文红线测试仍禁止「一定会」「必然」「疾病」。 +- 相关记录:BUG-1272、BUG-1277。 +- 复发自:无。 +- 修复版本:本分支,未合入 staging。 diff --git a/docs/tasks/PROGRESS-report-full-data-fix2-20261008.md b/docs/tasks/PROGRESS-report-full-data-fix2-20261008.md new file mode 100644 index 00000000..29ccd79c --- /dev/null +++ b/docs/tasks/PROGRESS-report-full-data-fix2-20261008.md @@ -0,0 +1,112 @@ +# 进度 · 完整数据版第二轮(2026-10-08) + +- 分支:`codex/report-full-data-edition-20261007` +- 基线:本分支已在 `origin/staging` `1a8c97f6` 之上。本轮之前的 HEAD 是 `9bf4731e`(fix),父提交 `2a43fc95`(feat)。远端功能分支仍是旧的 `32f19f07`。本轮没有再次 rebase,没有推送。 +- 状态:代码在本分支,**未合入 staging**,未上线。等验收。 +- 章动修复(BUG-1275)不在本分支。`scripts/jyotish_engine.py` 与本轮开工时的 HEAD 无差异。BUG-1275 仍是 investigating。 + +## 结论 + +年运章写出资料包里已有的节。大运解释补回四段「可能」,乔布斯盘该章 295707 字,约为任务书上游 202000 字的 146%。瑜伽作用不再把寿命当断语。罗睺、计都不写「守护第未列出宫」。术语替换不进字段名。英文四张盘汉字数仍是 0。没有新算 Patyayini、Raman Sahams、Tripataki、月返照、八年总览、生命周期年度返照。没有把 F6 的 80% 降到 70%。 + +上游约 29 节 / 约 11.8 万字、大运约 20.2 万字、Mudda 约 3 万字,都是任务书里的数字。本机没有重跑上游 `23be1807` 的同盘导出。对照用的是该提交里的年运标题。 + +## 乔布斯盘(本机 Windows,Anaconda;没有屏蔽 PyJHora) + +汉字范围与接口一致。没有写入出生时刻。年龄用目标年减出生年。英文是渲染前的深拷贝,`report_language=en`,走 `render_full_data_markdown`。 + +| 项 | 数字 | +| --- | --- | +| 中文全文 | 445636 | +| 英文全文 | 1011366 | +| 英文汉字 | 0 | +| 「大运解释」 | 295707;三级标题 9;四级标题 734 | +| 相对任务书上游约 202000 | 约 146% | +| 「年度图盘与 Tajika」 | 23753;三级标题 18 | +| Mudda 段(到下一节三级标题) | 14039 | +| 「守护第未列出宫」 | 0 | +| 汉字后接英文复数 s | 0 | +| 大运章「死亡」 | 0 | +| 英文单词 blocked | 无 | +| Patyayini 用户可见 | 「没有实现」/ not implemented | + +英文汉字:乔布斯、奥巴马、爱因斯坦、虚构盘都是 0。 + +大运三级标题按行星名:北交点、木星、土星、水星、南交点、金星、太阳、月亮、火星。日期不与上游对齐。章动单会改日期,不在本分支。 + +中文去掉「长寿格局」和 Ayur Yoga (Strong Longevity) 之后,「长寿」「寿命长」「短寿」「寿命」「一定会」「必然」「疾病」「再婚」「几段关系」「早夭」都没有。全文仍有 3 处「死亡」,都在既有贫困瑜伽的「上升/死亡/损耗宫」标签里,不在「大运解释」。没有改瑜伽规则文件。 + +附录里仍有更早替换留下的标识符汉字,例如 Arudha 上升、Upapada 上升、Bhava 上升。本轮测试锁的是样例渲染里下划线贴着汉字为 0,以及全文不得是「汉字 + s」。没有为了清这些附录标签去改全局 Lagna 替换。 + +## 年运:上游每个三级标题 + +决策 2:没有数据就不新算。下表「缺字段」按字段计 8 个,不超过 10。同一字段的多行不重复计节。决策 4 的七种大运另计,不在这 8 个里。 + +| 上游标题 | 判定 | 字段或决策 | +| --- | --- | --- | +| Varshaphala 基础资料与年度行星表 | 已有 | `annual_chart` | +| Varshesha / Year Lord(四级) | 已有 | `year_lord`。中文清理后标题里的 Lord 仍显示成宫主,这是既有清理,没有把汉字塞回英文 | +| Varshaphala 年度结果解释 | 已有 | `pl9_current_year_results`,状态 partial_verified,口吻「可能」 | +| Annual Dasha 结果解释 | 已有 | `pl9_annual_dasha_results`,状态 partial_verified | +| Mudda / Patyayini 年度大运表 | 已有 Mudda | `mudda_dasha`。14039 字,任务书上游约 3 万字。有主期和裁进本年内的子运,加短的「可能」读法。没有日级行,没有第二套余额口径。不新算 | +| Patyayini ending-date 表 | 缺字段 | `patyayini_dasha`。用户可见「没有实现」。决策 2 | +| Tajika Yoga Presence 对照 | 已有 | `tajika_yogas` | +| Sahams 点位表 | 已有 | `sahams`。这是普通 Sahams | +| 公开 Panchavargiya 数值复核 | 缺字段 | `public_panchavargiya_replay`。决策 2 | +| 年度锚点 | 已有 | `annual_chart` | +| 年度行星资料 | 已有 | `annual_chart` | +| True Solar Return | 已有 | `solar_return`,标题「真实太阳返照」 | +| Panchadhikari | 已有 | `year_lord` | +| Tripataki Placement | 缺字段 | `tripataki_chakra`。决策 2 | +| Tripataki Vedha | 缺字段,同一字段 | `tripataki_chakra` | +| 年度结果(与上面的结果解释重复的一块) | 已有 | 主节已写。没有另填一套 `current_year_result_rows` | +| Tajika Presence | 已有 | `tajika_yogas` | +| Tajika Condition | 已有 | `tajika_yogas` | +| Tajika Forming Pair | 已有 | `tajika_yogas` | +| Tajika Strength | 已有 | `tajika_strength` | +| Raman Sahams | 缺字段 | `raman_saham_rows`。不用普通 Sahams 冒充。覆盖函数里的字段名曾写成 `sahams`,这里按缺口字段记 | +| 年度特殊点 | 缺字段 | `annual_special_points` | +| Tripataki Chakra | 缺字段,同一字段 | `tripataki_chakra` | +| 年度月返照 | 缺字段 | `monthly_chart_pack`。代码里标成延后。决策 2 | +| 生命周期年度返照 | 缺字段 | `lifetime_annual_chart_pages` | +| 出生地八年总览 | 缺字段 | `annual_chart_series_pack`。决策 2 | +| 现居地八年总览 | 缺字段,同一字段 | `annual_chart_series_pack` | +| 年度北印度图盘 | 已有 | `annual_chart` | +| 未来年 Varshaphala / Tajika 细项 | 已有 | 乔布斯盘写出 2027 与 2028。`annual_tajika_series` | +| Mudda 月段窗口 | 已有 | `monthly_windows`。这是本仓多出来的一节 | + +乔布斯盘实际写出的 18 个三级标题:Varshaphala 基础资料与年度行星表、Varshaphala 年度结果解释、年度大运结果解释、Mudda / Patyayini 年度大运表、Mudda 月段窗口、Tajika 瑜伽呈现对照表、Sahams 点位表、年度锚点、年度行星资料、真实太阳返照、Panchadhikari、Tajika 呈现、Tajika Condition、Tajika Forming Pair、Tajika 力量、年度北印度图盘、2027 细项、2028 细项。 + +缺字段共 8 个:`patyayini_dasha`、`public_panchavargiya_replay`、`tripataki_chakra`、`raman_saham_rows`、`annual_special_points`、`monthly_chart_pack`、`lifetime_annual_chart_pages`、`annual_chart_series_pack`。 + +## 大运与其余二级标题 + +决策 4,正文不出现,状态 out_of_scope:sthira、drig、shoola、navamsha、niryaana_shoola、lagna_kendradi、chara。 + +已有:大运解释、行星解释、宫主逐宫、Dosha、瑜伽解释、Mudda、普通 Sahams、Tajika、月段窗口、太阳返照、当年结果、年度大运结果。 + +正文二级标题(附录之前)仍是:重点、出生资料与图盘、行星与宫位解释、强度与 Ashtakavarga、标准大运表格、KP 支持页、年度图盘与 Tajika、Bhavesh 与 Dosha 瑜伽、大运解释、结构化资料附录。大运日期不同,按行星名配对,见上表。除决策 4 的七种外,缺字段按上面 8 个字段计。 + +保留的四句:这段时间可能突出的主题、条件好时可能、受压时可能表现为、怎么细读。 + +## 断言三栏 + +| 测试 | 原值 | 新值 | 原因 | +| --- | --- | --- | --- | +| 年主无法确定的英文行 | 含「年主暂无法确定」,并包成 source text retained | `Year lord cannot be determined yet`,不含那句中文,也不含 source text retained | 英文汉字已清零,旧断言没有对象。不得把汉字塞回英文 | +| 瑜伽作用列 | 「长寿;健康良好」/ Longevity | 体质与恢复力的参考句 | 名称可留,作用不再把寿命当断语 | +| 术语替换 | 「功能吉星s」「角宫s」「宫条件s」,字段名里的 sign 换成星座 | 散文里的 s 吃进英文词;反引号和标识符保持原文 | 替换只作用于自然语言 | +| 空守护列表 | 「守护第未列出宫」 | 罗睺、计都「不守护宫位」/ rules no house;其他「守护宫未单独列出」 | 罗睺、计都不守护宫位 | + +## 测试 + +| 项 | 结果 | +| --- | --- | +| 相关 pytest | 退出码 0。210 通过,1 跳过。约 173 秒。文件:`test_report_full_data_fix.py`、`test_year_lord_blocked_no_fallback.py`、`test_year_lord_basis_label.py`、`test_tajika_panchadhikari_year_lord.py`、`test_tajika_mudda.py`、`test_annual_strength_five_fold.py` | +| 英文版与 jhora 导入扫描 | 退出码 0。22 通过。约 477 秒。`test_report_english_edition.py`、`test_runtime_jhora_import_scan.py` | +| `py_compile`(改过的 7 个脚本) | 退出码 0 | +| ruff,三个历史文件 | 7 条,都在既有提交的行上(`2a43fc95`、`3c2f7bd5`、`5fdf2f74`、`ae18952e`)。没有为了消警告改格式。本机没有打出第 8 条 | +| 运行时 `from jhora` / `import jhora` | 改过的报告脚本里没有 | +| Linux 快速门、Python 全量 ⊆ 62、`npm test`、tsc、lint、`next build`、浏览器走查 | 未跑。本机没有登录态。不能当成通过 | + +没有改 `scripts/jyotish_api_server.py`。没有增加 `Home()` 的 useState / useRef。`DEFAULT_REPORT_VERSION` 仍是 `pl9_personal_long_report.v3`。`ENGINE_TIMEOUT_MS` 仍是 180000。`FULL_DATA_CHAPTER_PAGE_CHARS` 仍是 200000。生成按钮仍是「正在生成报告,大约 10–30 秒」。咨询行为没有改。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 255ce4b0..2576df74 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -203,7 +203,7 @@ | --- | --- | --- | --- | --- | | `TASK-report-full-data-edition-20261007.md` | `PROGRESS-report-full-data-edition-20261007.md` | **报告改为完整数据版、删除阅读版**:网站阅读版中文 7.6 万字符 vs 上游 `pl9_ai_density` 97 万;移植原始数据附录 / 清理器 / 时间补充,中英文;我方报告口径(BUG-1199~1229)全部保持;旧报告仍可打开;排在 sync6 之后 | 未合入(验收未通过,见 fix-20261008) | `codex/report-full-data-edition-20261007`(未推 staging) | | `TASK-report-full-data-fix-20261008.md` | `PROGRESS-report-full-data-fix-20261008.md` | **完整数据版补齐**(同分支继续):解释段、年运字段、开篇重点、英文汉字清零。Patyayini 不新算。生成按钮文案不改。等验收后再合入 staging | 验收未通过(见 fix2) | `codex/report-full-data-edition-20261007`(未合入 staging) | -| `TASK-report-full-data-fix2-20261008.md` | — | **完整数据版第二轮**(同分支续):英文已过;未过——全量 +1 旧断言、寿命断言、半译词与字段名被改坏、「守护第未列出宫」163 处、年运章 6/29 节、大运解释只有上游 42%(产品:补回传统段落改「可能」口吻) | 待领取 | — | +| `TASK-report-full-data-fix2-20261008.md` | `PROGRESS-report-full-data-fix2-20261008.md` | **完整数据版第二轮**(同分支续):年运章投影已有字段,大运解释补「可能」段落,瑜伽作用不再断言寿命,术语不进标识符,罗睺/计都不写「守护第未列出宫」。章动单不在本分支 | 待验收 | `codex/report-full-data-edition-20261007`(未合入) | | `TASK-report-reader-main-fix-20260925.md` | `PROGRESS-report-reader-main-fix-20260925.md` | **读者版修复**:真实年主比较、分字段绑定与未来返照;保留 Bhava Bala 和标题,隔离报告/聊天规则 | 已验收(Claude 09-25:年运表逐年填齐且不同、Bhava Bala 保留、泄漏 0) | `4d801e53`(已部署,health 核对一致) | | `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 恒火星。产品拍板接入读者版、旧报告不回填。 | 部分通过(Claude 09-25:读者版切换、泄漏 0 命中、KP 已修;年运 Year Lord/Muntha 全为「-」P1、投影误删 Bhava Bala P2 → 修复单) | `3c2f7bd5`(已部署) | | `TASK-report-reader-actions-20260924.md` | — | **报告操作收敛**:列表只留「查看报告」;阅读页删「下载原始附录」「打印」,导出改图标按钮;导出对话框 ≥860 居中、窄屏仍贴底(修订 chapter-export D9/D10)。产品变更,不开 BUG 号 | 基本通过(Claude 09-24:入口收敛、图标、≥860 居中均符合;原始附录三文件因执行方权限未删、PROGRESS/清单未写 → 并入修复单) | `f125fae0`(未部署) | diff --git a/docs/testing/report-full-data-fix2-20261008.md b/docs/testing/report-full-data-fix2-20261008.md new file mode 100644 index 00000000..bb903d24 --- /dev/null +++ b/docs/testing/report-full-data-fix2-20261008.md @@ -0,0 +1,21 @@ +# 真机清单 · 完整数据版第二轮(2026-10-08) + +对应 `docs/tasks/TASK-report-full-data-fix2-20261008.md`。电脑和手机各走一遍。第 1 步会用掉一次生成次数。本机没有登录态,也没有用浏览器点过,下表结果留空。 + +这一轮和上一份清单的差别,供对照: + +- 年运里应能看到真实太阳返照、年主依据、年度锚点和 Tajika 分项。Patyayini 仍写没有实现,不是新算出来的表。 +- 大运解释里应有「可能突出的主题」「条件好时可能」「受压时可能表现为」「怎么细读」。不出现疾病诊断、寿命断语或「一定会」。 +- 瑜伽表里长寿类格局的名字可以还在,作用应是体质和恢复力的参考,而不是断定寿命。 +- 罗睺、计都应写不守护宫位,不应出现「守护第未列出宫」。 +- 生成按钮仍写「大约 10–30 秒」。以前生成的报告仍按当时保存的正文打开。 + +| # | 操作 | 应该看到 | 结果 | +| --- | --- | --- | --- | +| 1 | 「我的报告」→ 生成报告,等它完成 → 打开 | 正文是中文。有「中文 / English」。生成卡片仍写大约 10–30 秒 | | +| 2 | 点「English」,再点「中文」 | 正文和目录换成另一种语言,还停在刚才那一章。英文里看不到汉字 | | +| 3 | 打开年运一章 | 能看到真实太阳返照、年主、年度行星和 Tajika。Patyayini 写没有实现 | | +| 4 | 打开大运解释 | 能看到上面四句「可能」。看不到「一定会」,也看不到把死亡或寿命说死的句子 | | +| 5 | 打开瑜伽表里的长寿类格局 | 名字可以还在。作用是体质、恢复力的参考,不是寿命断语 | | +| 6 | 在大运解释里看罗睺或计都 | 写不守护宫位。没有「守护第未列出宫」 | | +| 7 | 打开一份这次更新之前生成的旧报告 | 能打开,正文仍是当时保存的那一版 | | diff --git a/scripts/annual_tajika_pack.py b/scripts/annual_tajika_pack.py index 4c3a2c5f..468657a1 100755 --- a/scripts/annual_tajika_pack.py +++ b/scripts/annual_tajika_pack.py @@ -62,6 +62,7 @@ def build_annual_tajika_pack( "muntha": muntha, "year_lord": year_lord, "tajika_yogas": tajika_yogas, + "tajika_strength": _tajika_strength_field(solar_report if isinstance(solar_report, dict) else {}), "sahams": sahams, "mudda_dasha": mudda_dasha, "patyayini_dasha": patyayini_dasha, @@ -194,11 +195,51 @@ def _field_from_report(report: dict[str, Any], key: str, label: str) -> dict[str def _annual_chart_from_solar_report(report: dict[str, Any]) -> dict[str, Any]: field = _field_from_report(report, "sr_chart_info", "Solar Return Annual Chart") - if field["status"] == "partial_verified": + if field["status"] == "partial_verified" and isinstance(field.get("data"), dict): field["data_contract"] = "normalized_from_sr_chart_info" + longitudes = report.get("planet_lons") if isinstance(report, dict) else None + if isinstance(longitudes, dict) and longitudes: + data = dict(field["data"]) + data["planet_longitudes"] = { + name: float(value) + for name, value in longitudes.items() + if isinstance(value, (int, float)) and not isinstance(value, bool) + } + field["data"] = data return field +def _tajika_strength_field(report: dict[str, Any]) -> dict[str, Any]: + """Copy numeric strength scores. Internal status words stay off this surface.""" + + raw = report.get("tajika_strength") if isinstance(report, dict) else None + if not isinstance(raw, dict): + return {"status": "not_applicable", "producer": "Tajika strength", "reason": "tajika_strength_missing"} + + def scores(table: Any) -> dict[str, dict[str, float]]: + if not isinstance(table, dict): + return {} + kept = {} + for planet, item in table.items(): + if not isinstance(item, dict): + continue + score = item.get("score") + if isinstance(score, bool) or not isinstance(score, (int, float)): + continue + kept[str(planet)] = {"score": float(score)} + return kept + + harsha = scores(raw.get("harsha_bala")) + five_fold = scores(raw.get("panchavargiya_bala")) + if not harsha and not five_fold: + return {"status": "not_applicable", "producer": "Tajika strength", "reason": "tajika_strength_missing"} + return { + "status": "partial_verified", + "producer": "Tajika strength", + "data": {"harsha_bala": harsha, "panchavargiya_bala": five_fold}, + } + + def _tajika_yogas_field(solar_report: dict[str, Any]) -> dict[str, Any]: if not isinstance(solar_report, dict) or solar_report.get("error"): return _field_from_report(solar_report, "tajika_yogas", "Tajika Yogas") diff --git a/scripts/pl9_full_data_export.py b/scripts/pl9_full_data_export.py index 0cc62913..cc0a988b 100644 --- a/scripts/pl9_full_data_export.py +++ b/scripts/pl9_full_data_export.py @@ -95,9 +95,17 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星 """Keep the parity volume's facts while removing internal-only labels.""" try: - from pl9_language_terms import normalize_zh_markdown_terms + from pl9_language_terms import ( + normalize_zh_markdown_terms, + replace_natural_language_term, + strip_han_plural_s, + ) except ModuleNotFoundError: # pragma: no cover - package import compatibility - from scripts.pl9_language_terms import normalize_zh_markdown_terms + from scripts.pl9_language_terms import ( + normalize_zh_markdown_terms, + replace_natural_language_term, + strip_han_plural_s, + ) text = _sanitize_pl9_public_markdown(markdown) replacements = { @@ -141,9 +149,6 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星 "functional_role_not_returned": "功能属性未单独返回", "natural_benefic": "自然吉星", "natural_malefic": "自然凶星", - "functional_benefic": "功能吉星", - "functional_malefic": "功能凶星", - "functional_neutral": "功能中性", "current_dasha": "当前 Dasha", "birth_balance": "出生余额", "Cycle / phase": "周期 / 阶段", @@ -196,6 +201,12 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星 } for raw, public in replacements.items(): text = text.replace(raw, public) + for raw, public in ( + ("functional_benefic", "功能吉星"), + ("functional_malefic", "功能凶星"), + ("functional_neutral", "功能中性"), + ): + text = replace_natural_language_term(text, raw, public) text = re.sub( r"\n## Bhavesh House-Lord Interpretations\n\n.*?(?=\n## |\Z)", "\n", @@ -218,7 +229,7 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星 text = text.replace("Planetary", "行星") text = text.replace("Planets", "行星") text = text.replace("Planet", "行星") - text = text.replace("Sign", "星座") + text = replace_natural_language_term(text, "Sign", "星座") text = text.replace("Degree in sign", "落座度数") text = text.replace("Degree", "度数") text = text.replace("Longitude", "黄经") @@ -451,12 +462,12 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星 "weak": "弱", "medium": "中等", "strong": "强", - "house condition": "宫位条件", "house ": "第", - " condition": "宫条件", } + text = replace_natural_language_term(text, "house condition", "宫位条件") for raw, public in zh_heading_repairs.items(): text = text.replace(raw, public) + text = replace_natural_language_term(text, " condition", "宫条件") zh_public_key_repairs = { "mangal_dosha": "火星婚姻煞", "kaal_sarp_dosha": "时蛇煞", @@ -506,7 +517,7 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星 ] ) text = re.sub(r"^#\s+.*$", summary, text, count=1, flags=re.MULTILINE) - return text + "\n" + return strip_han_plural_s(text) + "\n" @@ -1001,7 +1012,8 @@ def _sanitize_pl9_ai_density_markdown_en(markdown: str, title: str = "Vedic Astr "王权/事业": "authority/career", "天象格局": "configuration", "子女/创造": "children/creativity", - "寿命/健康": "longevity/health", + "体质参考": "constitution reference", + "寿命/健康": "constitution reference", "愿望/收益": "desire/gains", "解脱/内在": "moksha/inner life", "贵人/助力": "patronage/support", @@ -1494,10 +1506,18 @@ def _render_pl9_ai_density_raw_data_markdown_en(packet: dict) -> str: stripped = value.strip() translated = EFFECTS_EN.get(stripped) if translated: - return translated + try: + from pl9_language_terms import soften_lifespan_wording + except ModuleNotFoundError: # pragma: no cover + from scripts.pl9_language_terms import soften_lifespan_wording + return soften_lifespan_wording(translated, "en") # 已是英文短语(无 CJK)时原样透传,避免被误当占位删掉。 if re.search(r"[A-Za-z]", stripped) and not re.search(r"[\u4e00-\u9fff]", stripped): - return stripped + try: + from pl9_language_terms import soften_lifespan_wording + except ModuleNotFoundError: # pragma: no cover + from scripts.pl9_language_terms import soften_lifespan_wording + return soften_lifespan_wording(stripped, "en") return "" def table(headers: list[str], rows: list[list[Any]]) -> str: diff --git a/scripts/pl9_interpretation_packs.py b/scripts/pl9_interpretation_packs.py index caa7a390..60abfb30 100644 --- a/scripts/pl9_interpretation_packs.py +++ b/scripts/pl9_interpretation_packs.py @@ -79,6 +79,49 @@ _THEMES: dict[str, dict[str, tuple[str, str]]] = { }, } +# Traditional period topics, rewritten as possibilities. No diagnosis, death, +# lifespan, or certain reputation loss. +_PHALA: dict[str, dict[str, tuple[str, str, str]]] = { + "Sun": { + "zh": ("责任、可见职务、父亲或权威人物、土地,以及需要当场决定的公共事务", "被认可、得到行政上的协助,或把一件公共事务做完", "与权威的摩擦、离开熟悉环境办事、心里发热或坐不住,名声也可能受到议论"), + "en": ("responsibility, visible office, a father or authority figure, land, and public decisions", "recognition, administrative help, or finishing a public task", "friction with authority, leaving a familiar place for duty, heat or restlessness, and talk about reputation"), + }, + "Moon": { + "zh": ("情绪节奏、母亲或照料、居住、出行、公众气氛和学习", "家里更安稳、交易或学习有进展,或换一个地方之后事情变顺", "判断摇摆、家人意见不一致、出行变密,旧的安全感变弱"), + "en": ("emotional pace, care, residence, travel, public mood, and study", "a steadier home, progress in trade or study, or a change of place that helps", "wavering judgment, family disagreement, denser travel, and a weaker sense of security"), + }, + "Mars": { + "zh": ("行动、勇气、土地、兄弟姐妹、竞争和技术上的决断", "体力更够用、不动产或操作类工作有进展,竞争里更容易把事做完", "话说得硬、争执、火或器械方面的压力,冲动之下把资源花在搬迁或对抗上"), + "en": ("action, courage, land, siblings, competition, and technical decisions", "more stamina, progress in property or hands-on work, and finishing a contest", "harsh speech, quarrels, pressure around fire or tools, and spending resources on a move or a fight"), + }, + "Mercury": { + "zh": ("学习、书写、交易、计算、合同和来回沟通", "文书更清楚、结盟更有用,或靠技能把问题拆开", "判断变散、文书带来额外开支,生意节奏忽快忽慢"), + "en": ("study, writing, trade, calculation, contracts, and back-and-forth talk", "clearer paperwork, more useful alliances, or taking a problem apart by skill", "scattered judgment, extra cost from paperwork, and an uneven business pace"), + }, + "Jupiter": { + "zh": ("老师、判断、信仰、顾问、学习和体面的扩张", "得到尊重、顾问角色更清楚,或一件长期目标有着落", "建议不一致、目标推迟、家族义务变重"), + "en": ("teachers, judgment, belief, counsel, study, and a respectable expansion", "more respect, a clearer advisory role, or a long aim that finds a place", "conflicting advice, delayed aims, and heavier family obligations"), + }, + "Venus": { + "zh": ("合作、审美、合意、资源交换、艺术和社交里的舒适", "协商更顺、作品或设计被接受,关系里的舒适感增加", "开销变大、享乐挤占正事,合作约定需要重新谈"), + "en": ("cooperation, taste, agreement, exchange, art, and social comfort", "smoother negotiation, accepted design, and more comfort in a relationship", "larger spending, pleasure crowding out work, and agreements that need to be discussed again"), + }, + "Saturn": { + "zh": ("劳动、延迟、资历、界限、土地和长期功课", "工作更稳、责任被看见,后半段比开头更容易出成果", "拖延、负担变重、和熟人的距离拉开,事情要慢慢熬"), + "en": ("labor, delay, seniority, boundaries, land, and long-term work", "steadier work, visible duty, and more results in the later part than at the start", "delay, a heavier load, more distance from familiar people, and work that has to be endured"), + }, + "Rahu": { + "zh": ("放大、外来机会、不按旧习惯走的路径,以及突然的转向", "非传统渠道带来开口,远方或圈层之外的事情更活跃", "想要得很急、信息前后不一致,名声可能受到议论"), + "en": ("amplification, outside opportunities, unfamiliar routes, and sudden turns", "an opening through a nontraditional channel, with distant or outside circles more active", "urgent wanting, inconsistent information, and talk about reputation"), + }, + "Ketu": { + "zh": ("收束、分离、向内整理、专精和减少外界噪声", "看清一件旧目标可以放下,或在压力之后出现间歇的进展", "突然的停顿、来回奔波,或把注意力从外面收回来"), + "en": ("release, separation, inward sorting, specialization, and less outer noise", "seeing that an old aim can be set down, or intermittent progress after pressure", "a sudden pause, restless travel, or attention pulled back from the outside"), + }, +} + +_NODE_LORDS = {"Rahu", "Ketu"} + _HOUSE_TOPIC_ZH = { 1: "自我呈现与人生主轴", 2: "资源、表达与家庭积累", @@ -295,6 +338,49 @@ def _lord_facts(packet: dict) -> dict[str, dict[str, Any]]: return facts +def _period_beats(lord: str, language: str) -> str: + """Four traditional beats. Wording stays possible, not certain.""" + + bank = _PHALA.get(lord) or _PHALA["Saturn"] + topic, good, pressure = bank["en" if language == "en" else "zh"] + name = _planet(lord, language) + if language == "zh": + return ( + f"这段时间可能突出的主题:传统阅读里,{name}容易把{topic}推到眼前。" + "这只是可能被注意到的主题,不是已经发生的事。" + f"条件好时可能:若落宫、尊贵和当前大运同向,可能出现{good}。" + "仍要与宫主、分盘和星宿合看,不能单凭这一层下结论。" + f"受压时可能表现为:若该星受压或与当前节奏不一致,可能感到{pressure}。" + "这是压力下的可能表现,不是确定会发生的事。" + f"怎么细读:先核对{name}的落宫、守护宫、尊贵、星宿和功能吉凶," + "再看它是否出现在主运、子运或次子运。只把它当成阶段线索。" + ) + return ( + f"Topics this period may bring forward: in the traditional reading, {name} may bring {topic} forward. " + "These are topics that may be noticed, not events that have already happened. " + f"When conditions are supportive, it may show {good}. " + "House lords, divisional charts, and the nakshatra still have to be read with it. " + f"Under pressure, it may show as {pressure}. " + "That is a possible expression under pressure, not a certain event. " + f"How to read it more closely: check the house, ownership, dignity, nakshatra, and functional nature of {name}, " + "then whether it is the Maha, Antar, or Pratyantar lord. Treat it as a period clue." + ) + + +def _ruled_houses_clause(lord: str, owned: list, language: str) -> str: + """Rahu and Ketu rule no houses. Other empty lists stay an explicit gap.""" + + if lord in _NODE_LORDS: + return "不守护宫位" if language == "zh" else "rules no house" + if language == "zh": + if owned: + return "守护第" + "、".join(str(item) for item in owned) + "宫" + return "守护宫未单独列出" + if owned: + return "owns houses " + ", ".join(str(item) for item in owned) + return "owned houses are not listed" + + def _themes(lord: str, language: str) -> list[str]: bank = _THEMES.get(lord) or {} pair = bank.get(language) or bank.get("en") or () @@ -424,7 +510,11 @@ def _dasha_pages(master: dict, items: list[dict], language: str) -> list[dict[st ) current_ad = next((row for row in antardashas if isinstance(row, dict) and row.get("is_current")), None) if language == "zh": - general = f"{_planet(lord, language)} 主运期间,这段时间可能突出该星的自然主题。下面只按已算好的落宫、守护宫、尊贵、星宿和功能吉凶来读,不是确定会发生的事。" + general = ( + f"{_planet(lord, language)} 主运期间,这段时间可能突出该星的自然主题。" + "下面只按已算好的落宫、守护宫、尊贵、星宿和功能吉凶来读,不是确定会发生的事。" + + _period_beats(lord, language) + ) natal = _natal_zh(lord, facts, trigger) current_text = ( f"当前子运(AD)是 {_planet(_dict(current_ad).get('lord'), language)},{current_ad.get('start') or '?'} 至 {current_ad.get('end') or '?'}。它只细化当前节奏。" @@ -432,7 +522,12 @@ def _dasha_pages(master: dict, items: list[dict], language: str) -> list[dict[st else "这一主运不是当前阶段,子运保留在时间表里。" ) else: - general = f"During the Maha Dasha of {lord}, this period may bring the planet's natural topics forward. The reading uses only the calculated house, ownership, dignity, nakshatra, and functional nature. It is not a certain event." + general = ( + f"During the Maha Dasha of {lord}, this period may bring the planet's natural topics forward. " + "The reading uses only the calculated house, ownership, dignity, nakshatra, and functional nature. " + "It is not a certain event. " + + _period_beats(lord, language) + ) natal = _natal_en(lord, facts, trigger) current_text = ( f"The current Antar Dasha (AD) is {current_ad.get('lord')}, {current_ad.get('start') or '?'} to {current_ad.get('end') or '?'} . It only refines the current pace." @@ -494,8 +589,7 @@ def _natal_zh(lord: str, facts: dict, trigger: dict) -> str: house = facts.get("house") owned = facts.get("ownership") if isinstance(facts.get("ownership"), list) else [] bits = [f"{_planet(lord, 'zh')}在本命盘位于第{house}宫" if isinstance(house, int) else f"{_planet(lord, 'zh')}的落宫未列出"] - if owned: - bits.append("守护第" + "、".join(str(item) for item in owned) + "宫") + bits.append(_ruled_houses_clause(lord, owned, "zh")) bits.append(f"尊贵为{_dignity(facts.get('dignity'), 'zh')}") bits.append(f"星宿为{facts.get('nakshatra') or '未列出'}") bits.append(f"功能吉凶为{_role(trigger.get('functional_classification'), 'zh')}") @@ -505,12 +599,12 @@ def _natal_zh(lord: str, facts: dict, trigger: dict) -> str: def _natal_en(lord: str, facts: dict, trigger: dict) -> str: house = facts.get("house") if isinstance(facts.get("house"), int) else "not listed" owned = facts.get("ownership") if isinstance(facts.get("ownership"), list) else [] - owned_text = ", ".join(str(item) for item in owned) if owned else "not listed" + ownership = _ruled_houses_clause(lord, owned, "en") nakshatra = facts.get("nakshatra") or "not listed" if re.search(r"[\u4e00-\u9fff]", str(nakshatra)): nakshatra = "not listed separately" return ( - f"Natal basis: {lord} is in house {house}, owns houses {owned_text}, " + f"Natal basis: {lord} is in house {house}, {ownership}, " f"dignity {_dignity(facts.get('dignity'), 'en')}, nakshatra {nakshatra}, " f"functional nature {_role(trigger.get('functional_classification'), 'en')}. " "Functional nature comes from this site's functional benefic/malefic layer." @@ -533,25 +627,26 @@ def _sub_paragraph(parent: str, lord: str, row: dict, facts: dict, language: str owned = facts.get("ownership") if isinstance(facts.get("ownership"), list) else [] start = row.get("start") or "?" end = row.get("end") or "?" + ruled = _ruled_houses_clause(lord, owned, language) if language == "zh": - owned_text = "、".join(str(item) for item in owned) if owned else "未列出" house_text = str(house) if house is not None else "未列出" return ( f"{_planet(parent, language)} / {_planet(lord, language)}({level}):{start} 至 {end}。" - f"这段时间可能突出{_planet(lord, language)}的第{house_text}宫、守护第{owned_text}宫、" + f"这段时间可能突出{_planet(lord, language)}的第{house_text}宫、{ruled}、" f"尊贵{_dignity(facts.get('dignity'), language)}与星宿{facts.get('nakshatra') or '未列出'}。" "这是阶段线索,不是确定会发生的事。" + + _period_beats(lord, language) ) - owned_text = ", ".join(str(item) for item in owned) if owned else "not listed" house_text = str(house) if house is not None else "not listed" nakshatra = facts.get("nakshatra") or "not listed" if re.search(r"[\u4e00-\u9fff]", str(nakshatra)): nakshatra = "not listed separately" return ( f"{parent} / {lord} ({level}): {start} to {end}. " - f"This period may bring forward {lord}'s house {house_text}, owned houses {owned_text}, " + f"This period may bring forward {lord}'s house {house_text}, {ruled}, " f"dignity {_dignity(facts.get('dignity'), language)}, and nakshatra {nakshatra}. " - "This is a period clue, not a certain event." + "This is a period clue, not a certain event. " + + _period_beats(lord, language) ) @@ -959,28 +1054,48 @@ def _field_usable(field: Any) -> bool: return field.get("status") in {"partial_verified", "verified", "conflict", "parameter_sensitive"} -def project_annual_tables(annual: dict) -> None: - """Copy existing annual fields onto the table names the reader already prints.""" +def project_annual_tables(annual: dict, language: str = "zh") -> None: + """Copy existing annual fields onto the table names the reader already prints. + + Nothing here calculates a new chart. Empty producers stay empty. + """ if not isinstance(annual, dict): return + language = language if language in {"zh", "en"} else "zh" normalized = annual.get("normalized_tables") if not isinstance(normalized, dict): normalized = {} annual["normalized_tables"] = normalized mudda = _dict(annual.get("mudda_dasha")) + mudda_data = _annual_data(mudda) if not normalized.get("mudda_rows"): - periods = _list(mudda.get("periods") or _dict(mudda.get("data")).get("periods") or _dict(mudda.get("data")).get("dasha_sequence")) + periods = _list(mudda.get("periods") or mudda_data.get("periods") or mudda_data.get("dasha_sequence")) rows = [] for period in periods: if not isinstance(period, dict): continue + lord = period.get("lord") or period.get("main_lord") rows.append({ - "balance_profile": period.get("balance_profile") or "native", - "lord": period.get("lord"), + "balance_profile": period.get("balance_profile") or mudda_data.get("balance_profile") or "native", + "lord": lord, "start": period.get("start") or period.get("start_date"), "end": period.get("end") or period.get("end_date"), - "boundary_status": mudda.get("status") or "partial_verified", + "boundary_status": "listed", + "reading": _mudda_reading(str(lord or ""), language), + }) + for period in _list(mudda.get("antardasha_periods") or mudda_data.get("antardasha_periods")): + if not isinstance(period, dict): + continue + main_lord = period.get("main_lord") + sub_lord = period.get("sub_lord") + rows.append({ + "balance_profile": "antardasha", + "lord": f"{main_lord}/{sub_lord}", + "start": period.get("start"), + "end": period.get("end"), + "boundary_status": "listed", + "reading": _mudda_reading(str(sub_lord or main_lord or ""), language), }) if rows: normalized["mudda_rows"] = rows @@ -988,6 +1103,141 @@ def project_annual_tables(annual: dict) -> None: normalized["saham_rows"] = _saham_rows(annual.get("sahams")) if not normalized.get("tajika_yoga_rows"): normalized["tajika_yoga_rows"] = _tajika_rows(annual.get("tajika_yogas")) + _project_annual_surfaces(annual, normalized, language) + + +def _project_annual_surfaces(annual: dict, normalized: dict, language: str) -> None: + """Fill reader tables from fields the pack already holds.""" + + solar = _annual_data(annual.get("solar_return")) + chart = _annual_data(annual.get("annual_chart")) + year_lord = _annual_data(annual.get("year_lord")) + muntha = _annual_data(annual.get("muntha")) + if not normalized.get("true_solar_return_rows"): + rows = [] + for key in ("dt_local", "dt_ut", "sun_lon"): + value = solar.get(key) + if value not in (None, ""): + rows.append({"field": key, "value": value, "dt_local": solar.get("dt_local")}) + if rows: + normalized["true_solar_return_rows"] = rows + if not normalized.get("panchadhikari_rows"): + rows = [] + for item in _list(year_lord.get("candidates")): + if not isinstance(item, dict): + continue + rows.append({ + "planet": item.get("planet"), + "role": item.get("source") or item.get("role"), + "value": item.get("panchavargiya_score"), + "sign": item.get("planet_sign"), + "house": item.get("planet_house"), + }) + if not rows and year_lord.get("year_lord"): + rows.append({ + "planet": year_lord.get("year_lord"), + "role": year_lord.get("selection_basis"), + "value": year_lord.get("selection_basis_text_en") if language == "en" else year_lord.get("selection_basis_text"), + "sign": year_lord.get("year_lord_sign"), + "house": year_lord.get("year_lord_house"), + }) + if rows: + normalized["panchadhikari_rows"] = rows + if not normalized.get("annual_anchor_rows"): + anchors = [] + if solar.get("dt_local"): + anchors.append({"name": "Varshapravesha", "label": "solar return", "value": solar.get("dt_local")}) + if chart.get("asc_sign"): + anchors.append({"name": "annual Lagna", "label": "ascendant", "sign": chart.get("asc_sign"), "value": chart.get("asc_sign")}) + if muntha.get("muntha_sign"): + anchors.append({"name": "Muntha", "label": "muntha", "sign": muntha.get("muntha_sign"), "house": muntha.get("muntha_house"), "value": muntha.get("muntha_lord")}) + if year_lord.get("year_lord"): + anchors.append({"name": "year lord", "label": "Panchadhikari", "value": year_lord.get("year_lord")}) + if anchors: + normalized["annual_anchor_rows"] = anchors + longitudes = chart.get("planet_longitudes") if isinstance(chart.get("planet_longitudes"), dict) else {} + if longitudes and not normalized.get("annual_planet_rows"): + rows = [] + for planet in PLANETS: + longitude = longitudes.get(planet) + if not isinstance(longitude, (int, float)) or isinstance(longitude, bool): + continue + rows.append({"planet": planet, "longitude": longitude, "degree": round(float(longitude) % 30, 2)}) + if rows: + normalized["annual_planet_rows"] = rows + yoga_rows = _list(normalized.get("tajika_yoga_rows")) + if yoga_rows and not normalized.get("tajika_presence_rows"): + normalized["tajika_presence_rows"] = [ + {"yoga": row.get("yoga"), "name": row.get("name"), "present": row.get("present"), "forming_planets": row.get("forming_planets")} + for row in yoga_rows + ] + normalized["tajika_condition_status_rows"] = [ + {"yoga": row.get("yoga"), "name": row.get("name"), "condition": row.get("condition"), "present": row.get("present")} + for row in yoga_rows + ] + normalized["tajika_forming_pair_rows"] = [ + { + "yoga": row.get("yoga"), + "name": row.get("name"), + "planet_a": row.get("planet_a"), + "planet_b": row.get("planet_b"), + "forming_planets": row.get("forming_planets"), + } + for row in yoga_rows + ] + strength = [ + {"planet": row.get("planet_a"), "yoga": row.get("yoga"), "strength": row.get("average_deeptamsa") or row.get("residual"), "value": row.get("average_deeptamsa") or row.get("residual")} + for row in yoga_rows + if row.get("average_deeptamsa") not in (None, "") or row.get("residual") not in (None, "") + ] + if strength: + normalized["tajika_strength_rows"] = strength + if not normalized.get("tajika_strength_rows"): + scored = _strength_rows(annual.get("tajika_strength")) + if scored: + normalized["tajika_strength_rows"] = scored + if not annual.get("pl9_current_year_results"): + sections = _current_year_sections(solar, chart, year_lord, muntha, language) + if sections: + annual["pl9_current_year_results"] = {"status": "partial_verified", "sections": sections} + if not annual.get("pl9_annual_dasha_results"): + dasha_rows = [] + for row in _list(normalized.get("mudda_rows")): + if row.get("balance_profile") == "antardasha": + continue + dasha_rows.append({ + "order": len(dasha_rows) + 1, + "lord": row.get("lord"), + "start": row.get("start"), + "end": row.get("end"), + "themes": [row.get("reading")], + }) + if dasha_rows: + annual["pl9_annual_dasha_results"] = {"status": "partial_verified", "rows": dasha_rows} + + +def _current_year_sections(solar: dict, chart: dict, year_lord: dict, muntha: dict, language: str) -> list[dict[str, Any]]: + sections = [] + if solar.get("dt_local") or chart.get("asc_sign"): + if language == "zh": + theme = "真实太阳返照已经算出。这一年可能从年返上升和返照时刻看出环境变化,不是确定会发生的事。" + else: + theme = "The solar return is already calculated. The year may show a change of setting from the annual ascendant and the return time. This is not a certain event." + sections.append({"title": "solar return", "anchor": {"dt_local": solar.get("dt_local"), "asc_sign": chart.get("asc_sign")}, "themes": [theme]}) + lord = year_lord.get("year_lord") + if lord: + if language == "zh": + theme = f"年主是{_planet(lord, 'zh')}。条件好时可能突出该星的年度主题;受压时可能表现为节奏需要重看。年主依据来自 Panchadhikari,不是另选一颗星。" + else: + theme = f"The year lord is {lord}. Supportive conditions may bring that planet's annual topics forward. Under pressure the pace may need another look. The basis is Panchadhikari." + sections.append({"title": "year lord", "anchor": {"year_lord": lord}, "themes": [theme]}) + if muntha.get("muntha_sign") or muntha.get("muntha_lord"): + if language == "zh": + theme = "Muntha 标出这一年更容易被注意到的宫位。它可能加重该宫主题,不是单独的事件结论。" + else: + theme = "Muntha marks the house that may be easier to notice this year. It may emphasize that house. It is not a separate event conclusion." + sections.append({"title": "Muntha", "anchor": {"sign": muntha.get("muntha_sign"), "lord": muntha.get("muntha_lord")}, "themes": [theme]}) + return sections def _saham_rows(field: Any) -> list[dict[str, Any]]: @@ -1028,15 +1278,57 @@ def _tajika_rows(field: Any) -> list[dict[str, Any]]: for item in candidates: if not isinstance(item, dict): continue + planets = item.get("forming_planets") or item.get("planets") + pair = list(planets) if isinstance(planets, list) else [] rows.append({ - "yoga": item.get("yoga") or item.get("name") or item.get("id"), - "name": item.get("name") or item.get("yoga"), - "present": item.get("present") if item.get("present") is not None else item.get("status") or item.get("formed"), - "forming_planets": item.get("forming_planets") or item.get("planets"), + "yoga": item.get("yoga") or item.get("named_yoga") or item.get("name") or item.get("candidate_name") or item.get("id"), + "name": item.get("name") or item.get("named_yoga") or item.get("candidate_name") or item.get("yoga"), + "present": item.get("present") if item.get("present") is not None else item.get("motion") or item.get("formed"), + "forming_planets": planets, + "motion": item.get("motion"), + "aspect": item.get("aspect"), + "average_deeptamsa": item.get("average_deeptamsa"), + "residual": item.get("residual"), + "planet_a": pair[0] if pair else None, + "planet_b": pair[1] if len(pair) > 1 else None, + "condition": item.get("condition") or item.get("motion") or item.get("aspect"), }) return [row for row in rows if row.get("yoga") or row.get("name")] +def _annual_data(field: Any) -> dict: + if not isinstance(field, dict): + return {} + data = field.get("data") + return data if isinstance(data, dict) else field + + +def _mudda_reading(lord: str, language: str) -> str: + name = _planet(lord, "zh" if language == "zh" else "en") + if language == "zh": + return f"{name}这一段可能突出该星的年度主题。条件好时可能事情更顺,受压时可能表现为节奏变慢或需要重谈。这不是确定会发生的事。" + return f"The {lord} span may bring that planet's annual topics forward. Supportive conditions may make the work easier. Under pressure the pace may slow or an agreement may need another look. This is not a certain event." + + +def _strength_rows(field: Any) -> list[dict[str, Any]]: + """Numeric Harsha and five-fold scores only. Do not print the internal status word.""" + + data = _annual_data(field) + rows = [] + for layer, key in (("Harsha", "harsha_bala"), ("five-fold", "panchavargiya_bala")): + table = data.get(key) + if not isinstance(table, dict): + continue + for planet, item in table.items(): + if not isinstance(item, dict): + continue + score = item.get("score") + if not isinstance(score, (int, float)) or isinstance(score, bool): + continue + rows.append({"planet": planet, "yoga": layer, "strength": score, "value": score}) + return rows + + def annual_coverage(annual: dict | None) -> list[dict[str, str]]: """Each upstream annual chapter is present or named with the missing field.""" @@ -1224,5 +1516,5 @@ def attach_full_data_interpretation(packet: dict) -> dict: ) annual = timing.get("annual_tajika_pack") if isinstance(annual, dict): - project_annual_tables(annual) + project_annual_tables(annual, language) return packet diff --git a/scripts/pl9_language_terms.py b/scripts/pl9_language_terms.py index 7ab94556..b493f5b1 100644 --- a/scripts/pl9_language_terms.py +++ b/scripts/pl9_language_terms.py @@ -11,6 +11,48 @@ import re from typing import Any +def replace_natural_language_term(text: str, raw: str, public: str) -> str: + """Replace one glossary term in prose only. + + Identifiers and backtick spans stay as written. A trailing English plural + ``s`` is consumed with the English word, so it is not left after Chinese. + """ + + if not text or not raw: + return text + plural = "s?" if re.search(r"[A-Za-z]$", raw) and not raw.endswith("s") else "" + pattern = re.compile(rf"(? str: + """Remove a Latin plural s left on Chinese after a glossary replace.""" + + return re.sub(r"([一-鿿])s\b", r"\1", text) + + +def soften_lifespan_wording(text: str, language: str = "zh") -> str: + """Drop lifespan assertions from an effect cell. Yoga names are not passed here.""" + + if not text or not _LIFESPAN_ASSERTION.search(str(text)): + return text + if language == "en": + return _LIFESPAN_EFFECT_EN + return _LIFESPAN_EFFECT_ZH + + ZH_TERM_REPLACEMENTS: tuple[tuple[str, str], ...] = ( ("Natural friends", "天然友星"), ("Natural enemies", "天然敌星"), @@ -221,10 +263,22 @@ _ZH_LITERAL_REPLACEMENTS = tuple( if not re.fullmatch(r"[A-Za-z0-9_ /:-]+", raw) ) _ZH_REGEX_REPLACEMENT_MAP = {raw: public for raw, public in _ZH_REGEX_REPLACEMENTS} + + +def _zh_regex_body(raw: str) -> str: + body = re.escape(raw) + if re.search(r"[A-Za-z]$", raw) and not raw.endswith("s"): + body += "s?" + return body + + _ZH_REGEX_REPLACEMENT_PATTERN = re.compile( - r"(? str: + if token in _ZH_REGEX_REPLACEMENT_MAP: + return _ZH_REGEX_REPLACEMENT_MAP[token] + if token.endswith("s") and token[:-1] in _ZH_REGEX_REPLACEMENT_MAP: + return _ZH_REGEX_REPLACEMENT_MAP[token[:-1]] + return token + + def _replace_zh_regex_terms(markdown: str) -> str: - return _ZH_REGEX_REPLACEMENT_PATTERN.sub( - lambda match: _ZH_REGEX_REPLACEMENT_MAP[match.group(1)], - markdown, - ) + def apply(chunk: str) -> str: + return _ZH_REGEX_REPLACEMENT_PATTERN.sub( + lambda match: _zh_regex_public(match.group(1)), + chunk, + ) + + parts = re.split(r"(`[^`]*`)", markdown) + return "".join(part if index % 2 else apply(part) for index, part in enumerate(parts)) def normalize_zh_markdown_terms(markdown: str) -> str: @@ -321,7 +387,7 @@ def normalize_zh_markdown_terms(markdown: str) -> str: text = _normalize_zh_dasha_prose(text) text = _thicken_zh_dasha_density(text) text = _normalize_mula_gloss(_restore_zh_nakshatra_glosses(_collapse_duplicate_zh_parentheses(text))) - return text + return strip_han_plural_s(text) def _normalize_zh_bhava_factor_text(markdown: str) -> str: diff --git a/scripts/pl9_reader_export.py b/scripts/pl9_reader_export.py index 7891a4be..22b8e84a 100644 --- a/scripts/pl9_reader_export.py +++ b/scripts/pl9_reader_export.py @@ -226,7 +226,7 @@ def _strip_pl9_user_engineering_markers(markdown: str) -> str: "neecha_bhanga": "落陷取消", "dhana": "财富", "putra": "子女/创造", - "ayur": "寿命/健康", + "ayur": "体质参考", "auspicious": "吉性", "nabhasa": "天象格局", "surya": "太阳类", @@ -258,8 +258,12 @@ def _strip_pl9_user_engineering_markers(markdown: str) -> str: "Aquarius": "水瓶座", "Pisces": "双鱼座", } + try: + from pl9_language_terms import replace_natural_language_term as _replace_term + except ModuleNotFoundError: # pragma: no cover + from scripts.pl9_language_terms import replace_natural_language_term as _replace_term for raw, public in sorted(glossary_terms.items(), key=lambda item: len(item[0]), reverse=True): - text = re.sub(rf"(? str: "太阳 and 水星": "太阳与水星", "金星 and 水星": "金星与水星", "Lagna": "上升", - "Kendra": "角宫", "Trikona": "三方宫", "Trine": "三方宫", "Navamsa": "九分盘", @@ -331,6 +334,7 @@ def _strip_pl9_user_engineering_markers(markdown: str) -> str: } for raw, public in natural_replacements.items(): text = text.replace(raw, public) + text = _replace_term(text, "Kendra", "角宫") cleanups = { "Special 上升s and Points": "特殊上升与派生点", "Special 上升s": "特殊上升", @@ -353,7 +357,11 @@ def _strip_pl9_user_engineering_markers(markdown: str) -> str: text = re.sub(r"(?<=[\u4e00-\u9fff])\s+大运", "大运", text) text = re.sub(r"大运\s+(?=[\u4e00-\u9fff])", "大运", text) text = re.sub(r"(太阳|月亮|火星|水星|木星|金星|土星|北交点/罗睺|南交点/计都)\1", r"\1", text) - return text + try: + from pl9_language_terms import strip_han_plural_s + except ModuleNotFoundError: # pragma: no cover + from scripts.pl9_language_terms import strip_han_plural_s + return strip_han_plural_s(text) def _sanitize_pl9_public_markdown(markdown: str) -> str: @@ -410,9 +418,6 @@ def _render_pl9_user_markdown(packet: dict) -> str: "{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 的算法口径会影响总分,阅读时以同一口径内比较为主。", @@ -428,6 +433,16 @@ def _render_pl9_user_markdown(packet: dict) -> str: } for raw, public in replacements.items(): text = text.replace(raw, public) + try: + from pl9_language_terms import replace_natural_language_term + except ModuleNotFoundError: # pragma: no cover + from scripts.pl9_language_terms import replace_natural_language_term + for raw, public in ( + ("functional_benefic", "功能吉星"), + ("functional_malefic", "功能凶星"), + ("functional_neutral", "功能中性"), + ): + text = replace_natural_language_term(text, raw, public) text = re.sub(r"^#\s+.*$", f"# {title}", text, count=1, flags=re.MULTILINE) drop_fragments = ( "本册只编排原版", @@ -559,15 +574,25 @@ def render_pl9_parity_markdown(packet: dict) -> str: if field.get('status') in {'blocked', 'not_applicable'}: return '-' if field.get('status') == 'conflict': - labels = {'solar_return': '太阳返照', 'tajika': 'Tajika'} + labels = ( + {'solar_return': 'solar return', 'tajika': 'Tajika'} + if _lang == 'en' + else {'solar_return': '太阳返照', 'tajika': 'Tajika'} + ) + other = 'another method' if _lang == 'en' else '另一口径' values = [] for item in _list(field.get('values')): item = _dict(item) data = _dict(item.get('value')) value = next((data.get(key) for key in keys if data.get(key)), None) if value is not None: - values.append(f"{labels.get(item.get('producer'), '另一口径')}:{_token(value)}") - return ';'.join(values) + '(两者不同)' if values else '-' + sep = ': ' if _lang == 'en' else ':' + values.append(f"{labels.get(item.get('producer'), other)}{sep}{_token(value)}") + if not values: + return '-' + if _lang == 'en': + return '; '.join(values) + ' (the two differ)' + return ';'.join(values) + '(两者不同)' data = _dict(field.get('data') or field) return _token(next((data.get(key) for key in keys if data.get(key)), None)) @@ -575,13 +600,20 @@ def render_pl9_parity_markdown(packet: dict) -> str: year_lord = _dict(year_lord) blocked_detail = _dict(year_lord.get('blocked_detail')) if year_lord.get('status') == 'blocked' and blocked_detail.get('selection_basis_text'): - # BUG-1223: no stand-in year lord; 「年主暂无法确定(原因)」. + # BUG-1223: no stand-in year lord. Chinese keeps 「年主暂无法确定(原因)」. + # English uses the twin sentence and does not keep the Chinese source line. + if _lang == 'en' and blocked_detail.get('selection_basis_text_en'): + return _cell(blocked_detail['selection_basis_text_en']) return _cell(blocked_detail['selection_basis_text']) if year_lord.get('status') == 'conflict': + if _lang == 'en': + return 'The two calculations name different year lords, so they are not merged' return '两种计算口径给出的年主不同,暂不合并' data = _dict(year_lord.get('data') or year_lord) # BUG-1214: the producer's one-sentence Panchadhikari basis (replaces # the BUG-1212 "Muntha lord for now" stand-in). + if _lang == 'en' and data.get('selection_basis_text_en'): + return _cell(data['selection_basis_text_en']) if data.get('selection_basis_text'): return _cell(data['selection_basis_text']) if data.get('selection_basis'): @@ -666,18 +698,31 @@ def render_pl9_parity_markdown(packet: dict) -> str: 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', '外部行为参考') + if _lang == 'en': + replacements = { + 'blocked_external_callable': 'external replay still to review', + 'pyjhora_behavior_only / not_multiengine_parity': 'external behavior note', + 'pyjhora_behavior_only': 'external behavior note', + 'parameter_sensitive': 'reference', + 'blocked': 'not supplied', + 'observed_relationships': 'formed', + 'observed_empty': 'not formed', + 'external_replay_pending': 'to be supplied', + 'Evidence label': 'note', + } + else: + replacements = { + 'blocked_external_callable': '外部回放待校对', + 'pyjhora_behavior_only / not_multiengine_parity': '外部行为参考', + 'pyjhora_behavior_only': '外部行为参考', + 'parameter_sensitive': '参考', + 'blocked': '未列', + 'observed_relationships': '已形成', + 'observed_empty': '未形成', + 'external_replay_pending': '待补充', + 'Evidence label': '证据口径', + '证据标签': '证据口径', + } for raw, public in replacements.items(): text = text.replace(raw, public) return text @@ -2047,12 +2092,20 @@ def render_pl9_parity_markdown(packet: dict) -> str: current_year = _dict(annual.get('pl9_current_year_results')) current_sections = _list(current_year.get('sections')) if current_sections: - lines.extend([ - '### p139-p141 Varshaphala 年度结果解释', - '', - '| 项目 | 锚点 | 年度主题 |', - '|------|------|----------|', - ]) + if _lang == 'en': + lines.extend([ + '### Varshaphala annual result notes', + '', + '| Item | Anchor | Annual theme |', + '|------|--------|---------------|', + ]) + else: + lines.extend([ + '### p139-p141 Varshaphala 年度结果解释', + '', + '| 项目 | 锚点 | 年度主题 |', + '|------|------|----------|', + ]) for section in current_sections: if not isinstance(section, dict): continue @@ -2070,12 +2123,20 @@ def render_pl9_parity_markdown(packet: dict) -> str: 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 结果解释', - '', - '| 阶段 | 主星 | 年盘宫位 | 日期边界 | 主题 |', - '|------|------|----------|----------|------|', - ]) + if _lang == 'en': + lines.extend([ + '### Annual Dasha result notes', + '', + '| Stage | Lord | Annual house | Date boundary | Theme |', + '|-------|------|---------------|---------------|-------|', + ]) + else: + lines.extend([ + '### p144-p145 Annual Dasha 结果解释', + '', + '| 阶段 | 主星 | 年盘宫位 | 日期边界 | 主题 |', + '|------|------|----------|----------|------|', + ]) for row in dasha_rows: if not isinstance(row, dict): continue @@ -2102,6 +2163,20 @@ def render_pl9_parity_markdown(packet: dict) -> str: f"{_cell(row.get('start'))} | {_cell(row.get('end'))} | {_public_status_note(row.get('boundary_status'))} |" ) lines.append('') + readings = [str(row.get('reading')).strip() for row in mudda_rows if isinstance(row, dict) and str(row.get('reading') or '').strip()] + if readings: + lines.extend([ + '#### Mudda period notes' if _lang == 'en' else '#### Mudda 阶段可能主题', + '', + ]) + for reading in readings: + lines.append(f"- {_public_report_text(reading)}") + lines.append('') + if _lang == 'en': + lines.append('Day-level rows and a second balance profile are not in this table.') + else: + lines.append('这张表没有日级行,也没有第二套余额口径。') + lines.append('') patyayini_rows = _list(normalized.get('patyayini_rows')) if patyayini_rows: lines.extend([ @@ -2151,9 +2226,15 @@ def render_pl9_parity_markdown(packet: dict) -> str: if not isinstance(row, dict): continue forming = row.get('forming_planets') or row.get('planets') or row.get('raw_relationships') + motion_key = str(row.get('motion') or row.get('aspect_motion') or '-') + motion_labels = ( + {'applying': 'applying', 'separating': 'separating'} + if _lang == 'en' + else {'applying': '入相', 'separating': '分离'} + ) 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 '-'))} |" + f"{_cell(forming)} | {_cell(motion_labels.get(motion_key, motion_key))} |" ) lines.append('') saham_rows = _list(normalized.get('saham_rows')) @@ -2175,26 +2256,40 @@ def render_pl9_parity_markdown(packet: dict) -> str: 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'))} |", - '', - ]) + if _lang == 'en': + lines.extend([ + '### Public Panchavargiya value check', + '', + '| Item | Result |', + '|------|--------|', + f"| Public check status | {_cell(public_replay.get('status'))} |", + f"| Planets covered | {_cell(replay_coverage.get('planet_count'))} |", + f"| Components matched | {_cell(replay_coverage.get('exact_component_match_count'))} / {_cell(replay_coverage.get('component_count'))} |", + f"| Totals matched | {_cell(replay_coverage.get('exact_total_match_count'))} / {_cell(replay_coverage.get('planet_count'))} |", + f"| Usable for a formula upgrade | {_cell(public_replay.get('promotion_allowed'))} |", + '', + ]) + else: + 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')), + ('annual_anchor_rows', 'Annual anchors' if _lang == 'en' else 'p126 年度锚点', ('name', 'label', 'value', 'sign', 'degree', 'house')), + ('annual_planet_rows', 'Annual planet data' if _lang == 'en' else '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')), + ('current_year_result_rows', 'Annual result notes' if _lang == 'en' else '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')), @@ -2282,12 +2377,20 @@ def render_pl9_parity_markdown(packet: dict) -> str: monthly_pack = _dict(annual.get('monthly_chart_pack')) monthly_rows = _list(monthly_pack.get('rows')) if monthly_rows: - lines.extend([ - '### 年度月返照图盘数据', - '', - '| 月份 | 本地时刻 | 年盘上升 | 上升度数 | 行星黄经(度) |', - '|------|----------|----------|----------|-----------------|', - ]) + if _lang == 'en': + lines.extend([ + '### Monthly returns within the year', + '', + '| Month | Local time | Annual ascendant | Ascendant degree | Planet longitudes (deg) |', + '|-------|------------|------------------|------------------|--------------------------|', + ]) + else: + lines.extend([ + '### 年度月返照图盘数据', + '', + '| 月份 | 本地时刻 | 年盘上升 | 上升度数 | 行星黄经(度) |', + '|------|----------|----------|----------|-----------------|', + ]) for row in monthly_rows: if not isinstance(row, dict): continue @@ -2311,12 +2414,20 @@ def render_pl9_parity_markdown(packet: dict) -> str: for row in _list(page.get('rows')) if isinstance(row, dict) ] if lifetime_rows: - lines.extend([ - '### 生命周期年度返照图盘数据', - '', - '| 年龄 | 年份 | 年返时刻 | 上升 | 上升度数 | 行星黄经(度) |', - '|------|------|----------|------|----------|-----------------|', - ]) + if _lang == 'en': + lines.extend([ + '### Lifetime annual return charts', + '', + '| Age | Year | Return time | Ascendant | Ascendant degree | Planet longitudes (deg) |', + '|-----|------|-------------|-----------|------------------|--------------------------|', + ]) + else: + lines.extend([ + '### 生命周期年度返照图盘数据', + '', + '| 年龄 | 年份 | 年返时刻 | 上升 | 上升度数 | 行星黄经(度) |', + '|------|------|----------|------|----------|-----------------|', + ]) for row in lifetime_rows: asc = _dict(row.get('ascendant')) longitudes = _dict(row.get('planet_longitudes')) @@ -2332,20 +2443,36 @@ def render_pl9_parity_markdown(packet: dict) -> str: ) 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', '现居地八年年度总览'), - ): + eight_year_titles = ( + ( + ('birth_place_eight_year_overview', 'Eight-year overview at the birthplace'), + ('local_eight_year_overview', 'Eight-year overview at the current residence'), + ) + if _lang == 'en' + else ( + ('birth_place_eight_year_overview', '出生地八年年度总览'), + ('local_eight_year_overview', '现居地八年年度总览'), + ) + ) + for series_key, title in eight_year_titles: series = _dict(annual_series.get(series_key)) series_rows = _list(series.get('rows')) if not series_rows: continue - lines.extend([ - f'### {title}', - '', - '| 年份 | 年龄 | 年返时刻 | 年盘上升 |', - '|------|------|----------|----------|', - ]) + if _lang == 'en': + lines.extend([ + f'### {title}', + '', + '| Year | Age | Return time | Annual ascendant |', + '|------|-----|-------------|-------------------|', + ]) + else: + lines.extend([ + f'### {title}', + '', + '| 年份 | 年龄 | 年返时刻 | 年盘上升 |', + '|------|------|----------|----------|', + ]) for row in series_rows: if not isinstance(row, dict): continue @@ -2358,7 +2485,41 @@ def render_pl9_parity_markdown(packet: dict) -> str: 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 年度北印度图盘', '']) + lines.extend([ + '### Annual north-Indian chart' if _lang == 'en' else '### p139-p155 年度北印度图盘', + '', + ]) + else: + occupancy_longitudes = _dict(_dict(annual_chart.get('data')).get('planet_longitudes')) + if occupancy_longitudes: + signs = list(globals().get('SIGNS') or []) + asc_idx = _dict(annual_chart.get('data')).get('asc_sign_idx') + asc_name = _dict(annual_chart.get('data')).get('asc_sign') + if not isinstance(asc_idx, int) and signs and asc_name in signs: + asc_idx = signs.index(asc_name) + lines.extend([ + '### Annual north-Indian chart' if _lang == 'en' else '### p139-p155 年度北印度图盘', + '', + ( + 'Sign occupancy from the annual longitudes already calculated. This is a text table, not a drawn chart.' + if _lang == 'en' + else '下面按已算出的年度黄经,把行星放进年返上升起的整宫。这是文字表,不是另画的图。' + ), + '', + '| Planet | Sign | House from annual Lagna |' if _lang == 'en' else '| 行星 | 星座 | 自年返上升的宫 |', + '|--------|------|--------------------------|' if _lang == 'en' else '|------|------|----------------|', + ]) + if isinstance(asc_idx, int) and signs: + lines.append(f"| Lagna | {_cell(_token(signs[asc_idx % 12]))} | 1 |") + for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'): + longitude = occupancy_longitudes.get(planet) + if not isinstance(longitude, (int, float)) or isinstance(longitude, bool): + continue + sign_idx = int(float(longitude) // 30) % 12 + sign = signs[sign_idx] if signs else '-' + house = ((sign_idx - asc_idx) % 12) + 1 if isinstance(asc_idx, int) else '-' + lines.append(f"| {_cell(_token(planet))} | {_cell(_token(sign))} | {_cell(house)} |") + lines.append('') for panel in panels: if not isinstance(panel, dict): continue @@ -2378,13 +2539,25 @@ def render_pl9_parity_markdown(packet: dict) -> str: 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 _lang == 'en': + lines.extend([f'### {year} annual Varshaphala / Tajika detail', '']) + else: + lines.extend([f'### {year} 年度 Varshaphala / Tajika 细项', '']) if solar_return_data or annual_chart_data: + asc_label = 'Annual ascendant' if _lang == 'en' else '年返上升' + if _lang == 'en': + lines.extend([ + '| Item | Value |', + '|------|-------|', + ]) + else: + lines.extend([ + '| 项目 | 数值 |', + '|------|------|', + ]) 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')))} |", + f"| {asc_label} | {_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) @@ -2398,12 +2571,20 @@ def render_pl9_parity_markdown(packet: dict) -> str: planet_longitudes = _dict(annual_chart_data.get('planet_longitudes')) if planet_longitudes: signs = globals().get('SIGNS', []) - lines.extend([ - '#### 年度行星资料', - '', - '| 行星 | 星座 | 落座度数 | 黄经 |', - '|------|------|----------|------|', - ]) + if _lang == 'en': + lines.extend([ + '#### Annual planet data', + '', + '| Planet | Sign | Degree in sign | Longitude |', + '|--------|------|----------------|-----------|', + ]) + else: + 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'): @@ -2419,12 +2600,20 @@ def render_pl9_parity_markdown(packet: dict) -> str: annual_dasha = _dict(annual_pack.get('pl9_annual_dasha_results')) dasha_rows = _list(annual_dasha.get('rows')) if dasha_rows: - lines.extend([ - '#### Annual Dasha 阶段表', - '', - '| 阶段 | 主星 | 年盘宫位 | 日期边界 | 主题 |', - '|------|------|----------|----------|------|', - ]) + if _lang == 'en': + lines.extend([ + '#### Annual Dasha periods', + '', + '| Stage | Lord | Annual house | Date boundary | Theme |', + '|-------|------|---------------|---------------|-------|', + ]) + else: + lines.extend([ + '#### Annual Dasha 阶段表', + '', + '| 阶段 | 主星 | 年盘宫位 | 日期边界 | 主题 |', + '|------|------|----------|----------|------|', + ]) for row in dasha_rows: if not isinstance(row, dict): continue @@ -2435,15 +2624,26 @@ def render_pl9_parity_markdown(packet: dict) -> str: ) 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')), - ) + if _lang == 'en': + future_specs = ( + ('mudda_rows', 'Mudda dasha table', ('balance_profile', 'lord', 'start', 'end', 'boundary_status')), + ('patyayini_rows', 'Patyayini ending dates', ('main', 'sub', 'boundary', 'boundary_status')), + ('saham_rows', 'Saham positions', ('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', 'Annual planet extras', ('planet', 'sign', 'degree', 'longitude', 'nakshatra', 'pada')), + ('annual_anchor_rows', 'Annual anchors', ('name', 'label', 'value', 'sign', 'degree', 'house')), + ) + else: + 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)] @@ -2473,12 +2673,20 @@ def render_pl9_parity_markdown(packet: dict) -> str: special = _dict(annual_pack.get('annual_special_points')) special_data = _dict(special.get('data')) if special_data: - lines.extend([ - '#### 年度特殊点', - '', - '| 点位 | 星座 | 落座度数 | 黄经 | 守护星 |', - '|------|------|----------|------|--------|', - ]) + if _lang == 'en': + lines.extend([ + '#### Annual special points', + '', + '| Point | Sign | Degree in sign | Longitude | Ruler |', + '|-------|------|----------------|-----------|-------|', + ]) + else: + lines.extend([ + '#### 年度特殊点', + '', + '| 点位 | 星座 | 落座度数 | 黄经 | 守护星 |', + '|------|------|----------|------|--------|', + ]) for name, row in special_data.items(): if not isinstance(row, dict): continue @@ -2491,12 +2699,20 @@ def render_pl9_parity_markdown(packet: dict) -> str: monthly_pack = _dict(annual_pack.get('monthly_chart_pack')) monthly_rows = _list(monthly_pack.get('rows')) if monthly_rows: - lines.extend([ - '#### 年度月返照图盘数据', - '', - '| 月份 | 本地时刻 | 年盘上升 | 上升度数 |', - '|------|----------|----------|----------|', - ]) + if _lang == 'en': + lines.extend([ + '#### Monthly returns within the year', + '', + '| Month | Local time | Annual ascendant | Ascendant degree |', + '|-------|------------|------------------|-------------------|', + ]) + else: + lines.extend([ + '#### 年度月返照图盘数据', + '', + '| 月份 | 本地时刻 | 年盘上升 | 上升度数 |', + '|------|----------|----------|----------|', + ]) for row in monthly_rows: if not isinstance(row, dict): continue @@ -2677,10 +2893,16 @@ def render_pl9_parity_markdown(packet: dict) -> str: effects = row.get('effects') if isinstance(row.get('effects'), list) else [] yoga_name = row.get('name_cn') or row.get('name') or row.get('type') combination = row.get('combination') or row.get('source_reference') or row.get('reason') + effect_text = _public_report_text(_brief(effects, 3) or row.get('interpretation') or row.get('summary')) + try: + from pl9_language_terms import soften_lifespan_wording + except ModuleNotFoundError: # pragma: no cover + from scripts.pl9_language_terms import soften_lifespan_wording + effect_text = soften_lifespan_wording(str(effect_text or ''), _lang) lines.append( f"| {_cell(yoga_name)} | {_cell(row.get('category') or row.get('subtype'))} | " f"{_cell(row.get('strength'))} | {_cell(combination)} | " - f"{_cell(_public_report_text(_brief(effects, 3) or row.get('interpretation') or row.get('summary')))} |" + f"{_cell(effect_text)} |" ) lines.append('') if yoga_pages: diff --git a/scripts/solar_return.py b/scripts/solar_return.py index dd92657d..0fd3d0ec 100644 --- a/scripts/solar_return.py +++ b/scripts/solar_return.py @@ -670,6 +670,17 @@ def solar_return_full_report( # 9. 综合总结 result['summary'] = _make_sr_summary(result) + # Keep the longitudes and chart already calculated above so the report + # pack can print them. This does not recompute the return. + if not degraded: + result['planet_lons'] = { + name: float(value) + for name, value in planet_lons.items() + if isinstance(value, (int, float)) and not isinstance(value, bool) + } + chart = sr.get('chart') if isinstance(sr, dict) else None + if isinstance(chart, dict): + result['chart'] = chart return result diff --git a/scripts/tajika_mudda.py b/scripts/tajika_mudda.py index 8372f9ee..fed1a3b0 100644 --- a/scripts/tajika_mudda.py +++ b/scripts/tajika_mudda.py @@ -18,8 +18,10 @@ of the same rule. What it adds on top of the upstream function: - `periods` are cropped to the annual window [annual_start, annual_end) and carry `months` (share of 12) for the existing month-window consumers; the upstream ten-group surface (the last group runs past the year end) stays - in `periods_full`. Upstream's sub-periods are not carried (no product - surface shows them). + in `periods_full`. Sub-periods already computed by + `calc_mudda_with_dasha_balance` are copied as `antardasha_periods` and + cropped to the same annual window. Day rows and a second balance profile + are not added here. The legacy `tajika.calc_mudda_dasha` (first lord = year lord) is no longer called from any product path. @@ -90,6 +92,23 @@ def annual_mudda_dasha( "start": p_start.isoformat(sep=" "), "end": p_end.isoformat(sep=" "), }) + antardasha_periods = [] + for row in raw.get("antardasha_periods") or []: + if not isinstance(row, dict) or not row.get("start") or not row.get("end"): + continue + p_start = max(_parse(row["start"]), start) + p_end = min(_parse(row["end"]), end) + if p_start >= p_end: + continue + antardasha_periods.append({ + "main_order": row.get("main_order"), + "sub_order": row.get("sub_order"), + "main_lord": row.get("main_lord"), + "sub_lord": row.get("sub_lord"), + "duration_days": (p_end - p_start).total_seconds() / 86400.0, + "start": p_start.isoformat(sep=" "), + "end": p_end.isoformat(sep=" "), + }) return { # The first-lord rule is the astrologer's round-5 ruling; dates are # not externally replayed (upstream keeps its own parameter_sensitive). @@ -110,6 +129,7 @@ def annual_mudda_dasha( "balance_profile": raw.get("balance_profile"), "balance_fraction_remaining": raw.get("balance_fraction_remaining"), "periods": periods, + "antardasha_periods": antardasha_periods, "periods_full": raw.get("periods"), "independent_of_year_lord": True, "source": SOURCE, diff --git a/tests/test_report_full_data_fix.py b/tests/test_report_full_data_fix.py index 6b2d93d0..ecbbbcc3 100644 --- a/tests/test_report_full_data_fix.py +++ b/tests/test_report_full_data_fix.py @@ -208,6 +208,82 @@ def test_english_focus_and_new_sections_have_no_han() -> None: assert leaked == [], leaked[:12] +def test_yoga_effect_does_not_assert_lifespan() -> None: + # 原值:作用列写「长寿;健康良好」/ Longevity。 + # 新值:作用列改成体质与恢复力的参考句。名称「长寿格局」和 Ayur Yoga (Strong Longevity) 可以留下。 + # 原因:格局名称是传统叫法,作用说明不再把寿命当断语。 + packet = _packet("zh") + packet["worksheets"]["advanced_systems"]["yoga"]["detected_yogas"].append({ + "name": "Ayur Yoga (Strong Longevity)", + "name_cn": "长寿格局", + "category": "ayur", + "strength": "strong", + "effects": ["长寿", "健康良好"], + "effects_en": ["Longevity", "Good health"], + "combination": "traditional name only", + }) + chinese = render_full_data_markdown(packet) + english_packet = copy.deepcopy(packet) + english_packet["report_language"] = "en" + english = render_full_data_markdown(english_packet) + + def without_names(text: str) -> str: + text = text.replace("长寿格局", "") + return re.sub(r"Ayur Yoga \(Strong Longevity\)", "", text, flags=re.IGNORECASE) + + for phrase in ("长寿", "寿命长", "短寿"): + assert phrase not in without_names(chinese) + assert "longevity" not in without_names(english).lower() + assert "传统上与体质和恢复力有关,仅作参考" in chinese + assert "constitution and recovery" in english.lower() + + +def test_term_replacement_does_not_enter_identifiers() -> None: + # 原值:functional_benefics / Kendras / conditions 变成「功能吉星s」「角宫s」「宫条件s」,字段名里的 sign 被换成星座。 + # 新值:散文里的复数 s 吃进英文词;反引号和标识符保持原文。 + # 原因:术语替换只能作用于自然语言。 + raw = "\n".join([ + "# 标题", + "", + "strict_functional_benefic_malefic_v2", + "bphs_ch34_general_with_sign_exceptions_v2", + "functional_benefics and Kendras and house conditions", + "`functional_benefics`", + ]) + text = _sanitize_pl9_ai_density_markdown(raw) + assert "strict_functional_benefic_malefic_v2" in text + assert "bphs_ch34_general_with_sign_exceptions_v2" in text + assert "`functional_benefics`" in text + assert "功能吉星s" not in text + assert "角宫s" not in text + assert "宫条件s" not in text + assert "功能吉星" in text + assert re.search(r"[一-鿿]+s\b", text) is None + rendered = render_full_data_markdown(_packet("zh")) + assert re.search(r"[一-鿿]+s\b", rendered) is None + mixed = re.search(r"[A-Za-z0-9_]*_[一-鿿]|[一-鿿]_[A-Za-z0-9_]", rendered) + assert mixed is None, mixed.group(0) if mixed else "" + + +def test_rahu_ketu_do_not_rule_an_unlisted_house() -> None: + from scripts.pl9_interpretation_packs import _natal_en, _natal_zh, _sub_paragraph + + # 原值:空守护列表写成「守护第未列出宫」。 + # 新值:罗睺、计都写「不守护宫位」/ rules no house。中文全文该病句计数为 0。 + # 原因:这两颗星不守护宫位。其他「未列出」按句意保留。 + empty = {"ownership": [], "house": 6, "nakshatra": "Mula"} + filled = {"ownership": [1], "house": 6} + row = {"start": "2001-01-01", "end": "2001-02-01"} + assert "不守护宫位" in _sub_paragraph("Sun", "Rahu", row, empty, "zh", level="PD") + assert "守护第未列出宫" not in _sub_paragraph("Sun", "Rahu", row, empty, "zh", level="PD") + assert "不守护宫位" in _natal_zh("Ketu", filled, {}) + assert "守护第1宫" in _natal_zh("Sun", filled, {}) + assert "守护宫未单独列出" in _sub_paragraph("Sun", "Mars", row, empty, "zh", level="AD") + assert "rules no house" in _natal_en("Rahu", filled, {}) + text = render_full_data_markdown(_packet("zh")) + assert text.count("守护第未列出宫") == 0 + + def test_chinese_render_stays_stable_when_english_uses_a_copy() -> None: packet = _packet("zh") before = render_full_data_markdown(copy.deepcopy(packet)) diff --git a/tests/test_year_lord_blocked_no_fallback.py b/tests/test_year_lord_blocked_no_fallback.py index a2f4dd03..96c06e43 100644 --- a/tests/test_year_lord_blocked_no_fallback.py +++ b/tests/test_year_lord_blocked_no_fallback.py @@ -128,14 +128,12 @@ def test_report_shows_the_blocked_sentence_in_both_editions(blocked_selector) -> zh_rows = [line for line in zh.splitlines() if line.startswith("| Selection basis |")] en_rows = [line for line in en.splitlines() if line.startswith("| Selection basis |")] assert zh_rows and all("年主暂无法确定(年盘缺太阳或月亮位置)" in row for row in zh_rows) - # 原值:英文行是 Year lord cannot be determined yet,且无汉字;年主格是 "-" - # 新值:清干净时仍要求那句英文;否则必须保留中文阻断句并标成 source text retained。年主格是 "-" 或「未列」 - # 原因:BUG-1272。不得改回 Muntha 主星。空位标记随完整数据版清理器变成「未列」 + # 原值:英文行含「年主暂无法确定」,并包成 [source text retained: ...]。 + # 新值:英文行是 Year lord cannot be determined yet,不要求中文,也不要求 source text retained。 + # 原因:英文汉字已清零,那条中文断言没有对象。不得把汉字塞回英文。 assert en_rows - if any(re.search(r"[一-鿿]", row) for row in en_rows): - assert all("年主暂无法确定" in row and "[source text retained:" in row for row in en_rows) - else: - assert all("Year lord cannot be determined yet" in row for row in en_rows) + assert all("Year lord cannot be determined yet" in row for row in en_rows) + assert all("年主暂无法确定" not in row and "[source text retained:" not in row for row in en_rows) assert any( line.startswith("| Varshesha | - |") or line.startswith("| Varshesha | 未列 |") for line in zh.splitlines()