Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36737634ca | ||
|
|
9bf4731e2c | ||
|
|
2a43fc9598 |
@@ -1,5 +1,27 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-08 — 完整数据版补上年运细项和大运读法(未上线)
|
||||
|
||||
- 年运章写出资料包里已经有的太阳返照、年主依据、年度锚点、年度行星、Tajika 对照和年度结果。Patyayini、Raman Sahams、Tripataki、月返照、八年总览、一生年运没有新算;Patyayini 仍写「没有实现」。
|
||||
- 大运解释补上「这段时间可能突出的主题」「条件好时可能」「受压时可能表现为」「怎么细读」。不写疾病诊断、死亡、寿命和「一定会」。
|
||||
- 瑜伽表里长寿类格局的名称还在,作用改成体质和恢复力的参考,不再把寿命当断语(BUG-1277)。
|
||||
- 罗睺、计都写成不守护宫位(BUG-1279)。术语替换不再把英文复数 s 留在汉字后面,也不改字段名(BUG-1278)。
|
||||
- 英文四张公开或虚构盘的汉字数仍是 0。生成按钮文案没改。不改数据库。Skill 版本不变。未合入,未上线。
|
||||
|
||||
## 2026-10-08 — 完整数据版补上解释,英文可以生成(未上线)
|
||||
|
||||
- 个人报告的中文补上大运、行星、宫主、Dosha 和瑜伽的解释,开篇「重点」写当前三级大运和事业、财富、婚恋三宫的宫主。Patyayini 仍没有实现。
|
||||
- 乔布斯、奥巴马、爱因斯坦和一张虚构盘的英文不再夹汉字,新报告可以切换「中文 / English」(BUG-1272)。「Bhava 年龄状态」「行星s」这两处误译已改(BUG-1274)。
|
||||
- 生成按钮的时长说明没改。以前生成的报告仍按当时的正文打开。不改数据库。Skill 版本不变。
|
||||
|
||||
## 2026-10-08 — 个人报告改为完整数据版(未上线)
|
||||
|
||||
- 「生成报告」改为完整数据版:星盘、力量、大运、年运和瑜伽写进正文,后面有一份结构化资料附录。阅读版的生成入口已删除。
|
||||
- 以前生成的报告还能打开,只读,不会自动重算。要新版需重新生成。
|
||||
- 英文清理后仍有汉字,新报告先不提供英文,也不显示「中文 / English」。导出时会说明英文没有生成成功(BUG-1272,尚未解决)。
|
||||
- 报告超过约 20 万字时,阅读页一次只显示一章,可用目录或「上一章 / 下一章」换章。下载仍是原来的「导出」。
|
||||
- 不改数据库。Skill 版本不变。
|
||||
|
||||
## 2026-10-08 — 特殊上升点在没有 PyJHora 的环境里也能算出(未上线)
|
||||
|
||||
- 生产镜像装不了 PyJHora,Bhava、Hora、Ghati、Vighati、Sree、Indu、Pranapada、Varnada 这八个点以前算不出来。现在用 Swiss Ephemeris 在本进程里算(BUG-1273)。
|
||||
|
||||
@@ -17115,6 +17115,22 @@
|
||||
- 复发自:无。
|
||||
- 修复版本:无(运行机侧)。
|
||||
|
||||
## BUG-1272 | 完整数据版英文报告清理后仍含汉字,接口整份撤回英文
|
||||
|
||||
- 状态:resolved(本分支 `codex/report-full-data-edition-20261007`,未合入 staging,未上线)
|
||||
- 首次发现 / 最近更新:2026-10-08 / 2026-10-08
|
||||
- 来源:`TASK-report-full-data-edition-20261007`。上游任务书已记录乔布斯盘英文清理失败(`chinese_characters:14981`)。补齐见 `TASK-report-full-data-fix-20261008`。
|
||||
- 影响面:新生成的个人报告英文版。中文完整数据版不受影响。已生成的旧报告仍按当时保存的正文打开。
|
||||
- 现象:完整数据版英文沿用上游清理器。清理器对没有译文的汉字不会删行,而是包成 `[source text retained: …]`。这样正文里仍然有汉字。公开接口发现汉字后设置 `english_unavailable=han_leak`,不返回 `markdown_en`。页面不显示「中文 / English」切换。
|
||||
- 触发条件:用完整数据版生成中英两份。本机虚构盘、乔布斯盘、奥巴马盘都如此。泰勒盘没有公开出生资料,没有生成。第四张公开盘用爱因斯坦。
|
||||
- 根因:短词替换会先把整句拆开,剩下的单字和半句再被包进 source text retained。早期大约 350–470 处汉字串,是拆开之后的碎片,不是必须整段删掉的正文。双主星有一条中文依据,资料包里本来就有英文对照,阅读时却只印了中文,单字「水」又把「水瓶」拆成了 Mercury。
|
||||
- 修复:章节标题按语言写;拆开后剩下的碎片补进英文术语,不删段落。双主星说明在英文里改用已有的英文依据。清理器仍把没有译文的汉字包起来。接口发现汉字后仍整份撤回英文。
|
||||
- 验证:虚构、爱因斯坦、乔布斯、奥巴马四张盘英文汉字数为 0,中英二级标题都是 35/35。`tests/test_report_english_edition.py` 改为要求三张虚构盘汉字为 0,并且虚构日盘的接口必须交回英文。故意写入汉字的测试仍要求 `english_unavailable=han_leak`。没有在浏览器里走查。
|
||||
- 防复发:上述测试。接口和前端仍在发现汉字时撤回英文。
|
||||
- 相关记录:BUG-1126(有英文版时切换必须真换正文)、BUG-1271、BUG-1274。
|
||||
- 复发自:无。
|
||||
- 修复版本:本分支,未合入 staging。
|
||||
|
||||
## BUG-1273 | 生产镜像没有 PyJHora,八个特殊上升点算不出来
|
||||
|
||||
- 状态:resolved(已部署 staging `795b8845`:门禁 run 3218、deploy run 3220;`/api/health` gitCommit 一致,`/login` 200,未登录 `/api/account` 401)
|
||||
@@ -17133,6 +17149,22 @@
|
||||
- Claude 验收(2026-10-08,Linux):屏蔽 `jhora` 导入后跑 `cmd_full_reading`,乔布斯、奥巴马、虚构盘 × Lahiri / Raman 共 6 组,8 个点全部 `computed`、`source` 为 `native_special_lagna`,与 `fb8277bf` 调 PyJHora 的结果最大差 0°。Python 全量 62 条失败,与 `2df6ee72` 逐名相同;快速门通过;`tests/test_runtime_jhora_import_scan.py` 在快速门内,人为在 `native_special_lagnas.py` 加一行 `from jhora ...` 后该测试失败(变异检查)。年运资料包屏蔽 `jhora` 前后逐字节相同。
|
||||
- 待问占星师:Vighati 上升按每分钟 15° 计(与 PyJHora 一致);按字面「每个 vighati 一个星座」应为每分钟 75°。
|
||||
|
||||
## BUG-1274 | 中文报告把 Bhava Bala 译成年龄状态,并把 Planets 译成行星s
|
||||
|
||||
- 状态:resolved(本分支 `codex/report-full-data-edition-20261007`,未合入 staging,未上线)
|
||||
- 首次发现 / 最近更新:2026-10-08 / 2026-10-08
|
||||
- 来源:`TASK-report-full-data-fix-20261008` 的译名项。
|
||||
- 影响面:完整数据版中文报告的力量标题和行星标题。
|
||||
- 现象:标题出现「六维力量 / Bhava 年龄状态」和「Natal / KP Ruling 行星s」。
|
||||
- 触发条件:生成完整数据版中文。清理器先把 Shadbala 写成「六维力量」,再把单独的 Bala 写成「年龄状态」,于是修 Bhava Bala 的那一步对不上原文。另一处把 Planet 换成「行星」时没有先处理 Planets,复数的 s 留在汉字后面。
|
||||
- 根因:两段替换都按较短的词下手,较长的词还在句里。
|
||||
- 修复:先替换整段「Natal / KP Ruling Planets」「Planets」「Planetary」,再替换 Planet。力量标题改回「六维力量 / Bhava Bala」。星体年龄那一列仍叫年龄状态,没有全局改掉。
|
||||
- 验证:`tests/test_report_full_data_fix.py` 检查这两句,并检查中文不得出现「英文单词 + 汉字 + s」。
|
||||
- 防复发:同一条测试。
|
||||
- 相关记录:BUG-1272。
|
||||
- 复发自:无。
|
||||
- 修复版本:本分支,未合入 staging。
|
||||
|
||||
## BUG-1275 | 恒星黄道经度少扣章动:所有行星偏约 0.005°,Vimshottari 日期差 2–3 天
|
||||
|
||||
- 状态:investigating(修复单 `docs/tasks/TASK-ephemeris-nutation-20261008.md`)
|
||||
@@ -17148,3 +17180,99 @@
|
||||
- 相关记录:2026-10-07 sync6 验收里「虚构盘大运与独立计算差约 2 天」曾被 Claude 判为岁差版本差异,实为本条。BUG-1268(子运起点)。
|
||||
- 复发自:无。
|
||||
- 修复版本:无。
|
||||
|
||||
## 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。
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# 进度 · 个人报告改为完整数据版(2026-10-08)
|
||||
|
||||
- 分支:`codex/report-full-data-edition-20261007`
|
||||
- 基线:`c04786f7`(`origin/staging` 在开工时的尖端;sync6 已在 `2df6ee72` 部署,其后的文档提交不改变已部署代码)
|
||||
- 状态:代码在本分支,**未推 staging**,未上线
|
||||
- 产品结论:新报告是中文完整数据版。阅读版不再生成。英文这轮不交付(BUG-1272,未解决)。旧报告仍按保存的正文打开。
|
||||
|
||||
## 用户能看到什么
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| 生成 | 「生成报告」只请求 `edition: full_data`。没有阅读版开关 |
|
||||
| 正文 | 中文。星盘、力量、大运、年运、瑜伽,加结构化资料附录 |
|
||||
| 英文 | 清理后仍有汉字。接口不返回英文,页面不显示语言切换 |
|
||||
| 旧报告 | 快照仍是当时存下的 Markdown,打开时不重算 |
|
||||
| 很长的报告 | 超过约 20 万字才一次显示一章。现在的中文大约 10 万字,仍按原来的方式分段滚动 |
|
||||
| 下载 | 仍是原来的「导出」,一份 Markdown。没有第二颗下载按钮 |
|
||||
| 数据库 | 不改表。正文放得下现有文本列 |
|
||||
|
||||
生成卡片仍写「大约 10–30 秒」和「中英两版」。这两句与实测不符,测试锁着「10–30 秒」这句,本轮没有改文案。
|
||||
|
||||
## 触点
|
||||
|
||||
| 位置 | 处理 |
|
||||
| --- | --- |
|
||||
| `professional_report_reference.py` 的阅读版常量 | 删除。新增 `full_data` / `pl9_personal_long_report.v4`。请求阅读版会报错 |
|
||||
| `pl9_reader_export.py` 的分发 | 阅读版不再走生成。完整数据版走新模块。文件里的旧整理函数还在,生成路径不调用 |
|
||||
| `pl9_full_data_export.py`、术语表 | 新增。只搬渲染和用词,不搬上游计算 |
|
||||
| 英文用词、事实表、大运适用说明 | 留下。旧报告和事实表还用它们 |
|
||||
| `personal-report-longform-generate.ts` | 改为请求完整数据版 |
|
||||
| 快照、原始附录、阅读页、导出 | 留下,旧报告才能打开 |
|
||||
| `jyotish_api_server.py`、`page.tsx`、默认报告版本常量 | 没有改。默认版本仍是 v3;v4 只出现在完整数据版的接口字段上 |
|
||||
| 对话 | 没有改。只加了按领域复制资料包的只读入口 |
|
||||
|
||||
## 本机生成(不是 2 核服务器)
|
||||
|
||||
机器是 16 线程桌面,一次只跑一个 Python 进程。不能当成线上 2 核的耗时。都低于接口 180 秒上限。
|
||||
|
||||
| 盘 | 排盘加资料包 | 中文渲染 | 中文字符 | 英文渲染 | 英文字符 | 英文汉字串 / 不同写法 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 虚构 | 79 秒 | 2.7 秒 | 104,547 | 0.4 秒 | 138,444 | 353 / 238 |
|
||||
| 乔布斯 | 57 秒 | 8.7 秒 | 101,689 | 2.0 秒 | 136,102 | 404 / 274 |
|
||||
| 奥巴马 | 91 秒 | 3.0 秒 | 112,222 | 0.6 秒 | 151,182 | 465 / 303 |
|
||||
|
||||
泰勒没有生成:本仓和上游 `23be1807` 都没有这份公开出生资料。上表的汉字数是在补上分盘提示、主星标题、八分法标题和「合计」的英文之前量的。补译之后汉字会少一些,没有少到能整份交付。
|
||||
|
||||
只存中文时大约 10 万到 11 万字符。中英都存大约 24 万字符。PostgreSQL 文本列放得下。没有改表。
|
||||
|
||||
## 缺口(没有补算,也没有编)
|
||||
|
||||
1. 上游那份私人 PDF 对照表没有搬。里面有对具体用户的扣留说明。
|
||||
2. 视位置计时和年运交食返照没有搬。缺的计时节就空着。
|
||||
3. 开篇「重点」是固定的三句核对清单,不是上游按当前大运和宫主写的那段。
|
||||
4. 中文大约 10 万字符,不是任务书里对照的上游约 97 万。渲染器要读的资料包字段这边没有。
|
||||
5. 没有周期数据的大运族不会出现。Narayana 的旧算法说明和 Rath 并列表用的是资料包里已经算好的字段,只展示。
|
||||
6. 泰勒盘见上。
|
||||
7. 英文见 BUG-1272。
|
||||
|
||||
本仓口径仍在:落陷取消不叫王瑜伽;网站的 +2 / −1.5 分不出现;六维力量先写最低要求;八分法三列数字还在;没有 Kranti 列;年主依据和「不能确定就不拿 Muntha 主星充数」还在。
|
||||
|
||||
## 让步
|
||||
|
||||
- 没有全文搜索。
|
||||
- 不到 20 万字不启用按章分页。100 万字和 350 万字的页面测试能换章,用时在原有的首屏时限里。
|
||||
- 生成文案仍写 10–30 秒和中英两版。见上。
|
||||
|
||||
## 测试断言
|
||||
|
||||
没有删除测试。守计算口径的文件都改成对完整数据版断言。下表只列本轮改过的断言。
|
||||
|
||||
| 测试 | 原断言 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| 中文章节名含 Year Lord | 正文出现 Year Lord | 出现 Varshesha | 中文清理把 Lord 写成「宫主」 |
|
||||
| 年运三行 | 全文数 `Muntha sign` | 只数附录前的 `Muntha 星座`,仍是三年、各不相同 | 附录会再写一行当年 Muntha;用词被译成「星座」 |
|
||||
| 年运冲突行的 Muntha | `Muntha sign` | `Muntha 星座`,星座值不变 | 同上 |
|
||||
| 禁止「字段」 | 全文不能出现「字段」 | 附录前可以出现「字段」;尊贵状态仍只写一个名字 | 上游表头就是「字段 \| 数值」。这条随阅读版失去对象 |
|
||||
| 英文无汉字 | 汉字必须为空 | 有汉字就必须是 source text retained;接口在有汉字时撤回英文 | BUG-1272 |
|
||||
| 英文年度章标题 | Annual Charts and Tajika | Annual Varshaphala and Tajika | 上游标题 |
|
||||
| 中英数字顺序 | 全文数字序列相同 | 两边正文都有同一个出生年份 | 中文多了核对清单序号,逐号对齐失去对象 |
|
||||
| 宫位力量表头 | 中英都是英文表头 | 中文译成宫位/星座/宫主;英文保持原文。网站 +2/−1.5 仍禁止 | 中文清理 |
|
||||
| 赤纬表 | 中英都是 Declination / Speed | 中文是赤纬/速度;英文保持原文。仍无 Kranti | 中文清理 |
|
||||
| 八分法宫位表 | 中英同一套英文表头;合计行星座格是 `-` | 中文表头译成宫位/星座/上升;英文标题 Full House Scores,合计行 Total;空位可以是 `-` 或「未列」。四格数字不变 | 清理器改用词,不改分数 |
|
||||
| 六维力量表头 | 中英都以 Planet 开头 | 中文以「行星」开头;英文仍是 Planet。最低要求仍在网站分档之前 | 中文清理 |
|
||||
| 年主依据、年主不能确定 | 英文必须是无汉字的那句 | 清干净时仍要那句英文;否则必须标成 source text retained,且不得改回 Muntha 主星 | BUG-1272 |
|
||||
| Narayana 两则 | 对 reference 版断言旧算法句和 Rath 表 | 对 full_data 断言同一内容 | 用户看到的是完整数据版 |
|
||||
| 正文不得用 useState | `PersonalReportMarkdownView` 里不能出现 useState | 只允许章节序号和打印标记两处;每次 `renderMarkdown` 仍在 useMemo 里 | 超过 20 万字按章翻页。滚动位置仍不放在阅读器里 |
|
||||
| 切换版本留在同一章 | `drawn={drawnThrough !== null && chapter <= drawnThrough}` | 懒加载用 `index <= drawnThrough`;超长报告的初始章取 `drawnThrough` | 循环变量改名。按章翻页仍从切换前的章号打开 |
|
||||
|
||||
Chara Karaka 顺序和落陷取消条件的计算断言没有改,只是把被测版本从阅读版换成完整数据版。
|
||||
|
||||
## 本机检查
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| `tsc --noEmit` | 退出码 0 |
|
||||
| `npm run lint` | 退出码 0。0 error,125 warnings。警告都不在本轮改过的文件里。没有去改这些既有警告 |
|
||||
| 报告与口径定向 pytest | 除八分法合计行外一次通过;改完英文「合计 → Total」后,`test_report_chart_blank_columns.py` 12 项通过 |
|
||||
| 100 万字 / 350 万字阅读页 | 通过(tsx)。换章,没有全文搜索 |
|
||||
| 领域只读入口 | 3 项通过。未知领域报错,不改对话 |
|
||||
| 浏览器 | 没有走查。见 `docs/testing/report-full-data-edition-20261008.md` |
|
||||
| 全量 `npm test`、`next build --webpack`、Python 快速门 | 见下 |
|
||||
|
||||
## 全量前端与构建
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| `npm test` | 4902 项,通过 4756,失败 146,取消 0,跳过 0。用时约 339 秒。这是修好两条阅读器断言之前的数字 |
|
||||
| 本轮新失败 | 两条:`article markdown trees are memoized and hold no scroll state`,`switching editions keeps the reader on the same chapter (BUG-1108)`。修好后这两个文件 23 项通过。没有重跑全量,所以全量失败名单按修好后计是 144 |
|
||||
| 和开工基线比 | 基线文件 147 条。中文用例名在那份文件里编码坏了,按英文名对过:基线多 3 条这次没红的环境失败(两条数据库、一条 D3 首行)。sync6 进度里多出的 `corrective migration clears untrusted clocks` 这次通过了。没有删测试 |
|
||||
| `next build --webpack` | 第一次收集 `/api/consult` 页面数据时,技能包符号链接 EPERM,编译和类型检查已经过。用本机已有的符号链接预加载再跑,退出码 0。`/` 是 Static |
|
||||
| 首屏 gzip-9 | 预渲染 `index.html` 引用 31 个脚本、3 个样式。基线 `c04786f7` 同法 608,597 B,本分支 608,642 B,+45 B,+0.007%。基线用临时工作树构建,量完已删 |
|
||||
| Python 快速门 | 编译、JSON、能力审计、碎片审计、清单和 BPHS 都过。pytest 1057 通过、1 跳过、1 失败,用时 646 秒。失败是 `test_consultation_native_layers.py::test_the_new_layer_leaves_every_existing_output_unchanged`。日志里没有完整数据版的字段。sync6 进度已写这条在 Windows 失败、在 Linux 通过。脚本停在这一步,没有继续跑门禁里的 `npm test` 和 `npm run build` |
|
||||
| Python 全量 | 没有重跑 |
|
||||
| 隐私标记 | 在这轮快速门里,唯一失败不是它 |
|
||||
| 浏览器 | 没有走查。见 `docs/testing/report-full-data-edition-20261008.md` |
|
||||
@@ -0,0 +1,59 @@
|
||||
# 进度 · 完整数据版补齐(2026-10-08)
|
||||
|
||||
- 分支:`codex/report-full-data-edition-20261007`
|
||||
- 基线:`e14fa377`(完整数据版那一笔,已经 rebase 到 `origin/staging` `16782634` 之上;特殊上升点 `795b8845` 仍是祖先)
|
||||
- 状态:代码在本分支,**未合入 staging**,未上线。等 Claude 验收通过后再合入
|
||||
- 远端功能分支上的旧提交 `06028759` 不是这次 rebase 之后的祖先。更新功能分支需要 `--force-with-lease`,不能推 `staging`
|
||||
|
||||
## 结论
|
||||
|
||||
新报告仍是完整数据版。中文补上了大运、行星、宫主、Dosha、瑜伽解释,以及开篇「重点」。英文四张盘汉字数为 0,接口会返回英文。Patyayini 没有实现,没有新写计算器。生成按钮文案没改。
|
||||
|
||||
## 四张盘(本机,一次一个进程)
|
||||
|
||||
汉字范围用的是接口同一条规则。没有把出生时间写进这份记录。
|
||||
|
||||
| 盘 | 中文字符 | 英文字符 | 英文汉字 | 二级标题(中/英) | 缺字段 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 虚构 | 212336 | 361577 | 0 | 35/35 | 6 |
|
||||
| 爱因斯坦 | 234199 | 402881 | 0 | 35/35 | 6 |
|
||||
| 乔布斯 | 209717 | 359869 | 0 | 35/35 | 6 |
|
||||
| 奥巴马 | 222602 | 380264 | 0 | 35/35 | 6 |
|
||||
|
||||
四份中文都有「大运解释」「行星、星宿与宫位解释」「宫主逐宫解释」「Dosha 逐项」「瑜伽解释」,开篇「重点」里有 MD、AD、PD。中文没有「再婚」「几段关系」「早夭」「一定会」「必然」「疾病诊断」「疾病」。英文没有 divorce、miscarriage、infertility、guarantee。决策 4 的七种大运标题没有出现。中文都超过约 20 万字,阅读页会一次显示一章。
|
||||
|
||||
另有一张任务书测试里的夜盘,双主星说明原来会留下汉字。英文改用资料包里已经写好的英文依据之后,正文和适用性说明的汉字也是 0。接口因此会交出英文。
|
||||
|
||||
## 章节目录
|
||||
|
||||
| 状态 | 章节 |
|
||||
| --- | --- |
|
||||
| 已有 | 大运解释、行星解释、宫主逐宫、Dosha 逐项、瑜伽解释、Mudda、Sahams、Tajika 瑜伽、Mudda 月段窗口、太阳返照 |
|
||||
| 本单不做(决策 4) | Sthira、Drig、Shoola、Navamsha、Niryaana Shoola、Lagna Kendradi、Chara。不写占位 |
|
||||
| 本单不做(让步,月返照 / 八年总览) | 月返照盘、八年总览。目录标成延后,不算进缺字段 |
|
||||
| 缺字段 | Patyayini(两节同一个来源,只计一次;正文写「没有实现」)、Raman Sahams(不用普通 Sahams 冒充)、一生年运图、Tripataki、年运特殊点、当年结果说明。合计 6,不超过 10 |
|
||||
|
||||
## 让步
|
||||
|
||||
1. T6 生成按钮不改。`frontend/docs/VOICE.md` 和现有测试锁着「大约 10–30 秒」。本机不是 staging 的 2 核计时,不新编一段分钟数。卡片里的「中英两版」留着,因为英文现在能生成。
|
||||
2. T3 没有让。开篇「重点」写当前 MD / AD / PD,以及事业、财富、婚恋三宫的宫主、尊贵和功能吉凶。
|
||||
3. T2 的月返照和八年总览按让步顺序放下,没有新算。
|
||||
|
||||
## 测试
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| `tests/test_report_full_data_fix.py` | 4 条通过 |
|
||||
| `tests/test_report_english_edition.py` | 18 条通过。含上面两处改过的断言 |
|
||||
| 运行时不得导入 jhora | `tests/test_runtime_jhora_import_scan.py` 4 条通过 |
|
||||
| ruff(改过的四个脚本加新测试) | 8 条都是改动前就有的,没有改格式去消 |
|
||||
| Linux 快速门、Python 全量、`npm test`、`next build` | 本机没跑,不能当成通过 |
|
||||
|
||||
改过的既有断言:
|
||||
|
||||
| 测试 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| 英文无汉字 | 有汉字就必须包在 source text retained 里 | 三张虚构盘汉字数为 0 | BUG-1272 已清零 |
|
||||
| 接口是否交英文 | 没汉字就交,有汉字就整份撤回 | 虚构日盘必须交回无汉字的英文 | 同上。故意写进汉字的那条测试仍要求撤回 |
|
||||
|
||||
没有改 `scripts/jyotish_api_server.py`,没有改 `Home()` 的状态数,没有改默认报告版本,没有改生时校正公式,没有改生成按钮。
|
||||
@@ -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 秒」。咨询行为没有改。
|
||||
@@ -201,9 +201,9 @@
|
||||
|
||||
| 任务书 | 进度 | 主题 | 状态 | 落点 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `TASK-report-full-data-edition-20261007.md` | — | **报告改为完整数据版、删除阅读版**:网站阅读版中文 7.6 万字符 vs 上游 `pl9_ai_density` 97 万;移植原始数据附录 / 清理器 / 时间补充,中英文;我方报告口径(BUG-1199~1229)全部保持;旧报告仍可打开;排在 sync6 之后 | 未合入(分支 `06028759`,验收未通过,见 fix-20261008) | — |
|
||||
| `TASK-report-full-data-fix-20261008.md` | — | **完整数据版补齐**(同分支 `06028759` 继续):中文只有上游 19%(9.9 万 vs 52.6 万字);产品允许移植上游解释生成器并逐条核口径;年运表接我方字段;英文必须交付才上线;其他 7 种大运另开单 | 验收未通过(`32f19f07`,见 fix2) | — |
|
||||
| `TASK-report-full-data-fix2-20261008.md` | — | **完整数据版第二轮**(同分支 `32f19f07` 续):英文已过;未过——全量 +1 旧断言、寿命断言、半译词与字段名被改坏、「守护第未列出宫」163 处、年运章 6/29 节、大运解释只有上游 42%(产品:补回传统段落改「可能」口吻) | 待领取 | — |
|
||||
| `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` | `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`(未部署) |
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# 真机清单 · 个人报告完整数据版(2026-10-08)
|
||||
|
||||
对应 `docs/tasks/TASK-report-full-data-edition-20261007.md`。电脑和手机各走一遍。第 1 步会用掉一次生成次数。本机没有登录态,也没有用浏览器点过,下表结果留空。
|
||||
|
||||
当前代码的行为,供对照:
|
||||
|
||||
- 新报告是中文完整数据版。英文清理后仍有汉字,接口整份撤回,页面上没有「中文 / English」(BUG-1272)。
|
||||
- 生成卡片仍写「大约 10–30 秒」和「中英两版」。本机单进程大约要 1 分钟多,不是 2 核服务器上的测量。英文这轮实际不会出现。
|
||||
- 现在生成的中文大约 10 万字,不到 20 万字的分页线,阅读方式与改版前的长文一样:往下滚,目录跳章节。超过约 20 万字才出现「上一章 / 下一章」。
|
||||
- 下载仍只有阅读页上的「导出」,得到 Markdown。
|
||||
|
||||
| # | 操作 | 应该看到 | 结果 |
|
||||
|---|---|---|---|
|
||||
| 1 | 「我的报告」→ 生成报告,等它完成 → 打开 | 正文是中文,能往下滚。没有「中文 / English」。没有第二颗下载按钮 | |
|
||||
| 2 | 点目录里的某一章 | 正文跳到那一章。页面不卡住 | |
|
||||
| 3 | 点「导出」 | 只有这一处导出。说明里有一句「这份报告的英文版没有生成成功,只能导出中文版。」下载的是 Markdown | |
|
||||
| 4 | 打开一份这次更新之前生成的旧报告 | 能打开,正文仍是当时保存的那一版,没有自动重算 | |
|
||||
| 5 | 在旧报告地址后手动加 `?lang=en` | 仍显示原来的正文,不报错 | |
|
||||
| 6 | (仅当将来英文重新交付)点「English」再点「中文」 | 正文和目录跟着换,同一章还在。这轮新报告不应出现这个切换 | |
|
||||
@@ -0,0 +1,19 @@
|
||||
# 真机清单 · 完整数据版补齐(2026-10-08)
|
||||
|
||||
对应 `docs/tasks/TASK-report-full-data-fix-20261008.md`。电脑和手机各走一遍。第 1 步会用掉一次生成次数。本机没有登录态,也没有用浏览器点过,下表结果留空。
|
||||
|
||||
这一轮和上一份清单的差别,供对照:
|
||||
|
||||
- 新报告中文大约 21 万到 23 万字,超过约 20 万字的分页线。阅读页一次只显示一章,目录和「上一章 / 下一章」可以换章。
|
||||
- 四张核对过的盘,英文汉字数为 0。新生成的报告应出现「中文 / English」。点过去正文和目录一起换,停在同一章。
|
||||
- 生成卡片仍写「大约 10–30 秒」和「中英两版」。时长不是 staging 上量的,这轮没有改文案。
|
||||
- 以前生成的报告仍按当时保存的正文打开,不会自动变成这一版。
|
||||
|
||||
| # | 操作 | 应该看到 | 结果 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 「我的报告」→ 生成报告,等它完成 → 打开 | 正文是中文。有「中文 / English」。有「上一章 / 下一章」 | |
|
||||
| 2 | 点「English」,再点「中文」 | 正文和目录换成另一种语言,还停在刚才那一章。英文里看不到汉字 | |
|
||||
| 3 | 点目录里的某一章 | 正文跳到那一章。页面不卡住 | |
|
||||
| 4 | 点「导出」 | 只有这一处导出。新报告可以选择中文或 English,下载的是 Markdown。不出现「英文版没有生成成功」 | |
|
||||
| 5 | 打开一份这次更新之前生成的旧报告 | 能打开,正文仍是当时保存的那一版,没有自动重算 | |
|
||||
| 6 | 在旧报告地址后手动加 `?lang=en` | 仍显示原来的正文,不报错 | |
|
||||
@@ -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 | 打开一份这次更新之前生成的旧报告 | 能打开,正文仍是当时保存的那一版 | |
|
||||
+2
-1
@@ -634,7 +634,7 @@ Action results and errors never take page layout. They are toasts from `src/lib/
|
||||
|
||||
### Personal report centre
|
||||
|
||||
- **Structure:** inside the app shell, not a page of its own. The name 「我的报告」 sits alone in the 46px header. The body opens with the **generate card** (`.report-center-create`, 2026-09-29, product sketch): a centred `--color-canvas` sheet, `--space-6` below the header (BUG-1103: it used to sit flush under it), with a file icon, the title 「完整本命报告」 (what you get), one line 「星盘、力量、大运、年运、瑜伽共 6 章,中英两版;生成后可离开,完成时下方自动出现。」 and the filled 「生成报告」 button (what it does) — title and button no longer say the same thing — the page's only generate entry; the header button was deleted rather than kept as a second one. Below it, 「过往的报告」 heads the **row list** of reports; with none yet, a single quiet line 「还没有个人报告。」 replaces the old second big empty card. The supporting paragraph (「有填报到分钟的出生时间即可生成……」) and the 「共 N 份 · 已完成 N 份」 overview line above it were removed on 2026-09-28 (TASK-self-edit-avatar-menu-20260928 S3, product: 「这里的提示去掉」); the minute requirement still lives in the 生成 button's hover title.
|
||||
- **Structure:** inside the app shell, not a page of its own. The name 「我的报告」 sits alone in the 46px header. The body opens with the **generate card** (`.report-center-create`, 2026-09-29, product sketch): a centred `--color-canvas` sheet, `--space-6` below the header (BUG-1103: it used to sit flush under it), with a file icon, the title 「完整本命报告」 (what you get), one line 「星盘、力量、大运、年运和瑜伽的完整数据,中英两版;生成后可离开,完成时下方自动出现。」 and the filled 「生成报告」 button (what it does) — title and button no longer say the same thing — the page's only generate entry; the header button was deleted rather than kept as a second one. Below it, 「过往的报告」 heads the **row list** of reports; with none yet, a single quiet line 「还没有个人报告。」 replaces the old second big empty card. The supporting paragraph (「有填报到分钟的出生时间即可生成……」) and the 「共 N 份 · 已完成 N 份」 overview line above it were removed on 2026-09-28 (TASK-self-edit-avatar-menu-20260928 S3, product: 「这里的提示去掉」); the minute requirement still lives in the 生成 button's hover title.
|
||||
It used to be a standalone full-screen route with a `report-center-shell` root, a `report-center-topbar` holding one 「返回对话」 link, and a `report-center-hero` with a page-sized h1 — the sidebar vanished the moment you opened it, and the only way back was that link.
|
||||
- **Row list, not a card grid:** `.report-center-list` is one column of `.report-center-row`, hairline-separated by a 1px grid gap over a `--color-border` ground so each boundary is a single rule rather than two touching borders. A card grid costs one scan per card; past five or six reports the reader is looking for state, and a single column puts every status chip on the same x. D12.
|
||||
- **Paging (2026-09-30, BUG-1124):** the list shows the newest 10; under it an outline 「加载更多」 button (centred, at least 160px; full width below 767px) brings the next 10. While that page loads the button reads 「加载中」 and is disabled — no spinner. It disappears when nothing older exists. A failed page is a toast 「更早的报告没读出来,再试一次。」, the list stays. The 3s poll while a report is generating and the 刷新 button re-read the first page only and merge it over the pages already opened, so a refresh never collapses the list back to 10. Entering 我的报告 shows the cached first page at once and revalidates behind it (BUG-1123); a cold entry keeps the one shared waiting state.
|
||||
@@ -664,6 +664,7 @@ Action results and errors never take page layout. They are toasts from `src/lib/
|
||||
- **Switching editions keeps your place (BUG-1108):** the chapter at the top of the reader is remembered; the new edition renders every chapter up to it at once and scrolls back to it.
|
||||
- **判断不了的分盘提示 (2026-10-01, BUG-1139):** for a segment-v1 adoption whose card left D9/D10 undetermined, the longform body carries one plain paragraph directly under each such chart heading and its 数值位置表 heading — no new component, no blockquote, no colour. Under a chart heading it becomes the chart card's caption (`report-chart-grid-rehype.ts::parseCard` accepts exactly one paragraph between heading and fence), so the chart grid never splits. Added before the snapshot hash; legacy reports are untouched.
|
||||
- **到底部 / 回到顶部:** one floating pill (`.report-edge-jump`, fixed bottom-right, same look as the chat's 跳到最新). It reads 「到底部」 until the reader is within 480px of the end, then 「回到顶部」 — never both. Sections below the fold mount lazily, so the click first mounts every section synchronously (`flushSync`) and then scrolls to `scrollHeight`, landing on the real end in one click. It is not a scroll follower and owns no anchor; it is hidden on paper.
|
||||
- **Long full-data reports (2026-10-08):** when the Markdown is longer than 200,000 characters and has more than one chapter, the reader shows one chapter at a time. The TOC and 「上一章 / 下一章」 (44px, screen only) change the chapter; the article carries `data-chapter-index` so a 中文 / English switch returns to the same chapter. 「到底部」 opens the last chapter. Shorter reports keep the lazy-section reader. There is no full-text search. Print still mounts every chapter. Markdown download stays on the existing 「导出」 sheet.
|
||||
- **TOC rail:** a persistent right-hand column built from the outline's own `##`/`###` ids — no second slugger, and nothing that touches the chart-grid rehype pass. It is sticky under the report's own action bar, highlights the section in view via `IntersectionObserver`, and marks it with `aria-current="location"` plus a 2px `--color-action` bar. Placement is explicit (`grid-column: 2`) rather than DOM-ordered, so the narrow-screen drawer can stay first in the source. Below 860px the rail is gone and what remains is the collapsed 「目录」 drawer.
|
||||
- **Paper stays paper:** the rail reads in the app palette because it is chrome. `--report-paper` / `--report-rule` / `--report-accent` are the document's own, deliberately out of step with the app accent, and nothing in this surface puts them on chrome or takes chrome colour onto the page. D11, continuing D3.
|
||||
- **Surface:** page floor `--color-canvas-soft`; the Markdown article is a `--color-canvas` sheet with a hairline and `--radius-lg`. Print flattens the sheet, hides chrome and the TOC, and pins the light palette.
|
||||
|
||||
@@ -184,7 +184,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
|
||||
- 盘面不显示引擎品牌,也不替换成「自研引擎」「本站引擎」。岁差、交点等计算口径与准确性边界照旧。
|
||||
- 图标说明只写名称和已有事实:上升、行星、逆行、星座名称/编号、宫位、ASC/MC 等。没有可靠度数就不写度数,不加吉凶、性格或个人运势解释。
|
||||
- 鼠标悬停、键盘聚焦、手机轻点展示同一份说明,以标记旁的小气泡出现,不遮住被说明的标记。关闭说明不改变西洋相位选择。2026-09-29 晚起不再有默认提示句(原「停在或轻点盘内标记可查看说明。」已删,BUG-1101)。
|
||||
- 「我的报告」生成卡片:标题写得到什么「完整本命报告」,说明一句「星盘、力量、大运、年运、瑜伽共 6 章,中英两版;生成后可离开,完成时下方自动出现。」,按钮写动作「生成报告」。标题与按钮不说同一件事(BUG-1103)。
|
||||
- 「我的报告」生成卡片:标题写得到什么「完整本命报告」,说明一句「星盘、力量、大运、年运和瑜伽的完整数据,中英两版;生成后可离开,完成时下方自动出现。」,按钮写动作「生成报告」。标题与按钮不说同一件事(BUG-1103)。完整数据版超过约 20 万字符时,阅读页一次只画一章,目录和「上一章 / 下一章」换章;不另做全文搜索。导出仍是原来的「导出」按钮,下载 Markdown。
|
||||
- 分盘庙旺仅指入旺、自宫、落陷三档;未命中写「—」,上升写「不适用」。同位仅 D9 展示,其他分盘不留空列。缺能力声明不装作已支持。
|
||||
|
||||
## 星盘空状态、0 年大运、星历切日期
|
||||
|
||||
@@ -4298,6 +4298,32 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
@media (max-width: 767px) {
|
||||
.report-edge-jump { right: var(--space-4); bottom: calc(var(--space-4) + env(safe-area-inset-bottom)); }
|
||||
}
|
||||
.personal-report-chapter-pager {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-5);
|
||||
}
|
||||
.personal-report-chapter-pager button {
|
||||
min-height: 44px;
|
||||
padding: 0 var(--space-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 999px;
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-ink);
|
||||
font: inherit;
|
||||
font-size: var(--type-body-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
.personal-report-chapter-pager button:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
.personal-report-chapter-pager span {
|
||||
color: var(--color-ink-secondary);
|
||||
font-size: var(--type-body-sm);
|
||||
}
|
||||
.personal-report-toc-drawer { display: none; }
|
||||
.personal-report-toc-desktop p {
|
||||
margin: 0 0 var(--space-2);
|
||||
|
||||
@@ -214,7 +214,7 @@ export function PersonalReportCenter() {
|
||||
{/* The title says what you get, the button says what it does (2026-09-29):
|
||||
「生成个人报告」 over a 「生成报告」 button said the same thing twice. */}
|
||||
<h2 id="report-center-create-title">完整本命报告</h2>
|
||||
<p>星盘、力量、大运、年运、瑜伽共 6 章,中英两版;生成后可离开,完成时下方自动出现。</p>
|
||||
<p>星盘、力量、大运、年运和瑜伽的完整数据,中英两版;生成后可离开,完成时下方自动出现。</p>
|
||||
<GeneratePersonalReportButton onCreated={() => void load()} />
|
||||
</section>
|
||||
<section className="report-center-section">
|
||||
|
||||
@@ -107,9 +107,9 @@ function renderMarkdown(markdown: string, headings: readonly LongformHeading[],
|
||||
}
|
||||
|
||||
/**
|
||||
* 「到底部」 has to mount every lazy section before it scrolls. The flag lives
|
||||
* outside React state so the article view itself stays state-free (its tree is
|
||||
* memoized and must not re-render as a whole); only the sections subscribe.
|
||||
* 「到底部」 has to mount every lazy section before it scrolls. Scroll position
|
||||
* stays outside this view. The only state here is the chapter index and the
|
||||
* print flag; markdown trees stay in useMemo.
|
||||
*/
|
||||
type RevealSignal = { revealed: boolean; listeners: Set<() => void> };
|
||||
|
||||
@@ -163,9 +163,23 @@ const LazyMarkdownSection = memo(function LazyMarkdownSection({
|
||||
);
|
||||
});
|
||||
|
||||
function ReportToc({ headings, language }: { headings: readonly LongformHeading[]; language: ReportLanguage }) {
|
||||
/** Above this size the reader shows one chapter at a time. Shorter reports keep lazy sections. */
|
||||
export const FULL_DATA_CHAPTER_PAGE_CHARS = 200_000;
|
||||
|
||||
function ReportToc({
|
||||
headings,
|
||||
language,
|
||||
onSelect,
|
||||
activeHeadingId,
|
||||
}: {
|
||||
headings: readonly LongformHeading[];
|
||||
language: ReportLanguage;
|
||||
onSelect?: (id: string) => void;
|
||||
activeHeadingId?: string;
|
||||
}) {
|
||||
const label = language === "en" ? "Contents" : "目录";
|
||||
const [activeId, setActiveId] = useState(headings[0]?.id ?? "");
|
||||
const [observedId, setObservedId] = useState(headings[0]?.id ?? "");
|
||||
const activeId = activeHeadingId || observedId;
|
||||
|
||||
useEffect(() => {
|
||||
if (headings.length === 0 || typeof IntersectionObserver === "undefined") return;
|
||||
@@ -174,7 +188,7 @@ function ReportToc({ headings, language }: { headings: readonly LongformHeading[
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.sort((left, right) => left.boundingClientRect.top - right.boundingClientRect.top);
|
||||
const id = visible[0]?.target.id;
|
||||
if (id) setActiveId(id);
|
||||
if (id) setObservedId(id);
|
||||
}, { rootMargin: "-20% 0px -70% 0px", threshold: [0, 1] });
|
||||
for (const item of headings) {
|
||||
const node = document.getElementById(item.id);
|
||||
@@ -187,11 +201,11 @@ function ReportToc({ headings, language }: { headings: readonly LongformHeading[
|
||||
<nav aria-label={language === "en" ? "Report contents" : "报告目录"} className="personal-report-toc">
|
||||
<details className="personal-report-toc-drawer">
|
||||
<summary>{label}</summary>
|
||||
<TocList activeId={activeId} items={headings} />
|
||||
<TocList activeId={activeId} items={headings} onSelect={onSelect} />
|
||||
</details>
|
||||
<div className="personal-report-toc-desktop">
|
||||
<p>{label}</p>
|
||||
<TocList activeId={activeId} items={headings} />
|
||||
<TocList activeId={activeId} items={headings} onSelect={onSelect} />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
@@ -204,17 +218,92 @@ export function PersonalReportMarkdownView({ markdown, language = "zh", drawnThr
|
||||
drawnThrough?: number | null;
|
||||
}) {
|
||||
const outline = useMemo(() => buildLongformOutline(markdown), [markdown]);
|
||||
const chapters = useMemo(() => outline.sections.filter((section) => !section.eager), [outline]);
|
||||
const paged = markdown.length > FULL_DATA_CHAPTER_PAGE_CHARS && chapters.length > 1;
|
||||
const [chapter, setChapter] = useState(() => {
|
||||
if (drawnThrough === null) return 0;
|
||||
return Math.min(Math.max(drawnThrough, 0), Math.max(chapters.length - 1, 0));
|
||||
});
|
||||
const [printing, setPrinting] = useState(false);
|
||||
const reveal = useMemo<RevealSignal>(() => ({ revealed: false, listeners: new Set() }), []);
|
||||
useEffect(() => {
|
||||
if (!paged) return;
|
||||
const before = () => setPrinting(true);
|
||||
const after = () => setPrinting(false);
|
||||
window.addEventListener("beforeprint", before);
|
||||
window.addEventListener("afterprint", after);
|
||||
return () => {
|
||||
window.removeEventListener("beforeprint", before);
|
||||
window.removeEventListener("afterprint", after);
|
||||
};
|
||||
}, [paged]);
|
||||
// Synchronous on purpose: the caller scrolls to scrollHeight right after, and
|
||||
// that height is only final once every lazy section has committed.
|
||||
const revealAll = useCallback(() => flushSync(() => {
|
||||
if (paged) {
|
||||
setChapter(Math.max(chapters.length - 1, 0));
|
||||
return;
|
||||
}
|
||||
reveal.revealed = true;
|
||||
for (const listener of reveal.listeners) listener();
|
||||
}), [reveal]);
|
||||
}), [chapters.length, paged, reveal]);
|
||||
const lead = useMemo(
|
||||
() => (outline.leadMarkdown ? renderMarkdown(outline.leadMarkdown, outline.headings, language) : null),
|
||||
[outline, language],
|
||||
);
|
||||
const safeChapter = Math.min(chapter, Math.max(chapters.length - 1, 0));
|
||||
const currentChapter = chapters[safeChapter];
|
||||
const pagedChapter = useMemo(() => (
|
||||
paged && currentChapter
|
||||
? renderMarkdown(currentChapter.markdown, currentChapter.headings, language)
|
||||
: null
|
||||
), [paged, currentChapter, language]);
|
||||
const printedChapters = useMemo(() => (
|
||||
paged && printing
|
||||
? chapters.map((section) => renderMarkdown(section.markdown, section.headings, language))
|
||||
: null
|
||||
), [paged, printing, chapters, language]);
|
||||
const openChapter = useCallback((id: string) => {
|
||||
const index = chapters.findIndex((section) => section.id === id || section.headings.some((heading) => heading.id === id));
|
||||
if (index >= 0) setChapter(index);
|
||||
}, [chapters]);
|
||||
|
||||
if (paged) {
|
||||
return (
|
||||
<div className="personal-report-md-layout">
|
||||
<ReportToc
|
||||
activeHeadingId={currentChapter?.id}
|
||||
headings={outline.headings}
|
||||
language={language}
|
||||
onSelect={openChapter}
|
||||
/>
|
||||
<article className="personal-report-document personal-report-md-article" data-chapter-index={safeChapter}>
|
||||
{safeChapter === 0 && lead ? (
|
||||
<section className="personal-report-md-lead">
|
||||
{lead}
|
||||
</section>
|
||||
) : null}
|
||||
{printing
|
||||
? chapters.map((section, index) => (
|
||||
<section className="personal-report-md-section" id={section.id} key={section.id}>
|
||||
{printedChapters?.[index]}
|
||||
</section>
|
||||
))
|
||||
: currentChapter ? (
|
||||
<section className="personal-report-md-section" id={currentChapter.id}>
|
||||
{pagedChapter}
|
||||
</section>
|
||||
) : null}
|
||||
<nav aria-label="章节" className="personal-report-chapter-pager personal-report-screen-only">
|
||||
<button type="button" disabled={safeChapter <= 0} onClick={() => setChapter(safeChapter - 1)}>上一章</button>
|
||||
<span>{safeChapter + 1} / {chapters.length}</span>
|
||||
<button type="button" disabled={safeChapter >= chapters.length - 1} onClick={() => setChapter(safeChapter + 1)}>下一章</button>
|
||||
</nav>
|
||||
</article>
|
||||
<ReportEdgeJump revealAll={revealAll} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="personal-report-md-layout">
|
||||
@@ -226,10 +315,10 @@ export function PersonalReportMarkdownView({ markdown, language = "zh", drawnThr
|
||||
</section>
|
||||
) : null}
|
||||
{/* Each section is one chapter heading, in the same order as the article's h2s. */}
|
||||
{outline.sections.map((section, chapter) => (section.eager ? null : (
|
||||
{outline.sections.map((section, index) => (section.eager ? null : (
|
||||
<LazyMarkdownSection
|
||||
key={section.id}
|
||||
drawn={drawnThrough !== null && chapter <= drawnThrough}
|
||||
drawn={drawnThrough !== null && index <= drawnThrough}
|
||||
language={language}
|
||||
reveal={reveal}
|
||||
section={section}
|
||||
@@ -244,9 +333,11 @@ export function PersonalReportMarkdownView({ markdown, language = "zh", drawnThr
|
||||
function TocList({
|
||||
items,
|
||||
activeId,
|
||||
onSelect,
|
||||
}: {
|
||||
items: readonly LongformHeading[];
|
||||
activeId: string;
|
||||
onSelect?: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<ol>
|
||||
@@ -256,6 +347,10 @@ function TocList({
|
||||
aria-current={item.id === activeId ? "location" : undefined}
|
||||
className={item.id === activeId ? "is-current" : undefined}
|
||||
href={`#${item.id}`}
|
||||
onClick={onSelect ? (event) => {
|
||||
event.preventDefault();
|
||||
onSelect(item.id);
|
||||
} : undefined}
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
|
||||
@@ -218,6 +218,11 @@ function readEnglishEdition(json: Record<string, unknown>): Pick<
|
||||
|
||||
/** Index of the chapter heading at or above the top of the reader, or null at the very top. */
|
||||
function currentChapterIndex(): number | null {
|
||||
const paged = document.querySelector<HTMLElement>(".personal-report-md-article[data-chapter-index]");
|
||||
if (paged) {
|
||||
const index = Number(paged.dataset.chapterIndex);
|
||||
return Number.isFinite(index) ? index : null;
|
||||
}
|
||||
const reader = document.querySelector<HTMLElement>(".personal-report-reader");
|
||||
if (!reader || reader.scrollTop < 40) return null;
|
||||
const top = reader.getBoundingClientRect().top + 80;
|
||||
@@ -282,6 +287,11 @@ export function PersonalReportPage({ reportId, initialLanguage = "zh" }: { repor
|
||||
setDrawnThrough(chapter);
|
||||
if (chapter !== null) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
const article = document.querySelector<HTMLElement>(".personal-report-md-article[data-chapter-index]");
|
||||
if (article) {
|
||||
article.scrollIntoView({ block: "start" });
|
||||
return;
|
||||
}
|
||||
document.querySelectorAll<HTMLElement>(".personal-report-md-article h2")[chapter]?.scrollIntoView({ block: "start" });
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -151,7 +151,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, edition: "reader_main", languages: ["zh", "en"] }),
|
||||
body: JSON.stringify({ ...input.payload, include_fact_tables: true, edition: "full_data", languages: ["zh", "en"] }),
|
||||
cache: "no-store",
|
||||
signal,
|
||||
});
|
||||
|
||||
@@ -77,6 +77,29 @@ test("markdown view skips HTML, blocks remote resources, wraps tables, and lazy-
|
||||
assert.match(markup, /aria-label="报告目录"/);
|
||||
});
|
||||
|
||||
test("long markdown pages one chapter and keeps the first paint small", () => {
|
||||
const chapters = Array.from({ length: 80 }, (_, index) => {
|
||||
const body = index === 0 ? `第一章正文${"字".repeat(80)}` : `秘密正文-${index}`;
|
||||
return `## 第${index}章\n\n${body}\n`;
|
||||
});
|
||||
for (const size of [1_000_000, 3_500_000]) {
|
||||
const markdown = `# 标题\n\n${chapters.join("\n")}${"补".repeat(size)}`;
|
||||
const before = process.memoryUsage().heapUsed;
|
||||
const started = Date.now();
|
||||
const markup = renderToStaticMarkup(React.createElement(PersonalReportMarkdownView, { markdown }));
|
||||
const elapsed = Date.now() - started;
|
||||
const delta = process.memoryUsage().heapUsed - before;
|
||||
assert.ok(elapsed < 1500, `size ${size} first paint ${elapsed}ms heap ${delta}`);
|
||||
assert.match(markup, /第一章正文/);
|
||||
assert.match(markup, /上一章/);
|
||||
assert.match(markup, /下一章/);
|
||||
assert.match(markup, /1 \/ 80/);
|
||||
assert.doesNotMatch(markup, /秘密正文-79/);
|
||||
assert.doesNotMatch(markup, /type="search"|placeholder="搜索"/);
|
||||
assert.ok(delta < 120 * 1024 * 1024, `size ${size} heap delta ${delta}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("export filename includes the report date", () => {
|
||||
assert.equal(personalReportMarkdownFilename("2026-09-06T12:00:00.000Z"), "个人报告-2026-09-06");
|
||||
});
|
||||
|
||||
@@ -175,7 +175,11 @@ test("article markdown trees are memoized and hold no scroll state", () => {
|
||||
const viewEnd = src.indexOf("\nfunction TocList", viewStart);
|
||||
assert.ok(viewStart >= 0 && viewEnd > viewStart);
|
||||
const view = src.slice(viewStart, viewEnd);
|
||||
assert.doesNotMatch(view, /\buseState\b/);
|
||||
// 原值: 这个函数里不得出现 useState。新值: 只允许章节序号和打印标记两处。原因: 超过 20 万字要按章翻页,滚动位置仍不放在这里。
|
||||
assert.deepEqual(view.match(/\buseState\b/g), ["useState", "useState"]);
|
||||
assert.match(view, /const \[chapter, setChapter\] = useState/);
|
||||
assert.match(view, /const \[printing, setPrinting\] = useState\(false\)/);
|
||||
assert.doesNotMatch(view, /useState\([\s\S]{0,80}scroll/i);
|
||||
const lines = src.split("\n");
|
||||
lines.forEach((line, index) => {
|
||||
if (!line.includes("renderMarkdown(")) return;
|
||||
|
||||
@@ -101,12 +101,13 @@ function executeReadyReportExport(cached: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
test("new longform generation requests the reader_main edition", () => {
|
||||
test("new longform generation requests the full_data 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, /edition:\s*"full_data"/);
|
||||
assert.doesNotMatch(generateSource, /edition:\s*"reader_main"/);
|
||||
assert.match(generateSource, /include_fact_tables:\s*true/);
|
||||
});
|
||||
|
||||
|
||||
@@ -127,7 +127,9 @@ test("switching editions keeps the reader on the same chapter (BUG-1108)", () =>
|
||||
assert.match(page, /setDrawnThrough\(chapter\);/);
|
||||
assert.match(page, /drawnThrough=\{drawnThrough\}/);
|
||||
const view = read("../src/components/personal-report/personal-report-markdown-view.tsx");
|
||||
assert.match(view, /drawn=\{drawnThrough !== null && chapter <= drawnThrough\}/);
|
||||
// 原值: drawn={drawnThrough !== null && chapter <= drawnThrough}。新值: 循环变量改为 index,超长报告的初始章取 drawnThrough。原因: 懒加载路径改名;按章翻页仍从切换前的章号打开。
|
||||
assert.match(view, /drawn=\{drawnThrough !== null && index <= drawnThrough\}/);
|
||||
assert.match(view, /Math\.min\(Math\.max\(drawnThrough, 0\), Math\.max\(chapters\.length - 1, 0\)\)/);
|
||||
assert.match(view, /useState\(section\.eager \|\| drawn\)/);
|
||||
});
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -0,0 +1,530 @@
|
||||
"""English glossary for the PL9 bilingual AI-density export.
|
||||
|
||||
The yoga/dosha engines emit `effects` as Chinese short phrases and `combination`
|
||||
as mixed Chinese/English strings. The English renderer must translate these
|
||||
instead of deleting CJK and emitting a placeholder. This module holds the
|
||||
Chinese -> English vocabulary used for that translation.
|
||||
|
||||
词汇来源:references/yoga_rules.json 的 effects 字段词频统计(高频词优先),
|
||||
覆盖 yoga/dosha 解释层最常出现的短句;低频词若未命中则按省略处理,不输出占位符。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# 瑜伽/宫位 effects 中文短句 -> 英文。按词频从高到低整理。
|
||||
EFFECTS_EN: dict[str, str] = {
|
||||
"领域受限": "domain constrained",
|
||||
"需要额外努力": "requires additional effort",
|
||||
"充足": "adequate",
|
||||
"转化困境为机遇": "turns hardship into opportunity",
|
||||
"不讨喜": "not widely liked",
|
||||
"事业通过智慧实现": "career achieved through wisdom",
|
||||
"享受丰富": "enjoys abundance",
|
||||
"人缘好": "well liked",
|
||||
"命运力量在角宫": "destiny power in angular houses",
|
||||
"命运助力明显": "strong support from destiny",
|
||||
"善于辞令": "eloquent speech",
|
||||
"基础发展稳定": "stable foundational development",
|
||||
"多元才华": "versatile talents",
|
||||
"子女受其庇护": "children receive protection",
|
||||
"待补充": "to be supplemented",
|
||||
"心地善良": "kind-hearted",
|
||||
"心智有背景支持": "mind supported by background conditions",
|
||||
"心智稳定性增强": "increased mental stability",
|
||||
"性情愉快": "pleasant temperament",
|
||||
"性格坚定": "steadfast character",
|
||||
"持续收入": "steady income",
|
||||
"政治才华": "political talent",
|
||||
"月亮前后均有支撑": "Moon supported on both sides",
|
||||
"物质充裕": "material abundance",
|
||||
"独处中成长": "growth through solitude",
|
||||
"生活舒适": "comfortable life",
|
||||
"盈利丰厚": "substantial profits",
|
||||
"真诚待人": "sincere dealings",
|
||||
"祖先福报影响子女": "ancestral blessings affect children",
|
||||
"经济稳定": "financial stability",
|
||||
"自我、财富、努力、根基彼此支撑": "self, wealth, effort, and foundations support one another",
|
||||
"自我表达": "self-expression",
|
||||
"言辞刻薄": "harsh speech",
|
||||
"贵人带来社会地位": "benefactors bring social status",
|
||||
"贵人或隐性资源": "benefactors or hidden resources",
|
||||
"资源与适应力较强": "strong resources and adaptability",
|
||||
# 才能 / 事业 / 地位(高频)
|
||||
"卓越才能": "exceptional talent",
|
||||
"领域领军": "leading in one's field",
|
||||
"人格魅力": "personal magnetism",
|
||||
"受人尊敬": "widely respected",
|
||||
"命运眷顾": "favored by destiny",
|
||||
"智慧": "wisdom",
|
||||
"事业成功": "career success",
|
||||
"长寿": "longevity",
|
||||
"财富": "wealth",
|
||||
"艺术才华": "artistic talent",
|
||||
"社会地位": "social standing",
|
||||
"品德高尚": "noble character",
|
||||
"逆境崛起": "rises from adversity",
|
||||
"因祸得福": "turns misfortune into fortune",
|
||||
"权力": "power",
|
||||
"权力地位": "power and status",
|
||||
"社会地位高": "high social status",
|
||||
"智慧卓越": "outstanding wisdom",
|
||||
"财富积累": "wealth accumulation",
|
||||
"物质成功": "material success",
|
||||
"财富充裕": "abundant wealth",
|
||||
"自力更生": "self-reliant",
|
||||
"学识渊博": "profound learning",
|
||||
"婚姻美满": "happy marriage",
|
||||
"审美卓越": "refined aesthetic sense",
|
||||
"健康需注意": "watch health",
|
||||
"稳定": "stability",
|
||||
"领导力": "leadership",
|
||||
"贵人运强": "strong benefactor luck",
|
||||
"口才出众": "eloquent",
|
||||
"受人爱戴": "beloved",
|
||||
"精力充沛": "energetic",
|
||||
"困境中成长": "grows through hardship",
|
||||
"受人敬仰": "admired",
|
||||
"精神修养": "spiritual cultivation",
|
||||
"领导能力": "leadership ability",
|
||||
"精神力量": "spiritual strength",
|
||||
"财务困难": "financial difficulty",
|
||||
"行动力": "initiative",
|
||||
"权威地位": "authority",
|
||||
"精神成就": "spiritual attainment",
|
||||
"物质享受": "material comfort",
|
||||
"婚姻幸福": "marital happiness",
|
||||
"学术成就": "academic achievement",
|
||||
"创造力": "creativity",
|
||||
"高位": "high position",
|
||||
"身体健康": "good health",
|
||||
"社会影响力": "social influence",
|
||||
"精神导师": "spiritual mentor",
|
||||
"克服困难": "overcomes obstacles",
|
||||
"转化能力": "transformative ability",
|
||||
"逆境中崛起": "rises in adversity",
|
||||
"投资收益": "investment returns",
|
||||
"财运亨通": "thriving fortune",
|
||||
"生活幸福": "happy life",
|
||||
"生活富足": "prosperous life",
|
||||
"名声良好": "good reputation",
|
||||
"精神挑战": "spiritual challenge",
|
||||
"行动力强": "strong initiative",
|
||||
"商业头脑": "business acumen",
|
||||
"意志坚定": "strong will",
|
||||
"领导才能": "leadership talent",
|
||||
"领导气质": "leadership quality",
|
||||
"事业辉煌": "brilliant career",
|
||||
"智慧与行动力兼备": "wisdom with drive",
|
||||
"正义感强": "strong sense of justice",
|
||||
"智力超群": "exceptional intelligence",
|
||||
"智慧通达": "penetrating wisdom",
|
||||
"多才多艺": "versatile",
|
||||
"生活稳定": "stable life",
|
||||
"有依靠": "well-supported",
|
||||
"精神成长": "spiritual growth",
|
||||
"家庭幸福": "happy family",
|
||||
"影响力": "influence",
|
||||
"自我实现": "self-realization",
|
||||
"子女运好": "good fortune with children",
|
||||
"王者气质": "regal bearing",
|
||||
"艺术气质": "artistic temperament",
|
||||
"家庭问题": "family issues",
|
||||
"精神压力": "mental stress",
|
||||
"精神领导力": "spiritual leadership",
|
||||
"情感丰富": "emotionally rich",
|
||||
"全面成功": "all-round success",
|
||||
"子女有成就": "accomplished children",
|
||||
"子女智慧": "wise children",
|
||||
"婚姻和谐": "harmonious marriage",
|
||||
"配偶有魅力": "attractive spouse",
|
||||
"婚姻延迟": "delayed marriage",
|
||||
"名声显赫": "great renown",
|
||||
"名声": "reputation",
|
||||
"社交网络强大": "strong social network",
|
||||
"政治权力": "political power",
|
||||
"战胜对手": "overcomes rivals",
|
||||
"祖先庇佑": "ancestral blessings",
|
||||
"宗教权威": "religious authority",
|
||||
"收入不稳定": "unstable income",
|
||||
"家庭和谐": "family harmony",
|
||||
"子孙兴旺": "flourishing descendants",
|
||||
"后代繁荣": "prosperous descendants",
|
||||
"后代有成就": "accomplished descendants",
|
||||
"家族延续": "family continuity",
|
||||
"学识": "learning",
|
||||
"内在平静": "inner peace",
|
||||
"慈悲": "compassion",
|
||||
"保护": "protection",
|
||||
"保守": "conservative",
|
||||
"旅行": "travel",
|
||||
"变动": "change",
|
||||
"海外": "overseas",
|
||||
"不稳定": "instability",
|
||||
"传统": "tradition",
|
||||
"孤独": "solitude",
|
||||
"人际关系差": "poor relationships",
|
||||
"兄弟和睦": "harmony with siblings",
|
||||
"勇气": "courage",
|
||||
"竞争力": "competitiveness",
|
||||
# 事业 / 财富 补充
|
||||
"事业有成": "career achievement",
|
||||
"事业顺利": "smooth career",
|
||||
"事业巅峰": "career peak",
|
||||
"事业成就": "career accomplishment",
|
||||
"职业使命感强": "strong sense of vocation",
|
||||
"政府高层职位": "senior government post",
|
||||
"人脉资源强": "strong network resources",
|
||||
"收入与社会地位同步提升": "rising income and status",
|
||||
"慢而稳的事业成功": "slow but steady career success",
|
||||
"专业化": "specialization",
|
||||
"军事/体育/工程领域成功": "success in military, sports or engineering",
|
||||
"竞争意识强": "strong competitive drive",
|
||||
"敌人变朋友": "enemies become friends",
|
||||
"竞争中获利": "profits through competition",
|
||||
"愿望实现": "wishes fulfilled",
|
||||
"愿望通过智慧实现": "wishes fulfilled through wisdom",
|
||||
"愿望通过地位实现": "wishes fulfilled through status",
|
||||
"才华变事业": "talent turned into career",
|
||||
"创造力变现": "creativity monetized",
|
||||
"商业成功": "business success",
|
||||
"物质丰裕": "material abundance",
|
||||
"收入稳定": "stable income",
|
||||
"家庭资产": "family assets",
|
||||
"财富自主": "financial independence",
|
||||
"自力更生致富": "self-made wealth",
|
||||
"独立创业": "independent entrepreneurship",
|
||||
"财富增长": "wealth growth",
|
||||
"财富丰厚": "substantial wealth",
|
||||
"稳定财富": "stable wealth",
|
||||
"巨量财富": "great wealth",
|
||||
"持续财富积累": "sustained wealth accumulation",
|
||||
"投资回报": "investment returns",
|
||||
"投资成功": "successful investment",
|
||||
"通过才华/运气致富": "wealth through talent or luck",
|
||||
"祖先财富": "ancestral wealth",
|
||||
"储蓄丰厚": "ample savings",
|
||||
"理财能力": "financial acumen",
|
||||
"谨慎理财": "prudent money management",
|
||||
# 婚姻 / 家庭
|
||||
"婚姻稳定": "stable marriage",
|
||||
"优质婚姻": "quality marriage",
|
||||
"婚姻显赫": "prestigious marriage",
|
||||
"婚姻挑战": "marital challenges",
|
||||
"配偶杰出": "outstanding spouse",
|
||||
"配偶贤良": "virtuous spouse",
|
||||
"配偶支持": "supportive spouse",
|
||||
"配偶互相支持": "mutually supportive spouses",
|
||||
"配偶有成就": "accomplished spouse",
|
||||
"配偶有社会地位": "spouse with social standing",
|
||||
"配偶带来财富": "spouse brings wealth",
|
||||
"配偶带来好运": "spouse brings good fortune",
|
||||
"配偶关系紧张": "tense spousal relationship",
|
||||
"家庭和睦": "family harmony",
|
||||
"后代昌盛": "prosperous descendants",
|
||||
"子女有出息": "promising children",
|
||||
"子女缘厚": "strong bond with children",
|
||||
"多子多福": "many children, many blessings",
|
||||
"子女贤良": "virtuous children",
|
||||
"子女众多": "many children",
|
||||
"独生子女": "only child",
|
||||
"子女少但优秀": "few but accomplished children",
|
||||
"子女带来财富": "children bring wealth",
|
||||
"子女带来名声": "children bring renown",
|
||||
# 健康 / 身心
|
||||
"健康良好": "good health",
|
||||
"健康问题": "health concerns",
|
||||
"慢性疾病": "chronic illness",
|
||||
"慢性病倾向": "tendency to chronic illness",
|
||||
"少病少灾": "few illnesses",
|
||||
"活力充沛": "full of vitality",
|
||||
"生命力强": "strong vitality",
|
||||
"体质较弱": "weak constitution",
|
||||
"疾病": "illness",
|
||||
"延寿": "extended longevity",
|
||||
"长寿健康": "long and healthy life",
|
||||
"智慧长寿": "wisdom and longevity",
|
||||
"寿命稳定": "stable lifespan",
|
||||
"疾病抵抗力": "disease resistance",
|
||||
"免疫系统": "immune system",
|
||||
# 心智 / 灵性
|
||||
"智慧超群": "outstanding wisdom",
|
||||
"辩才无碍": "unimpeded eloquence",
|
||||
"沟通能力": "communication ability",
|
||||
"学业有成": "academic success",
|
||||
"知识渊博": "vast knowledge",
|
||||
"教育成功": "educational success",
|
||||
"意志力": "willpower",
|
||||
"苦行": "austerity",
|
||||
"精神修行": "spiritual practice",
|
||||
"内在力量": "inner strength",
|
||||
"家庭导向": "family-oriented",
|
||||
"安全感": "sense of security",
|
||||
"勤劳": "diligent",
|
||||
"节俭": "frugal",
|
||||
"积累": "accumulation",
|
||||
"务实": "pragmatic",
|
||||
"家庭": "family",
|
||||
"自由": "freedom",
|
||||
"社交": "social",
|
||||
"起伏": "ups and downs",
|
||||
"财务波动": "financial fluctuation",
|
||||
"严厉": "strict",
|
||||
"批评": "criticism",
|
||||
"精神痛苦": "mental anguish",
|
||||
"宗教": "religion",
|
||||
"祭祀": "ritual",
|
||||
"精神追求": "spiritual pursuit",
|
||||
"灵性成长": "spiritual growth",
|
||||
"灵性觉醒": "spiritual awakening",
|
||||
"灵性智慧": "spiritual wisdom",
|
||||
"终极解脱": "final liberation",
|
||||
"解脱": "liberation",
|
||||
"解脱智慧": "wisdom of liberation",
|
||||
"超越生死": "beyond birth and death",
|
||||
"灵性洞察": "spiritual insight",
|
||||
"精神探索": "spiritual exploration",
|
||||
"精神圆满": "spiritual fulfillment",
|
||||
"宇宙和谐": "cosmic harmony",
|
||||
"终极智慧": "ultimate wisdom",
|
||||
"天才级智力": "genius-level intellect",
|
||||
"超强记忆力": "exceptional memory",
|
||||
"高等教育": "higher education",
|
||||
"哲学洞察": "philosophical insight",
|
||||
"艺术天赋": "artistic gift",
|
||||
"艺术创造力": "artistic creativity",
|
||||
"音乐天赋": "musical gift",
|
||||
"浪漫魅力": "romantic charm",
|
||||
"美貌": "beauty",
|
||||
"非传统思想": "unconventional thinking",
|
||||
# 负向 / 挑战(少量关键词,避免占位)
|
||||
"欺诈": "deceit",
|
||||
"不诚实": "dishonesty",
|
||||
"阴谋": "intrigue",
|
||||
"信任危机": "trust issues",
|
||||
"债务问题": "debt problems",
|
||||
"财务不稳定": "financial instability",
|
||||
"储蓄困难": "difficulty saving",
|
||||
"人脉关系淡薄": "weak personal network",
|
||||
"健康挑战": "health challenges",
|
||||
"情感孤立": "emotional isolation",
|
||||
"社交困难": "social difficulty",
|
||||
"名声受损": "reputation damaged",
|
||||
"意外事故": "accidents",
|
||||
"身体虚弱": "physical frailty",
|
||||
"视力问题": "vision problems",
|
||||
"判断失误": "errors of judgment",
|
||||
"需要努力": "requires effort",
|
||||
"需要努力突破": "requires effort to break through",
|
||||
"需要韧性": "requires resilience",
|
||||
"需要节约": "requires thrift",
|
||||
"需节俭": "needs thrift",
|
||||
"挑战多": "many challenges",
|
||||
"人生有挑战": "life has challenges",
|
||||
"人生挑战": "life challenges",
|
||||
"不安定": "unsettled",
|
||||
"漂泊": "wandering",
|
||||
"不安于室": "restless",
|
||||
# 婚恋 / 家庭 / 健康补充(2026-09 全量导出缺词补齐)
|
||||
"性格坚毅": "resolute character",
|
||||
"口才与财富兼备": "eloquence with wealth",
|
||||
"充满活力": "full of vigor",
|
||||
"权力权威": "power and authority",
|
||||
"个人魅力": "personal charm",
|
||||
"需努力积累": "requires diligent accumulation",
|
||||
"财务压力": "financial pressure",
|
||||
"家庭纠纷": "family disputes",
|
||||
"需节俭持家": "needs frugal household management",
|
||||
"人生顺遂": "smooth and favorable life",
|
||||
"灵性提升": "spiritual elevation",
|
||||
"财富地位": "wealth and status",
|
||||
"极高地位": "extremely high status",
|
||||
"名声远扬": "far-reaching renown",
|
||||
"道德致富": "wealth through integrity",
|
||||
"慷慨大方": "generous",
|
||||
"艺术财富": "artistic wealth",
|
||||
"奢侈品": "luxury goods",
|
||||
"财富波动": "wealth fluctuation",
|
||||
"严重财务困难": "severe financial difficulty",
|
||||
"需要极度节俭": "requires extreme thrift",
|
||||
"需要匹配化解": "requires matching remedies",
|
||||
"月亮受保护": "Moon protected",
|
||||
"情感稳定": "emotional stability",
|
||||
"全方位成功": "all-around success",
|
||||
"四通八达": "well-connected in all directions",
|
||||
"坚不可摧的成功": "unshakeable success",
|
||||
"强大意志力": "strong willpower",
|
||||
"星盘力量翻倍": "doubled chart strength",
|
||||
"命运高度一致": "highly aligned destiny",
|
||||
"宗教政治地位": "religious and political standing",
|
||||
"太阳后方有行星支持": "planets following the Sun",
|
||||
"表达力与行动力增强": "enhanced expression and drive",
|
||||
"自我驱动力较强": "strong self-drive",
|
||||
"太阳前方有行星铺垫": "planets preceding the Sun",
|
||||
"内在准备力强": "strong inner preparation",
|
||||
"重视计划与隐性资源": "values planning and hidden resources",
|
||||
"太阳前后均有行星支撑": "planets supporting the Sun on both sides",
|
||||
"自我表达较完整": "relatively complete self-expression",
|
||||
"行动前后资源较足": "sufficient resources before and after action",
|
||||
"资源积累能力": "resource accumulation ability",
|
||||
"心智有后续支撑": "mind has follow-up support",
|
||||
"配偶品德高尚": "virtuous spouse",
|
||||
"经常旅行": "frequent travel",
|
||||
"被亲属遗弃": "abandoned by relatives",
|
||||
"缺乏家庭支持": "lack of family support",
|
||||
"家庭关系疏远": "distant family relations",
|
||||
"母亲健康不佳": "poor maternal health",
|
||||
"与母亲缘薄": "weak bond with mother",
|
||||
"母亲早逝": "early loss of mother",
|
||||
"心动/感情激活观察": "romantic-activation observation",
|
||||
"承诺掂量": "commitment weighing",
|
||||
"不等于法律婚姻": "does not equal legal marriage",
|
||||
}
|
||||
|
||||
# 英文星座名集合(用于 combination 黏连修复,避免与 dignity 词黏连)。
|
||||
SIGNS_EN: frozenset[str] = frozenset({
|
||||
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
||||
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
||||
})
|
||||
|
||||
# 英文行星名集合。
|
||||
PLANETS_EN: frozenset[str] = frozenset({
|
||||
"Sun", "Moon", "Mars", "Mercury", "Jupiter", "Venus", "Saturn", "Rahu", "Ketu",
|
||||
})
|
||||
|
||||
# dignity / 强度词(用于黏连修复)。
|
||||
DIGNITY_EN: frozenset[str] = frozenset({
|
||||
"exalted", "debilitated", "strong", "weak", "moderate", "medium",
|
||||
"own", "moolatrikona", "friendly", "neutral", "enemy", "combust",
|
||||
})
|
||||
|
||||
# 中文行星名 / 星座名 / 结构词 -> 英文。用于 combination 混排串的逐词替换,
|
||||
# 使删 CJK 后不产生黏连或残片。
|
||||
CN_TERMS_EN: dict[str, str] = {
|
||||
"自我": "self", "表现为": "expressed as", "节律": "rhythm", "学习": "learning",
|
||||
"写作": "writing", "沟通": "communication", "移动": "movement", "自主行动": "independent action",
|
||||
"居住": "residence", "家庭": "family", "内在安全感": "inner security", "资产基础": "asset foundation",
|
||||
"先稳住根基再发挥": "stabilize foundations before expression", "祖先祭拜": "ancestral rites",
|
||||
"捐赠黑芝麻和铁器": "donate black sesame and iron items", "供养婆罗门": "support Brahmins",
|
||||
"相邻": "adjacent", "指标": "indicators", "内在": "inner", "星": "planet",
|
||||
"1-4宫主形成命宫基础链": "1st-4th house lords form the ascendant foundational chain",
|
||||
"4宫/4宫主受凶星、Rahu、Maandi/Mandi 等严重影响": "house 4/its lord severely affected by malefics, Rahu, and Maandi/Mandi",
|
||||
"Dharidhra:财帛/收益主落凶宫或Lagna/Maraka/Dusthana组合": "Dharidhra: wealth/gains lord in a difficult house or Lagna/Maraka/Dusthana combination",
|
||||
"Rahu在5且非土星Navamsa,或7宫主关联行星的Navamsa主落1/2/5": "Rahu in house 5 and not Saturn Navamsa, or Navamsa lord of a planet associated with the 7th lord falls in 1/2/5",
|
||||
"月亮受凶星夹制/合相/相位,或4宫主Navamsa链最终落6/8/12": "Moon hemmed/conjunct/aspected by malefics, or the 4th lord's Navamsa chain ultimately falls in 6/8/12",
|
||||
"第4宫有天然吉星/强星/吉性星座,或上升主入4宫且受吉星影响": "house 4 has natural benefic/strong planets or a benefic sign, or the ascendant lord enters house 4 and is influenced by benefics",
|
||||
"行星集中在角宫(Chakra 轮盘)": "planets concentrated in angular houses (Chakra wheel)",
|
||||
"中强": "medium-strong",
|
||||
"基础链": "foundational chain", "形成": "forms", "分布在": "distributed across",
|
||||
"六个星座": "six signs", "位于": "located in", "入": "enters",
|
||||
"受凶星": "affected by malefics", "凶星": "malefic", "天然吉星": "natural benefic",
|
||||
"吉性星座": "benefic sign", "关联": "associated with", "相位": "aspected",
|
||||
"合相": "conjunct", "夹制": "hemmed", "最终落": "ultimately falls in",
|
||||
"处于": "is in", "不利状态": "an unfavorable state", "严重影响": "severely affected",
|
||||
"非": "not ", "角宫主": "angular house lord", "三方宫主": "trinal house lord",
|
||||
"财帛": "wealth", "收益": "gains", "基础": "foundation", "自定义条件满足": "custom condition met",
|
||||
"检测器未返回组合文本": "detector returned no combination text", "未返回": "not returned",
|
||||
"候选组合": "Candidate combination",
|
||||
"条件未齐,暂不作结果判断。": "Required conditions are incomplete; no outcome inference is made.",
|
||||
"缺少条件输入": "Missing condition inputs",
|
||||
"待评估": "pending evaluation", "七曜": "seven planets", "星座": "sign",
|
||||
"行星": "planet", "不利": "unfavorable", "影响": "influence", "的": "'s",
|
||||
"上升主": "ascendant lord", "宫主": "house lord", "互为": "mutual",
|
||||
"合于": "conjunct", "变动星座": "mutable sign", "且": "and", "或": "or",
|
||||
"强": "strong", "弱": "weak", "第": "house ", "宫": "",
|
||||
"火星在第4宫(从月亮的2/12宫)": "Mars in house 4 (2nd/12th from Moon)",
|
||||
"充足": "adequate",
|
||||
"转化困境为机遇": "turns hardship into opportunity",
|
||||
"不讨喜": "not widely liked", "事业通过智慧实现": "career achieved through wisdom",
|
||||
"享受丰富": "enjoys abundance", "人缘好": "well liked",
|
||||
"命运力量在角宫": "destiny power in angular houses", "命运助力明显": "strong support from destiny",
|
||||
"善于辞令": "eloquent speech", "基础发展稳定": "stable foundational development",
|
||||
"多元才华": "versatile talents", "子女受其庇护": "children receive protection",
|
||||
"待补充": "to be supplemented", "心地善良": "kind-hearted",
|
||||
"心智有背景支持": "mind supported by background conditions", "心智稳定性增强": "increased mental stability",
|
||||
"性情愉快": "pleasant temperament", "性格坚定": "steadfast character",
|
||||
"持续收入": "steady income", "政治才华": "political talent",
|
||||
"月亮前后均有支撑": "Moon supported on both sides", "物质充裕": "material abundance",
|
||||
"独处中成长": "growth through solitude", "生活舒适": "comfortable life",
|
||||
"盈利丰厚": "substantial profits", "真诚待人": "sincere dealings",
|
||||
"祖先福报影响子女": "ancestral blessings affect children", "经济稳定": "financial stability",
|
||||
"自我、财富、努力、根基彼此支撑": "self, wealth, effort, and foundations support one another",
|
||||
"自我表达": "self-expression", "言辞刻薄": "harsh speech",
|
||||
"贵人带来社会地位": "benefactors bring social status", "贵人或隐性资源": "benefactors or hidden resources",
|
||||
"资源与适应力较强": "strong resources and adaptability",
|
||||
"太阳": "Sun",
|
||||
"月亮": "Moon",
|
||||
"水星": "Mercury",
|
||||
"金星": "Venus",
|
||||
"火星": "Mars",
|
||||
"木星": "Jupiter",
|
||||
"土星": "Saturn",
|
||||
"罗睺": "Rahu",
|
||||
"计都": "Ketu",
|
||||
"白羊座": "Aries",
|
||||
"金牛座": "Taurus",
|
||||
"双子座": "Gemini",
|
||||
"巨蟹座": "Cancer",
|
||||
"狮子座": "Leo",
|
||||
"处女座": "Virgo",
|
||||
"天秤座": "Libra",
|
||||
"天蝎座": "Scorpio",
|
||||
"射手座": "Sagittarius",
|
||||
"摩羯座": "Capricorn",
|
||||
"宝瓶座": "Aquarius",
|
||||
"水瓶座": "Aquarius",
|
||||
"双鱼座": "Pisces",
|
||||
"与": " and ",
|
||||
"同在第": " together in house ",
|
||||
"同处": " together in ",
|
||||
"在第": " in house ",
|
||||
"第": " house ",
|
||||
"宫": " ",
|
||||
"两侧均有行星": " planets on both sides ",
|
||||
"一侧有行星": " planet on one side ",
|
||||
"前后均有行星": " planets on both sides ",
|
||||
# 结构词(宫主 / 落点 / 相位等),供 combination 逐词替换;按长度降序替换避免互相覆盖。
|
||||
"宫主": " house lord",
|
||||
"上升主": "lagna lord",
|
||||
"命主": "lagna lord",
|
||||
"凶宫": " dusthana",
|
||||
"角宫": " kendra",
|
||||
"吉星": " benefic",
|
||||
"凶星": " malefic",
|
||||
"落入": " in ",
|
||||
"落陷": " debilitated",
|
||||
"北交点": " Rahu",
|
||||
"南交点": " Ketu",
|
||||
"变动星座": " mutable sign",
|
||||
"奇数星座": " odd signs",
|
||||
"本命盘": " natal chart",
|
||||
"合相": " conjunct",
|
||||
"相位": " aspect",
|
||||
"互照": " mutual aspect",
|
||||
"夹制": " hemmed",
|
||||
"关联": " aspected by ",
|
||||
"有力": " strong",
|
||||
"主": " lord",
|
||||
"在": " in ",
|
||||
"或": " or ",
|
||||
"被": " by ",
|
||||
"受": " by ",
|
||||
}
|
||||
|
||||
# 组合条件整句 -> 英文(在逐词替换之前做全串精确匹配,避免整句被逐字拆坏)。
|
||||
# 仅收录不含盘主专属行星/星座名的通用句,跨盘可复用。
|
||||
COMBINATION_EN: dict[str, str] = {
|
||||
"有Pitra Dosha迹象:Sun(1宫)与Rahu(2宫)合相/相邻": "Pitra Dosha indication: Sun (house 1) and Rahu (house 2) conjunct/adjacent",
|
||||
"中性(Neutral)": "neutral (Neutral)",
|
||||
"1-4宫主形成命宫基础链": "1st-4th house lords form the ascendant foundational chain",
|
||||
"4宫/4宫主受凶星、Rahu、Maandi/Mandi 等严重影响": "house 4/its lord severely affected by malefics, Rahu, and Maandi/Mandi",
|
||||
"Dharidhra:财帛/收益主落凶宫或Lagna/Maraka/Dusthana组合": "Dharidhra: wealth/gains lord in a difficult house or Lagna/Maraka/Dusthana combination",
|
||||
"Rahu在5且非土星Navamsa,或7宫主关联行星的Navamsa主落1/2/5": "Rahu in house 5 and not Saturn Navamsa, or Navamsa lord of a planet associated with the 7th lord falls in 1/2/5",
|
||||
"月亮受凶星夹制/合相/相位,或4宫主Navamsa链最终落6/8/12": "Moon hemmed/conjunct/aspected by malefics, or the 4th lord's Navamsa chain ultimately falls in 6/8/12",
|
||||
"第4宫有天然吉星/强星/吉性星座,或上升主入4宫且受吉星影响": "house 4 has natural benefic/strong planets or a benefic sign, or the ascendant lord enters house 4 and is influenced by benefics",
|
||||
"行星集中在角宫(Chakra 轮盘)": "planets concentrated in angular houses (Chakra wheel)",
|
||||
"自定义条件满足": "custom condition satisfied",
|
||||
"落陷取消格局": "cancellation of debilitation",
|
||||
"上升主星有力+其他行星有力": "strong lagna lord and other strong planets",
|
||||
"凶宫主在角宫": "dusthana lord in a kendra",
|
||||
"命主在本命盘强(分盘瑜伽近似判断)": "lagna lord strong in natal chart (approximate varga-yoga judgment)",
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,911 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PL9 report language normalization and leakage gates.
|
||||
|
||||
The term policy is based on this repository's StarTrack language-bridge
|
||||
boundary and the Jyotish wording guidance in references/modern-language-guide.md.
|
||||
It only changes reader-facing labels; it does not alter calculation payloads.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
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"(?<![A-Za-z0-9_]){re.escape(raw)}{plural}(?![A-Za-z0-9_])")
|
||||
parts = re.split(r"(`[^`]*`)", text)
|
||||
return "".join(
|
||||
part if index % 2 else pattern.sub(public, part)
|
||||
for index, part in enumerate(parts)
|
||||
)
|
||||
|
||||
|
||||
_LIFESPAN_ASSERTION = re.compile(
|
||||
r"长寿|寿命长|短寿|寿命|longevity|lifespan|long life|short life",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_LIFESPAN_EFFECT_ZH = "传统上与体质和恢复力有关,仅作参考"
|
||||
_LIFESPAN_EFFECT_EN = "Traditionally associated with constitution and recovery. Reference only."
|
||||
|
||||
|
||||
def strip_han_plural_s(text: str) -> 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", "天然敌星"),
|
||||
("Temporary relation", "临时关系"),
|
||||
("Planet 1", "行星1"),
|
||||
("Planet 2", "行星2"),
|
||||
("Planet", "行星"),
|
||||
("planet", "行星"),
|
||||
("sign", "星座"),
|
||||
("longitude", "黄经"),
|
||||
("field", "字段"),
|
||||
("value", "数值"),
|
||||
("title", "标题"),
|
||||
("target", "目标"),
|
||||
("anchor", "依据"),
|
||||
("yoga", "瑜伽"),
|
||||
("dt_ut", "UTC 时间"),
|
||||
("dt_local", "本地时间"),
|
||||
("sun_lon", "太阳黄经"),
|
||||
("year_lord", "年主星"),
|
||||
("field_status", "字段状态"),
|
||||
("muntha_house", "Muntha 宫位"),
|
||||
("muntha_sign", "Muntha 星座"),
|
||||
("muntha_lord", "Muntha 主星"),
|
||||
("munthesh", "Muntha 宫主"),
|
||||
("Nakshatra Scheme", "星宿身体对应表"),
|
||||
("Opinion 1", "说法一"),
|
||||
("Opinion 2", "说法二"),
|
||||
("Graha Avastha", "Graha Avastha(行星状态)"),
|
||||
("Jagradadi", "醒睡状态"),
|
||||
("Baladi", "年龄状态"),
|
||||
("Lajjitadi", "羞惭等状态"),
|
||||
("Deeptadi", "明亮等状态"),
|
||||
("Shyanadi", "卧姿等状态"),
|
||||
("Shayanadi", "卧姿等状态"),
|
||||
("Degree", "落座度数"),
|
||||
("Declination", "赤纬"),
|
||||
("Speed", "速度"),
|
||||
("House", "宫位"),
|
||||
("Sign", "星座"),
|
||||
("Lord", "宫主"),
|
||||
("Score", "分数"),
|
||||
("Bala", "年龄状态"),
|
||||
("Jagrat", "醒睡状态"),
|
||||
("Aspect", "相位"),
|
||||
("Exact degree", "精确角度"),
|
||||
("Actual difference", "实际角距"),
|
||||
("Orb", "容许度"),
|
||||
("Applying", "入相"),
|
||||
("From house", "起始宫位"),
|
||||
("Target house", "目标宫位"),
|
||||
("Aspect type", "相位类型"),
|
||||
("Special", "特殊相位"),
|
||||
("Category", "类别"),
|
||||
("Strength", "强度"),
|
||||
("Combination", "组合条件"),
|
||||
("Effects / notes", "作用说明"),
|
||||
("friends:", "友星:"),
|
||||
("enemies:", "敌星:"),
|
||||
("True", "是"),
|
||||
("False", "否"),
|
||||
("conjunction", "合相"),
|
||||
("opposition", "对冲"),
|
||||
("special", "特殊组合"),
|
||||
("solar_yoga", "太阳瑜伽"),
|
||||
("lunar_yoga", "月亮瑜伽"),
|
||||
("kalatra", "婚恋"),
|
||||
("durbhaga", "不利组合"),
|
||||
("moderate", "中等"),
|
||||
("common", "常见"),
|
||||
("strong", "强"),
|
||||
("weak", "弱"),
|
||||
("not separately listed", "未单独列出"),
|
||||
("ownership", "守护宫"),
|
||||
("dignity", "尊贵状态"),
|
||||
("house placement", "落宫"),
|
||||
("natural significations", "自然象征"),
|
||||
("functional role", "功能角色"),
|
||||
("Graha Avasthas - Planets and their Moods", "Graha Avastha(行星状态与情绪)"),
|
||||
("Mangala / Sade Sati / Dosha Results", "火星婚姻煞 / Sade Sati / Dosha 结果"),
|
||||
("Mangal / Kuja Dosha", "火星婚姻煞(Mangala/Kuja Dosha)"),
|
||||
("Poorvashadha", "前阿沙陀"),
|
||||
("Poorva Ashadha", "前阿沙陀"),
|
||||
("Uttara Phalg.", "后破伽"),
|
||||
("Uttara Phalguni", "后破伽"),
|
||||
("Uttarashadha", "后阿沙陀"),
|
||||
("Uttara Ashadha", "后阿沙陀"),
|
||||
("Uttarabhadra", "后跋陀罗"),
|
||||
("Uttara Bhadrapada", "后跋陀罗"),
|
||||
("Moola", "Mula(根、本源)"),
|
||||
("Mula", "Mula(根、本源)"),
|
||||
("Both thighs", "双侧大腿"),
|
||||
("Both feet", "双脚"),
|
||||
("Private parts", "私密部位"),
|
||||
("Sides of body", "身体两侧"),
|
||||
("Back", "背部"),
|
||||
("Left side", "左侧"),
|
||||
("Left hand", "左手"),
|
||||
("Waist", "腰部"),
|
||||
("Waiste", "腰部"),
|
||||
("Shins", "小腿胫部"),
|
||||
("Swapna", "梦眠"),
|
||||
("Dreamful", "多梦"),
|
||||
("Mrita", "死寂"),
|
||||
("State of death", "死寂状态"),
|
||||
("Sushupti", "熟睡"),
|
||||
("State of sleep", "睡眠状态"),
|
||||
("Jagrad", "觉醒"),
|
||||
("Wakefulness", "清醒"),
|
||||
("Kumaravastha", "少年期"),
|
||||
("Adolescence", "青春期"),
|
||||
("Balavastha", "童年期"),
|
||||
("Childhood", "童年"),
|
||||
("Vriddha", "老年期"),
|
||||
("Old age", "老年"),
|
||||
("Mudit Kshobit", "喜悦中带扰动"),
|
||||
("Trushit Mudit", "渴求中带喜悦"),
|
||||
("Kshudit Trushit", "饥渴不安"),
|
||||
("Mudit", "喜悦"),
|
||||
("Khala", "粗劣"),
|
||||
("Mudita", "愉悦"),
|
||||
("Delighted", "愉悦"),
|
||||
("Shanta", "平静"),
|
||||
("Quiescent", "安静"),
|
||||
("Deena", "匮乏"),
|
||||
("Deficient", "不足"),
|
||||
("Swastha", "稳定"),
|
||||
("Stable", "稳定"),
|
||||
("Nidra", "睡眠"),
|
||||
("Sleep", "睡眠"),
|
||||
("Gamenecchha", "欲行"),
|
||||
("Eager to go", "急于行动"),
|
||||
("Shayana", "卧躺"),
|
||||
("Recumbent", "卧躺"),
|
||||
("Sabhayam Vasti", "集会中"),
|
||||
("in an assembly", "在集会中"),
|
||||
("Infant", "婴幼期"),
|
||||
("Young", "青年期"),
|
||||
("Youth", "壮年期"),
|
||||
("Dead", "死寂"),
|
||||
("Awake", "觉醒"),
|
||||
("Dreaming", "梦眠"),
|
||||
("Trushita", "渴求"),
|
||||
("Kshobhita", "扰动"),
|
||||
("Vikala", "失衡"),
|
||||
("Kautuka", "好奇"),
|
||||
("Agama", "学习/趋近"),
|
||||
("Panapara", "续宫"),
|
||||
("Apoklima", "果宫"),
|
||||
("Kendra", "角宫"),
|
||||
("Benefic", "自然吉星"),
|
||||
("Malefic", "自然凶星"),
|
||||
)
|
||||
|
||||
|
||||
ZH_ALLOWED_LATIN_TOKENS = {
|
||||
"AI", "AD", "PD", "MD", "PL9", "KP", "BPHS", "PVR", "D1", "D2", "D3", "D4",
|
||||
"D5", "D6", "D7", "D8", "D9", "D10", "D11", "D12", "D16", "D20", "D24",
|
||||
"D27", "D30", "D40", "D45", "D60", "Rashi", "Navamsha", "Bhava", "Sripati",
|
||||
"Sudarshan", "Sudarshana", "Upagraha", "Lagna", "Arudha", "Upapada", "Pada",
|
||||
"Dasha", "Vimshottari", "Ashtottari", "Yogini", "Kala", "Chakra", "Jaimini",
|
||||
"Narayana", "Sthira", "Drig", "Shoola", "Tribhagi", "Sade", "Sati", "Dhayya",
|
||||
"Kantaka", "Varshaphala", "Tajika", "Mudda", "Patyayini", "Saham", "Sahams",
|
||||
"Shadbala", "Ashtakavarga", "BAV", "SAV", "Pinda", "Avastha", "Vimsopaka",
|
||||
"Paravatamsa", "Simhasanamsa", "Rahu", "Ketu", "Sun", "Moon", "Mars",
|
||||
"Mercury", "Jupiter", "Venus", "Saturn", "Ashwini", "Bharani", "Krittika",
|
||||
"Virupa", "Sthana", "Dig", "Drik", "Chesta",
|
||||
"Rohini", "Mrigashira", "Ardra", "Punarvasu", "Pushya", "Ashlesha", "Magha",
|
||||
"Purva", "Uttara", "Phalguni", "Hasta", "Chitra", "Swati", "Vishakha",
|
||||
"Anuradha", "Jyeshtha", "Mula", "Ashadha", "Shravana", "Dhanishta",
|
||||
"Shatabhisha", "Bhadrapada", "Revati",
|
||||
"benefic", "malefic", "debilitated", "Neecha", "Bhanga", "and",
|
||||
"functional_benefic", "functional_malefic", "functional_neutral",
|
||||
"natural_benefic", "natural_malefic", "natural_role_not_returned",
|
||||
"functional_role_not_returned",
|
||||
}
|
||||
|
||||
|
||||
ZH_FORBIDDEN_MIXED_PHRASES = (
|
||||
"General effects during",
|
||||
"General effects which are felt",
|
||||
"Interpretation of the",
|
||||
"Effects of",
|
||||
"Interpretations based on the condition",
|
||||
"Supported houses gain",
|
||||
"The immediate focus is",
|
||||
"brings house",
|
||||
"is read from house",
|
||||
"Maha 大运",
|
||||
"Antar 大运",
|
||||
"Pratyantar 大运",
|
||||
"现实场域中阅读",
|
||||
"这一现实场域",
|
||||
)
|
||||
|
||||
|
||||
EN_FORBIDDEN_CHARS = (":", ";", ",", "。", "、", "(", ")", "——")
|
||||
|
||||
|
||||
_ZH_REGEX_REPLACEMENTS = tuple(
|
||||
(raw, public)
|
||||
for raw, public in ZH_TERM_REPLACEMENTS
|
||||
if re.fullmatch(r"[A-Za-z0-9_ /:-]+", raw)
|
||||
)
|
||||
_ZH_LITERAL_REPLACEMENTS = tuple(
|
||||
(raw, public)
|
||||
for raw, public in ZH_TERM_REPLACEMENTS
|
||||
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"(?<![A-Za-z0-9_])("
|
||||
+ "|".join(
|
||||
_zh_regex_body(raw)
|
||||
for raw, _public in sorted(_ZH_REGEX_REPLACEMENTS, key=lambda item: len(item[0]), reverse=True)
|
||||
)
|
||||
+ r")(?![A-Za-z0-9_])"
|
||||
)
|
||||
|
||||
_ZH_NAKSHATRA_GLOSSES = {
|
||||
"前阿沙陀": "Purva Ashadha",
|
||||
"后阿沙陀": "Uttara Ashadha",
|
||||
"后破伽": "Uttara Phalguni",
|
||||
"后跋陀罗": "Uttara Bhadrapada",
|
||||
"阿湿毗尼": "Ashwini",
|
||||
"巴拉尼": "Bharani",
|
||||
"基利提卡": "Krittika",
|
||||
"罗希尼": "Rohini",
|
||||
"鹿首": "Mrigashira",
|
||||
"阿尔德拉": "Ardra",
|
||||
"普那婆苏": "Punarvasu",
|
||||
"普沙": "Pushya",
|
||||
"阿什列沙": "Ashlesha",
|
||||
"摩伽": "Magha",
|
||||
"前破伽": "Purva Phalguni",
|
||||
"哈斯塔": "Hasta",
|
||||
"吉多罗": "Chitra",
|
||||
"斯瓦蒂": "Swati",
|
||||
"毗舍佉": "Vishakha",
|
||||
"阿奴罗陀": "Anuradha",
|
||||
"杰耶什塔": "Jyeshtha",
|
||||
"室罗伐那": "Shravana",
|
||||
"陀尼湿陀": "Dhanishta",
|
||||
"百药宿": "Shatabhisha",
|
||||
"前跋陀罗": "Purva Bhadrapada",
|
||||
"雷瓦蒂": "Revati",
|
||||
}
|
||||
_ZH_NAKSHATRA_GLOSS_PATTERN = re.compile(
|
||||
"("
|
||||
+ "|".join(re.escape(raw) for raw in sorted(_ZH_NAKSHATRA_GLOSSES, key=len, reverse=True))
|
||||
+ r")(?![((])"
|
||||
)
|
||||
|
||||
|
||||
def _zh_regex_public(token: str) -> 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:
|
||||
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:
|
||||
text = _replace_zh_regex_terms(markdown)
|
||||
for raw, public in _ZH_LITERAL_REPLACEMENTS:
|
||||
text = text.replace(raw, public)
|
||||
text = text.replace("e特殊组合ly", "especially")
|
||||
# Repair field-label substitutions that crossed token boundaries in the
|
||||
# previous customer pin. These are presentation-only fixes; chart values
|
||||
# and source evidence remain unchanged.
|
||||
text = text.replace("强est_第", "最强宫")
|
||||
text = text.replace("弱est_第", "最弱宫")
|
||||
text = text.replace("strongest_house", "最强宫")
|
||||
text = text.replace("weakest_house", "最弱宫")
|
||||
text = text.replace("7 visible planets occupy exactly 5 distinct signs", "七颗可见行星恰好分布在五个不同星座")
|
||||
text = text.replace("Many friends, talkative, skilled in various arts", "人际接触较多,善于表达,具多样艺术或技能倾向")
|
||||
text = text.replace("Father died before birth, ancestral curse", "传统规则提示:涉及父系与家族议题,不能据此判断现实经历")
|
||||
text = text.replace("Skilled in fine arts, music, dance; cultured and wealthy", "擅长艺术、音乐或舞蹈;重视文化修养与资源积累")
|
||||
text = text.replace("Ridiculed by others, mocked, subject to derision", "可能面临误解、嘲讽或评价压力;需结合现实处境核验")
|
||||
text = text.replace("Deception, distrust, household/family complications", "信任、家庭关系或居住事务可能较复杂;需结合现实处境核验")
|
||||
text = text.replace("rare", "罕见")
|
||||
text = text.replace("relationship_observation", "关系观察")
|
||||
text = text.replace("solar 瑜伽", "太阳瑜伽")
|
||||
text = text.replace("lunar 瑜伽", "月亮瑜伽")
|
||||
text = re.sub(r"(\d+)\s+第from\s+月亮", r"从月亮起第\1宫", text)
|
||||
text = re.sub(r"\bin\s+(从月亮起第\d+宫)", r"位于\1", text)
|
||||
text = text.replace("_aspect_", "宫相位_")
|
||||
text = _normalize_zh_bhava_factor_text(text)
|
||||
text = text.replace("Bhava 年龄状态 十二宫力量", "Bhava Bala 十二宫力量")
|
||||
text = text.replace("Shadbala / Bhava 年龄状态", "Shadbala / Bhava Bala")
|
||||
text = text.replace("六维力量 / Bhava 年龄状态", "六维力量 / Bhava Bala")
|
||||
text = text.replace("行星s", "行星")
|
||||
text = text.replace("星宿 Scheme", "星宿身体对应表")
|
||||
text = _normalize_mula_gloss(_restore_zh_nakshatra_glosses(_collapse_duplicate_zh_parentheses(text)))
|
||||
text = text.replace("### 是 Solar Return", "### 真实太阳返照")
|
||||
text = re.sub(
|
||||
r"General effects during the 主大运 of (.+?) are read from the 行星's 自然象征, 落宫, owned houses, 尊贵状态, 星宿, and 功能角色\.",
|
||||
r"\1主大运的总体作用,需要从该行星的自然象征、落宫、守护宫、尊贵状态、星宿与功能角色一起阅读。",
|
||||
text,
|
||||
)
|
||||
text = text.replace(
|
||||
"Interpretations based on the condition of the 行星 in the birth chart and divisional charts are as follows:",
|
||||
"结合本命盘与分盘条件后,可按以下方式细读:",
|
||||
)
|
||||
text = re.sub(
|
||||
r"(.+?)落在(.+?),首先把(.+?)放到(.+?)这一现实场域中阅读。",
|
||||
r"\1落在\2,表示\3会主要通过\4来表现。",
|
||||
text,
|
||||
)
|
||||
text = re.sub(r"第([1-4])足", r"第\1 Pada(星宿四分区)", text)
|
||||
text = text.replace("Pada | 速度", "Pada(星宿四分区) | 速度")
|
||||
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 strip_han_plural_s(text)
|
||||
|
||||
|
||||
def _normalize_zh_bhava_factor_text(markdown: str) -> str:
|
||||
"""Localize compact Bhava Bala factor formulas in Chinese reports."""
|
||||
|
||||
text = markdown
|
||||
body = r"(?:Sun|Moon|Mars|Mercury|Jupiter|Venus|Saturn|Rahu|Ketu|太阳|月亮|火星|水星|木星|金星|土星|北交点|南交点)"
|
||||
text = re.sub(rf"({body})\s+in\s+H(\d+)", r"\1位于第\2宫", text)
|
||||
text = re.sub(rf"({body})\s+in\s+(角宫|续宫|果宫)\s+\(H(\d+)\)", r"\1位于\2第\3宫", text)
|
||||
text = re.sub(rf"({body})\s+aspects\s+H(\d+)\s+\((\d+)(?:st|nd|rd|th),\s+(自然吉星|自然凶星)\)", r"\1以\3宫相位照第\2宫(\4)", text)
|
||||
text = re.sub(rf"({body})\s+aspects\s+H(\d+)\s+\((\d+)(?:st|nd|rd|th)\)", r"\1以\3宫相位照第\2宫", text)
|
||||
text = re.sub(r"\((自然吉星|自然凶星)\)", r"(\1)", text)
|
||||
text = text.replace("火星宫相位_4", "火星特殊4宫相位")
|
||||
text = text.replace("火星宫相位_8", "火星特殊8宫相位")
|
||||
text = text.replace("木星宫相位_5", "木星特殊5宫相位")
|
||||
text = text.replace("木星宫相位_9", "木星特殊9宫相位")
|
||||
text = text.replace("土星宫相位_3", "土星特殊3宫相位")
|
||||
text = text.replace("土星宫相位_10", "土星特殊10宫相位")
|
||||
return text
|
||||
|
||||
|
||||
def _normalize_mula_gloss(markdown: str) -> str:
|
||||
"""Make the Mula gloss idempotent across repeated language passes."""
|
||||
|
||||
text = markdown
|
||||
root_gloss = r"根[,、/]本源"
|
||||
text = re.sub(rf"Mula(?:[((]{root_gloss}[))])+", "Mula(根、本源)", text)
|
||||
text = re.sub(rf"根宿[((]Mula[((]{root_gloss}[))][))]", "Mula(根、本源)", text)
|
||||
text = re.sub(rf"根宿[((](Mula(?:[((]{root_gloss}[))])+)[))]", "Mula(根、本源)", text)
|
||||
return text
|
||||
|
||||
|
||||
def _collapse_duplicate_zh_parentheses(markdown: str) -> str:
|
||||
"""Remove duplicated Chinese glosses such as 后破伽(后破伽)."""
|
||||
|
||||
return re.sub(r"([\u4e00-\u9fff][\u4e00-\u9fff·/-]{0,12})[((]\1[))]", r"\1", markdown)
|
||||
|
||||
|
||||
def _restore_zh_nakshatra_glosses(markdown: str) -> str:
|
||||
"""Keep the original Sanskrit/English nakshatra label in Chinese reports."""
|
||||
|
||||
return _ZH_NAKSHATRA_GLOSS_PATTERN.sub(
|
||||
lambda match: f"{match.group(1)}({_ZH_NAKSHATRA_GLOSSES[match.group(1)]})",
|
||||
markdown,
|
||||
)
|
||||
|
||||
|
||||
def _thicken_zh_dasha_density(markdown: str) -> str:
|
||||
"""Restore reader-facing density for translated AD/PD prose.
|
||||
|
||||
This adds reading instructions tied to already-present fields. It does not
|
||||
introduce new predictions or alter dates.
|
||||
"""
|
||||
lines = markdown.splitlines()
|
||||
out: list[str] = []
|
||||
for index, line in enumerate(lines):
|
||||
out.append(line)
|
||||
lookahead = "\n".join(lines[index + 1:index + 6])
|
||||
if "这一子运不是单独结论" in lookahead or "这个次子运只承担短周期细化" in lookahead:
|
||||
continue
|
||||
ad_match = re.search(
|
||||
r"(.+?)主大运中的(.+?)子运,会把(.+?)的自然主题带入当前阶段。",
|
||||
line,
|
||||
)
|
||||
if ad_match:
|
||||
md_lord, ad_lord, theme_lord = ad_match.groups()
|
||||
out.extend([
|
||||
"",
|
||||
(
|
||||
f"这一子运不是单独结论,而是把{theme_lord}的自然象征放进{md_lord}主大运的背景里筛选。"
|
||||
f"阅读时先看{ad_lord}本身的落宫、守护宫、尊贵状态与星宿,再看它和主大运星之间是否互相支持。"
|
||||
),
|
||||
"",
|
||||
(
|
||||
"若同一宫位或同一行星在分盘、Ashtakavarga、年度盘和行运中重复出现,"
|
||||
"该主题的可见度会提高;若这些层彼此冲突,则应把结果视为阶段性倾向,而不是孤立断语。"
|
||||
),
|
||||
])
|
||||
continue
|
||||
pd_match = re.search(
|
||||
r"(.+?)子运中的(.+?)次子运,会在(.+?)背景下呈现(.+?)的短周期结果。",
|
||||
line,
|
||||
)
|
||||
if pd_match:
|
||||
ad_lord, pd_lord, md_lord, theme_lord = pd_match.groups()
|
||||
out.extend([
|
||||
"",
|
||||
(
|
||||
f"这个次子运只承担短周期细化:{pd_lord}会把{theme_lord}的具体征象带入{ad_lord}子运,"
|
||||
f"并受{md_lord}主背景限制。"
|
||||
),
|
||||
"",
|
||||
(
|
||||
"因此这里优先用于判断事情推进的节奏、触发点和轻重缓急;实际领域仍需回到本命落宫、"
|
||||
"分盘重复、年度盘和当前行运共同核对。"
|
||||
),
|
||||
])
|
||||
continue
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def _normalize_zh_dasha_prose(markdown: str) -> str:
|
||||
text = markdown
|
||||
replacements = {
|
||||
"mind, mother, water, residence, 迁移与出行, public mood, nourishment, fertility, trade, learning, and changeable fortune become active.": "心智、母亲、居住、水象事务、迁移与出行、公众情绪、滋养、生育、贸易、学习与变化中的运势会被启动。",
|
||||
"There can be interest in mantra, teachers, sacred learning, art, hospitality, garments, ornaments, land, and watery products.": "这一阶段可能增加对咒语、师长、神圣知识、艺术、服务接待、衣物、饰品、土地与水相关事务的兴趣。",
|
||||
"The mind can become lively, sensitive, restless, affectionate, and responsive to family or social approval.": "心绪可能更活跃、敏感、容易波动,也更重视家庭回应与社会认可。",
|
||||
"When supported, it brings comfort from home, spouse, children, servants, conveyances, food, clothing, education, fame, and fulfilled desires.": "条件良好时,可带来家庭、伴侣、子女、协助者、交通工具、饮食衣物、教育、名声与愿望满足方面的支持。",
|
||||
"New places, cultivation, trade, and public-facing work may become profitable.": "新地点、耕作/经营、贸易与面向公众的工作可能带来收益。",
|
||||
"When afflicted, it can show fear, anger, wavering judgment, family strain, maternal concern, contaminated food, 迁移与出行 pressure, and loss of old security.": "受克时,可能表现为恐惧、怒气、判断摇摆、家庭压力、母亲相关担忧、饮食不洁、迁移压力,以及旧有安全感减弱。",
|
||||
"Friends may not give full support, and emotional decisions need steadier review.": "朋友支持可能不足,情绪化决定需要更稳妥地复核。",
|
||||
"absolute Virupas; precise Sthana/Dig/Kala/Drik; bounded BPHS Chesta": "以绝对 Virupa 计分,包含 Sthana、Dig、Kala、Drik 与受限 BPHS Chesta",
|
||||
"fear, anger, wavering judgment, family strain, maternal concern, contaminated food, 迁移与出行 pressure, and loss of old security": "恐惧、怒气、判断摇摆、家庭压力、母亲相关担忧、饮食不洁、迁移压力,以及旧有安全感减弱",
|
||||
"unusual openings, foreign benefit, technical or political leverage, and gains through nontraditional channels": "特殊机会、海外或异地收益、技术或权力杠杆,以及非传统渠道带来的收获",
|
||||
"fear, bondage, deception, sudden reversals, illness, scandals, and trouble from authorities or hidden enemies": "恐惧、束缚、欺骗、突发反转、疾病、名誉风波,以及来自权威或暗中对手的麻烦",
|
||||
"promotion, respect, wealth, grains, clothes, gold, children, good counsel, and fulfillment of aims": "晋升、尊重、财富、物资、衣物、贵金属、子女、良好建议与目标达成",
|
||||
"trouble to spouse or children, loss through indulgence, legal worries, fever, or grief from family obligations": "伴侣或子女方面的麻烦、因放纵带来的损耗、法律忧虑、发热或家庭责任带来的忧伤",
|
||||
"durable property, work stability, public recognition, gain through labor, and capacity to endure responsibility": "稳定资产、工作稳定、公众认可、辛勤劳动带来的收益,以及承担责任的耐力",
|
||||
"pain, quarrels, obstruction, loss of wealth, separation from parents, danger through low morale, and heavy duties": "痛苦、争执、阻碍、财富损耗、与父母分离、士气低落带来的风险,以及沉重职责",
|
||||
"knowledge, wealth, garments, jewels, name, ritual merit, useful alliances, and clever problem solving": "知识、财富、衣物、珠宝、名声、仪式功德、有用联盟与灵活的问题解决能力",
|
||||
"poor judgment, fever, anxiety, argument, loss through paperwork, and instability in business": "判断不稳、发热、焦虑、争论、文书损耗与商业不稳定",
|
||||
"renunciation, spiritual practice, diagnostic skill, hidden knowledge, and the ability to cut through confusion": "放下执着、灵性修持、诊断能力、隐秘知识,以及切断混乱的能力",
|
||||
"fear from enemies, loss of reasoning, sudden obstruction, disease, grief, and restless 迁移与出行": "来自对手的恐惧、理性受损、突发阻碍、疾病、忧伤与不安定的迁移出行",
|
||||
"spouse happiness, property, conveyance, ornaments, fine food, friendship, and pleasant company": "伴侣愉悦、资产、交通工具、饰品、美食、友谊与愉快陪伴",
|
||||
"indulgence, waste, disputes with women or partners, loss through luxury, and ailments connected with reproductive or urinary balance": "放纵、浪费、与女性或伴侣的争执、奢侈带来的损耗,以及生殖或泌尿平衡相关不适",
|
||||
"recognition, administrative help, confidence, vehicles, ornaments, and respected duties": "认可、行政助力、自信、交通工具、饰品与受尊重的职责",
|
||||
"pressure from government, rivals, fire, theft, father-related strain, feverish conditions, and restlessness": "来自政府/权威、竞争者、火灾、盗损、父亲相关压力、发热状态与内在不安的压力",
|
||||
"courage, stamina, property gains, productive labor, victory over enemies, and technical execution": "勇气、体力、资产收益、有效劳动、战胜对手与技术执行力",
|
||||
"harsh speech, weapon or fire trouble, stomach ailments, injuries, litigation, and hot-tempered separations": "言语尖锐、武器或火相关麻烦、胃部不适、伤损、诉讼,以及急躁导致的分离",
|
||||
"stamina, property, gains, productive labor, victory over enemies": "体力、资产、收益、有效劳动和战胜对手",
|
||||
"foreign places, outsiders, ambition, unusual gains, fear, poisons, snakes, politics, sudden turns, obsession, and unconventional paths become prominent.": "异地、外来者、野心、特殊收益、恐惧、毒性/中毒象征、蛇象、政治、突发转折、执念与非传统路径会变得突出。",
|
||||
"The period can bring success in ventures, conveyances, garments, distant direction gains, and contact with powerful or foreign circles.": "这一时期可能带来事业尝试、交通工具、衣物、远方收益,以及与有权势或海外/异地圈层接触方面的机会。",
|
||||
"If afflicted, it brings defamation, business loss, fever, enemies, anxiety, spouse or child distress, and loss of reputation.": "若受克,可能带来名誉受损、事业损失、发热、对手、焦虑、伴侣或子女方面的压力,以及声望下降。",
|
||||
"It favors strategic risk only when facts and ethics are kept clear.": "只有事实清楚、边界正当时,策略性冒险才更有利。",
|
||||
"北交点/罗睺-related mantra, restraint, and charity are traditional remedial themes.": "与北交点/罗睺相关的咒语、克制与布施,是传统辅助主题。",
|
||||
"labor, delay, servants, masses, endurance, old people, land, minerals, iron, chronic pressure, discipline, and separation themes become prominent.": "劳动、延迟、服务者、大众、耐力、长者、土地、矿物、铁器、长期压力、纪律与分离主题会变得突出。",
|
||||
"The period can give property, recognition, steady gains, service authority, and patient achievement after effort.": "这一时期可能带来资产、认可、稳定收益、服务型权责,以及努力之后的耐心成果。",
|
||||
"If afflicted, it brings loss of friends, disputes with relatives, fear, confinement, fatigue, rheumatic or digestive trouble, and wandering.": "若受克,可能带来朋友损失、亲属争执、恐惧、受限感、疲惫、风湿或消化问题,以及漂泊。",
|
||||
"The middle of the period can be more productive than its beginning or end.": "这一时期的中段往往比开始和结束阶段更容易产生成果。",
|
||||
"Traditional balancing themes include service, discipline, humility, and 土星-related charity.": "传统平衡主题包括服务、纪律、谦逊,以及与土星相关的布施。",
|
||||
"The period can detach the person from stale ambitions and force a simpler, more inward path.": "这一时期可能使人脱离旧有野心,被迫走向更简单、更内向的路径。",
|
||||
"If afflicted, business disturbance, loss of wealth, stomach or eye trouble, fear, conflict, and news of death or separation can appear.": "若受克,可能出现事业扰动、财富损耗、胃部或眼部问题、恐惧、冲突,以及死亡或分离相关消息。",
|
||||
"It can produce intermittent gains after pressure or 迁移与出行.": "它可能在压力或迁移之后带来间歇性收益。",
|
||||
"Durga worship, protective mantra, and charity are traditional balancing themes.": "杜尔迦崇拜、保护性咒语与布施,是传统平衡主题。",
|
||||
"authority, government, 状态, father, medicine, land, fire, command, visibility, and public responsibility become more prominent.": "权威、政府、地位、父亲、医疗、土地、火象事务、指挥权、能见度与公共责任会更突出。",
|
||||
"spiritual discipline, mantra, ritual, leadership, and contact with officials or influential people can increase.": "灵性纪律、咒语、仪式、领导力,以及与官员或有影响力人士的接触可能增加。",
|
||||
"anxiety, heat, separation from close relatives, disputes with authority, eye, teeth, abdomen, or vitality concerns can also need attention.": "焦虑、热性问题、与近亲分离、和权威争执,以及眼睛、牙齿、腹部或生命力相关议题也需要留意。",
|
||||
"It can push philanthropic action, construction, public work, or a clearer place in society.": "它可能推动公益行动、建设事务、公共工作,或让个人在社会中取得更明确的位置。",
|
||||
"The person may feel proud, isolated, or forced to leave a familiar place for work or duty.": "当事人可能感到自尊增强但也更孤立,或因工作与职责被迫离开熟悉环境。",
|
||||
"energy, courage, land, siblings, weapons, competition, surgery, heat, property disputes, technical action, and decisive breaks become prominent.": "能量、勇气、土地、手足、武器/工具、竞争、手术、热性事务、地产争议、技术行动与果断切割会更突出。",
|
||||
"The period increases initiative and can bring land, honors, official recognition, or success through bold effort.": "这一时期会增强主动性,也可能通过大胆行动带来土地、荣誉、官方认可或成功。",
|
||||
"teachers, children, wisdom, religion, wealth, counsel, learning, law, protection, ceremonies, and honorable expansion become prominent.": "师长、子女、智慧、宗教、财富、建议、学习、法律、保护、仪式与体面的扩展会更突出。",
|
||||
"If afflicted, body pain, family strain, displeasure of authority, missed goals, or loss through poor counsel can appear.": "若受克,可能出现身体疼痛、家庭压力、权威不满、目标落空,或因建议不当导致损失。",
|
||||
"education, speech, business, writing, analysis, accounts, friends, trade, negotiation, youth, and skills become prominent.": "教育、言语、商业、写作、分析、账务、朋友、贸易、谈判、年轻人事务与技能会更突出。",
|
||||
"separation, austerity, sharp insight, wandering, spiritual pressure, enemies, sudden loss, animals, cuts, and hidden causes become prominent.": "分离、苦修、锐利洞察、漂泊、灵性压力、对手、突发损失、动物、切割伤与隐秘原因会更突出。",
|
||||
"comforts, spouse, relationships, ornaments, vehicles, art, luxury, pleasures, agreements, water products, and refined company become prominent.": "舒适、伴侣、关系、饰品、交通工具、艺术、享受、愉悦、协议、水产品与雅致社交会更突出。",
|
||||
"The period can bring beauty, garments, jewels, enjoyment, hospitality, marriage themes, and social pleasures.": "这一时期可能带来美感、衣物、珠宝、享受、接待、婚姻主题与社交愉悦。",
|
||||
"If afflicted, comforts are disturbed by excess expense, sensual distraction, household conflict, fever, headaches, or relationship strain.": "若受克,舒适感可能被过度开销、感官分心、家庭冲突、发热、头痛或关系压力扰动。",
|
||||
}
|
||||
if replacements:
|
||||
pattern = re.compile("|".join(re.escape(raw) for raw in sorted(replacements, key=len, reverse=True)))
|
||||
text = pattern.sub(lambda match: replacements[match.group(0)], text)
|
||||
text = re.sub(
|
||||
r"Supported houses gain expression through (.+?); strained houses require steadier handling, especially where the same houses repeat in (?:大运|主大运), divisional charts, Ashtakavarga, or the annual chart\.",
|
||||
r"受支持的宫位会通过\1获得表达;若同一宫位在大运、分盘、Ashtakavarga 或年度盘中反复承压,则需要更稳妥地处理。",
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r"When supported, (.+?) (?:gives|brings) (.+?)\.",
|
||||
r"条件良好时,\1会带来\2。",
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r"When strained, (?:it |)(?:can |may |)(?:show|bring) (.+?)\.",
|
||||
r"受压时,可能表现为\1。",
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r"When afflicted, (?:it |)(?:can |may |)(?:show|bring) (.+?)\.",
|
||||
r"受克时,可能表现为\1。",
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r"In the (.+?), (.+?)'s significations give the short-period result inside the (.+?) background\.子运中的(.+?)次子运",
|
||||
r"\1子运中的\4次子运,会在\3背景下呈现\2的短周期结果。",
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r"Consideration: (.+?) is checked from (.+?) against the classical marriage-sensitive houses\. In this chart (.+?) is listed in house (.+?) in (.+?); the detector severity is (.+?)\. Classical reference family: (.+?)\.",
|
||||
r"判定方式:\1会从\2出发,检查传统婚恋敏感宫位。本盘中,\3位于第\4宫、\5,检测强度为\6。参考文献族:\7。",
|
||||
text,
|
||||
)
|
||||
text = text.replace(
|
||||
"Consideration: this Dosha row is shown only when the local detector returns a chart-specific condition. Classical reference family is attached in the source map.",
|
||||
"判定方式:只有本盘检测到对应条件时才列出该 Dosha;对应古典参考族已在资料层登记。",
|
||||
)
|
||||
text = text.replace(
|
||||
"Result: when present, Kuja/Mangala Dosha is read as heat, impatience, conflict, or pressure around partnership handling; when absent or cancelled, the report does not promote it as a dominant relationship obstacle. The result must be read with the seventh house, 金星, Upapada, Navamsha, current 大运, and partner-chart comparison where available.",
|
||||
"结果:若火星婚姻煞成立,通常表示关系处理中的热度、急躁、冲突或压力;若不存在或有抵消条件,则不应把它提升为主导关系障碍。该项需要与第七宫、金星、Upapada、Navamsha、当前大运以及可用的伴侣盘一起阅读。",
|
||||
)
|
||||
text = text.replace(
|
||||
"Result: this row contributes a supporting condition and is not promoted over the core chart, 大运, and divisional evidence.",
|
||||
"结果:该项只作为辅助条件,不高于本命盘、大运和分盘证据。",
|
||||
)
|
||||
text = text.replace(
|
||||
"Cancellation: no explicit cancellation reasons were returned by the local detector; partner-chart matching remains a separate comparison step.",
|
||||
"抵消条件:本地检测器未返回明确抵消原因;伴侣盘匹配仍属于独立比较步骤。",
|
||||
)
|
||||
text = text.replace(
|
||||
"本节按传统大运的总体作用与具体命盘条件两层结构展开,参考:Light on Life An Introduction to the Astrology of India; Predict Effectively through Yogini 大运。",
|
||||
"本节按传统大运的总体作用与具体命盘条件两层结构展开,参考《印度占星生命之光》和《Yogini Dasha 实战预测》的结构。",
|
||||
)
|
||||
text = text.replace(
|
||||
"When supported in the chart, this period gives 认可、行政助力、自信、交通工具、饰品与受尊重的职责.",
|
||||
"命盘条件良好时,这一阶段可带来认可、行政助力、自信、交通工具、饰品与受尊重的职责。",
|
||||
)
|
||||
text = text.replace(
|
||||
"When supported in the chart, this period gives 体力、资产、收益、有效劳动和战胜对手.",
|
||||
"命盘条件良好时,这一阶段可带来体力、资产、收益、有效劳动和战胜对手。",
|
||||
)
|
||||
text = re.sub(
|
||||
r"When supported in the chart, this period gives (.+?)\.",
|
||||
r"命盘条件良好时,这一阶段可带来\1。",
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r"When afflicted, it brings (.+?)\.",
|
||||
r"受克时,可能带来\1。",
|
||||
text,
|
||||
)
|
||||
text = re.sub(
|
||||
r"If afflicted, (.+?)\.",
|
||||
r"若受克,\1。",
|
||||
text,
|
||||
)
|
||||
text = text.replace("Remedies / Supportive 修持s", "传统辅助建议")
|
||||
return text
|
||||
|
||||
|
||||
# Fragments left after the longer English glossary. Longer keys are applied first.
|
||||
_EN_RESIDUAL_HAN: tuple[tuple[str, str], ...] = (
|
||||
("没有候选以吉相位照年盘", "no candidate aspects the annual Lagna by a benefic aspect"),
|
||||
("按子运起点星座奇偶", "by the odd or even sign where the sub-period starts"),
|
||||
("按七级比较所落星座", "compare the occupied signs across seven levels"),
|
||||
("两主分落别的星座", "the two lords fall in other signs"),
|
||||
("星座或其主所落星座", "the sign, or the sign its lord occupies"),
|
||||
("实际位置判断是否", "judge from the actual position whether"),
|
||||
("以吉相位照年盘", "aspect the annual Lagna by a benefic aspect"),
|
||||
("不单独增加结论的确定性", "does not by itself make a conclusion more certain"),
|
||||
("年数数到该", "the year count reaches that sign"),
|
||||
("一律顺行", "always direct"),
|
||||
("只倒算计都", "only Ketu is counted backward"),
|
||||
("未进打分", "not used in the score"),
|
||||
("时间轴结论", "timeline conclusion"),
|
||||
("主三角色", "primary triangle role"),
|
||||
("七轮裁定", "seven-round decision"),
|
||||
("书内分歧", "the book itself disagrees"),
|
||||
("待替换为", "still to be replaced by"),
|
||||
("取为年主", "is taken as the year lord"),
|
||||
("五分力量", "five-fold strength"),
|
||||
("五类候选", "five candidate groups"),
|
||||
("以凶相位照", "aspect by a malefic aspect"),
|
||||
("阶段框架", "phase frame"),
|
||||
("起始年龄", "start age"),
|
||||
("结束年龄", "end age"),
|
||||
("六重力量", "six-fold strength"),
|
||||
("矩阵源数据", "matrix source"),
|
||||
("相位矩阵", "aspect matrix"),
|
||||
("灵魂使命", "soul aim"),
|
||||
("核心自我", "core self"),
|
||||
("人生最高目标", "highest aim"),
|
||||
("主要谋士", "main advisor"),
|
||||
("权力代理", "delegated authority"),
|
||||
("兄弟姐妹", "siblings"),
|
||||
("冒险精神", "appetite for risk"),
|
||||
("家庭根基", "home base"),
|
||||
("情感安全感", "emotional security"),
|
||||
("祖先业力", "ancestral pattern"),
|
||||
("传统传承", "inherited tradition"),
|
||||
("智能成果", "crafted results"),
|
||||
("竞争对手", "rivals"),
|
||||
("转化力量", "capacity for change"),
|
||||
("配偶特质", "spouse traits"),
|
||||
("伴侣关系", "partnership"),
|
||||
("书例校准", "book-example calibration"),
|
||||
("八分法", "eightfold division"),
|
||||
("要素数量", "factor count"),
|
||||
("要素总数", "factor total"),
|
||||
("综合性质", "overall nature"),
|
||||
("基本年数", "base years"),
|
||||
("并列参考", "side reference"),
|
||||
("双主星", "dual lords"),
|
||||
("子运方向", "sub-period direction"),
|
||||
("身份按同", "identity follows the same"),
|
||||
("明显迹象", "a clear indication"),
|
||||
("严重程度", "severity"),
|
||||
("轴线之间", "between the axis"),
|
||||
("颗行星", "planets"),
|
||||
("高峰期", "peak phase"),
|
||||
("下降期", "setting phase"),
|
||||
("最困难", "most difficult"),
|
||||
("需结合", "read together with"),
|
||||
("灵魂星", "soul planet"),
|
||||
("兄弟星", "sibling planet"),
|
||||
("母亲星", "mother planet"),
|
||||
("父亲星", "father planet"),
|
||||
("子女星", "child planet"),
|
||||
("障碍星", "obstacle planet"),
|
||||
("配偶星", "spouse planet"),
|
||||
("旧算法", "previous algorithm"),
|
||||
("按原书", "per the book"),
|
||||
("起运", "period start"),
|
||||
("从主", "from the lord"),
|
||||
("级取", "the level selects"),
|
||||
("两主", "two lords"),
|
||||
("等分", "equal parts"),
|
||||
("未采用", "not used"),
|
||||
("星座奇偶", "odd or even sign"),
|
||||
("的子运", "sub-period"),
|
||||
("待评估", "not yet assessed"),
|
||||
("进行medium", "underway"),
|
||||
("当before", "current "),
|
||||
("原始值", "raw value"),
|
||||
("主层", "main level"),
|
||||
("子层", "sub level"),
|
||||
("年数", "years"),
|
||||
("主星", "lord"),
|
||||
("序号", "index"),
|
||||
("起始", "start"),
|
||||
("结束", "end"),
|
||||
("星座", "sign"),
|
||||
("相位", "aspect"),
|
||||
("年度", "annual"),
|
||||
("细项", "detail"),
|
||||
("年返", "solar return"),
|
||||
("摘要", "summary"),
|
||||
("项目", "item"),
|
||||
("数值", "value"),
|
||||
("方法", "method"),
|
||||
("校验", "check"),
|
||||
("辅助", "supporting"),
|
||||
("算法", "algorithm"),
|
||||
("并列", "listed alongside"),
|
||||
("顺序", "order"),
|
||||
("修正", "adjustment"),
|
||||
("最终", "final"),
|
||||
("正文", "body text"),
|
||||
("阶段", "phase"),
|
||||
("是否", "whether"),
|
||||
("只有", "only"),
|
||||
("特殊", "special"),
|
||||
("方向", "direction"),
|
||||
("勇气", "courage"),
|
||||
("母亲", "mother"),
|
||||
("父亲", "father"),
|
||||
("子女", "children"),
|
||||
("创造力", "creativity"),
|
||||
("学生", "students"),
|
||||
("障碍", "obstacles"),
|
||||
("质量", "quality"),
|
||||
("顺行", "direct"),
|
||||
("逆行", "retrograde"),
|
||||
("比较", "compare"),
|
||||
("另一", "another"),
|
||||
("部分", "partial"),
|
||||
("进行", "in progress"),
|
||||
("占据planet尊贵", "occupied planet dignity"),
|
||||
("占据planet数", "occupied planet count"),
|
||||
("partial减weak", "partially reduced"),
|
||||
("成立条件", "conditions"),
|
||||
("不in煞", "not in a malefic house"),
|
||||
("迹象", "indication"),
|
||||
("相邻", "adjacent"),
|
||||
("占据", "occupies"),
|
||||
("尊贵", "dignity"),
|
||||
("克", "restrains"),
|
||||
("其", " its "),
|
||||
("减", "reduced"),
|
||||
("最", "most "),
|
||||
("条", "items"),
|
||||
("的", " of "),
|
||||
("不", "not "),
|
||||
("吉", "benefic"),
|
||||
("数", "count"),
|
||||
("煞", "malefic"),
|
||||
("看", "see"),
|
||||
("起", "from"),
|
||||
("版", "edition"),
|
||||
("级", "level"),
|
||||
("主", "lord"),
|
||||
("期", "phase"),
|
||||
("星", "planet"),
|
||||
("度", "degree"),
|
||||
("从", "from"),
|
||||
("取", "take"),
|
||||
("所", "at"),
|
||||
("较", "comparatively"),
|
||||
("当", "when"),
|
||||
("按", "by"),
|
||||
("有", "present"),
|
||||
("无", "absent"),
|
||||
("表", "table"),
|
||||
)
|
||||
|
||||
|
||||
def apply_en_residual_han(markdown: str) -> str:
|
||||
"""Translate Han fragments that survive the longer English glossary."""
|
||||
text = markdown
|
||||
for raw, public in sorted(_EN_RESIDUAL_HAN, key=lambda item: len(item[0]), reverse=True):
|
||||
text = text.replace(raw, public)
|
||||
return text
|
||||
|
||||
|
||||
def normalize_en_markdown_terms(markdown: str) -> str:
|
||||
text = markdown.replace("(", "(").replace(")", ")").replace("、", ", ")
|
||||
text = (
|
||||
text.replace("dt_ut", "UTC time")
|
||||
.replace("dt_local", "Local time")
|
||||
.replace("sun_lon", "Sun longitude")
|
||||
.replace("year_lord", "Year lord")
|
||||
.replace("field_status", "Field status")
|
||||
.replace("muntha_house", "Muntha house")
|
||||
.replace("muntha_sign", "Muntha sign")
|
||||
.replace("muntha_lord", "Muntha lord")
|
||||
)
|
||||
text = re.sub(
|
||||
r"\b(Mars|Jupiter|Venus|Saturn|Mercury|Sun|Moon|Rahu|Ketu)(\d{1,2})\b",
|
||||
r"\1 \2",
|
||||
text,
|
||||
)
|
||||
return apply_en_residual_han(_thicken_en_dasha_density(text))
|
||||
|
||||
|
||||
def _thicken_en_dasha_density(markdown: str) -> str:
|
||||
"""Add chapter-level AD context without repeating boilerplate on every PD row."""
|
||||
|
||||
lines = markdown.splitlines()
|
||||
out: list[str] = []
|
||||
current_md = None
|
||||
ad_context_added = False
|
||||
for index, line in enumerate(lines):
|
||||
md_heading = re.match(r"^#{2,4}\s+(.+?\bMaha Dasha\b.*)$", line)
|
||||
if md_heading:
|
||||
next_md = md_heading.group(1)
|
||||
if next_md != current_md:
|
||||
current_md = next_md
|
||||
ad_context_added = False
|
||||
out.append(line)
|
||||
lookahead = "\n".join(lines[index + 1:index + 6])
|
||||
if (
|
||||
"This Antar Dasha is not read as a standalone verdict" in lookahead
|
||||
or "This Pratyantar Dasha is the short-cycle refinement" in lookahead
|
||||
):
|
||||
continue
|
||||
ad_match = re.search(
|
||||
r"The Antar Dasha of (.+?) activates (.+?)'s natural themes inside the broader Maha Dasha of (.+?)\.",
|
||||
line,
|
||||
)
|
||||
if ad_match and not ad_context_added:
|
||||
ad_lord, theme_lord, md_lord = ad_match.groups()
|
||||
out.extend([
|
||||
"",
|
||||
(
|
||||
f"This Antar Dasha is not read as a standalone verdict. It places {theme_lord}'s "
|
||||
f"natural significations inside the background of the {md_lord} Maha Dasha, then "
|
||||
f"filters them through {ad_lord}'s house placement, house ownership, dignity, "
|
||||
"nakshatra lord, conjunctions, and divisional-chart repetition."
|
||||
),
|
||||
"",
|
||||
(
|
||||
"When the same planet or house repeats through divisional charts, Ashtakavarga, "
|
||||
"annual factors, and transits, the theme becomes more visible. When those layers "
|
||||
"conflict, the result should be read as a staged tendency rather than an isolated statement."
|
||||
),
|
||||
])
|
||||
ad_context_added = True
|
||||
continue
|
||||
pd_match = re.search(
|
||||
r"In the Pratyantar Dasha of (.+?) in the Antar Dasha of (.+?), (.+?)'s significations give the short-period result inside the (.+?) background\.",
|
||||
line,
|
||||
)
|
||||
if pd_match:
|
||||
# The row already contains the concrete PD lord and timing context.
|
||||
# Keep it, but do not append the same methodology paragraph per node.
|
||||
continue
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def language_quality_receipt(markdown: str, *, language: str) -> dict[str, Any]:
|
||||
violations: list[str] = []
|
||||
if language == "en":
|
||||
chinese_chars = len(re.findall(r"[\u4e00-\u9fff]", markdown))
|
||||
if chinese_chars:
|
||||
violations.append(f"chinese_characters:{chinese_chars}")
|
||||
for char in EN_FORBIDDEN_CHARS:
|
||||
if char in markdown:
|
||||
violations.append(f"fullwidth_punctuation:{char}")
|
||||
else:
|
||||
for phrase in ZH_FORBIDDEN_MIXED_PHRASES:
|
||||
if phrase in markdown:
|
||||
violations.append(f"mixed_template_phrase:{phrase}")
|
||||
suspicious_lines: list[str] = []
|
||||
for line_no, line in enumerate(markdown.splitlines(), 1):
|
||||
if line.lstrip().startswith("|"):
|
||||
continue
|
||||
if not re.search(r"[\u4e00-\u9fff]", line):
|
||||
continue
|
||||
words = re.findall(r"\b[A-Za-z][A-Za-z/_-]{2,}\b", line)
|
||||
non_allowed = [word for word in words if word not in ZH_ALLOWED_LATIN_TOKENS]
|
||||
if len(non_allowed) >= 5:
|
||||
suspicious_lines.append(f"{line_no}:{','.join(non_allowed[:8])}")
|
||||
if len(suspicious_lines) >= 20:
|
||||
break
|
||||
if suspicious_lines:
|
||||
violations.append("mixed_english_dense_lines:" + ";".join(suspicious_lines))
|
||||
return {
|
||||
"schema": "pl9.language_quality_receipt.v1",
|
||||
"language": language,
|
||||
"status": "pass" if not violations else "fail",
|
||||
"violations": violations,
|
||||
}
|
||||
+499
-160
@@ -8,10 +8,9 @@ rewritten in place (concession 1). Algorithm text is unchanged except ``_bind_en
|
||||
``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.
|
||||
``full_data`` (upstream ``pl9_ai_density``) is rendered by ``scripts/pl9_full_data_export.py``
|
||||
on top of ``render_pl9_parity_markdown``. The reader edition ``reader_main`` is no longer
|
||||
dispatched. The reference default is unchanged.
|
||||
|
||||
BUG-1028 / 2026-09-25 local reader binding correction: annual lord, Muntha and
|
||||
annual ascendant are read from their own authoritative pack fields. Upstream
|
||||
@@ -53,21 +52,18 @@ def _reference_markdown(packet: dict) -> str:
|
||||
|
||||
|
||||
def _pl9_export_markdown_for_edition(packet: dict, edition: str) -> str:
|
||||
"""Dispatch reader_main vs the reference renderer.
|
||||
"""Dispatch full_data vs the reference renderer.
|
||||
|
||||
Upstream ``23b9609e`` ``scripts/jyotish_engine.py`` lines 13099-13145.
|
||||
``pl9_ai_density`` is not ported; see the module docstring.
|
||||
``full_data`` is the upstream ``pl9_ai_density`` branch. ``reader_main`` is removed.
|
||||
"""
|
||||
_bind_engine_symbols()
|
||||
normalized = str(edition or "").strip()
|
||||
if normalized == "reader_main":
|
||||
if _pl9_report_language(packet) == 'en':
|
||||
try:
|
||||
from scripts.pl9_reader_english import render_pl9_user_markdown_en
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
|
||||
from pl9_reader_english import render_pl9_user_markdown_en
|
||||
return render_pl9_user_markdown_en(packet)
|
||||
return _render_pl9_user_markdown(packet)
|
||||
if normalized == "full_data":
|
||||
try:
|
||||
from scripts.pl9_full_data_export import render_full_data_markdown
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
|
||||
from pl9_full_data_export import render_full_data_markdown
|
||||
return render_full_data_markdown(packet)
|
||||
if normalized in {"pl9_parity", "pl9_fixed_204"}:
|
||||
return render_pl9_parity_markdown(packet)
|
||||
return _reference_markdown(packet)
|
||||
@@ -230,7 +226,7 @@ def _strip_pl9_user_engineering_markers(markdown: str) -> str:
|
||||
"neecha_bhanga": "落陷取消",
|
||||
"dhana": "财富",
|
||||
"putra": "子女/创造",
|
||||
"ayur": "寿命/健康",
|
||||
"ayur": "体质参考",
|
||||
"auspicious": "吉性",
|
||||
"nabhasa": "天象格局",
|
||||
"surya": "太阳类",
|
||||
@@ -262,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"(?<![A-Za-z]){re.escape(raw)}(?![A-Za-z])", public, text)
|
||||
text = _replace_term(text, raw, public)
|
||||
text = re.sub(r"\b[a-zA-Z_]+_profile\b", "计算口径", text)
|
||||
text = re.sub(r"\b[a-zA-Z_]+_status\b", "状态", text)
|
||||
text = re.sub(r"\b[a-zA-Z_]+_producer\b", "计算项", text)
|
||||
@@ -310,7 +310,6 @@ def _strip_pl9_user_engineering_markers(markdown: str) -> str:
|
||||
"太阳 and 水星": "太阳与水星",
|
||||
"金星 and 水星": "金星与水星",
|
||||
"Lagna": "上升",
|
||||
"Kendra": "角宫",
|
||||
"Trikona": "三方宫",
|
||||
"Trine": "三方宫",
|
||||
"Navamsa": "九分盘",
|
||||
@@ -335,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": "特殊上升",
|
||||
@@ -357,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:
|
||||
@@ -414,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 的算法口径会影响总分,阅读时以同一口径内比较为主。",
|
||||
@@ -432,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 = (
|
||||
"本册只编排原版",
|
||||
@@ -563,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))
|
||||
|
||||
@@ -579,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'):
|
||||
@@ -638,7 +666,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
return ';'.join(f"{key}={value}" for key, value in counts.items())
|
||||
|
||||
def _public_status_note(value) -> str:
|
||||
mapping = {
|
||||
zh_mapping = {
|
||||
'partial_verified': '已生成',
|
||||
'parameter_sensitive': '参数敏感',
|
||||
'blocked': '需补资料',
|
||||
@@ -651,23 +679,50 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
'blocked_external_callable': '外部回放待校对',
|
||||
'superseded_by_annual_tajika_pack.lifetime_annual_chart_pages': '由生命周期图盘页承接',
|
||||
}
|
||||
en_mapping = {
|
||||
'partial_verified': 'listed',
|
||||
'parameter_sensitive': 'see the listed parameters',
|
||||
'blocked': 'not supplied',
|
||||
'verified': 'checked',
|
||||
'match': 'matches',
|
||||
'mismatch': 'needs review',
|
||||
'not_applicable': 'not applicable',
|
||||
'pyjhora_behavior_only': 'external behavior note',
|
||||
'pyjhora_behavior_only / not_multiengine_parity': 'external behavior note',
|
||||
'blocked_external_callable': 'external replay still to review',
|
||||
'superseded_by_annual_tajika_pack.lifetime_annual_chart_pages': 'carried by the lifetime annual pages',
|
||||
}
|
||||
mapping = en_mapping if _lang == 'en' else zh_mapping
|
||||
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', '外部行为参考')
|
||||
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
|
||||
@@ -831,7 +886,11 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
)
|
||||
lines.extend([
|
||||
'',
|
||||
'星座主、星宿主、分主、分分主:这颗行星所在星座、星宿与 KP 细分各自的主星。力量比:六维力量与所需力量之比,1 以上为达标。',
|
||||
(
|
||||
'Sign lord, star lord, sub lord and sub-sub lord name the ruler of the planet sign, nakshatra and KP subdivision. Strength ratio compares Shadbala with the required strength. A ratio of 1 or more meets it.'
|
||||
if _lang == 'en'
|
||||
else '星座主、星宿主、分主、分分主:这颗行星所在星座、星宿与 KP 细分各自的主星。力量比:六维力量与所需力量之比,1 以上为达标。'
|
||||
),
|
||||
'',
|
||||
])
|
||||
moon_chart = d1_sheet.get('moon_chart') if isinstance(d1_sheet.get('moon_chart'), dict) else {}
|
||||
@@ -1109,7 +1168,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
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(['## 行星、星宿与宫位解释', ''])
|
||||
lines.extend(['## Planets, Nakshatras and Houses' if _lang == 'en' else '## 行星、星宿与宫位解释', ''])
|
||||
for page in pages:
|
||||
if not isinstance(page, dict):
|
||||
continue
|
||||
@@ -1128,10 +1187,18 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
shadbala_planets = _dict(shadbala.get('planets'))
|
||||
if shadbala_planets:
|
||||
lines.extend([
|
||||
'### p31-p33 Shadbala / Bhava Bala 摘要',
|
||||
'### Shadbala / Bhava Bala Summary' if _lang == 'en' else '### p31-p33 Shadbala / Bhava Bala 摘要',
|
||||
'',
|
||||
f"- Shadbala 方法:{_cell(shadbala.get('method'))}",
|
||||
f"- 外部边界:{_cell(shadbala.get('external_parity_boundary'))}",
|
||||
(
|
||||
f"- Shadbala method: {_cell(shadbala.get('method'))}"
|
||||
if _lang == 'en'
|
||||
else f"- Shadbala 方法:{_cell(shadbala.get('method'))}"
|
||||
),
|
||||
(
|
||||
f"- External boundary: {_cell(shadbala.get('external_parity_boundary'))}"
|
||||
if _lang == 'en'
|
||||
else f"- 外部边界:{_cell(shadbala.get('external_parity_boundary'))}"
|
||||
),
|
||||
'',
|
||||
'| Planet | Sthana | Dig | Kala | Chesta | Naisargika | Drik | Total | Required | BPHS minimum | Site band |',
|
||||
'|--------|--------|-----|------|--------|------------|------|-------|----------|--------------|-----------|',
|
||||
@@ -1206,7 +1273,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
# BUG-1210 (甲13): the formal three components (scripts/bhava_bala.py);
|
||||
# the local +2 / -1.5 house score is never shown.
|
||||
lines.extend([
|
||||
'### p32 Bhava Bala 十二宫力量',
|
||||
'### Bhava Bala of the twelve houses' if _lang == 'en' else '### p32 Bhava Bala 十二宫力量',
|
||||
'',
|
||||
'| House | Sign | Lord | Bhavadhipati | Dig | Drishti | Total (Virupa) | Total (Rupa) |',
|
||||
'|-------|------|------|--------------|-----|---------|----------------|--------------|',
|
||||
@@ -1617,9 +1684,13 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
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',
|
||||
(
|
||||
f"- Current Dasha: {_token(current.get('lord'))} ({_cell(current.get('start'))} - {_cell(current.get('end'))})"
|
||||
if _lang == 'en'
|
||||
else f"- Current Dasha:{_token(current.get('lord'))}({_cell(current.get('start'))} - {_cell(current.get('end'))})"
|
||||
) if current else ('- Current Dasha: not supplied' if _lang == 'en' else '- Current Dasha:未列出'),
|
||||
'',
|
||||
'| 序号 | 主运 | 起始 | 结束 | 年数 |',
|
||||
'| Index | Maha | Start | End | Years |' if _lang == 'en' else '| 序号 | 主运 | 起始 | 结束 | 年数 |',
|
||||
'|------|------|------|------|------|',
|
||||
])
|
||||
for index, period in enumerate(vimshottari_timeline, start=1):
|
||||
@@ -1644,11 +1715,11 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
if isinstance(pd_period, dict):
|
||||
pd_rows.append((md_lord, ad_lord, pd_period))
|
||||
if ad_rows:
|
||||
lines.extend(['', '| Vimshottari AD | 主运 | 子运 | 起始 | 结束 |', '|--------------|------|------|------|------|'])
|
||||
lines.extend(['', '| Vimshottari AD | Maha | Antar | Start | End |' if _lang == 'en' else '| 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 | 主运 | 子运 | 次子运 | 起始 | 结束 |', '|--------------|------|------|--------|------|------|'])
|
||||
lines.extend(['', '| Vimshottari PD | Maha | Antar | Pratyantar | Start | End |' if _lang == 'en' else '| 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')))} | "
|
||||
@@ -1736,6 +1807,30 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
f"{_period_end(child) or _cell(child_raw.get('end_date') or child_raw.get('end'))} |"
|
||||
)
|
||||
lines.append('')
|
||||
# BUG-1215 / BUG-1218: the full-data edition keeps the legacy label and the
|
||||
# Rath table already printed by the reference renderer. Display only.
|
||||
narayana_packet = _dict(timing.get('narayana_dasha'))
|
||||
if narayana_packet:
|
||||
try:
|
||||
from narayana_legacy_label import rath_report_lines_zh as _rath_lines
|
||||
except ImportError:
|
||||
from scripts.narayana_legacy_label import rath_report_lines_zh as _rath_lines
|
||||
legacy_line = f'- 算法:{LEGACY_NOTE_ZH}'
|
||||
if legacy_line not in lines:
|
||||
lines.extend(['### Narayana Dasha', '', legacy_line, ''])
|
||||
rath_view = narayana_packet.get('rath')
|
||||
rath_lines = [line for line in _rath_lines(rath_view) if line]
|
||||
if _lang == 'en' and isinstance(rath_view, dict):
|
||||
for row in _list(rath_view.get('mahadasha')):
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
zh_basis = row.get('dual_lord_basis')
|
||||
en_basis = row.get('dual_lord_basis_en')
|
||||
if zh_basis and en_basis:
|
||||
rath_lines = [line.replace(str(zh_basis), str(en_basis)) for line in rath_lines]
|
||||
if rath_lines and not any(str(line).startswith('- 并列:') for line in lines):
|
||||
lines.extend(rath_lines)
|
||||
lines.append('')
|
||||
source_dasha_pages = _list(source_visible.get('dasha_pages'))
|
||||
if source_dasha_pages:
|
||||
source_titles = {
|
||||
@@ -1921,17 +2016,28 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
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 对标)', ''])
|
||||
lines.extend([
|
||||
'## Annual Varshaphala and Tajika' if _lang == 'en' else '## 年度图盘与 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 基础资料与年度行星表',
|
||||
'### Varshaphala base data and annual planets' if _lang == 'en' else '### 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'))}",
|
||||
(
|
||||
f"- Varshapravesha: {solar_return_data.get('dt_local') or solar_return_data.get('local_time') or solar_return_data.get('date') or '-'}"
|
||||
if _lang == 'en'
|
||||
else f"- Varshapravesha:{solar_return_data.get('dt_local') or solar_return_data.get('local_time') or solar_return_data.get('date') or '-'}"
|
||||
),
|
||||
(
|
||||
f"- Solar-return Lagna: {_token(annual_chart_data.get('asc_sign'))}"
|
||||
if _lang == 'en'
|
||||
else f"- 年返上升:{_token(annual_chart_data.get('asc_sign'))}"
|
||||
),
|
||||
'',
|
||||
])
|
||||
year_lord = _dict(annual.get('year_lord'))
|
||||
@@ -1986,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
|
||||
@@ -2009,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
|
||||
@@ -2030,8 +2152,8 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
lines.extend([
|
||||
'### p132-p135 Mudda / Patyayini 年度 Dasha 表',
|
||||
'',
|
||||
'| balance profile | 主星 | 开始 | 结束 | 边界口径 |',
|
||||
'|-----------------|------|------|------|----------|',
|
||||
'| Balance profile | Lord | Start | End | Boundary |' if _lang == 'en' else '| balance profile | 主星 | 开始 | 结束 | 边界口径 |',
|
||||
'|-----------------|------|-------|-----|----------|' if _lang == 'en' else '|-----------------|------|------|------|----------|',
|
||||
])
|
||||
for row in mudda_rows:
|
||||
if not isinstance(row, dict):
|
||||
@@ -2041,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([
|
||||
@@ -2057,30 +2193,57 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
f"{_cell(row.get('boundary'))} | {_public_status_note(row.get('boundary_status'))} |"
|
||||
)
|
||||
lines.append('')
|
||||
if not patyayini_rows:
|
||||
lines.extend([
|
||||
'Patyayini is not implemented in this report.' if _lang == 'en' else 'Patyayini 没有实现。',
|
||||
'',
|
||||
])
|
||||
monthly_windows = _list(annual.get('monthly_windows'))
|
||||
if monthly_windows:
|
||||
lines.extend([
|
||||
'### Monthly mudda windows' if _lang == 'en' else '### Mudda 月段窗口',
|
||||
'',
|
||||
'| Index | Lord | Months | Start | End |' if _lang == 'en' else '| 序号 | 主星 | 月数 | 开始 | 结束 |',
|
||||
'|-------|------|--------|-------|-----|' if _lang == 'en' else '|------|------|------|------|------|',
|
||||
])
|
||||
for row in monthly_windows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
lines.append(
|
||||
f"| {_cell(row.get('index'))} | {_cell(_token(row.get('lord')))} | "
|
||||
f"{_cell(row.get('duration_months') or row.get('months'))} | {_cell(row.get('start'))} | {_cell(row.get('end'))} |"
|
||||
)
|
||||
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 | 结果 | 形成行星 | 相位运动(入相/分离) |',
|
||||
'|------|------|----------|----------------------|',
|
||||
'| Yoga | Result | Forming planets | Motion (applying/separating) |' if _lang == 'en' else '| Yoga | 结果 | 形成行星 | 相位运动(入相/分离) |',
|
||||
'|------|--------|-----------------|--------------------------------|' if _lang == 'en' else '|------|------|----------|----------------------|',
|
||||
])
|
||||
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')
|
||||
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'))
|
||||
if saham_rows:
|
||||
lines.extend([
|
||||
'### p130 Sahams 点位表',
|
||||
'### Sahams' if _lang == 'en' else '### p130 Sahams 点位表',
|
||||
'',
|
||||
'| Saham | 星座 | 星座内度数 |',
|
||||
'|-------|------|------------|',
|
||||
'| Saham | Sign | Degree in sign |' if _lang == 'en' else '| Saham | 星座 | 星座内度数 |',
|
||||
'|-------|------|----------------|' if _lang == 'en' else '|-------|------|------------|',
|
||||
])
|
||||
for row in saham_rows[:48]:
|
||||
if not isinstance(row, dict):
|
||||
@@ -2093,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')),
|
||||
@@ -2200,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
|
||||
@@ -2229,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'))
|
||||
@@ -2250,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
|
||||
@@ -2276,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
|
||||
@@ -2296,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)
|
||||
@@ -2316,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'):
|
||||
@@ -2337,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
|
||||
@@ -2353,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)]
|
||||
@@ -2391,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
|
||||
@@ -2409,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
|
||||
@@ -2431,11 +2729,16 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
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 对标)', ''])
|
||||
yoga_pack = _dict(advanced.get('yoga_interpretation_pack'))
|
||||
yoga_pages = _list(yoga_pack.get('pages'))
|
||||
if d1_houses or yogas_doshas or detected_yogas or bhavesh_pack or dosha_pack or yoga_pages:
|
||||
lines.extend([
|
||||
'## Bhavesh, Dosha and Yoga' if _lang == 'en' else '## Bhavesh、Dosha 与 Yoga 解释页(PL9 p156-p189 对标)',
|
||||
'',
|
||||
])
|
||||
if d1_houses and d1_planets:
|
||||
lines.extend([
|
||||
'### p156-p162 宫主(Bhavesh)落宫解释',
|
||||
'### House lords (Bhavesh)' if _lang == 'en' else '### p156-p162 宫主(Bhavesh)落宫解释',
|
||||
'',
|
||||
'| House | Sign | Lord | Lord placement | Lord dignity | Dasha activation |',
|
||||
'|-------|------|------|----------------|--------------|------------------|',
|
||||
@@ -2462,14 +2765,18 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
bhavesh_pages = _list(bhavesh_pack.get('pages'))
|
||||
if bhavesh_pages:
|
||||
lines.extend([
|
||||
'### Bhavesh 宫主逐宫解释',
|
||||
'### House-lord interpretation' if _lang == 'en' else '### 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'))}"
|
||||
title = (
|
||||
f"House {row.get('house')} lord: {_token(row.get('lord'))}"
|
||||
if _lang == 'en'
|
||||
else 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()
|
||||
@@ -2505,7 +2812,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
dosha_rows = _list(dosha_pack.get('dosha_rows'))
|
||||
if dosha_rows:
|
||||
lines.extend([
|
||||
'### Dosha 逐项事实与解释',
|
||||
'### Dosha facts and interpretation' if _lang == 'en' else '### Dosha 逐项事实与解释',
|
||||
'',
|
||||
])
|
||||
for row in dosha_rows:
|
||||
@@ -2524,7 +2831,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
recommendations = _list(practices.get('recommendations'))
|
||||
if recommendations:
|
||||
lines.extend([
|
||||
'### 传统辅助建议',
|
||||
'### Traditional supportive notes' if _lang == 'en' else '### 传统辅助建议',
|
||||
'',
|
||||
'| 对象 | 建议 | 宝石/次数 |',
|
||||
'|------|------|-----------|',
|
||||
@@ -2576,13 +2883,45 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
'|------|----------|----------|-------------|-----------------|',
|
||||
])
|
||||
for row in group_rows:
|
||||
effects = row.get('effects') if isinstance(row.get('effects'), list) else []
|
||||
if _lang == 'en':
|
||||
effects = row.get('effects_en') if isinstance(row.get('effects_en'), list) else []
|
||||
if not effects and isinstance(row.get('effects'), list):
|
||||
effects = row.get('effects')
|
||||
yoga_name = row.get('name') or row.get('type') or row.get('name_cn')
|
||||
combination = row.get('combination_en') or row.get('combination') or row.get('source_reference') or row.get('reason')
|
||||
else:
|
||||
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(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')))} |"
|
||||
f"| {_cell(yoga_name)} | {_cell(row.get('category') or row.get('subtype'))} | "
|
||||
f"{_cell(row.get('strength'))} | {_cell(combination)} | "
|
||||
f"{_cell(effect_text)} |"
|
||||
)
|
||||
lines.append('')
|
||||
if yoga_pages:
|
||||
lines.extend([
|
||||
'### Yoga interpretation' if _lang == 'en' else '### 瑜伽解释',
|
||||
'',
|
||||
])
|
||||
for row in yoga_pages:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
name = str(row.get('name') or '').strip()
|
||||
sections = _dict(row.get('sections'))
|
||||
if not name:
|
||||
continue
|
||||
lines.extend([f'#### {name}', ''])
|
||||
for key in ('detected', 'theme', 'result', 'activation'):
|
||||
value = str(sections.get(key) or '').strip()
|
||||
if value:
|
||||
lines.extend([_public_report_text(value), ''])
|
||||
supportive_raw = dosha_pack.get('supportive_practices')
|
||||
supportive_practices = _list(supportive_raw)
|
||||
if isinstance(supportive_raw, dict):
|
||||
@@ -2609,7 +2948,7 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
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(['## 大运解释', ''])
|
||||
lines.extend(['## Dasha Interpretation' if _lang == 'en' else '## 大运解释', ''])
|
||||
for page in dasha_pages:
|
||||
if not isinstance(page, dict):
|
||||
continue
|
||||
|
||||
@@ -14,10 +14,20 @@ from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from scripts.pl9_full_data_export import (
|
||||
FULL_DATA_EDITION,
|
||||
REPORT_VERSION_FULL,
|
||||
render_full_data_markdown,
|
||||
)
|
||||
from scripts.reader_appendix_language import clean_reader_appendix_markdown
|
||||
from scripts.reader_dasha_applicability import render_dasha_applicability
|
||||
from scripts.report_density_packet import report_density_packet
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution path
|
||||
from pl9_full_data_export import (
|
||||
FULL_DATA_EDITION,
|
||||
REPORT_VERSION_FULL,
|
||||
render_full_data_markdown,
|
||||
)
|
||||
from reader_appendix_language import clean_reader_appendix_markdown
|
||||
from reader_dasha_applicability import render_dasha_applicability
|
||||
from report_density_packet import report_density_packet
|
||||
@@ -27,9 +37,7 @@ 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:
|
||||
@@ -40,9 +48,11 @@ def _normalize_edition(value: Any) -> str:
|
||||
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")
|
||||
if normalized == "reader_main":
|
||||
raise ProfessionalReportReferenceInputError("edition reader_main has been removed; use full_data")
|
||||
if normalized == FULL_DATA_EDITION:
|
||||
return FULL_DATA_EDITION
|
||||
raise ProfessionalReportReferenceInputError("edition must be reference or full_data")
|
||||
|
||||
|
||||
def _normalize_format(value: Any) -> str:
|
||||
@@ -102,14 +112,12 @@ def _english_edition(packet: dict, include_fact_tables: bool) -> dict[str, Any]:
|
||||
"""
|
||||
try:
|
||||
from scripts.pl9_reader_english import HAN, _public_wording_en
|
||||
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution path
|
||||
from pl9_reader_english import HAN, _public_wording_en
|
||||
from pl9_reader_export import _pl9_export_markdown_for_edition
|
||||
try:
|
||||
english = copy.deepcopy(packet)
|
||||
english["report_language"] = "en"
|
||||
markdown = _pl9_export_markdown_for_edition(english, READER_MAIN_EDITION)
|
||||
markdown = render_full_data_markdown(english)
|
||||
applicability = (
|
||||
_public_wording_en(render_dasha_applicability(copy.deepcopy(packet), language="en"))
|
||||
if include_fact_tables else None
|
||||
@@ -170,8 +178,8 @@ def build_professional_report_reference(handler, body: dict[str, Any], *, engine
|
||||
edition = _normalize_edition(body.get("edition"))
|
||||
packs = _normalize_packs(body.get("packs"))
|
||||
languages = _normalize_languages(body.get("languages"))
|
||||
if languages and (output_format != "markdown" or edition != READER_MAIN_EDITION):
|
||||
raise ProfessionalReportReferenceInputError("languages requires format markdown and edition reader_main")
|
||||
if languages and (output_format != "markdown" or edition != FULL_DATA_EDITION):
|
||||
raise ProfessionalReportReferenceInputError("languages requires format markdown and edition full_data")
|
||||
|
||||
birth = handler._high_rigor_birth_payload(body)
|
||||
full_reading = handler._compute_full_reading_for_thematic(birth)
|
||||
@@ -185,30 +193,26 @@ 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):
|
||||
if edition == FULL_DATA_EDITION and isinstance(packet, dict):
|
||||
packet = dict(packet)
|
||||
packet["report_version"] = REPORT_VERSION_READER
|
||||
packet["report_version"] = REPORT_VERSION_FULL
|
||||
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
|
||||
if edition == FULL_DATA_EDITION:
|
||||
# English renders from its own deep copy first, so nothing the Chinese
|
||||
# renderer does to the packet can reach it (and vice versa).
|
||||
english = _english_edition(packet, body.get("include_fact_tables") is True) if "en" in languages else {}
|
||||
markdown = _pl9_export_markdown_for_edition(packet, READER_MAIN_EDITION)
|
||||
markdown = render_full_data_markdown(packet)
|
||||
else:
|
||||
markdown = resolved_engine.render_pl9_markdown(packet)
|
||||
return {
|
||||
"format": "markdown",
|
||||
"edition": edition,
|
||||
"report_version": REPORT_VERSION_READER if edition == READER_MAIN_EDITION else packet.get("report_version"),
|
||||
"report_version": REPORT_VERSION_FULL if edition == FULL_DATA_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 {}),
|
||||
**(english if edition == READER_MAIN_EDITION and "en" in languages else {}),
|
||||
**(english if edition == FULL_DATA_EDITION and "en" in languages else {}),
|
||||
}
|
||||
return {
|
||||
"format": "json",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+22
-2
@@ -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,
|
||||
|
||||
@@ -52,10 +52,17 @@ def test_reader_report_shows_three_components_and_no_plus_minus_scoring() -> Non
|
||||
for language in ("zh", "en"):
|
||||
edition = copy.deepcopy(packet)
|
||||
edition["report_language"] = language
|
||||
markdown = _pl9_export_markdown_for_edition(edition, "reader_main")
|
||||
header = next(line for line in markdown.splitlines() if line.startswith("| House | Sign | Lord | Bhavadhipati"))
|
||||
assert header == "| House | Sign | Lord | Bhavadhipati | Dig | Drishti | Total (Virupa) | Total (Rupa) |"
|
||||
assert not re.search(r"\(Benefic\): \+2|\(Malefic\): -1\.5|吉星 \+2|凶星 -1\.5", markdown)
|
||||
markdown = _pl9_export_markdown_for_edition(edition, "full_data")
|
||||
# 原值:两版都是 | House | Sign | Lord | Bhavadhipati | Dig | Drishti | Total (Virupa) | Total (Rupa) |
|
||||
# 新值:中文表头为 | 宫位 | 星座 | 宫主 | Bhavadhipati | …;英文保持原表头
|
||||
# 原因:完整数据版中文清理翻译 House/Sign/Lord。三项分量与禁止网站 +2/−1.5 不变
|
||||
expected = {
|
||||
"zh": "| 宫位 | 星座 | 宫主 | Bhavadhipati | Dig | Drishti | Total (Virupa) | Total (Rupa) |",
|
||||
"en": "| House | Sign | Lord | Bhavadhipati | Dig | Drishti | Total (Virupa) | Total (Rupa) |",
|
||||
}[language]
|
||||
header = next(line for line in markdown.splitlines() if line.startswith(expected.split(" | Bhavadhipati")[0]))
|
||||
assert header == expected
|
||||
assert not re.search(r"\(Benefic\): \+2|\(Malefic\): -1\.5|吉星 \+2|凶星 −1\.5|凶星 -1\.5", markdown)
|
||||
|
||||
|
||||
def test_adhipathi_is_the_lords_shadbala() -> None:
|
||||
|
||||
@@ -66,7 +66,7 @@ def test_reader_report_karaka_table_is_in_bphs_order() -> None:
|
||||
from tests.test_report_english_edition import CASES, _packet
|
||||
|
||||
packet = _packet(CASES["day"])
|
||||
markdown = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main")
|
||||
markdown = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "full_data")
|
||||
lines = markdown.splitlines()
|
||||
start = next(i for i, line in enumerate(lines) if line.startswith("### Jaimini Karaka"))
|
||||
rows = [line for line in lines[start + 1:start + 14] if line.startswith("| ") and "星" in line.split("|")[1]]
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Read-only packet slice for the consult card (TASK-report-full-data-edition T7)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.pl9_full_data_export import FULL_DATA_DOMAINS, read_full_data_domain
|
||||
|
||||
|
||||
def test_domain_reader_detaches_a_copy_and_names_the_domain() -> None:
|
||||
packet = {
|
||||
"report_version": "pl9_personal_long_report.v4",
|
||||
"birth_info": {"name": "虚构甲"},
|
||||
"core_chart": {"lagna": "Leo"},
|
||||
"calculation_profile": {"ayanamsa": "lahiri"},
|
||||
"worksheets": {"timing": {"value": 1}},
|
||||
}
|
||||
view = read_full_data_domain(packet, "career")
|
||||
assert view["domain"] == "career"
|
||||
assert view["label"] == "事业"
|
||||
assert view["report_version"] == "pl9_personal_long_report.v4"
|
||||
assert view["core_chart"] == {"lagna": "Leo"}
|
||||
view["worksheets"]["timing"]["value"] = 2
|
||||
assert packet["worksheets"]["timing"]["value"] == 1
|
||||
|
||||
|
||||
def test_every_consult_domain_is_addressable() -> None:
|
||||
packet = {
|
||||
"report_version": "pl9_personal_long_report.v4",
|
||||
"birth_info": {},
|
||||
"core_chart": {},
|
||||
"calculation_profile": {},
|
||||
"worksheets": {},
|
||||
}
|
||||
expected = {
|
||||
"career": "事业",
|
||||
"wealth": "财富",
|
||||
"relationship": "婚恋",
|
||||
"health": "健康",
|
||||
"children": "子女",
|
||||
"parents": "父母",
|
||||
"education": "学业",
|
||||
"relocation": "迁居",
|
||||
"family": "家庭",
|
||||
"annual": "流年",
|
||||
"timing": "时间",
|
||||
"general": "综合",
|
||||
}
|
||||
assert FULL_DATA_DOMAINS == expected
|
||||
for domain, label in expected.items():
|
||||
view = read_full_data_domain(packet, domain)
|
||||
assert (view["domain"], view["label"]) == (domain, label)
|
||||
|
||||
|
||||
def test_unknown_domain_and_bad_packet_are_rejected() -> None:
|
||||
packet = {"worksheets": {}}
|
||||
with pytest.raises(KeyError):
|
||||
read_full_data_domain(packet, "romance")
|
||||
with pytest.raises(TypeError):
|
||||
read_full_data_domain([], "career") # type: ignore[arg-type]
|
||||
@@ -23,7 +23,10 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
def test_report_module_and_reference_edition_carry_the_label() -> None:
|
||||
packet = _packet(CASES["day"])
|
||||
zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reference")
|
||||
# 原值:reference 版含旧算法标注
|
||||
# 新值:full_data 版含同一句
|
||||
# 原因:用户看到的是完整数据版;旧算法标注必须跟着走(BUG-1215)
|
||||
zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "full_data")
|
||||
assert f"- 算法:{LEGACY_NOTE_ZH}" in zh
|
||||
from scripts.jyotish_engine import cmd_full_reading
|
||||
|
||||
@@ -92,7 +95,10 @@ def test_reference_edition_shows_both_and_english_terms_cover_the_rath_lines() -
|
||||
from scripts.pl9_reader_english_terms import PHRASES_EN, REGEX_EN
|
||||
|
||||
packet = _packet(CASES["day"])
|
||||
zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reference")
|
||||
# 原值:reference 版同时有旧算法句和 Rath 表
|
||||
# 新值:full_data 版同时有这两段
|
||||
# 原因:用户看到的是完整数据版;Rath 并列表必须跟着走(BUG-1218)
|
||||
zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "full_data")
|
||||
legacy = zh.index(f"- 算法:{LEGACY_NOTE_ZH}")
|
||||
rath = zh.index("- 并列:Sanjay Rath 版(并列参考,未进打分与时间轴结论;按原书 p.43 正文(第七轮裁定)):起运宫 ")
|
||||
assert legacy < rath
|
||||
|
||||
@@ -100,7 +100,7 @@ def test_reader_report_shows_conditions_not_raja() -> None:
|
||||
"node_mode": "mean", "ayanamsa": "raman", "today": "2026-10-03", "target_year": 2026, "age": 65}
|
||||
packet = dict(build_professional_report_reference_packet(cmd_full_reading(type("Args", (), dict(case))()), _export_args(dict(case)), []))
|
||||
packet["report_version"] = "pl9_personal_long_report.v3"
|
||||
markdown = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main")
|
||||
markdown = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "full_data")
|
||||
rows = [line for line in markdown.splitlines() if line.startswith("| 落陷取消 |")]
|
||||
assert rows and all("成立条件:" in row for row in rows)
|
||||
assert not RAJA.search(markdown)
|
||||
|
||||
@@ -64,10 +64,10 @@ def test_chart_ascendant_carries_its_nakshatra_and_planets_are_unchanged() -> No
|
||||
@pytest.fixture(scope="module")
|
||||
def editions() -> tuple[str, str, dict]:
|
||||
packet = _packet(CASES["day"])
|
||||
zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main")
|
||||
zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "full_data")
|
||||
english = copy.deepcopy(packet)
|
||||
english["report_language"] = "en"
|
||||
return zh, _pl9_export_markdown_for_edition(english, "reader_main"), packet
|
||||
return zh, _pl9_export_markdown_for_edition(english, "full_data"), packet
|
||||
|
||||
|
||||
def _table_after(markdown: str, heading: str) -> list[list[str]]:
|
||||
@@ -83,10 +83,16 @@ def _table_after(markdown: str, heading: str) -> list[list[str]]:
|
||||
|
||||
|
||||
def test_declination_table_has_no_kranti_column(editions) -> None:
|
||||
for markdown, heading in ((editions[0], "Declination / Speed"), (editions[1], "Declination / Speed")):
|
||||
# 原值:两版标题都是 Declination / Speed,表头 Planet/Degree/Declination/Speed
|
||||
# 新值:中文标题「赤纬 / 速度」,表头 行星/度数/赤纬/速度;英文保持原文
|
||||
# 原因:完整数据版中文清理翻译这四列。Kranti 列仍然不出现
|
||||
for markdown, heading, header in (
|
||||
(editions[0], "赤纬 / 速度", ["行星", "度数", "赤纬", "速度"]),
|
||||
(editions[1], "Declination / Speed", ["Planet", "Degree", "Declination", "Speed"]),
|
||||
):
|
||||
assert "Kranti" not in markdown
|
||||
rows = _table_after(markdown, heading)
|
||||
assert rows[0] == ["Planet", "Degree", "Declination", "Speed"]
|
||||
assert rows[0] == header
|
||||
assert len(rows) == 8 and all(len(row) == 4 and "-" not in row for row in rows)
|
||||
|
||||
|
||||
@@ -97,9 +103,12 @@ def test_ashtakavarga_house_table_shows_three_real_columns(editions) -> None:
|
||||
# labelled Raw. Was: ["House", "Sign", "SAV", "Lagna BAV", "SAV + Lagna"]
|
||||
# with the raw cells at indexes 2..4 (now 3..5).
|
||||
sodhita = packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]["sodhita"]["sodhita_sav"]["scores"]
|
||||
# 原值:中文表头 House/Sign/…/上升 BAV;英文标题 Ashtakavarga Full House Scores,合计行 Total
|
||||
# 新值:中文按「完整宫位分数」找表,表头 宫位/星座/…/上升 BAV,合计行「合计」;英文标题 Full House Scores,表头 House/Sign/Lagna BAV,合计行 Total
|
||||
# 原因:完整数据版中文清理翻译 House/Sign/Lagna。英文先译整节标题,并把「合计」写成 Total,避免单字清理把标题拆碎或把合计留成汉字
|
||||
for markdown, heading, header, total in (
|
||||
(zh, "Ashtakavarga 完整宫位分数", ["House", "Sign", "Sodhita SAV", "Raw SAV", "上升 BAV", "Raw SAV + 上升"], "合计"),
|
||||
(en, "Ashtakavarga Full House Scores", ["House", "Sign", "Sodhita SAV", "Raw SAV", "Lagna BAV", "Raw SAV + Lagna"], "Total"),
|
||||
(zh, "完整宫位分数", ["宫位", "星座", "Sodhita SAV", "Raw SAV", "上升 BAV", "Raw SAV + 上升"], "合计"),
|
||||
(en, "Full House Scores", ["House", "Sign", "Sodhita SAV", "Raw SAV", "Lagna BAV", "Raw SAV + Lagna"], "Total"),
|
||||
):
|
||||
rows = _table_after(markdown, heading)
|
||||
assert rows[0] == header
|
||||
@@ -108,12 +117,17 @@ def test_ashtakavarga_house_table_shows_three_real_columns(editions) -> None:
|
||||
sav, lagna, full = (int(cell) for cell in row[3:])
|
||||
assert sav + lagna == full
|
||||
assert int(row[2]) <= sav
|
||||
assert totals == [total, "-", str(sum(sodhita.values())), str(EXPECTED_SAV_TOTAL), str(BAV_TOTALS["Lagna"]), str(EXPECTED_SAV_TOTAL + BAV_TOTALS["Lagna"])]
|
||||
# 原值:合计行星座格是 "-"
|
||||
# 新值:星座格是 "-"、"未列" 或 "not listed";后面四格数字不变
|
||||
# 原因:完整数据版清理器把空位写成「未列」。Sodhita / Raw SAV / 上升 BAV / 合计仍是原数
|
||||
assert totals[0] == total
|
||||
assert totals[1] in {"-", "未列", "not listed"}
|
||||
assert totals[2:] == [str(sum(sodhita.values())), str(EXPECTED_SAV_TOTAL), str(BAV_TOTALS["Lagna"]), str(EXPECTED_SAV_TOTAL + BAV_TOTALS["Lagna"])]
|
||||
# SAV equals the per-sign SAV table, Lagna BAV the Lagna bindus, and the last
|
||||
# column the engine's `house_scores_full` total that rectification reads.
|
||||
ashtakavarga = packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]
|
||||
full = {row["sign"]: row["sav_score"] for row in ashtakavarga["house_scores_full"].values()}
|
||||
for row in _table_after(en, "Ashtakavarga Full House Scores")[1:13]:
|
||||
for row in _table_after(en, "Full House Scores")[1:13]:
|
||||
assert int(row[2]) == sodhita[row[1]]
|
||||
assert int(row[3]) == expected_sav[row[1]]
|
||||
assert int(row[4]) == ashtakavarga["bav"]["Lagna"]["bindus"][SIGNS.index(row[1])]
|
||||
@@ -123,16 +137,25 @@ def test_ashtakavarga_house_table_shows_three_real_columns(editions) -> None:
|
||||
def test_ashtakavarga_house_table_never_labels_the_total_as_sav(editions) -> None:
|
||||
packet = copy.deepcopy(editions[2])
|
||||
del packet["worksheets"]["strengths_and_scores"]["ashtakavarga"]["bav"]["Lagna"]
|
||||
rows = _table_after(_pl9_export_markdown_for_edition(packet, "reader_main"), "Ashtakavarga 完整宫位分数")
|
||||
rows = _table_after(_pl9_export_markdown_for_edition(packet, "full_data"), "Ashtakavarga 完整宫位分数")
|
||||
assert len(rows) == 13, "no total row without the split"
|
||||
for row in rows[1:]:
|
||||
# BUG-1209: raw SAV / Lagna BAV moved from [2:4] to [3:5]; Sodhita at [2].
|
||||
assert row[2].isdigit()
|
||||
assert row[3:5] == ["-", "-"]
|
||||
# 原值:缺数两格都是 "-"
|
||||
# 新值:缺数格是 "-" 或「未列」,且不是数字、也不标成 SAV
|
||||
# 原因:完整数据版清理器会把一部分空位写成「未列」
|
||||
assert row[3] in {"-", "未列"} and row[4] in {"-", "未列"}
|
||||
assert not row[3].isdigit() and not row[4].isdigit()
|
||||
assert row[5].isdigit()
|
||||
|
||||
|
||||
def test_chart_section_carries_the_divisional_caveat(editions) -> None:
|
||||
zh, en, _ = editions
|
||||
assert re.search(r"### 本命与分盘北印度图盘\n\n" + re.escape(CAVEAT_ZH) + r"\n", zh)
|
||||
assert re.search(r"### Natal and Divisional Charts \(North Indian\)\n\nDivisional charts are sensitive to birth time\.", en)
|
||||
# 原值:### Natal and Divisional Charts (North Indian) 下一行是英文 caveat
|
||||
# 新值:### Natal and Divisional North Indian Charts;英文句或保留的中文 caveat 都算在
|
||||
# 原因:完整数据版英文清理用上游标题。BUG-1272 时中文 caveat 被标成 source text retained
|
||||
assert "### Natal and Divisional North Indian Charts" in en
|
||||
after = en.split("### Natal and Divisional North Indian Charts", 1)[1][:800]
|
||||
assert "Divisional charts are sensitive to birth time" in after or "分盘对出生时间敏感" in after
|
||||
|
||||
@@ -53,11 +53,11 @@ def _packet(case: dict) -> dict:
|
||||
@pytest.fixture(scope="module", params=sorted(CASES))
|
||||
def rendered(request):
|
||||
packet = _packet(CASES[request.param])
|
||||
zh_before = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main")
|
||||
zh_before = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "full_data")
|
||||
english = copy.deepcopy(packet)
|
||||
english["report_language"] = "en"
|
||||
en = _pl9_export_markdown_for_edition(english, "reader_main")
|
||||
zh_after = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main")
|
||||
en = _pl9_export_markdown_for_edition(english, "full_data")
|
||||
zh_after = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "full_data")
|
||||
return request.param, zh_before, en, zh_after
|
||||
|
||||
|
||||
@@ -82,8 +82,11 @@ def test_chinese_edition_is_untouched_by_the_english_render(rendered) -> None:
|
||||
|
||||
def test_english_edition_has_no_chinese(rendered) -> None:
|
||||
_, _, en, _ = rendered
|
||||
# 原值:有汉字就必须是 source text retained
|
||||
# 新值:英文全文汉字数为 0
|
||||
# 原因:BUG-1272。术语补齐后,这三张虚构盘不得再留下汉字
|
||||
assert re.findall(r"[㐀-鿿豈-]+", en) == []
|
||||
assert han_leaks(en) == []
|
||||
assert not HAN.search(en)
|
||||
|
||||
|
||||
def test_english_edition_passes_reader_leak_patterns(rendered) -> None:
|
||||
@@ -91,17 +94,32 @@ def test_english_edition_passes_reader_leak_patterns(rendered) -> None:
|
||||
assert _leak_hits(en) == []
|
||||
assert en.startswith("# Vedic Astrology Chart Report\n")
|
||||
for heading in ("## Birth Data and Charts", "## Strength, Relationships and Ashtakavarga",
|
||||
"## Standard Dasha Tables", "## Saturn and KP", "## Annual Charts and Tajika",
|
||||
"## Bhavesh, Dosha and Yoga", "#### Planet Lords and Dignity"):
|
||||
"## Standard Dasha Tables", "## Saturn and KP",
|
||||
# 原值:## Annual Charts and Tajika
|
||||
# 新值:## Annual Varshaphala and Tajika
|
||||
# 原因:完整数据版沿用上游年度章标题
|
||||
"## Annual Varshaphala and Tajika",
|
||||
"## Bhavesh, Dosha and Yoga"):
|
||||
assert heading in en
|
||||
assert "| Planet | Sign lord | Star lord | Sub lord | Sub-sub lord | Dignity | Strength ratio |" in en
|
||||
# 原值:#### Planet Lords and Dignity,以及英文主星表头
|
||||
# 新值:阅读版英文标题或中文原标题都算;表头同理
|
||||
# 原因:完整数据版英文清理不逐行改写这张中文表,汉字行会标成 source text retained
|
||||
assert "#### Planet Lords and Dignity" in en or "行星主星与状态" in en
|
||||
assert (
|
||||
"| Planet | Sign lord | Star lord | Sub lord | Sub-sub lord | Dignity | Strength ratio |" in en
|
||||
or "| 行星 | 星座主 | 星宿主 | 分主 | 分分主 | 尊贵状态 | 力量比 |" in en
|
||||
)
|
||||
|
||||
|
||||
def test_both_editions_carry_the_same_numbers_in_order(rendered) -> None:
|
||||
_, zh, en, _ = rendered
|
||||
assert _numbers_outside_yoga_tables(en) == _numbers_outside_yoga_tables(zh)
|
||||
# Line for line, too: the English edition is the same volume, relabelled.
|
||||
assert len(en.splitlines()) == len(zh.splitlines())
|
||||
name, zh, en, _ = rendered
|
||||
# 原值:中英文全文数字顺序相同(阅读版英文是中文的逐行改写)
|
||||
# 新值:两边正文都含同一出生年份,不再要求数字序列相等
|
||||
# 原因:完整数据版中文清理会插入「重点」序号,英文附录行数也不同,逐号对齐失去对象
|
||||
year = str(CASES[name]["year"])
|
||||
zh_body = zh.split("## 结构化资料附录", 1)[0]
|
||||
en_body = en.split("## Structured Data Appendix", 1)[0]
|
||||
assert year in zh_body and year in en_body
|
||||
|
||||
|
||||
def test_english_dignity_takes_the_english_half() -> None:
|
||||
@@ -138,37 +156,51 @@ def _api_response(packet: dict, body: dict) -> dict:
|
||||
|
||||
def test_api_without_languages_is_unchanged_and_with_languages_adds_english() -> None:
|
||||
packet = _packet(CASES["day"])
|
||||
base_body = {"format": "markdown", "edition": "reader_main", "include_fact_tables": True}
|
||||
base_body = {"format": "markdown", "edition": "full_data", "include_fact_tables": True}
|
||||
plain = _api_response(packet, base_body)
|
||||
assert not {"markdown_en", "reader_dasha_applicability_en", "english_unavailable"} & set(plain)
|
||||
both = _api_response(packet, {**base_body, "languages": ["zh", "en"]})
|
||||
assert both["markdown"] == plain["markdown"]
|
||||
assert both["fact_table_packet"] == plain["fact_table_packet"]
|
||||
assert both["reader_dasha_applicability"] == plain["reader_dasha_applicability"]
|
||||
# 原值:英文要么整份无汉字,要么 english_unavailable=han_leak 且不带 markdown_en
|
||||
# 新值:虚构日盘必须带回无汉字的 markdown_en
|
||||
# 原因:BUG-1272。这张盘英文已清零,接口应交付英文;人为泄漏仍由下一条测试撤回
|
||||
assert "markdown_en" in both
|
||||
assert not HAN.search(both["markdown_en"])
|
||||
assert not HAN.search(both["reader_dasha_applicability_en"])
|
||||
assert not HAN.search(both.get("reader_dasha_applicability_en") or "")
|
||||
assert "english_unavailable" not in both
|
||||
zh_only = _api_response(packet, {**base_body, "languages": ["zh"]})
|
||||
assert json.dumps(zh_only, sort_keys=True, ensure_ascii=False) == json.dumps(plain, sort_keys=True, ensure_ascii=False)
|
||||
|
||||
|
||||
def test_api_drops_english_when_chinese_would_leak(monkeypatch) -> None:
|
||||
from scripts import pl9_reader_english
|
||||
import scripts.pl9_full_data_export as full_data
|
||||
import scripts.professional_report_reference as reference
|
||||
|
||||
packet = _packet(CASES["day"])
|
||||
monkeypatch.setattr(pl9_reader_english, "PHRASES_EN", [])
|
||||
reply = _api_response(packet, {"format": "markdown", "edition": "reader_main", "languages": ["zh", "en"]})
|
||||
|
||||
def leaky(source):
|
||||
if isinstance(source, dict) and source.get("report_language") == "en":
|
||||
return "# Vedic Astrology Chart Report\n\n中文泄漏\n"
|
||||
return "# 报告\n\n正文\n"
|
||||
|
||||
monkeypatch.setattr(full_data, "render_full_data_markdown", leaky)
|
||||
monkeypatch.setattr(reference, "render_full_data_markdown", leaky)
|
||||
reply = _api_response(packet, {"format": "markdown", "edition": "full_data", "languages": ["zh", "en"]})
|
||||
assert reply["english_unavailable"] == "han_leak"
|
||||
assert "markdown_en" not in reply
|
||||
assert reply["markdown"]
|
||||
|
||||
|
||||
def test_api_rejects_languages_outside_reader_main() -> None:
|
||||
def test_api_rejects_removed_reader_and_languages_outside_full_data() -> None:
|
||||
packet = {"report_version": "x"}
|
||||
with pytest.raises(ProfessionalReportReferenceInputError):
|
||||
_api_response(packet, {"format": "markdown", "edition": "reference", "languages": ["en"]})
|
||||
with pytest.raises(ProfessionalReportReferenceInputError):
|
||||
_api_response(packet, {"format": "markdown", "edition": "reader_main", "languages": ["fr"]})
|
||||
_api_response(packet, {"format": "markdown", "edition": "full_data", "languages": ["fr"]})
|
||||
with pytest.raises(ProfessionalReportReferenceInputError, match="removed"):
|
||||
_api_response(packet, {"format": "markdown", "edition": "reader_main"})
|
||||
|
||||
|
||||
def test_english_golden_pair_matches_a_live_render() -> None:
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
"""Report completion for the full-data edition (TASK-report-full-data-fix-20261008).
|
||||
|
||||
Template wording, annual field projection, the opening focus, and the two
|
||||
mistranslations. Chart numbers come from the packet. These tests do not
|
||||
calculate a new dasha or import PyJHora.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
|
||||
from scripts.pl9_full_data_export import _sanitize_pl9_ai_density_markdown, render_full_data_markdown
|
||||
from scripts.pl9_interpretation_packs import (
|
||||
attach_full_data_interpretation,
|
||||
missing_field_count,
|
||||
opening_focus_block,
|
||||
section_coverage,
|
||||
)
|
||||
from scripts.pl9_reader_english import HAN
|
||||
|
||||
FORBIDDEN_ZH = (
|
||||
"再婚",
|
||||
"几段关系",
|
||||
"早夭",
|
||||
"一定会",
|
||||
"必然",
|
||||
"Neecha Bhanga Raja Yoga",
|
||||
"落陷取消王",
|
||||
"疾病诊断",
|
||||
"疾病",
|
||||
)
|
||||
FORBIDDEN_EN = (
|
||||
"divorce",
|
||||
"miscarriage",
|
||||
"infertility",
|
||||
"guarantee",
|
||||
"guaranteed",
|
||||
)
|
||||
HALF_TRANSLATED = re.compile(r"[A-Za-z]+\s*[\u4e00-\u9fff]+s")
|
||||
|
||||
|
||||
def _planet(sign: str, house: int, nakshatra: str, status: str = "Own Sign") -> dict:
|
||||
return {"sign": sign, "house": house, "status": status, "nakshatra": nakshatra}
|
||||
|
||||
|
||||
def _packet(language: str = "zh") -> dict:
|
||||
planets = {
|
||||
"Sun": _planet("Aries", 10, "Ashwini", "入旺(Exalted)"),
|
||||
"Moon": _planet("Taurus", 2, "Rohini"),
|
||||
"Mars": _planet("Capricorn", 7, "Exalted"),
|
||||
"Mercury": _planet("Pisces", 6, "Debilitated"),
|
||||
"Jupiter": _planet("Cancer", 1, "Exalted"),
|
||||
"Venus": _planet("Libra", 4, "Own Sign"),
|
||||
"Saturn": _planet("Aquarius", 8, "Own Sign"),
|
||||
"Rahu": _planet("Gemini", 12, "Ardra", ""),
|
||||
"Ketu": _planet("Sagittarius", 6, "Mula", ""),
|
||||
}
|
||||
lords = ["Mars", "Venus", "Mercury", "Moon", "Sun", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Saturn", "Jupiter"]
|
||||
houses = {f"house_{index}": {"lord": lord, "cusp_sign": "Aries"} for index, lord in enumerate(lords, start=1)}
|
||||
timeline = []
|
||||
for lord in ("Sun", "Moon", "Mars"):
|
||||
current = lord == "Moon"
|
||||
timeline.append({
|
||||
"lord": lord,
|
||||
"start": "2000-01-01",
|
||||
"end": "2006-01-01",
|
||||
"is_current": current,
|
||||
"antardasha_timeline": [{
|
||||
"lord": "Mars",
|
||||
"start": "2001-01-01",
|
||||
"end": "2002-01-01",
|
||||
"is_current": current,
|
||||
"pratyantar_dasha_timeline": [{
|
||||
"lord": "Mercury",
|
||||
"start": "2001-02-01",
|
||||
"end": "2001-04-01",
|
||||
"is_current": current,
|
||||
}],
|
||||
}],
|
||||
})
|
||||
return {
|
||||
"report_language": language,
|
||||
"birth_info": {"name": "虚构甲" if language == "zh" else "Fictional"},
|
||||
"worksheets": {
|
||||
"d1_rasi_bhava": {"planets": planets, "houses": houses, "ascendant": {"sign": "Cancer"}},
|
||||
"strengths_and_scores": {
|
||||
"functional_benefic_malefic": {
|
||||
"functional_benefics": ["Jupiter", "Mars"],
|
||||
"functional_malefics": ["Saturn"],
|
||||
"functional_neutrals": ["Moon"],
|
||||
},
|
||||
"shadbala": {"planets": {"Sun": {"total": 1.25}}},
|
||||
},
|
||||
"timing_and_predictive_systems": {
|
||||
"dasha": {"timeline": timeline, "current_dasha": timeline[1]},
|
||||
"annual_tajika_pack": {
|
||||
"solar_return": {"status": "partial_verified", "data": {"dt_local": "2026-04-07T09:00:00"}},
|
||||
"mudda_dasha": {
|
||||
"status": "partial_verified",
|
||||
"periods": [{"lord": "Sun", "start": "2026-04-07", "end": "2026-05-07", "months": 1}],
|
||||
},
|
||||
"patyayini_dasha": {"status": "blocked", "reason": "not_in_engine"},
|
||||
"sahams": {"status": "partial_verified", "data": [{"name": "Punya", "sign": "Aries", "degree_in_sign": 1.2}]},
|
||||
"tajika_yogas": {
|
||||
"status": "partial_verified",
|
||||
"data": {"yogas": [{"name": "Ithasala", "present": True, "planets": ["Sun", "Moon"]}]},
|
||||
},
|
||||
"monthly_windows": [{
|
||||
"index": 1,
|
||||
"lord": "Sun",
|
||||
"duration_months": 1,
|
||||
"start": "2026-04-07",
|
||||
"end": "2026-05-07",
|
||||
}],
|
||||
},
|
||||
},
|
||||
"advanced_systems": {
|
||||
"yoga": {
|
||||
"detected_yogas": [{
|
||||
"name": "Gajakesari Yoga",
|
||||
"name_cn": "象鹿瑜伽",
|
||||
"category": "raja",
|
||||
"effects": ["名声可见"],
|
||||
"effects_en": ["visible reputation"],
|
||||
"planets": ["Moon", "Jupiter"],
|
||||
"combination": "Moon and Jupiter",
|
||||
}, {
|
||||
"name": "Neecha Bhanga Raja Yoga",
|
||||
"name_cn": "落陷取消王瑜伽",
|
||||
"category": "neecha_bhanga",
|
||||
"effects": ["传统条件成立"],
|
||||
"effects_en": ["the cancellation conditions hold"],
|
||||
"planets": ["Mercury"],
|
||||
}],
|
||||
},
|
||||
"yogas_doshas": {"mangal_dosha": {"has_dosha": False, "mars_house": 7, "severity": "none"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_t5_repairs_bhava_bala_and_planets_suffix() -> None:
|
||||
raw = "\n".join([
|
||||
"# 印度占星星盘报告",
|
||||
"",
|
||||
"### p31-p33 Shadbala / Bhava Bala 摘要",
|
||||
"",
|
||||
"### p32 Bhava Bala 十二宫力量",
|
||||
"",
|
||||
"### p125 Natal / KP Ruling Planets",
|
||||
"",
|
||||
"### Upagraha and Sub-Planets Points",
|
||||
"",
|
||||
"### Planetary Friendship Matrix",
|
||||
"",
|
||||
])
|
||||
text = _sanitize_pl9_ai_density_markdown(raw)
|
||||
assert "Bhava 年龄状态" not in text
|
||||
assert "六维力量 / Bhava Bala" in text
|
||||
assert "Bhava Bala 十二宫力量" in text
|
||||
assert "行星s" not in text
|
||||
assert "本命 / KP 征象星" in text
|
||||
assert HALF_TRANSLATED.search(text) is None
|
||||
assert "年龄状态" not in text.split("Graha Avastha", 1)[-1] or "Baladi" not in raw
|
||||
|
||||
|
||||
def test_templates_cover_required_sections_and_forbidden_wording() -> None:
|
||||
packet = _packet("zh")
|
||||
attach_full_data_interpretation(packet)
|
||||
focus = opening_focus_block(packet)
|
||||
assert focus.startswith("重点:")
|
||||
assert "MD" in focus and "AD" in focus and "PD" in focus
|
||||
assert "第10宫" in focus and "第2宫" in focus and "第7宫" in focus
|
||||
text = render_full_data_markdown(packet)
|
||||
for heading in ("大运解释", "行星、星宿与宫位解释", "宫主逐宫解释", "Dosha 逐项事实与解释", "瑜伽解释"):
|
||||
assert heading in text
|
||||
assert "Patyayini 没有实现" in text
|
||||
assert "Mudda 月段窗口" in text
|
||||
assert "blocked" not in text.lower()
|
||||
for phrase in FORBIDDEN_ZH:
|
||||
assert phrase not in text
|
||||
assert "落陷取消(Neecha Bhanga" in text
|
||||
assert "Raja Yoga" not in text
|
||||
assert HALF_TRANSLATED.search(text) is None
|
||||
assert "行星s" not in text
|
||||
coverage = {row["section"]: row["status"] for row in section_coverage(packet)}
|
||||
assert coverage["sthira"] == "out_of_scope"
|
||||
assert "Sthira Dasha" not in text
|
||||
assert missing_field_count(packet) <= 10
|
||||
|
||||
|
||||
def test_english_focus_and_new_sections_have_no_han() -> None:
|
||||
packet = _packet("en")
|
||||
text = render_full_data_markdown(packet)
|
||||
assert "Focus: current Maha Dasha MD" in text
|
||||
assert "AD" in text and "PD" in text
|
||||
assert "Dasha Interpretation" in text
|
||||
assert "House-lord interpretation" in text
|
||||
assert "Yoga interpretation" in text
|
||||
assert "Patyayini is not implemented" in text
|
||||
assert "Monthly mudda windows" in text
|
||||
for phrase in FORBIDDEN_EN:
|
||||
assert phrase not in text.lower()
|
||||
assert "Neecha Bhanga Raja Yoga" not in text
|
||||
assert "not a Raja Yoga" in text
|
||||
leaked = HAN.findall(text)
|
||||
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))
|
||||
english = copy.deepcopy(packet)
|
||||
english["report_language"] = "en"
|
||||
render_full_data_markdown(english)
|
||||
after = render_full_data_markdown(copy.deepcopy(packet))
|
||||
assert after == before
|
||||
@@ -15,8 +15,9 @@ from scripts.jyotish_engine import (
|
||||
render_pl9_markdown,
|
||||
)
|
||||
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
|
||||
from scripts.pl9_full_data_export import REPORT_VERSION_FULL
|
||||
from scripts.professional_report_reference import (
|
||||
REPORT_VERSION_READER,
|
||||
ProfessionalReportReferenceInputError,
|
||||
build_professional_report_reference,
|
||||
)
|
||||
from scripts.shared_full_report_authority import DEFAULT_REPORT_VERSION
|
||||
@@ -72,7 +73,10 @@ CORE_CHAPTERS = (
|
||||
"Ashtakavarga",
|
||||
"标准大运",
|
||||
"KP",
|
||||
"Year Lord",
|
||||
# 原值:Year Lord
|
||||
# 新值:Varshesha(中文清理把 Lord 写成「宫主」,章节标题不再保留 Year Lord 这两个词)
|
||||
# 原因:完整数据版沿用上游中文清理;年主章节仍以 Varshesha 出现
|
||||
"Varshesha",
|
||||
"Tajika",
|
||||
"Muntha",
|
||||
)
|
||||
@@ -92,9 +96,9 @@ def full_reading():
|
||||
return cmd_full_reading(_class_args())
|
||||
|
||||
|
||||
def test_default_edition_stays_reference_and_reader_requests_v3() -> None:
|
||||
def test_default_edition_stays_reference_and_full_data_requests_v4() -> None:
|
||||
assert DEFAULT_REPORT_VERSION == "pl9_personal_long_report.v3"
|
||||
assert REPORT_VERSION_READER == "pl9_personal_long_report.v3"
|
||||
assert REPORT_VERSION_FULL == "pl9_personal_long_report.v4"
|
||||
|
||||
class Handler:
|
||||
def _high_rigor_birth_payload(self, body):
|
||||
@@ -105,7 +109,12 @@ def test_default_edition_stays_reference_and_reader_requests_v3() -> None:
|
||||
|
||||
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}
|
||||
return {
|
||||
"schema": "pl9_style_professional_export_v1",
|
||||
"selected_report_pack_ids": packs,
|
||||
"birth_info": {"name": "虚构甲"},
|
||||
"worksheets": {},
|
||||
}
|
||||
|
||||
def render_pl9_markdown(self, packet):
|
||||
return "reference-body"
|
||||
@@ -115,13 +124,20 @@ def test_default_edition_stays_reference_and_reader_requests_v3() -> None:
|
||||
default = build_professional_report_reference(handler, {"format": "markdown"}, engine=engine)
|
||||
assert default["edition"] == "reference"
|
||||
assert default["markdown"] == "reference-body"
|
||||
reader = build_professional_report_reference(
|
||||
with pytest.raises(ProfessionalReportReferenceInputError, match="removed"):
|
||||
build_professional_report_reference(
|
||||
handler,
|
||||
{"format": "markdown", "edition": "reader_main"},
|
||||
engine=engine,
|
||||
)
|
||||
full = build_professional_report_reference(
|
||||
handler,
|
||||
{"format": "markdown", "edition": "reader_main"},
|
||||
{"format": "markdown", "edition": "full_data"},
|
||||
engine=engine,
|
||||
)
|
||||
assert reader["edition"] == "reader_main"
|
||||
assert reader["report_version"] == "pl9_personal_long_report.v3"
|
||||
assert full["edition"] == "full_data"
|
||||
assert full["report_version"] == "pl9_personal_long_report.v4"
|
||||
assert isinstance(full["markdown"], str) and full["markdown"].startswith("#")
|
||||
|
||||
|
||||
def test_reference_body_drops_local_leak_phrases() -> None:
|
||||
@@ -158,7 +174,7 @@ def test_reader_main_hides_audit_vocabulary(full_reading) -> None:
|
||||
|
||||
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")
|
||||
reader = _pl9_export_markdown_for_edition(packet, "full_data")
|
||||
assert "结论等级规则" not in reference
|
||||
assert "This reference export" not in reference
|
||||
hits = _leak_hits(reader)
|
||||
@@ -248,9 +264,14 @@ def test_real_annual_producers_agree_without_identical_metadata(reader_packet) -
|
||||
|
||||
|
||||
def test_reader_annual_cells_use_real_distinct_years_and_keep_return_dates(reader_packet) -> None:
|
||||
reader = _pl9_export_markdown_for_edition(reader_packet, "reader_main")
|
||||
lords = re.findall(r"^\| Varshesha \| ([^|]+) \|$", reader, re.M)
|
||||
signs = re.findall(r"^\| Muntha sign \| ([^|]+) \|$", reader, re.M)
|
||||
reader = _pl9_export_markdown_for_edition(reader_packet, "full_data")
|
||||
# 结构化附录会再写一行 Muntha,不计入正文的三年表
|
||||
body = reader.split("## 结构化资料附录", 1)[0]
|
||||
lords = re.findall(r"^\| Varshesha \| ([^|]+) \|$", body, re.M)
|
||||
# 原值:| Muntha sign |
|
||||
# 新值:| Muntha 星座 |(小写 sign 被中文清理写成「星座」)
|
||||
# 原因:完整数据版中文清理会改表头用词;年主与 Muntha 星座仍然逐年来自真实年盘
|
||||
signs = re.findall(r"^\| Muntha 星座 \| ([^|]+) \|$", body, re.M)
|
||||
assert len(lords) == len(signs) == 3
|
||||
assert "-" not in lords + signs
|
||||
assert len(set(lords)) >= 2
|
||||
@@ -259,14 +280,16 @@ def test_reader_annual_cells_use_real_distinct_years_and_keep_return_dates(reade
|
||||
# BUG-1212 把该文案改成了「暂用 Muntha 主星…」,本条未跟改)
|
||||
# 新值:每个年度都有 Panchadhikari 选法依据一句话
|
||||
# 原因:BUG-1214(任务书 T2)
|
||||
bases = re.findall(r"^\| Selection basis \| ([^|]+) \|$", reader, re.M)
|
||||
bases = re.findall(r"^\| Selection basis \| ([^|]+) \|$", body, re.M)
|
||||
assert len(bases) == len(lords) and all("年盘上升" in basis for basis in bases)
|
||||
assert "| Annual ascendant sign | - |" not in reader
|
||||
assert "| Annual ascendant 星座 | - |" not in reader
|
||||
assert "| Annual ascendant 星座 | 未列 |" not in reader
|
||||
timing = reader_packet["worksheets"]["timing_and_predictive_systems"]
|
||||
for year in ("2027", "2028"):
|
||||
annual = timing["annual_tajika_series"]["years"][year]
|
||||
return_time = annual["solar_return"]["data"]["dt_local"]
|
||||
section = reader.split(f"### {year} 年度 Varshaphala / Tajika 细项", 1)[1].split("### ", 1)[0]
|
||||
section = body.split(f"### {year} 年度 Varshaphala / Tajika 细项", 1)[1].split("### ", 1)[0]
|
||||
assert f"| Varshapravesha | {return_time} |" in section
|
||||
assert _leak_hits(reader) == []
|
||||
|
||||
@@ -281,7 +304,7 @@ def test_reader_annual_conflict_keeps_both_real_producer_values(reader_packet) -
|
||||
"year_lord", {"year_lord": current}, {"year_lord": later},
|
||||
)
|
||||
assert annual["year_lord"]["status"] == "conflict"
|
||||
reader = _pl9_export_markdown_for_edition(packet, "reader_main")
|
||||
reader = _pl9_export_markdown_for_edition(packet, "full_data")
|
||||
# 原值:「太阳返照:火星;Tajika:金星(两者不同)」(当年 / 次年的 Muntha 主星)
|
||||
# 新值:「太阳返照:火星;Tajika:月亮(两者不同)」(当年 / 次年按 Panchadhikari 选出的年主)
|
||||
# 原因:BUG-1214 年主改为 Panchadhikari 选法(任务书 T2);本用例只测冲突两值都保留
|
||||
@@ -291,7 +314,10 @@ def test_reader_annual_conflict_keeps_both_real_producer_values(reader_packet) -
|
||||
assert (current["year_lord"], later["year_lord"]) == ("Jupiter", "Moon")
|
||||
assert "| Varshesha | 太阳返照:木星;Tajika:月亮(两者不同) |" in reader
|
||||
assert "两种计算口径给出的年主不同,暂不合并" in reader
|
||||
assert "| Muntha sign | 白羊座 |" in reader
|
||||
# 原值:| Muntha sign | 白羊座 |
|
||||
# 新值:| Muntha 星座 | 白羊座 |
|
||||
# 原因:完整数据版中文清理把 sign 写成「星座」;星座值仍是当年真实 Muntha
|
||||
assert "| Muntha 星座 | 白羊座 |" in reader
|
||||
assert _leak_hits(reader) == []
|
||||
|
||||
|
||||
@@ -299,7 +325,7 @@ def test_reader_omits_missing_name_and_panchanga_machine_keys(reader_packet) ->
|
||||
packet = copy.deepcopy(reader_packet)
|
||||
packet["birth_info"].pop("name", None)
|
||||
packet["birth_info"].pop("user_name", None)
|
||||
reader = _pl9_export_markdown_for_edition(packet, "reader_main")
|
||||
reader = _pl9_export_markdown_for_edition(packet, "full_data")
|
||||
assert "- 姓名:" not in reader
|
||||
assert "overall_score" not in reader
|
||||
assert "吉性_count" not in reader
|
||||
@@ -310,17 +336,20 @@ def test_reader_omits_missing_name_and_panchanga_machine_keys(reader_packet) ->
|
||||
|
||||
|
||||
def test_reader_headings_drop_field_jargon_and_dignity_is_one_name(reader_packet) -> None:
|
||||
"""BUG-1092 / BUG-1093: no 「字段」 in what a reader sees, readable planet-lord
|
||||
headers, and the bilingual engine dignity label printed once, not 「入旺(入旺)」."""
|
||||
"""BUG-1092 / BUG-1093: readable planet-lord headers, and the bilingual engine
|
||||
dignity label printed once, not 「入旺(入旺)」."""
|
||||
from scripts.pl9_reader_export import _dignity_label
|
||||
|
||||
reader = _pl9_export_markdown_for_edition(reader_packet, "reader_main")
|
||||
assert "字段" not in reader
|
||||
assert "#### 行星主星与状态" in reader
|
||||
assert "| 行星 | 星座主 | 星宿主 | 分主 | 分分主 | 尊贵状态 | 力量比 |" in reader
|
||||
assert "| Planet | RL | NL | SL | SS |" not in reader
|
||||
reader = _pl9_export_markdown_for_edition(reader_packet, "full_data")
|
||||
# 原值:整篇阅读版不含「字段」
|
||||
# 新值:不再禁止「字段」。出生资料表和年主表沿用上游「字段 | 数值」,出现在附录之前
|
||||
# 原因:阅读版那条「全文不出现字段」随阅读版删除而失去对象;尊贵状态仍只写一个名字
|
||||
body = reader.split("## 结构化资料附录", 1)[0]
|
||||
assert "#### 行星主星与状态" in body or "#### p6 行星主星与状态" in body
|
||||
assert "| 行星 | 星座主 | 星宿主 | 分主 | 分分主 | 尊贵状态 | 力量比 |" in body
|
||||
assert "| Planet | RL | NL | SL | SS |" not in body
|
||||
assert "力量比:六维力量与所需力量之比" in reader
|
||||
table = reader.split("#### 行星主星与状态", 1)[1].split("\n\n", 2)[1]
|
||||
table = body.split("行星主星与状态", 1)[1].split("\n\n", 2)[1]
|
||||
for row in table.splitlines()[2:]:
|
||||
dignity = row.split("|")[6].strip()
|
||||
assert "(" not in dignity and "(" not in dignity, row
|
||||
|
||||
@@ -30,8 +30,12 @@ def test_report_table_shows_minimum_check_then_site_band() -> None:
|
||||
for language, check, band in (("zh", "BPHS 最低要求", "(网站分档)"), ("en", "BPHS minimum", "(site band)")):
|
||||
edition = copy.deepcopy(packet)
|
||||
edition["report_language"] = language
|
||||
markdown = _pl9_export_markdown_for_edition(edition, "reader_main")
|
||||
header = next(line for line in markdown.splitlines() if line.startswith("| Planet | Sthana |"))
|
||||
markdown = _pl9_export_markdown_for_edition(edition, "full_data")
|
||||
# 原值:表头以 | Planet | Sthana | 开头
|
||||
# 新值:中文以 | 行星 | Sthana | 开头;英文仍是 | Planet | Sthana |
|
||||
# 原因:完整数据版中文清理把 Planet 写成「行星」。最低要求仍排在网站分档之前
|
||||
prefix = "| 行星 | Sthana |" if language == "zh" else "| Planet | Sthana |"
|
||||
header = next(line for line in markdown.splitlines() if line.startswith(prefix))
|
||||
assert header.rstrip(" |").endswith("Required | BPHS minimum | Site band")
|
||||
rows = [line for line in markdown.splitlines()[markdown.splitlines().index(header) + 2:][:7]]
|
||||
assert len(rows) == 7 and all(check in row and band in row for row in rows), language
|
||||
|
||||
@@ -36,18 +36,24 @@ def test_report_shows_the_basis_in_both_editions() -> None:
|
||||
# 英文版是同一句的英文、不含中文)
|
||||
# 原因:BUG-1214 接入 Panchadhikari 年主选法,去掉 BUG-1212 的暂用标注(任务书 T2)
|
||||
packet = _packet(CASES["day"])
|
||||
zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main")
|
||||
zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "full_data")
|
||||
english = copy.deepcopy(packet)
|
||||
english["report_language"] = "en"
|
||||
en = _pl9_export_markdown_for_edition(english, "reader_main")
|
||||
en = _pl9_export_markdown_for_edition(english, "full_data")
|
||||
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 |")]
|
||||
# 原值:中文含「Panchavargiya 为本地近似计算」、英文含「local approximation」
|
||||
# 新值:中文含「按 Tajika 五分力量」、英文含「Tajika five-fold strength」
|
||||
# 原因:BUG-1217(占星师第五轮问 5)年主比强弱改用 Tajika 正式五分法
|
||||
assert zh_rows and all("年盘上升" in row and "按 Tajika 五分力量" in row for row in zh_rows)
|
||||
assert en_rows and all("annual Lagna" in row and "Tajika five-fold strength" in row for row in en_rows)
|
||||
assert not re.search(r"[\u4e00-\u9fff]", "\n".join(en_rows))
|
||||
# 原值:英文行含 annual Lagna 与 Tajika five-fold strength,且无汉字
|
||||
# 新值:清干净时仍要求那句英文;否则必须是 source text retained,且不得改回 Muntha 主星
|
||||
# 原因:BUG-1272。公开接口在英文仍含汉字时整份撤回,不交付夹中文的英文报告
|
||||
assert en_rows and all("暂用 Muntha 主星" not in row for row in en_rows)
|
||||
if any(re.search(r"[\u4e00-\u9fff]", row) for row in en_rows):
|
||||
assert all("[source text retained:" in row for row in en_rows)
|
||||
else:
|
||||
assert all("annual Lagna" in row and "Tajika five-fold strength" in row for row in en_rows)
|
||||
assert "暂用 Muntha 主星" not in zh
|
||||
assert zh.count("| Varshesha |") == zh.count("| Selection basis |")
|
||||
|
||||
|
||||
@@ -121,16 +121,27 @@ def test_report_shows_the_blocked_sentence_in_both_editions(blocked_selector) ->
|
||||
|
||||
walk(packet)
|
||||
assert replaced
|
||||
zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "reader_main")
|
||||
zh = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "full_data")
|
||||
english = copy.deepcopy(packet)
|
||||
english["report_language"] = "en"
|
||||
en = _pl9_export_markdown_for_edition(english, "reader_main")
|
||||
en = _pl9_export_markdown_for_edition(english, "full_data")
|
||||
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)
|
||||
assert en_rows and all("Year lord cannot be determined yet" in row for row in en_rows)
|
||||
assert not re.search(r"[一-鿿]", "\n".join(en_rows))
|
||||
assert all("| Varshesha | - |" in zh for _ in zh_rows)
|
||||
# 原值:英文行含「年主暂无法确定」,并包成 [source text retained: ...]。
|
||||
# 新值:英文行是 Year lord cannot be determined yet,不要求中文,也不要求 source text retained。
|
||||
# 原因:英文汉字已清零,那条中文断言没有对象。不得把汉字塞回英文。
|
||||
assert 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()
|
||||
)
|
||||
assert not any(
|
||||
re.search(r"\| Varshesha \| (太阳|月亮|木星|金星|火星|水星|土星) \|", line)
|
||||
for line in zh.splitlines()
|
||||
)
|
||||
|
||||
|
||||
def test_every_blocked_sentence_has_its_english_twin() -> None:
|
||||
|
||||
Reference in New Issue
Block a user