Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bccceff91f | ||
|
|
0c04755560 | ||
|
|
963276ca2f | ||
|
|
856bc850ba | ||
|
|
3cc7fe23d1 | ||
|
|
830e5d0267 | ||
|
|
7242648e45 | ||
|
|
a295afa40f |
@@ -1,5 +1,18 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-10-13 — 明说要做生时校正时,第一步就能打开(待合入 staging)
|
||||
|
||||
- 新对话第一句就说「帮我做生时校正 / 生时纠正 / 核对出生时间」,会直接打开校正,只留一句过渡,不先写一段解读。
|
||||
- 问事业、感情这类普通问题,仍是先排盘再回答。只是觉得解读不准,仍是认下来改着读,不会被拉去做校正。
|
||||
- Skill 版本不变(BUG-1317,是 BUG-1306 的复发)。
|
||||
|
||||
## 2026-10-13 — 新报告正文画出分盘,中文更干净,上升行和同位列能读懂(待合入 staging)
|
||||
|
||||
- 重新生成的报告里,「本命与分盘北印度图盘」每张有数据的盘下面有一张北印度图。同一张盘只画一次。报告最末不再重复「分盘核对」。旧报告打开仍是当时保存的正文。
|
||||
- 中文报告正文去掉 listed、background、native、Custom condition met 这类英文,也不再把中文和英文粘成一个词。括号里的星宿、瑜伽名保留。
|
||||
- 「本命盘行星原始位置」第一行是上升,星宿和 pada 有值。尊贵状态写「不适用」。没有可填的格子与其它空格一样写「未列」。
|
||||
- 星盘页 D9 表的「同位」列:同座写「是」,不同座写「否」。其它分盘仍没有这一列。Skill 版本不变(BUG-1312~1315)。
|
||||
|
||||
## 2026-10-12 — 设置里的岁差说明改对:新开的生时校正也跟着设置走(已合入 staging)
|
||||
|
||||
- 设置页换岁差时的说明原来写「生时校正目前仍固定按 Raman」,与实际相反。改为:之后的咨询、报告、星盘库、每日星语和新开的生时校正会按新岁差算;已经开始的那次校正仍按开始时的岁差。Skill 版本不变(BUG-1310)。
|
||||
|
||||
@@ -17556,6 +17556,7 @@
|
||||
- 防复发:`frontend/tests/rectification-explicit-entry-20261012.test.ts` 锁住直达、同一轮不重复开案、无 caseId 的 opened 绑定与找回、四种拒绝说明、两条路径不重试、普通咨询仍重试一次、规则只定义一处、申报时段「在哪一段附近」、结算调用 `complete_consultation_free`。F3 另锁:结算记录是 `offered` 且 `autoOpen: true`、卡片在绑定前不说「已经开始」、绑定后才说已经开始、打开失败仍是 `offered`、旧的 `offered` / `opened` / `returned` 照常解析且 `returned` 不出卡。用户可见句长度 ≤20。真机清单 `docs/testing/rectification-explicit-entry-20261012.md`。
|
||||
- 相关记录:BUG-1247、BUG-1248(入口改为提议卡)、BUG-976/977(禁正则判意图)。
|
||||
- 复发自:无。
|
||||
- 复发:BUG-1317(2026-10-13。直达已部署,但第 0 步仍只有排盘工具,模型排完盘后没有带 explicitRequest 打开校正)。
|
||||
- 修复版本:staging `b8bdc003`(实现 `6a799582` 变基到 `9f1e8257`,代码逐字节相同;2026-10-12 Claude 验收通过、产品要求合入;部署核对:health gitCommit=b8bdc003、`requiredMigrationsPresent` true,`/login` 200,未登录 `/api/account` 401)。跟进项 F3(打开失败卡片仍说已开始)见 `TASK-rectification-1012-batch-fix-20261012`。 F3 跟进(`51a77721`)已部署 staging `586561eb`(2026-10-12 核对:health gitCommit / apiGitCommit=586561eb、`requiredMigrationsPresent` true,`/login` 200,未登录 `/api/account` 401;合并后 npm test 5025 条、失败 24 逐名同 `798e15ef` 基线)。
|
||||
- 决策日志(2026-10-12,产品):选 A——用户明确要求做生时校正时直接进入校正,不先写解读、不用点卡;意图由模型判断,禁正则 / 关键词。**推翻 BUG-1248 两处**:①明确要求的这一轮不再「只出卡、点卡才进」;②`product_disabled`、`pricing_unavailable`「正文不要提校正」改为必须一句话说明做不了,不得静默。普通对话模型不得承诺收窄出生时间、不得在对话里收集经历假装校正;申报时段下不得写大运 / 子运精确起止。服务端本人 / 开关 / 进行中 case / 价格判定不变。同日另立 BUG-1307(最近分界开场)、BUG-1308(父母静态题研究)。
|
||||
|
||||
@@ -17630,3 +17631,78 @@
|
||||
- 相关记录:无。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/composer-first-glyph-clip-20261013`,尚未部署。推送后的 40 位 SHA 写在 issue #52 的执行回报里。
|
||||
|
||||
## BUG-1312 | 中文完整数据报告正文仍有英文功能词,并与汉字粘在一起
|
||||
|
||||
- 状态:fixed-pending-verify(只对新生成的报告生效;真机清单 `docs/testing/report-translation-gaps-20261013.md` 未走,不标 resolved)
|
||||
- 首次发现 / 最近更新:2026-10-13 / 2026-10-13
|
||||
- 来源:issue #53。任务书在 `941b06c19` 重跑虚构日盘:附录之前英文功能词 218 处、汉字与英文粘连 14 处。
|
||||
- 现象:中文报告里出现 listed、background、native、Custom condition met,以及「木星Jupiter…」这种中英粘在一起的字。星宿、瑜伽名放在括号里是允许的。
|
||||
- 根因:中文完整数据版是在英文原文上做短词替换。短词先换会把长句切碎;有的格子本身就是英文枚举,中文版原样印出。瑜伽组合在中文路径仍优先取了英文组合句。行星中文名已经带英文,庙旺又紧挨着拼上去,中间没有空格。
|
||||
- 修复:中文路径先整句、长词,再短词。宫主状态换成「当前运行 / 背景」,listed 换成「已列出」,native 换成「本命」,自定义条件换成「满足自定义条件」。中文瑜伽用中文组合句。不改行星中文名本身,也不改八分法引擎文件;报告层把粘在一起的方法名改成带空格的读法。残留门只数附录之前的正文,图盘围栏不计入:汉字与至少 3 个拉丁字母直接粘连为 0,功能词 of/in/the/and/by/from/with/listed/background/native/condition/met/satisfied/occupied 为 0。括号里的梵文和表头缩写 SAV、BAV、KP、D1–D60 不在这两项里。
|
||||
- 验证:故意写入「木星Jupiterexalted」时残留门为 fail。虚构日盘、虚构夜盘、一张公开名人盘(爱因斯坦)的中文完整数据版都是粘连 0、功能词 0。`tests/test_report_chart_blank_columns.py` 16 通过。打回第 1 次:报告星位表已印成「灵魂星(AK)」这类带括号的名字,`test_reader_report_karaka_table_is_in_bphs_order` 仍按 `KARAKA_CN` 原字面值比对,已改为经 `translate_zh_glued_phrases` 后再比对。`KARAKA_CN` 未改。
|
||||
- 防复发:`_pl9_markdown_hygiene_receipt(language="zh")` 把上述两项写成违规。
|
||||
- 相关记录:BUG-1313、BUG-1314。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/report-translation-gaps-20261013`,尚未合入 staging。
|
||||
|
||||
## BUG-1313 | 完整数据版「本命与分盘北印度图盘」只有空标题,正文不画图
|
||||
|
||||
- 状态:fixed-pending-verify(旧报告正文不变,须重新生成;真机清单未走,不标 resolved)
|
||||
- 首次发现 / 最近更新:2026-10-13 / 2026-10-13
|
||||
- 来源:issue #53。
|
||||
- 现象:该节是一列盘名,没有北印度方形图。报告末尾因此仍出现第二遍「分盘核对」。
|
||||
- 根因:完整数据版只写空的小标题,不写图盘围栏。10-07 个人报告改用完整数据版之后,BUG-1203「正文画全分盘、末尾整段消失」的前提没了。当时的测试只用读者版样例,没拦住这次切换。
|
||||
- 修复:这一节里每张有星座可画的盘写一张北印度图,同一编号只画一次。去掉与九分盘重复的第二张,以及画不出来的空宫位标题。逆行跟本命。中英标题都有。末尾「分盘核对」仍用原来的去重,不新做折叠。旧样例保留,用来锁住「旧正文仍显示分盘核对」。
|
||||
- 验证:虚构日盘中英两版该节围栏可解析、版式为北印度、编号不重复、没有空标题。新的完整数据样例覆盖计算图全部编号时,末尾核对段渲染为空。前端相关测试 39 通过、0 失败、0 取消。
|
||||
- 防复发:`tests/test_report_chart_blank_columns.py::test_full_data_chart_section_draws_each_ready_chart_once`;`frontend/tests/report-calculation-charts.test.tsx` 用真实引擎输出的新样例锁住末尾消失,旧样例仍锁住旧正文保留该节。
|
||||
- 相关记录:BUG-1203、BUG-1312。
|
||||
- 复发自:BUG-1203(完整数据版切换后正文无围栏;当时只用读者版样例)。
|
||||
- 修复版本:分支 `codex/report-translation-gaps-20261013`,尚未合入 staging。
|
||||
|
||||
## BUG-1314 | 报告「本命盘行星原始位置」没有上升行
|
||||
|
||||
- 状态:fixed-pending-verify(真机清单未走,不标 resolved)
|
||||
- 首次发现 / 最近更新:2026-10-13 / 2026-10-13
|
||||
- 来源:issue #53。星盘页上升行已由 BUG-1201 补上星宿、宿主、pada;报告这张表没有。
|
||||
- 现象:报告本命位置表从九曜开始,看不到上升的星座、度数、星宿和 pada。
|
||||
- 根因:表只遍历九曜。上升点上的星宿字段已经算好,报告没用。
|
||||
- 修复:中文表首行写「上升」,英文写 Ascendant。宫位为 1,星宿用「中文(梵文)」,pada 与星宿计算一致。主星表上升行的尊贵状态写「不适用」(英文 not applicable),星宿主用星宿主星。不给上升另造庙旺。
|
||||
- 偏离:任务书要求逆行和速度两格写横杠。完整数据版原有的空格清理会把单独的横杠格改成「未列」(英文 not listed),和别的空格子一样。主星表里相邻的两个空格,后一格仍会留下一条横杠。尊贵状态保持「不适用」。测试不要求必须看见那条横杠。
|
||||
- 验证:虚构日盘上升行的星宿、pada、宫位 1、星宿主与星宿计算一致;尊贵状态为「不适用」。同文件既有用例仍通过(16 通过)。
|
||||
- 防复发:`test_birth_position_table_starts_with_the_ascendant`。
|
||||
- 相关记录:BUG-1201、BUG-1099。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/report-translation-gaps-20261013`,尚未合入 staging。
|
||||
|
||||
## BUG-1315 | 星盘页 D9「同位」列几乎全是横杠,读起来像没算
|
||||
|
||||
- 状态:fixed-pending-verify(真机清单第 7 步未走,不标 resolved)
|
||||
- 首次发现 / 最近更新:2026-10-13 / 2026-10-13
|
||||
- 来源:issue #53。庙旺列已由 BUG-1202 修好;这一列不是。
|
||||
- 现象:D9 表「同位」列里,不是同座的行星显示「—」。大多数行星都不是同座,整列几乎都是横杠。
|
||||
- 根因:横杠表示「不是同位」,和「这一格没数据」长得一样。
|
||||
- 决策:按任务书 D2 的推荐执行。同座写「是」,不同座写「否」,表头仍是「同位」。不改 BUG-1099:不是 D9 就不显示这一列;旧缓存没有能力声明时整列隐藏。
|
||||
- 修复:行星行和上升行都改成「是 / 否」。`frontend/DESIGN.md` 同步。缺星宿、庙旺、宫位时别处仍用横杠,本轮没改。
|
||||
- 验证:`frontend/tests/chart-varga-table.test.tsx` 断言三栏——原值:同位格「同位」、非同位「—」;新值:「是」「否」;原因:BUG-1315。另加「非同位行显示否」和「非 D9 没有同位列」。`chart-page-hook` 里表头「同位」的检查仍在。前端这三份测试 39 通过、0 失败、0 取消。`tsc --noEmit` 退出码 0。
|
||||
- 防复发:同上前端测试。
|
||||
- 相关记录:BUG-1099、BUG-1202。
|
||||
- 复发自:无。
|
||||
- 修复版本:分支 `codex/report-translation-gaps-20261013`,尚未合入 staging。
|
||||
|
||||
## BUG-1317 | 明说要做生时校正时,仍先排完盘再写解读,校正没打开
|
||||
|
||||
- 状态:fixed-pending-verify(真机清单未走,没有模型凭据,不标 resolved)
|
||||
- 首次发现 / 最近更新:2026-10-13 / 2026-10-13
|
||||
- 来源:issue #54。staging 首页普通对话第一句说要生时纠正,回答先显示排盘完成,再写校正开不了、走另一个入口。
|
||||
- 现象:没有打开校正。正文是模型自己写的解读,不是那句固定过渡。
|
||||
- 触发条件:新对话第一句明确要做校正。普通问题、以及只是觉得解读不准,不在这条里。
|
||||
- 根因:第 0 步只给排盘工具(BUG-923)。模型必须先排完盘,才看得到校正工具。规则里「明确要校正就打开」夹在禁止句中间,「纠正」又和「用户纠正解读」撞了。BUG-1306 的测试直接调用工具,没有让真实模型选工具;真机清单因没有模型钥匙没跑过。
|
||||
- 决策:产品选 B。第 0 步在校正工具确实挂上时,可以排盘,也可以打开校正。`toolChoice` 仍是 auto。两个都没按就写答案时,仍走原来的「运行合同不完整」重试,逼它先排盘。不加关键词判断。
|
||||
- 修复:本命和申报时段的第 0 步,在挂了校正工具时多放这一个工具。还没看盘就顺口提议,不算已经提议过,先去排盘。明确要求则第一步就打开,不先写解读。规则第一句改成「校正就在这个对话里能开」,并把「纠正规则」改称「改读规则」。`FOLLOW_UP_CORRECTION_RULE` 本身没改。没挂校正工具时,第 0 步仍只有排盘工具。
|
||||
- 验证:见 `docs/tasks/PROGRESS-rectification-step0-entry-20261013.md`。规则原 374 字,新 416 字,增加 42,不超过 80。
|
||||
- 防复发:第 0 步断言锁住「没挂只有排盘、挂了是两个工具」。另有三条:第一步明确要求不排盘且正文是固定过渡;一步都不按会重试到排盘后的回答;没看盘的顺口提议不占额度。
|
||||
- 相关记录:BUG-1306、BUG-923、BUG-937、BUG-922。
|
||||
- 复发自:BUG-1306。
|
||||
- 修复版本:分支 `codex/rectification-step0-entry-20261013`,尚未合入 staging。
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# 进度 · 明说要校正时第一步就能打开(2026-10-13)
|
||||
|
||||
- 分支:`codex/rectification-step0-entry-20261013`
|
||||
- 任务书:`docs/tasks/TASK-rectification-step0-entry-20261013.md`
|
||||
- 基线:`origin/staging` `0c04755560f0b6a772116130392b56eb2b40ae0e`(任务书写的 `963276ca2` 的后代,也是计划里的 base)
|
||||
- issue:#54,BUG-1317,复发自 BUG-1306
|
||||
|
||||
## 做了什么
|
||||
|
||||
1. 本命和申报时段的第 0 步,在校正工具挂上时可以排盘,也可以打开校正。没挂时仍只有排盘。选择仍是自动,没有改成必须调用某个工具。
|
||||
2. 还没看盘就顺口提议,不算已经提议过,先去排盘。这一轮明确要校正,第一步就打开,正文换成那句过渡。
|
||||
3. 规则第一句改成校正就在这个对话里能开,并写明校正、纠正、核对出生时间都算。不认同解读改称「改读规则」,避免和「生时纠正」撞词。
|
||||
4. 规则字数:原 374,新 416,增加 42,上限是加 80。
|
||||
|
||||
## 检查
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 定向测试 | `node node_modules/tsx/dist/cli.mjs --test` 五个相关测试文件 | 149 通过,0 失败,0 取消。类型收窄改完后,入口测试文件再跑 19 通过,0 失败,0 取消 |
|
||||
| 类型检查 | `frontend\node_modules\.bin\tsc --noEmit -p frontend` | 通过,退出码 0。第一次在测试里把「还没提议」收成永远是空,已改成先记在局部再断言 |
|
||||
| 前端 lint | `npm run lint --prefix frontend` | 退出码 0,0 error,125 条原有 warning,没有顺手改 |
|
||||
| 隐私标记 | `.venv\Scripts\python -m pytest tests/test_repo_privacy_markers.py -q` | 通过,退出码 0 |
|
||||
| 名人生平回测 | 未跑 | 本机没有模型钥匙 |
|
||||
| 数据库 | 未跑 | 不改表 |
|
||||
|
||||
## 偏离
|
||||
|
||||
- 没有。真机清单补在原来的 `docs/testing/rectification-explicit-entry-20261012.md`(第 1b 步和第 9 步),没有另写一份。
|
||||
|
||||
## 环境缺口
|
||||
|
||||
- 真实模型会不会在第一步打开校正,以及名人生平回测,都没有模型凭据,不能写成通过。部署后按真机清单,用 deepseek-v4-flash 和默认模型各走一遍。
|
||||
@@ -0,0 +1,50 @@
|
||||
# PROGRESS · 报告翻译缺口 · 2026-10-13
|
||||
|
||||
任务书:`docs/tasks/TASK-report-translation-gaps-20261013.md`。Issue #53。执行分支 `codex/report-translation-gaps-20261013`,基于当时的 `origin/staging` `05f880053`(任务书基线 `941b06c19` 的后代)。D1、D2 按任务书推荐执行:图盘并入正文,同位列写「是 / 否」。不需要另请拍板。
|
||||
|
||||
## 状态一览
|
||||
|
||||
| 任务 | 状态 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| T1 正文图盘 | 完成 | 有星座可画的盘各一张北印度图,编号不重复;去掉重复的九分盘和画不出的空宫位标题 |
|
||||
| T2 中文残留 | 完成 | 虚构日盘、虚构夜盘、公开名人盘(爱因斯坦)粘连 0、功能词 0。基线是粘连 14、功能词 218。未让步 |
|
||||
| T3 上升行 | 完成 | 位置表首行是上升;主星表尊贵状态「不适用」 |
|
||||
| T4 同位列 | 完成 | D9 写「是 / 否」,表头仍是「同位」;非 D9 无此列 |
|
||||
| T5 记录 | 完成 | BUG-1312~1315、本文件、CHANGELOG。真机清单在基线上已有,步骤与实现一致,未改写 |
|
||||
|
||||
## 偏离
|
||||
|
||||
- 上升行的逆行、速度,任务书写横杠。完整数据版原有清理会把单独的横杠格改成「未列」(英文 not listed),与其它空格相同。主星表相邻两个空格里,后一格仍留下一条横杠。尊贵状态仍是「不适用」。测试不把那条横杠当成必须出现。
|
||||
- `next build` 与首屏 gzip 未跑。任务书 §8 与执行提示把全量前端测试和 Python 快速门留给验收侧;本轮自测不含这两项,不写成通过。
|
||||
- `test:db` 未跑:没有改数据库。
|
||||
- 真机清单 `docs/testing/report-translation-gaps-20261013.md` 已在分支基线上,八步与当前行为一致,本轮没有改它,也没有往 `BLOCKED.md` 加浏览器项。
|
||||
|
||||
## 自测
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 类型 | `frontend\node_modules\.bin\tsc --noEmit -p frontend` | 退出码 0 |
|
||||
| 规范 | 在 `frontend` 下 `npm run lint` | 退出码 0;0 error,125 条既有 warning |
|
||||
| 改到的前端测试 | `node node_modules/tsx/dist/cli.mjs --test tests/chart-varga-table.test.tsx tests/chart-page-hook.test.ts tests/report-calculation-charts.test.tsx` | 39 通过,0 失败,0 取消 |
|
||||
| 图盘 / 上升 / 残留 | `.venv\Scripts\python -m pytest tests/test_report_chart_blank_columns.py -q --tb=line` | 16 通过,退出码 0 |
|
||||
| 英文版、附录、读者版、星盘表合同、完整数据修复、图盘围栏 | `.venv\Scripts\python -m pytest tests/test_report_english_edition.py tests/test_reader_appendix_language.py tests/test_reader_appendix_shared_rules.py tests/test_report_reader_main.py tests/test_chart_page_vedic_tables_contract.py tests/test_report_full_data_fix.py tests/test_report_chart_block.py -q --tb=line` | 88 通过,退出码 0 |
|
||||
| 隐私标记 | `.venv\Scripts\python -m pytest tests/test_repo_privacy_markers.py -q --tb=line` | 63 通过,退出码 0 |
|
||||
|
||||
未跑:前端全量 `npm test`、Python 快速门、`npm run test:db`、`next build`。
|
||||
|
||||
## 第 1 次打回(2026-10-13)
|
||||
|
||||
修复单:`docs/tasks/TASK-report-translation-gaps-fix-20261013.md`。报告四项本身不改。
|
||||
|
||||
- 修复单要求变基到当时的最新 `origin/staging`(`d79f42b04ba225e5be39d8567a7fdd38223e19fe`)。本地变基后的文件内容与现在相同,但这台机器不允许改写已经推送过的分支。因此改为把这份 staging 合并进本分支:测试版是本分支的祖先,推送不用改写历史。报告生成和星位中文原名没有再动。
|
||||
- 只改 `tests/test_chara_karaka_8_bphs_order.py` 里报告星位表那一条。期望值改成 `translate_zh_glued_phrases(KARAKA_CN[role])`,也就是「灵魂星(AK)」这一类。`scripts/jaimini.py` 的 `KARAKA_CN` 没改。
|
||||
- 检索 `tests` 与 `frontend/tests`:按 `KARAKA_CN` 原字面值断言完整数据版报告正文的,只有这一条。`frontend/tests/fixtures/report-reader-main-fictional-zh-pair.json` 未改。咨询金样和旧阅读版夹具里的「灵魂星AK」不是这份报告正文,未改。
|
||||
- `tests/test_prashna_sphuta_review_result_validator.py` 里基线已有的失败未动。
|
||||
|
||||
| 检查 | 命令 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 星位表、§8 六个文件、隐私标记 | `.venv\Scripts\python -m pytest tests/test_chara_karaka_8_bphs_order.py tests/test_report_chart_blank_columns.py tests/test_report_english_edition.py tests/test_reader_appendix_language.py tests/test_reader_appendix_shared_rules.py tests/test_report_reader_main.py tests/test_chart_page_vedic_tables_contract.py tests/test_repo_privacy_markers.py -q --tb=line` | 154 通过,退出码 0 |
|
||||
| 类型检查 | `frontend\node_modules\.bin\tsc --noEmit -p frontend` | 退出码 0 |
|
||||
| 前端 lint | `npm run lint --prefix frontend` | 0 error,125 warning |
|
||||
|
||||
未跑:前端全量 `npm test`、Python 快速门、`npm run test:db`(本轮没动数据库)、`next build`。
|
||||
@@ -211,6 +211,8 @@
|
||||
|
||||
| 任务书 | 进度 | 主题 | 状态 | 落点 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `TASK-rectification-step0-entry-20261013.md` | — | **明说要校正时第一步就能打开(agent 循环 issue #54,BUG-1306 复发)**:第 0 步只放排盘工具、规则正向句埋在禁止句里、「纠正」撞词,模型没带 explicitRequest 调工具(BUG-1317)。产品选 B:第 0 步放出校正工具 + 规则重排;推翻 BUG-923 第 0 步限制,保险用现有 runtime-contract-retry。执行方 Grok | 待领取 | `codex/rectification-step0-entry-20261013` |
|
||||
| `TASK-report-focus-card-20261013.md` | — | **报告开篇「重点」做成摘要卡(agent 循环 issue #55)**:10-08 加的开篇重点从未设计——三重「重点」、标题无样式、MD/AD/PD 行话、数据句堆砌(BUG-1316)。产品选 A 摘要卡 + 名字与日期 + 保留提醒句;引擎出 `jyotish-focus` 围栏,前端一个组件渲染。执行方 Grok | 待领取 | `codex/report-focus-card-20261013` |
|
||||
| `TASK-report-translation-gaps-20261013.md` | — | **个人报告中英混杂、正文图盘空白、上升行、同位列(agent 循环 issue #53)**:中文完整数据版靠短词替换英文,正文残留 218 处英文词与 14 处中英粘连,卫生检查只有黑名单(BUG-1312);完整数据版图盘节只有空标题、无围栏,BUG-1203 的回归(BUG-1313);报告本命表无上升行(BUG-1314);D9 同位列「—」读如没数据,改「是/否」(BUG-1315)。分盘核对段随 T1 自动消失。执行方 Grok | 待领取 | `codex/report-translation-gaps-20261013` |
|
||||
| `TASK-report-translation-gaps-fix-20261013.md` | — | **#53 第 1 次打回**:`a295afa4` 实现四项都验过,但分支未基于最新 staging,且全量 pytest 新增 1 条失败(报告 Jaimini 星位名改成「灵魂星(AK)」后 `test_chara_karaka_8_bphs_order` 未跟改)。要求 rebase + 按三栏改测试,不改 `KARAKA_CN` | 待领取 | `codex/report-translation-gaps-20261013` |
|
||||
| `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 之后 | 已验收(Claude 补修 BUG-1282 后合入 staging) | `codex/report-full-data-edition-20261007`(未推 staging) |
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# TASK · 明说要校正时第一步就能打开(issue #54,BUG-1306 复发)
|
||||
|
||||
- 日期:2026-10-13
|
||||
- 来源:Gitea issue #54(agent 循环试点),产品负责人在 staging 首页普通对话(deepseek-v4-flash)第一句说「请你帮我生时纠正」,回答「已完成 3 步、本轮技法 43 项」后写「生时校正这次我这边开不了,它走的是另一个入口……」,没有打开校正
|
||||
- 执行方:**Grok(Windows + Docker)**,分支 `codex/rectification-step0-entry-20261013`,只推这个分支
|
||||
- 验收:Claude(Linux)
|
||||
- BUG 编号:**BUG-1317**(BUG-1316 已预留给 issue #55;开工时核对 `docs/BUG_HISTORY.md` 最大号)。BUG-1317 标「复发自 BUG-1306」,并在 BUG-1306 记录里补一行指向它
|
||||
|
||||
## 1. 基线
|
||||
|
||||
- 基线 commit:`origin/staging` `963276ca2`
|
||||
- `git log 586561eb..origin/staging -- frontend/src/mastra frontend/src/lib/consultation-thinking-plan.ts frontend/src/lib/rectification-in-chat*.ts frontend/src/lib/stream-agent-response.ts frontend/src/app/api/consult frontend/src/hooks/use-consultation-run.ts` 为空:BUG-1306 部署后这条链没人动过,**不是代码回归**。
|
||||
|
||||
## 2. 事故实证
|
||||
|
||||
- 产品负责人这一步就是 `docs/testing/rectification-explicit-entry-20261012.md` 第 1 步(「请你帮我进行生时纠正」),未通过。该清单(BUG-1306 的 T5)因无模型凭据从未跑过(`BLOCKED.md`「TASK-rectification-explicit-entry-20261012:T0 日志、T5 模型」)。
|
||||
- 用户看到的是模型自由写的整段解读。而工具只要收到 `explicitRequest=true`,不论放行还是拒绝,服务端都会用固定句覆盖模型正文(`frontend/src/lib/rectification-in-chat.ts` 的 `RECTIFICATION_DIRECT_*_COPY`,`frontend/src/lib/stream-agent-response.ts::rectificationDirectRunCompleted`)。所以**模型没有带 `explicitRequest=true` 调用 `offer-birth-time-rectification`**,这是代码可证的,不是推测。
|
||||
- 「另一个入口」「开不了」在提示词、skill、`frontend/src` 里都搜不到,是模型自己编的;「我也不会先用一两件事去反推一个更窄的时间」是在复述规则里的禁止句。
|
||||
|
||||
BUG-1306 的防复发措施逐条核对仍在:唯一规则 `RECTIFICATION_OFFER_RULE`(`frontend/src/lib/consultation-thinking-plan.ts`)注入首轮、追问轮与时段路线;`createRectificationOfferTool` 与 `finishExplicitRectificationRequest`(`frontend/src/mastra/consultation-tools.ts`);`decideRectificationOfferWith`(`rectification-in-chat.ts`);`contractReady()` 对 `rectificationDirectEntry` 放行(`stream-agent-response.ts`);浏览器自动打开(`frontend/src/hooks/use-consultation-run.ts`)。
|
||||
|
||||
## 3. 根因
|
||||
|
||||
1. **第 0 步只给排盘工具。** `consultationNatalPrepareStep` / `consultationWindowPrepareStep`(`consultation-tools.ts`)在 `stepNumber === 0` 时 `activeTools` 只有排盘工具(BUG-923 定、BUG-937 改 `auto`)。模型必须先排完整盘(43 项),此时已进入解盘状态,才轮到它考虑校正工具。
|
||||
2. **规则正向句埋在禁止句中间。** `RECTIFICATION_OFFER_RULE` 以「用户说你推得不准时」开头,「明确要做校正就调用…explicitRequest=true」夹在「不承诺能收窄」「不在对话里收集经历」之间;模型照禁止句执行。
|
||||
3. **「纠正」撞词。** 同一规则写「按纠正规则认下、改读」,`FOLLOW_UP_CORRECTION_RULE` 讲的是用户「纠正」解读;用户说「生时纠正」被归到「读法问题、不提校正」那一支。
|
||||
4. **没告诉模型校正就在本对话里可开。**
|
||||
5. **为什么没拦住:** `frontend/tests/rectification-explicit-entry-20261012.test.ts` 直接 `tool.execute({explicitRequest:true})` 并用正则查规则文本;「真实模型是否选择调用」只能靠 T5,而 T5 一直是环境缺口。
|
||||
|
||||
## 4. 决策记录
|
||||
|
||||
- 产品负责人 2026-10-13 在 issue #54 选 **B:改规则 + 第 0 步可调用校正工具**(备选 A 只改规则、C 每个新对话加一次小模型意图判断均未选)。
|
||||
- **推翻 BUG-923「第 0 步只放排盘工具」**:第 0 步 `activeTools` 为「排盘工具 + `offer-birth-time-rectification`」(仅当本轮 `ctx.rectificationOffer` 存在,即该工具确实挂上时)。`toolChoice` 仍为 `"auto"`(BUG-282 / BUG-937 不变)。
|
||||
- 保险沿用现有机制,不另造:第 0 步模型既没排盘、也没走直接进入时,`contractReady()` 为假,`stream-agent-response.ts` 已有的 `runtime-contract-retry` 会重试,仍不就绪则按现有降级 / 失败处理。BUG-922 / 923 的「不看盘就写答案」不得因此复发。
|
||||
- 不加关键词 / 正则分类器(BUG-976/977、BUG-1247 的既有决定不变)。
|
||||
- 产品负责人已确认:普通问题(「看看事业」)仍先排盘再解读;「你说得不准」仍是认下改读、不主动拉去校正。
|
||||
|
||||
## 5. 硬红线
|
||||
|
||||
1. 不写任何按关键词判断意图的代码(含正则、词表匹配进入校正)。规则里列举「校正 / 纠正 / 核对出生时间」等说法只作为给模型的说明。
|
||||
2. `toolChoice` 不得改成 `required` 或指名工具(BUG-282 / BUG-937)。
|
||||
3. 第 0 步不得放出排盘与校正之外的工具;第 1 步起不变。
|
||||
4. 不改 `decideRectificationOfferWith` 的资格判断(本人 / 产品开关 / 进行中的校正 / 价格)、不改固定句文案、不改计费。
|
||||
5. 规则只在 `RECTIFICATION_OFFER_RULE` 一处定义(BUG-1248 / 1306),工具描述不复述规则。
|
||||
6. 不改数据库结构;不碰 `frontend/src/app/(app)/page.tsx`;不改 `.gitea/**`。
|
||||
7. 改既有断言按 §7.3 写「原值 / 新值 / 原因」三栏。开工先 grep:`grep -rn "consultationNatalPrepareStep\|consultationWindowPrepareStep\|RECTIFICATION_OFFER_RULE\|纠正规则\|activeTools" frontend/tests tests`(已知:`frontend/tests/consultation-agentic-runtime.test.ts` 「natal first step exposes only the chart calculation tool」与 window 对应用例、`frontend/tests/consult-single-pass-answer-20260927.test.ts`、`frontend/tests/rectification-explicit-entry-20261012.test.ts`)。
|
||||
8. 改了普通对话提示词:按 AGENTS.md §10 需要名人生平回测,本轮无模型凭据,写成环境缺口(见 §10),不得写成通过。
|
||||
|
||||
## 6. 任务分解
|
||||
|
||||
### T1 · 第 0 步放出校正工具
|
||||
|
||||
- `consultationNatalPrepareStep` / `consultationWindowPrepareStep` 改为能知道本轮是否挂了校正工具(例如改成工厂 `consultationNatalPrepareStepFor({ rectificationOffer: boolean })`,或在 `route.ts` 组装时传入),第 0 步 `activeTools` = 排盘工具 +(有则)`RECTIFICATION_OFFER_TOOL_ID`;没挂校正工具时与现在逐字节相同。
|
||||
- 第 0 步模型以**非** `explicitRequest` 方式调用校正工具(还没看过盘就想「顺口提议」):工具返回一句「先排盘,看过盘再决定要不要提」之类的说明,**不**记为已提议(不占用「同一次对话提议过就不再提」的额度),之后照常排盘。
|
||||
- 验收:
|
||||
- `consultation-agentic-runtime.test.ts` 两条第 0 步断言按三栏改,新增「没挂校正工具时第 0 步只有排盘工具」「挂了时第 0 步是两者」。
|
||||
- 新增运行时测试(用现有假模型 / 假流的写法):① 第 0 步直接 `explicitRequest=true` → 不调用排盘、正文是固定过渡句、`contractReady` 为真;② 第 0 步什么工具都不调用就写字 → 走 `runtime-contract-retry`,最终答案来自排盘之后;③ 第 0 步非 explicit 调用校正工具 → 不记为已提议,随后排盘、正常作答。
|
||||
|
||||
### T2 · 改写规则
|
||||
|
||||
- `RECTIFICATION_OFFER_RULE` 重排,**正向句放第一句**,大意:「校正就在这个对话里能开。用户这一轮明确要做出生时间校正(说法可能是校正、纠正、核对出生时间、看看出生时间准不准等),第一步就调用 offer-birth-time-rectification 并传 explicitRequest=true,不用先排盘,工具通过后只写那一句过渡。」其后才是「推得不准 → 先分读法 / 时间」与各禁止句。
|
||||
- 把规则里描述「用户不认同解释」那支的「纠正规则」改称「改读规则」(`FOLLOW_UP_CORRECTION_RULE` 的名字与内容不改,只改 `RECTIFICATION_OFFER_RULE` 里引用它的叫法),避免与「生时纠正」撞词。
|
||||
- 提示词总长不得明显增长(BUG-1256 瘦身后的口径):新规则字数 ≤ 原规则 + 80 字,进度记录写原 / 新字数。
|
||||
- 验收:`rectification-explicit-entry-20261012.test.ts` 里查规则文本的断言按三栏改;新增断言:规则第一句含 `explicitRequest=true`,规则文本不含「纠正规则」。
|
||||
|
||||
### T3 · 记录
|
||||
|
||||
- `docs/BUG_HISTORY.md` 新增 BUG-1317(复发自 BUG-1306:为何旧测试与 T5 没拦住、本次推翻 BUG-923 第 0 步限制的决策、保险机制),BUG-1306 记录补「复发:BUG-1317」。
|
||||
- `docs/tasks/PROGRESS-rectification-step0-entry-20261013.md`、`CHANGELOG.md`。
|
||||
- 真机清单:在 `docs/testing/rectification-explicit-entry-20261012.md` 第 1 步后补一行「新对话第一句:『请你帮我生时纠正』(不带『进行』)」,并加第 9 步「新对话问『看看我今年事业』:照常先排盘解读,不出现校正」;或另写 `docs/testing/rectification-step0-entry-20261013.md` 引用原清单。
|
||||
|
||||
## 7. 让步顺序
|
||||
|
||||
T1 与 T2 都必须做(只做 T2 就是产品没选的 A)。时间不够时 T1 的第③条(非 explicit 调用不占额度)可降为「记录在 BLOCKED.md,回报里说明」。
|
||||
|
||||
## 8. 验收命令分工
|
||||
|
||||
| 谁 | 命令 |
|
||||
| --- | --- |
|
||||
| Grok 自跑 | 前端 `tsc --noEmit`、`npm run lint`;`node --test` 跑 `consultation-agentic-runtime.test.ts`、`consult-single-pass-answer-20260927.test.ts`、`rectification-explicit-entry-20261012.test.ts` 及新增文件(fail 与 cancelled 都为 0)。前端全量 `npm test`、整站构建不跑,以 Claude 为准 |
|
||||
| Claude 验收 | 上述 + 前端全量 `npm test` 与基线逐条比对 + `next build`(`/` 仍 Static,首屏 gzip ±2%)+ 逐行审规则文本 |
|
||||
| 不需要 | `test:db`、Python 快速门(不动 Python、不动数据库访问层) |
|
||||
|
||||
## 9. 开工前置命令
|
||||
|
||||
```bash
|
||||
git fetch origin --prune
|
||||
git worktree add -b codex/rectification-step0-entry-20261013 .worktrees/rectification-step0-entry-20261013 origin/staging
|
||||
cd .worktrees/rectification-step0-entry-20261013
|
||||
git log -1 --format=%h # 应为 963276ca2 或其后代
|
||||
grep -n "^## BUG-" docs/BUG_HISTORY.md | tail -3 # 核对最大号;BUG-1316 已预留给 #55
|
||||
grep -rn "consultationNatalPrepareStep\|consultationWindowPrepareStep\|RECTIFICATION_OFFER_RULE\|纠正规则\|activeTools" frontend/tests tests
|
||||
```
|
||||
|
||||
## 10. 环境缺口(预告)
|
||||
|
||||
- **真实模型是否会在第一步调用校正工具**,本机与 Grok 侧都没有模型凭据(供应商 key 加密存于管理端数据库),无法自动验证;名人生平回测同理。部署后由产品负责人按真机清单用 deepseek-v4-flash 与默认模型各走一遍(每个场景至少两次)。在此之前 BUG-1317 只能是 `fixed-pending-verify`。
|
||||
@@ -0,0 +1,144 @@
|
||||
# TASK · 个人报告开篇「重点」页做成摘要卡(issue #55)
|
||||
|
||||
- 日期:2026-10-13
|
||||
- 来源:Gitea issue #55(agent 循环试点),产品负责人截图:报告第 1/39 章「重点」只有一行粗体标题、一行「重点:当前主运 MD …」、一个「## 重点」大标题和四句数据句,「一点 UI 设计都没有」
|
||||
- 执行方:**Grok(Windows + Docker)**,分支 `codex/report-focus-card-20261013`,只推这个分支
|
||||
- 验收:Claude(Linux)
|
||||
- BUG 编号:**BUG-1316**(开工时核对 `docs/BUG_HISTORY.md` 最大号;写作时最大号为 BUG-1315)
|
||||
|
||||
## 1. 基线
|
||||
|
||||
- 基线 commit:`origin/staging` `856bc850b`
|
||||
- 个人报告走完整数据版:`frontend/src/lib/personal-report-longform-generate.ts` 请求体 `edition: "full_data"` → `scripts/pl9_full_data_export.py::render_full_data_markdown` → `scripts/pl9_reader_export.py::render_pl9_parity_markdown`。
|
||||
- 复现(虚构盘):
|
||||
|
||||
```bash
|
||||
PYTHONPATH=.:scripts python - <<'EOF'
|
||||
import copy
|
||||
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
|
||||
from tests.test_report_english_edition import CASES, _packet
|
||||
print(_pl9_export_markdown_for_edition(copy.deepcopy(_packet(CASES["day"])), "full_data")[:1200])
|
||||
EOF
|
||||
```
|
||||
|
||||
## 2. 事故实证(`856bc850b`)
|
||||
|
||||
虚构日盘中文完整数据版开头,与产品截图结构一致:
|
||||
|
||||
```
|
||||
# 印度占星星盘报告
|
||||
重点:当前主运 MD 北交点,子运 AD 木星,次子运 PD 木星。
|
||||
## 重点
|
||||
- 事业第10宫主 土星 落在第9宫,尊贵为入庙,功能吉凶为功能吉星。
|
||||
- 财富第2宫主 … / 婚恋第7宫主 …
|
||||
- 这段时间可能突出这些宫主所守护的主题,不是确定会发生的事。
|
||||
## 出生资料与图盘 …
|
||||
```
|
||||
|
||||
| # | 问题 | 位置 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 「重点」三次:目录第 1 章、「重点:…」一行、`## 重点` | `scripts/pl9_interpretation_packs.py::opening_focus_block` 写死该行;`scripts/pl9_full_data_export.py::_sanitize_pl9_ai_density_markdown` 在首行没有「摘要:/结论:/重点:」时会注入一段固定的「摘要… / ## 重点 1.2.3.」清单,`_insert_opening_focus` 也按这一行去重,所以这一行是为了躲开注入才存在的 |
|
||||
| 2 | 报告标题与正文一样大 | `frontend/src/components/personal-report/personal-report-markdown-view.tsx::markdownComponents` 无 `h1` 映射;`frontend/src/app/globals.css` 中 `.personal-report-md-article h1` 只设字体(注释仍写「像 `.personal-report-cover h1`」,封面已于 09-29 去掉) |
|
||||
| 3 | 行话与车轱辘话:MD / AD / PD 无起止日期;「功能吉凶为功能吉星」;「宫主 火星 落在」多空格 | `opening_focus_block::lord_line`,`_role` 已返回「功能吉星」又被拼在「功能吉凶为」后 |
|
||||
| 4 | 无版式:四句纯文本列表 | 同上;阅读器把首个 `##` 之前的内容当 lead,与 `## 重点` 同在第 0 章卡片里 |
|
||||
|
||||
## 3. 根因
|
||||
|
||||
10-08 `TASK-report-full-data-fix-20261008` T3 按上游加了「开篇重点:当前三级大运 + 三宫宫主」,验收只要求内容存在,没有任何呈现规定;`frontend/DESIGN.md`「Personal report reader」也没有开篇或标题的条目。09-29 去掉报告封面后,标题只剩字体,没人回头补。**不是回归,是从未设计。** `docs/BUG_HISTORY.md` 检索「重点:」「opening_focus」「当前主运 MD」无记录。
|
||||
|
||||
## 4. 决策记录
|
||||
|
||||
- 产品负责人 2026-10-13 在 issue #55 选 **A:做成设计过的摘要卡**(备选 B 改写两三句、C 删页均未选)。
|
||||
- 标题下**写名字和生成日期**;**保留**末尾提醒句「这段时间可能突出这些主题,不是确定会发生的事」。
|
||||
- 问题 1~4 一并修。
|
||||
- 不推翻任何既有红线;不恢复 09-29 去掉的报告封面(只给标题本身样式)。
|
||||
|
||||
## 5. 硬红线
|
||||
|
||||
1. 不改引擎数值(大运、宫主、落宫、尊贵、功能吉凶的判定);只改呈现。
|
||||
2. 摘要卡的数据由 Python 生成结构化围栏(与 `jyotish-chart` 同一模式),前端只解析和渲染;前端不自算大运或宫主。
|
||||
3. 只做一个新组件;不写第二套 Markdown 渲染、不加 spinner / 骨架(AGENTS.md §6)。
|
||||
4. 旧报告(正文里没有新围栏)必须照旧显示,不崩、不空白。
|
||||
5. 名字只在报告阅读页(本人登录可见)显示;**不得**把名字加进 `frontend/src/lib/report-public-projection.ts` 的公开投影,也不写进任何测试 / 文档 / 提交里的真实姓名(测试用虚构名)。
|
||||
6. 不改数据库结构与迁移;不碰 `frontend/src/app/(app)/page.tsx`;`scripts/jyotish_api_server.py` 不增长。
|
||||
7. 改既有断言按 §7.3 写「原值 / 新值 / 原因」三栏;开工先 grep:`grep -rn "重点:\|## 重点\|Focus: current\|opening_focus\|personal-report-md-lead\|personal-report-md-article h1" tests frontend/tests`(已知:`tests/test_report_full_data_fix.py` 173–174、197 行附近)。
|
||||
8. 不升级依赖,不顺手修别的 warning。
|
||||
|
||||
## 6. 任务分解
|
||||
|
||||
### T1 · 引擎输出结构化「重点」围栏(Python)
|
||||
|
||||
- `opening_focus_block` 改为输出:
|
||||
|
||||
````
|
||||
## 重点
|
||||
```jyotish-focus
|
||||
{"version":1,"periods":[{"level":"md","lord":"Rahu","start":"YYYY-MM-DD","end":"YYYY-MM-DD"},…ad,pd],
|
||||
"domains":[{"key":"career","house":10,"lord":"Saturn","placed":9,"dignity":"…","role":"…"},…wealth/2,relationship/7],
|
||||
"caveat":"这段时间可能突出这些宫主所守护的主题,不是确定会发生的事。"}
|
||||
```
|
||||
````
|
||||
|
||||
起止日期取当前 MD / AD / PD 行自带的 `start` / `end`(`_timing(packet)["dasha"]`,见同文件英文分支已在用 `current_ad.get('start')`);拿不到的写 `null`,不编。`dignity` / `role` 用现有 `_dignity` / `_role` 的公开词(中文版中文、英文版英文)。
|
||||
- 删掉「重点:当前主运 MD …」那一行(中英两版)。
|
||||
- `_sanitize_pl9_ai_density_markdown` 的「开篇摘要」判断与 `_insert_opening_focus` 的去重,改为识别 `## 重点`(英文 `## Focus`)及其下的 `jyotish-focus` 围栏,**不得**因删掉那一行而重新注入固定清单。
|
||||
- 围栏格式写一个 Python 构造函数(建议放 `scripts/report_chart_block.py` 旁新建 `scripts/report_focus_block.py`,或同文件),附 `FOCUS_FENCE_RE`。
|
||||
- 中文残留门(BUG-1312 的 `_zh_body_residue`)对围栏的处理与 `jyotish-chart` 一致:结构化负载不计入正文。
|
||||
- 验收:
|
||||
- 新测试:虚构日盘中英两版,`## 重点` 下恰有一个可解析的 `jyotish-focus` 围栏;`periods` 三级 lord 与 `_current_lords` 一致、日期与大运表一致;`domains` 三行宫号 10 / 2 / 7;全文不再出现「重点:当前主运」与 `MD ` / `AD ` / `PD `(中文版);不出现固定清单「先核对出生资料、图盘」。
|
||||
- `tests/test_report_full_data_fix.py` 相关断言按三栏改。
|
||||
- `tests/test_report_chart_blank_columns.py`、`tests/test_report_english_edition.py`、`tests/test_reader_appendix_language.py`、`tests/test_report_reader_main.py` 全绿。
|
||||
|
||||
### T2 · 阅读页摘要卡组件(前端)
|
||||
|
||||
- `frontend/src/lib/` 新增解析器(参照 `report-chart-block.ts::parseReportChartBlock`),`personal-report-markdown-view.tsx` 的 `pre` 分支识别 `jyotish-focus` 渲染新组件 `ReportFocusCard`(放 `frontend/src/components/personal-report/`)。
|
||||
- 版式(写进 DESIGN.md,用现有 token,不新增颜色):
|
||||
- 第一行「现在走的大运」:主运 · 子运 · 次子运,各写行星中文名 + 起止(`YYYY.MM – YYYY.MM`),三者横排,窄屏换行;中文不出现 MD / AD / PD。
|
||||
- 下面三行:事业 / 财富 / 婚恋。每行左侧领域名,右侧「第 N 宫主 · 行星 · 落第 M 宫」+ 两个小标签(尊贵、功能吉凶),标签沿用站内已有的 pill / badge 样式。
|
||||
- 最后一行提醒句,弱化字色、小一号。
|
||||
- 手机宽度不横向滚动;深浅色都可读。
|
||||
- 解析失败:显示一句「重点数据无效」(英文 "Focus data is invalid"),与图盘无效同口径,不崩。
|
||||
- 导出与公开投影:`frontend/src/lib/report-export-blocks.ts` 把该围栏转成可读文字(参照 `chartBlockText`),导出文件里不得出现原始 JSON;`report-public-projection.ts` 对该围栏的处理与 `jyotish-chart` 一致(按其现有可见性规则),不新增名字字段。
|
||||
- 验收:新测试——用 T1 真实引擎输出生成的 golden(`frontend/tests/fixtures/`,`fixtureProvenance` 写明命令,虚构盘),渲染出三级大运与三行领域、无 `MD`、有提醒句;坏 JSON 渲染失败句;导出文本无 `{"version"`;旧正文(无围栏)快照不变。
|
||||
|
||||
### T3 · 标题样式 + 名字与日期(前端)
|
||||
|
||||
- `markdownComponents` 增加 `h1` 映射与样式:显示字体、明显大于 h2、上下留白;去掉 globals.css 里指向已删封面的过时注释。
|
||||
- 标题下一行小字:`{名字} · 生成于 YYYY-MM-DD`。名字取报告文档 `subject.displayName`(`personal-report-generation.ts` 已写入),日期取 `createdAt`;若阅读页当前拿不到 `displayName`,从报告读取接口带出(仅本人可见的接口,不改表结构)。没有名字时只写日期;标题本身已含名字(引擎 `_pl9_public_report_title` 在有名字时会写「X的印度占星星盘报告」)时不重复名字。
|
||||
- 英文版:`{name} · Generated YYYY-MM-DD`。
|
||||
- 验收:前端测试覆盖「有名字」「无名字」「标题已含名字」三种;`tsc --noEmit` 0 错、`npm run lint` 0 error。
|
||||
|
||||
### T4 · 记录
|
||||
|
||||
- `frontend/DESIGN.md`「Personal report reader」加「开篇重点卡」与「报告标题」两条(版式、token、旧报告兼容、导出口径)。
|
||||
- `docs/BUG_HISTORY.md` 新增 **BUG-1316**(报告开篇「重点」页无设计:三重「重点」、标题无样式、MD/AD/PD 行话、数据句堆砌)。
|
||||
- `docs/tasks/PROGRESS-report-focus-card-20261013.md`、`CHANGELOG.md`。
|
||||
- 真机清单 `docs/testing/report-focus-card-20261013.md`(已随本任务书提供,可补充)。
|
||||
|
||||
## 7. 让步顺序
|
||||
|
||||
T1 → T2 → T3 → T4。T3 的「名字」若需要改报告读取接口且发现牵涉数据库或公开投影,先只做日期 + 标题样式,名字写进 `BLOCKED.md` 并在回报里说明,由 Claude 判断是否 @产品负责人。
|
||||
|
||||
## 8. 验收命令分工
|
||||
|
||||
| 谁 | 命令 |
|
||||
| --- | --- |
|
||||
| Grok 自跑 | `python -m pytest tests/test_report_full_data_fix.py tests/test_report_chart_blank_columns.py tests/test_report_english_edition.py tests/test_reader_appendix_language.py tests/test_report_reader_main.py -q`;前端 `tsc --noEmit`、`npm run lint`、改到 / 新增的前端测试单文件(看 fail 与 cancelled 都为 0)。前端全量 `npm test`、Python 快速门、整站构建**不跑**,以 Claude 为准 |
|
||||
| Claude 验收 | 上述全部 + 全量 `pytest` 与基线逐条比对 + 前端全量 `npm test` + `next build`(`/` 仍 Static,首屏 gzip ±2%)+ 重生成虚构盘中英报告核对围栏 |
|
||||
| 不需要 | `test:db`(不动数据库访问层;若 T3 改了报告读取接口的查询,则需要并按 `windows-known-failures.txt` 比对) |
|
||||
|
||||
## 9. 开工前置命令
|
||||
|
||||
```bash
|
||||
git fetch origin --prune
|
||||
git worktree add -b codex/report-focus-card-20261013 .worktrees/report-focus-card-20261013 origin/staging
|
||||
cd .worktrees/report-focus-card-20261013
|
||||
git log -1 --format=%h # 应为 856bc850b 或其后代
|
||||
grep -n "^## BUG-" docs/BUG_HISTORY.md | tail -3 # 核对最大号
|
||||
grep -rn "重点:\|## 重点\|Focus: current\|opening_focus\|personal-report-md-lead\|personal-report-md-article h1" tests frontend/tests
|
||||
```
|
||||
|
||||
## 10. 环境缺口(预告)
|
||||
|
||||
- 只对**新生成**的报告生效;旧报告照旧显示。
|
||||
- 浏览器级观感留给 `docs/testing/report-focus-card-20261013.md`。
|
||||
@@ -5,6 +5,7 @@
|
||||
| 步 | 怎么做 | 通过标准 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 新对话第一句:「请你帮我进行生时纠正」 | 直接打开校正。这一轮不写解读,不用点卡。过渡只有一句 |
|
||||
| 1b | 新对话第一句:「请你帮我生时纠正」(不带「进行」) | 同样直接打开校正,不先出「已完成几步」再写解读。BUG-1317 |
|
||||
| 2 | 另开三条新对话,分别说「帮我校正出生时间」「想核对一下出生时间准不准」「我出生时间是估的,帮我对一下」 | 三次都直接打开,不先出卡 |
|
||||
| 3 | 新对话先问一件普通的事,答完后再说「请帮我做生时校正」 | 这一轮直接打开,不把刚才的解读再写一遍 |
|
||||
| 4 | 新对话问事业,答完后说「你说我事业不顺,我觉得挺顺的」 | 认下、改读。不进入校正,下面没有提议卡 |
|
||||
@@ -12,5 +13,6 @@
|
||||
| 6 | 换到一个家人档案,第一句就说「请帮我校正出生时间」 | 一句「给家人校正暂时还不支持」。不打开校正 |
|
||||
| 7 | 在校正开关关闭,或价格暂时不可用时,本人说「请帮我做生时校正」 | 一句「出生时间核对这会儿还做不了。」不沉默,不写价格,不写内部原因。构造不了这一步就在进度里标环境缺口,不要改开关去试 |
|
||||
| 8 | 申报出生时段的本人档案,问一件普通的事,不要提校正 | 正文不写大运、子运的起止年月日,也不承诺能收窄出生时间 |
|
||||
| 9 | 新对话问「看看我今年事业」 | 照常先排盘再解读,不出现校正,下面没有提议卡 |
|
||||
|
||||
做不了的两句和家人那句以界面上的原句为准。点数:直接进入的那一轮不按解读扣咨询点;校正本身仍按原来的开案扣费。
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# 真机清单 · 报告开篇「重点」摘要卡(issue #55,BUG-1316)
|
||||
|
||||
部署到 staging 后,**重新生成一份个人报告**(旧报告不会变),手机和电脑各看一遍。
|
||||
|
||||
1. 第 1 章最上面是报告标题,明显比正文大;标题下一行小字写「名字 · 生成于 年-月-日」(没有名字的盘只写日期;标题里已有名字时不重复)。
|
||||
2. 「重点」只出现一次(目录里那一条除外),没有单独一行「重点:当前主运 MD …」。
|
||||
3. 第一行写「现在走的大运」:主运、子运、次子运各是哪颗星、从哪年哪月到哪年哪月;没有 MD / AD / PD 字样。
|
||||
4. 下面三行:事业、财富、婚恋,各写「第几宫主、哪颗星、落第几宫」,后面跟两个小标签(强弱、吉凶);没有「功能吉凶为功能吉星」这种重复。
|
||||
5. 最后一行小字提醒「这段时间可能突出这些主题,不是确定会发生的事」。
|
||||
6. 手机上不需要左右滑;深色、浅色模式都看得清。
|
||||
7. 切到 English:同样的卡片,英文标签。
|
||||
8. 导出一份文件,打开后「重点」是文字,不是一串花括号代码。
|
||||
9. 打开一份**以前**生成的旧报告:照旧能看,不白屏。
|
||||
+1
-1
@@ -1184,7 +1184,7 @@ D40 / D45 / D60 的表格上方,出生时间不是「已校正」时多一句
|
||||
| 盘型 | 列 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 本命 | 上面八列 | 不变(与基线渲染逐字节相同) |
|
||||
| D2–D60 | 行星 · 分盘星座(度°分′)· 宫位 · 本命星座 · 庙旺;仅 D9 再有同位 | 宫位从该分盘上升数;庙旺是引擎给的十档(入旺 / 本垣 / 入庙 / 极友 / 入友 / 中性 / 入敌 / 极敌 / 落陷 / 落陷取消),措辞与本命「状态」列一字不差(2026-10-03,BUG-1202,取代原先「入旺 / 自宫 / 落陷,其余—」的三档);旧缓存没有引擎等级时整列不显示,页面不自算;上升此格「不适用」。2026-09-29 收口:仅 D9 的 D1↔D9 同座比较显示为同位,其他分盘不显示此列。两列各由服务端能力声明控制,缺声明保守隐藏,不以是否命中推断支持性。**没有星宿 / 宿主 / 足**:那是本命经度的属性。逆行在行星格后标「逆」(分盘不改顺逆)。首行是分盘上升 |
|
||||
| D2–D60 | 行星 · 分盘星座(度°分′)· 宫位 · 本命星座 · 庙旺;仅 D9 再有同位 | 宫位从该分盘上升数;庙旺是引擎给的十档(入旺 / 本垣 / 入庙 / 极友 / 入友 / 中性 / 入敌 / 极敌 / 落陷 / 落陷取消),措辞与本命「状态」列一字不差(2026-10-03,BUG-1202,取代原先「入旺 / 自宫 / 落陷,其余—」的三档);旧缓存没有引擎等级时整列不显示,页面不自算;上升此格「不适用」。2026-09-29 收口:仅 D9 的 D1↔D9 同座比较显示此列,其他分盘不显示。2026-10-13:同座写「是」,不同座写「否」,表头仍是「同位」(不再用横杠,避免看成没算)。两列各由服务端能力声明控制,缺声明保守隐藏,不以是否命中推断支持性。**没有星宿 / 宿主 / 足**:那是本命经度的属性。逆行在行星格后标「逆」(分盘不改顺逆)。首行是分盘上升 |
|
||||
| 月亮盘 | 本命八列 | 宫位从月亮所在星座数;表头「月亮盘 · 以月亮所在星座(X)为第 1 宫」;盘上「As」画在本命上升落的那一宫(`VedicChartSvg` 的 `ascendantHouse`,其它盘默认 1 宫不变) |
|
||||
| Bhava | 行星 · 黄道经度 · 星座宫 · Bhava 宫 · 换宫;另一张「宫 · 宫头 · 起点(sandhi)」12 行 | Sripati。换宫的行用 `--color-selected` 浅底。拿不到真实 MC 时这一栏走失败句,不画等宫(BUG-1077) |
|
||||
| 行运 | 行星 · 今天的位置 · 速度 · 落本命 · 本命同星 | 表头写计算时刻「行运 · YYYY-MM-DD 12:00(时区)」,上方一句「以本命上升为第 1 宫,放的是这一天中午的行星位置。岁差和交点与本命相同。」 |
|
||||
|
||||
@@ -25,8 +25,8 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、
|
||||
|
||||
## 生时校正从对话进入(2026-10-07,BUG-1247~1251;2026-10-12,BUG-1306)
|
||||
|
||||
- **何时提议**:用户说推得不准时先分清读法问题和时间问题。只是不认同某个解释,按纠正规则认下、改读,不提校正。不承诺能收窄出生时间,不在对话里收集经历做校正。说出过去的具体事情和盘对不上、说出生时间是估的或记不准、但没有明确要做校正,才调用提议工具。同一次对话提议过、用户没接,不再主动提;用户自己再要求时传 `userAsked=true`。规则只定义在 `consultation-thinking-plan.ts` 的 `RECTIFICATION_OFFER_RULE`。不用关键词判断。
|
||||
- **明确要求则直接进入**:用户这一轮明确要做校正,由模型判断,不用关键词。服务端放行后直接打开校正:不先写解读,不用点卡。过渡只有一句「好,我们开始核对出生时间。」已经有一次没做完的,直接接着做那一次,不新开。开案还没成功时,回答下面仍是普通提议卡,不说「已经开始」;打开失败可以再点「开始核对出生时间」。开案并绑定来源之后才改成「已经开始」。
|
||||
- **何时提议**:校正就在这个对话里能开。用户说推得不准时先分清读法问题和时间问题。只是不认同某个解释,按改读规则认下、改读,不提校正。不承诺能收窄出生时间,不在对话里收集经历做校正。说出过去的具体事情和盘对不上、说出生时间是估的或记不准、但没有明确要做校正,要先看过盘再提议。同一次对话提议过、用户没接,不再主动提;用户自己再要求时传 `userAsked=true`。还没看盘就顺口提议,不算已经提议过。规则只定义在 `consultation-thinking-plan.ts` 的 `RECTIFICATION_OFFER_RULE`。不用关键词判断。
|
||||
- **明确要求则直接进入**:用户这一轮明确要做校正(校正、纠正、核对出生时间都算),由模型判断,不用关键词。第一步就可以打开,不用先排盘写解读。服务端放行后直接打开校正,不用点卡。过渡只有一句「好,我们开始核对出生时间。」已经有一次没做完的,直接接着做那一次,不新开。开案还没成功时,回答下面仍是普通提议卡,不说「已经开始」;打开失败可以再点「开始核对出生时间」。开案并绑定来源之后才改成「已经开始」。
|
||||
- **做不了必须说一句**:开关关着,或暂时没有价格,都说「出生时间核对这会儿还做不了。」不写内部原因,不写价格,不得沉默。家人档案说「给家人校正暂时还不支持」,不开案。有进行中的校正但这次打不开原来的会话,说「可在历史对话里接着做。」
|
||||
- **正文怎么提**(没有明确要求、只是时间可能不准):一句,放在最后一段(例:「如果想确认出生时间准不准,可以用下面的卡片,答几道过去经历的题核对一下。」),不写价格和题数——卡片上写。
|
||||
- **卡片**:「可以用几道过去经历的题,核对一下你的出生时间。大约 N 题,会用掉 M 点;开始答第一题时才扣。」按钮「开始核对出生时间」;已开始(开案并绑定来源之后,不是结算当时)「出生时间核对已经开始。」/「继续核对出生时间」。点数来自 `feature_pricing`,不写死。
|
||||
|
||||
@@ -1215,11 +1215,17 @@ export async function POST(request: Request) {
|
||||
};
|
||||
const natalStreamOptions = {
|
||||
...streamOptions,
|
||||
prepareStep: consultationNatalPrepareStep,
|
||||
prepareStep: (input: { stepNumber: number }) => consultationNatalPrepareStep({
|
||||
stepNumber: input.stepNumber,
|
||||
rectificationOffer: true,
|
||||
}),
|
||||
};
|
||||
const windowStreamOptions = {
|
||||
...streamOptions,
|
||||
prepareStep: consultationWindowPrepareStep,
|
||||
prepareStep: (input: { stepNumber: number }) => consultationWindowPrepareStep({
|
||||
stepNumber: input.stepNumber,
|
||||
rectificationOffer: true,
|
||||
}),
|
||||
};
|
||||
const answerPhaseSignal = runClock.answerSignal;
|
||||
const startAnswerPhase = () => {
|
||||
|
||||
@@ -152,7 +152,7 @@ function VargaRow({ row, supportsDignity, supportsVargottama }: { row: ChartView
|
||||
<td>{row.house}宫</td>
|
||||
<td>{row.natalSignLabel}</td>
|
||||
{supportsDignity && <td className="chart-page-planet-mid">{row.dignity ? VARGA_DIGNITY_LABEL[row.dignity] : "—"}</td>}
|
||||
{supportsVargottama && <td className="chart-page-planet-mid">{row.vargottama ? "同位" : "—"}</td>}
|
||||
{supportsVargottama && <td className="chart-page-planet-mid">{row.vargottama ? "是" : "否"}</td>}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@@ -167,7 +167,7 @@ function VargaAscendantRow({ row, supportsDignity, supportsVargottama }: { row:
|
||||
<td>1宫</td>
|
||||
<td>{row.natalSignLabel}</td>
|
||||
{supportsDignity && <td className="chart-page-planet-mid">不适用</td>}
|
||||
{supportsVargottama && <td className="chart-page-planet-mid">{row.vargottama ? "同位" : "—"}</td>}
|
||||
{supportsVargottama && <td className="chart-page-planet-mid">{row.vargottama ? "是" : "否"}</td>}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -459,7 +459,7 @@ export function natalAnswerShapeInstruction(): string {
|
||||
* Never decided from keywords. Defined here once; the offer tool names the
|
||||
* same parameters and does not restate this rule.
|
||||
*/
|
||||
export const RECTIFICATION_OFFER_RULE = "出生时间核对:用户说你推得不准时,先分清是读法问题还是时间问题。只是不认同某个解释,按纠正规则认下、改读,不提校正。不承诺能收窄出生时间,不在对话里收集经历做校正。用户这一轮明确要做校正,就在写回答之前调用 offer-birth-time-rectification 并传 explicitRequest=true;工具通过后只写那一句过渡,不写解读。用户说出过去某件具体的事和盘对不上、说自己的出生时间是估的或记不准、但没有明确要做校正,才在写回答之前调用 offer-birth-time-rectification,再按工具结果在正文里用一句话提一下(不写价格和题数,卡片上有)。同一次对话提议过、用户没接,就不再主动提;用户自己再要求时调用并传 userAsked=true。工具说不能提供时(比如问的是家人),照工具说明用一句话说清楚。";
|
||||
export const RECTIFICATION_OFFER_RULE = "校正就在这个对话里能开:用户这一轮明确要做出生时间校正(说法可能是校正、纠正、核对出生时间、看看出生时间准不准等),第一步就调用 offer-birth-time-rectification 并传 explicitRequest=true,不用先排盘,工具通过后只写那一句过渡,不写解读。用户说你推得不准时,先分清是读法问题还是时间问题。只是不认同某个解释,按改读规则认下、改读,不提校正。不承诺能收窄出生时间,不在对话里收集经历做校正。用户说出过去某件具体的事和盘对不上、说自己的出生时间是估的或记不准、但没有明确要做校正,才在看过盘之后调用 offer-birth-time-rectification,再按工具结果在正文里用一句话提一下(不写价格和题数,卡片上有)。同一次对话提议过、用户没接,就不再主动提;用户自己再要求时调用并传 userAsked=true。工具说不能提供时(比如问的是家人),照工具说明用一句话说清楚。";
|
||||
|
||||
/** D4 / BUG-1166: the user corrects an earlier reading; acknowledge it, never defend the reading. */
|
||||
export const FOLLOW_UP_CORRECTION_RULE = "用户纠正了前面的判断(比如说父亲其实不常在身边):先认下这件事,再只说盘上真正对得上的那几条、原来哪句读偏了;盘本身分不出的,直说「盘本身分不出这一点」,不把普通配置说成必然;不为原判断辩护。";
|
||||
|
||||
@@ -176,34 +176,41 @@ export const MAX_EVIDENCE_LOOKUPS_PER_TURN = 3;
|
||||
export const CONSULTATION_WINDOW_CALC_TOOL_ID = "run-jyotish-window-consultation";
|
||||
|
||||
/**
|
||||
* Step 0 exposes only the natal chart tool. Some thinking-mode providers
|
||||
* reject named and required toolChoice (BUG-282, BUG-937); the first step
|
||||
* therefore narrows with activeTools and leaves the choice as auto. Later
|
||||
* steps stay auto so the model can write after the request-scoped
|
||||
* calculation is in hand.
|
||||
* Step 0 narrows with activeTools and leaves toolChoice auto. Thinking-mode
|
||||
* providers reject named and required toolChoice (BUG-282, BUG-937).
|
||||
* When this turn mounted the rectification offer, that tool is available
|
||||
* beside the chart tool so an explicit request can open before a reading
|
||||
* (BUG-1317). Without it, step 0 is still only the chart tool (BUG-923).
|
||||
* Later steps stay auto.
|
||||
*
|
||||
* Window and general agents must not share this hook: they do not own this tool.
|
||||
* Window and general agents must not share the natal hook.
|
||||
*/
|
||||
export function consultationNatalPrepareStep(input: { stepNumber: number }) {
|
||||
return input.stepNumber === 0
|
||||
? {
|
||||
export function consultationNatalPrepareStep(input: { stepNumber: number; rectificationOffer?: boolean }) {
|
||||
if (input.stepNumber !== 0) return { toolChoice: "auto" as const };
|
||||
if (!input.rectificationOffer) {
|
||||
return {
|
||||
activeTools: [CONSULTATION_NATAL_CALC_TOOL_ID],
|
||||
toolChoice: "auto" as const,
|
||||
}
|
||||
: {
|
||||
toolChoice: "auto" as const,
|
||||
};
|
||||
}
|
||||
return {
|
||||
activeTools: [CONSULTATION_NATAL_CALC_TOOL_ID, RECTIFICATION_OFFER_TOOL_ID],
|
||||
toolChoice: "auto" as const,
|
||||
};
|
||||
}
|
||||
|
||||
export function consultationWindowPrepareStep(input: { stepNumber: number }) {
|
||||
return input.stepNumber === 0
|
||||
? {
|
||||
export function consultationWindowPrepareStep(input: { stepNumber: number; rectificationOffer?: boolean }) {
|
||||
if (input.stepNumber !== 0) return { toolChoice: "auto" as const };
|
||||
if (!input.rectificationOffer) {
|
||||
return {
|
||||
activeTools: [CONSULTATION_WINDOW_CALC_TOOL_ID],
|
||||
toolChoice: "auto" as const,
|
||||
}
|
||||
: {
|
||||
toolChoice: "auto" as const,
|
||||
};
|
||||
}
|
||||
return {
|
||||
activeTools: [CONSULTATION_WINDOW_CALC_TOOL_ID, RECTIFICATION_OFFER_TOOL_ID],
|
||||
toolChoice: "auto" as const,
|
||||
};
|
||||
}
|
||||
|
||||
const CONSULTATION_DOMAIN_DURATION_MS = 31_000;
|
||||
@@ -1174,6 +1181,12 @@ function createRectificationOfferTool(ctx: Readonly<{ state: ConsultationRuntime
|
||||
}
|
||||
return { offered: false as const, note: "已经在打开。不要再写解读。" };
|
||||
}
|
||||
if (input.explicitRequest !== true && !ctx.state.consultationToolCompleted) {
|
||||
return {
|
||||
offered: false as const,
|
||||
note: "先排盘,看过盘再决定要不要提。这次不算已经提议过。",
|
||||
};
|
||||
}
|
||||
if (hook.alreadyOffered && input.userAsked !== true && input.explicitRequest !== true) {
|
||||
return { offered: false as const, reason: "already_offered_in_session", note: "这次对话里已经提议过一次,用户没接。不再主动提校正,正常回答。" };
|
||||
}
|
||||
|
||||
@@ -138,7 +138,11 @@ test("divisional rows carry engine dignity and vargottama, and drop nakshatra co
|
||||
for (const column of ["行星", "分盘星座", "宫位", "本命星座", "庙旺", "同位"]) assert.match(header, new RegExp(`>${column}<`));
|
||||
assert.doesNotMatch(header, /星宿|宿主|pada/);
|
||||
// Same wording as the D1 status column (BUG-1202), friendship levels included.
|
||||
assert.match(row(markup, "土星"), />入庙\(Own Sign\)<[\s\S]*>同位</);
|
||||
// 原值:同位行星单元格是「同位」,非同位是「—」
|
||||
// 新值:同位写「是」,非同位写「否」
|
||||
// 原因:BUG-1315。横杠和「没数据」长得一样,表头仍叫「同位」
|
||||
assert.match(row(markup, "土星"), />入庙\(Own Sign\)<[\s\S]*>是</);
|
||||
assert.match(row(markup, "太阳"), />否</);
|
||||
assert.match(row(markup, "太阳"), />落陷\(Debilitated\)</);
|
||||
assert.match(row(markup, "月亮"), />入友\(Friendly Sign\)</);
|
||||
assert.match(row(markup, "火星"), />中性\(Neutral\)</);
|
||||
@@ -168,6 +172,17 @@ function assertColumns(markup: string, dignity: boolean, vargottama: boolean) {
|
||||
for (const [, cells] of rows) assert.equal((cells!.match(/<(?:th|td)\b/g) ?? []).length, count);
|
||||
}
|
||||
|
||||
test("a non-vargottama row says 否 and a non-D9 chart has no same-sign column", async () => {
|
||||
const view = await vargaView();
|
||||
const d9 = render(view, "D9");
|
||||
assert.match(row(d9, "太阳"), />否</);
|
||||
const d10 = view.vedic.vargas.find((item) => item.id === "D10")!;
|
||||
assert.equal(d10.supportsVargottama, false);
|
||||
const markup = tableMarkup(d10);
|
||||
assert.doesNotMatch(markup, />同位</);
|
||||
assert.doesNotMatch(markup, />否</);
|
||||
});
|
||||
|
||||
test("server capabilities retain dignity for every Dn and vargottama only for D9", async () => {
|
||||
const view = await vargaView();
|
||||
for (const varga of view.vedic.vargas) {
|
||||
|
||||
@@ -679,26 +679,40 @@ test("guided-topic entrypoint ignores a model domain rewrite instead of executin
|
||||
|
||||
test("natal first step exposes only the chart calculation tool", () => {
|
||||
assert.equal(CONSULTATION_NATAL_CALC_TOOL_ID, "run-jyotish-consultation");
|
||||
// 原值: 第 0 步 toolChoice "required" / 新值: "auto"
|
||||
// 原因: BUG-282 供应商拒收 thinking 模式下的 required,BUG-937 撤回
|
||||
// 原值: 第 0 步 toolChoice "required",后来改为只有排盘工具且 toolChoice "auto"
|
||||
// 新值: 没挂校正工具时第 0 步仍只有排盘工具;挂了时是排盘工具和 offer-birth-time-rectification。toolChoice 仍是 "auto"。第 1 步不收窄。
|
||||
// 原因: BUG-937 保持 auto;BUG-1317 按产品选择,第 0 步在工具确实挂上时可以打开校正。
|
||||
assert.deepEqual(consultationNatalPrepareStep({ stepNumber: 0 }), {
|
||||
activeTools: ["run-jyotish-consultation"],
|
||||
toolChoice: "auto",
|
||||
});
|
||||
assert.deepEqual(consultationNatalPrepareStep({ stepNumber: 1 }), {
|
||||
assert.deepEqual(consultationNatalPrepareStep({ stepNumber: 0, rectificationOffer: false }), {
|
||||
activeTools: ["run-jyotish-consultation"],
|
||||
toolChoice: "auto",
|
||||
});
|
||||
assert.deepEqual(consultationNatalPrepareStep({ stepNumber: 0, rectificationOffer: true }), {
|
||||
activeTools: ["run-jyotish-consultation", "offer-birth-time-rectification"],
|
||||
toolChoice: "auto",
|
||||
});
|
||||
assert.deepEqual(consultationNatalPrepareStep({ stepNumber: 1, rectificationOffer: true }), {
|
||||
toolChoice: "auto",
|
||||
});
|
||||
});
|
||||
|
||||
test("window first step requires the window consultation tool", () => {
|
||||
assert.equal(CONSULTATION_WINDOW_CALC_TOOL_ID, "run-jyotish-window-consultation");
|
||||
// 原值: 第 0 步 toolChoice "required" / 新值: "auto"
|
||||
// 原因: BUG-282 供应商拒收 thinking 模式下的 required,BUG-937 撤回
|
||||
// 原值: 第 0 步 toolChoice "required",后来改为只有窗口排盘工具且 toolChoice "auto"
|
||||
// 新值: 没挂校正工具时第 0 步仍只有窗口排盘工具;挂了时再加上 offer-birth-time-rectification。toolChoice 仍是 "auto"。
|
||||
// 原因: 与本命同一条 BUG-1317 决定,申报时段也要能在第一步打开校正。
|
||||
assert.deepEqual(consultationWindowPrepareStep({ stepNumber: 0 }), {
|
||||
activeTools: ["run-jyotish-window-consultation"],
|
||||
toolChoice: "auto",
|
||||
});
|
||||
assert.deepEqual(consultationWindowPrepareStep({ stepNumber: 1 }), {
|
||||
assert.deepEqual(consultationWindowPrepareStep({ stepNumber: 0, rectificationOffer: true }), {
|
||||
activeTools: ["run-jyotish-window-consultation", "offer-birth-time-rectification"],
|
||||
toolChoice: "auto",
|
||||
});
|
||||
assert.deepEqual(consultationWindowPrepareStep({ stepNumber: 1, rectificationOffer: true }), {
|
||||
toolChoice: "auto",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -233,8 +233,12 @@ test("consult streams reserve an answer budget and keep provider thinking on a s
|
||||
route.indexOf("const natalStreamOptions = {"),
|
||||
);
|
||||
assert.doesNotMatch(sharedStreamOptions, /prepareStep/);
|
||||
assert.match(route, /prepareStep: consultationNatalPrepareStep/);
|
||||
assert.match(route, /prepareStep: consultationWindowPrepareStep/);
|
||||
// 原值:prepareStep: consultationNatalPrepareStep / consultationWindowPrepareStep,函数本身不知道校正工具在不在。
|
||||
// 新值:路由把 rectificationOffer: true 传进去,因为这两条路线都会挂上校正工具。
|
||||
// 原因:BUG-1317,第 0 步在工具挂上时要能打开校正;没传时函数仍只留排盘工具。
|
||||
assert.match(route, /prepareStep: \(input: \{ stepNumber: number \}\) => consultationNatalPrepareStep\(\{/);
|
||||
assert.match(route, /prepareStep: \(input: \{ stepNumber: number \}\) => consultationWindowPrepareStep\(\{/);
|
||||
assert.match(route, /rectificationOffer: true/);
|
||||
assert.match(route, /streamWithOverflowRetry\(agent, natalStreamOptions\)/);
|
||||
assert.match(route, /streamWithOverflowRetry\(agent, windowStreamOptions\)/);
|
||||
assert.match(
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -72,8 +72,14 @@ function offerTool(options: {
|
||||
alreadyOffered?: boolean;
|
||||
decide?: () => Promise<OfferDecision>;
|
||||
loadResumableSessionId?: () => Promise<string | null>;
|
||||
/** False only for a step-0 call that has not seen the chart yet. */
|
||||
chartReady?: boolean;
|
||||
} = {}) {
|
||||
const state = createConsultationRuntimeState();
|
||||
if (options.chartReady !== false) {
|
||||
state.consultationToolCompleted = true;
|
||||
state.consultationToolSuccessCount = 1;
|
||||
}
|
||||
let decisions = 0;
|
||||
let loads = 0;
|
||||
const tools = createConsultationTools({
|
||||
@@ -520,6 +526,111 @@ test("an ordinary consultation still retries once when the calculation never fin
|
||||
assert.equal(events.some((event) => event.type === "activity" && event.label === "正在补齐方法与计算步骤"), true);
|
||||
});
|
||||
|
||||
test("step 0 explicit request opens rectification without calculating a chart", async () => {
|
||||
const fresh = offerTool({ chartReady: false });
|
||||
const result = await fresh.tool.execute({ explicitRequest: true });
|
||||
assert.equal(result.directEntry, "new");
|
||||
assert.equal(fresh.state.consultationToolCallCount, 0);
|
||||
assert.equal(fresh.state.consultationToolCompleted, false);
|
||||
assert.equal(fresh.state.rectificationDirectEntry?.sentence, RECTIFICATION_DIRECT_NEW_COPY);
|
||||
let contractRetries = 0;
|
||||
const response = streamAgentResponse({
|
||||
runId: "run-step0-explicit",
|
||||
requestId: REQUEST_ID,
|
||||
state: fresh.state,
|
||||
requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
stepScopedAnswer: true,
|
||||
toolStatus: () => "ready",
|
||||
receipt: () => receipt(fresh.state),
|
||||
stream: (async function* () {
|
||||
yield { type: "text-delta", payload: { text: "我先看看盘,然后写一段解读。" } };
|
||||
yield STOP_FINISH;
|
||||
})(),
|
||||
retry: async () => {
|
||||
contractRetries += 1;
|
||||
return (async function* () { yield STOP_FINISH; })();
|
||||
},
|
||||
});
|
||||
const events = await readStream(response);
|
||||
assert.equal(contractRetries, 0);
|
||||
assert.equal(visibleAnswer(events), RECTIFICATION_DIRECT_NEW_COPY);
|
||||
assert.equal(events.some((event) => event.type === "run.completed" && event.responseKind === "smalltalk"), true);
|
||||
});
|
||||
|
||||
test("step 0 text without a tool still retries into a chart answer", async () => {
|
||||
const state = createConsultationRuntimeState();
|
||||
let contractRetries = 0;
|
||||
const response = streamAgentResponse({
|
||||
runId: "run-step0-empty",
|
||||
requestId: REQUEST_ID,
|
||||
state,
|
||||
requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
stepScopedAnswer: true,
|
||||
toolStatus: () => "ready",
|
||||
receipt: () => receipt(state),
|
||||
stream: (async function* () {
|
||||
yield { type: "text-delta", payload: { text: "还没排盘就想写。" } };
|
||||
yield STOP_FINISH;
|
||||
})(),
|
||||
retry: async () => {
|
||||
contractRetries += 1;
|
||||
state.consultationToolCompleted = true;
|
||||
state.consultationToolSuccessCount = 1;
|
||||
return (async function* () {
|
||||
yield { type: "text-delta", payload: { text: "事业方向可以看。" } };
|
||||
yield STOP_FINISH;
|
||||
})();
|
||||
},
|
||||
});
|
||||
const events = await readStream(response);
|
||||
assert.equal(contractRetries, 1);
|
||||
assert.equal(visibleAnswer(events), "事业方向可以看。");
|
||||
assert.equal(state.rectificationDirectEntry, undefined);
|
||||
assert.equal(events.some((event) => event.type === "activity" && event.label === "正在补齐方法与计算步骤"), true);
|
||||
});
|
||||
|
||||
test("a soft offer before the chart is not recorded, then a chart answer can still offer", async () => {
|
||||
const fresh = offerTool({ chartReady: false });
|
||||
const early = await fresh.tool.execute({});
|
||||
assert.equal(early.offered, false);
|
||||
assert.match(String(early.note), /先排盘,看过盘再决定要不要提/);
|
||||
// 记在局部上再断言。直接断言 state 上的字段会把它收成永远是空,排盘后再读就对不上类型。
|
||||
const offerBeforeChart = fresh.state.rectificationOffer;
|
||||
assert.equal(offerBeforeChart, undefined);
|
||||
assert.equal(fresh.state.rectificationDirectEntry, undefined);
|
||||
assert.equal(fresh.decisions(), 0);
|
||||
fresh.state.consultationToolCompleted = true;
|
||||
fresh.state.consultationToolSuccessCount = 1;
|
||||
const later = await fresh.tool.execute({});
|
||||
assert.equal(later.offered, true);
|
||||
const offerAfterChart = fresh.state.rectificationOffer;
|
||||
assert.equal(offerAfterChart?.status, "offered");
|
||||
assert.equal(offerAfterChart?.autoOpen, undefined);
|
||||
assert.equal(fresh.decisions(), 1);
|
||||
const response = streamAgentResponse({
|
||||
runId: "run-step0-soft",
|
||||
requestId: REQUEST_ID,
|
||||
state: fresh.state,
|
||||
requireTool: true,
|
||||
pass4Mode: "verified_chart",
|
||||
stepScopedAnswer: true,
|
||||
toolStatus: () => "ready",
|
||||
receipt: () => receipt(fresh.state),
|
||||
stream: (async function* () {
|
||||
yield { type: "text-delta", payload: { text: "事业方向可以看。" } };
|
||||
yield STOP_FINISH;
|
||||
})(),
|
||||
retry: async () => {
|
||||
throw new Error("chart contract should already be ready");
|
||||
},
|
||||
});
|
||||
const events = await readStream(response);
|
||||
assert.equal(visibleAnswer(events), "事业方向可以看。");
|
||||
assert.equal(visibleAnswer(events).includes(RECTIFICATION_DIRECT_NEW_COPY), false);
|
||||
});
|
||||
|
||||
test("the offer rule is defined once, and direct entry settles with the free completion", () => {
|
||||
const definitions = walkSources(fileURLToPath(new URL("../src/", import.meta.url))).filter((file) => (
|
||||
readFileSync(file, "utf8").includes("export const RECTIFICATION_OFFER_RULE")
|
||||
@@ -530,7 +641,12 @@ test("the offer rule is defined once, and direct entry settles with the free com
|
||||
assert.match(RECTIFICATION_OFFER_RULE, /不在对话里收集经历做校正/);
|
||||
assert.match(RECTIFICATION_OFFER_RULE, /explicitRequest=true/);
|
||||
assert.match(RECTIFICATION_OFFER_RULE, /只写那一句过渡,不写解读/);
|
||||
assert.match(RECTIFICATION_OFFER_RULE, /只是不认同某个解释,按纠正规则认下、改读,不提校正/);
|
||||
// 原值:规则以「用户说你推得不准时」开头,并写「按纠正规则认下」。
|
||||
// 新值:第一句就写 explicitRequest=true,并改称「改读规则」。全文不含「纠正规则」。
|
||||
// 原因:BUG-1317,「生时纠正」和「纠正解读」撞词,正向句被埋在禁止句中间。
|
||||
assert.match(RECTIFICATION_OFFER_RULE.split("。")[0] ?? "", /explicitRequest=true/);
|
||||
assert.match(RECTIFICATION_OFFER_RULE, /按改读规则认下、改读,不提校正/);
|
||||
assert.doesNotMatch(RECTIFICATION_OFFER_RULE, /纠正规则/);
|
||||
assert.match(RECTIFICATION_OFFER_RULE, /userAsked=true/);
|
||||
|
||||
const tools = src("../src/mastra/consultation-tools.ts");
|
||||
|
||||
@@ -117,6 +117,11 @@ test("the server decides whether a card may be shown: self, product on, no case
|
||||
|
||||
function offerTool(options: { alreadyOffered?: boolean; decide?: () => Promise<{ ok: true; creditCost: number; questionEstimate: number } | { ok: false; reason: "not_self" | "product_disabled" | "case_in_progress" | "pricing_unavailable" }> } = {}) {
|
||||
const state = createConsultationRuntimeState();
|
||||
// 原值:建出来还没排盘也能直接提议。
|
||||
// 新值:这些用例表示已经看过盘,所以先标计算完成。
|
||||
// 原因:BUG-1317,还没排盘的顺口提议不算已经提议过。
|
||||
state.consultationToolCompleted = true;
|
||||
state.consultationToolSuccessCount = 1;
|
||||
const tools = createConsultationTools({
|
||||
userId: "user-fictional",
|
||||
sessionId: SOURCE_SESSION,
|
||||
@@ -194,7 +199,10 @@ test("the C2 rule is defined once and reaches first turns, follow-ups and declar
|
||||
assert.ok(followUpAnswerShapeInstruction().includes(RECTIFICATION_OFFER_RULE));
|
||||
const route = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
|
||||
assert.match(route, /这只是粗看。" \+ RECTIFICATION_OFFER_RULE;/);
|
||||
assert.match(RECTIFICATION_OFFER_RULE, /只是不认同某个解释,按纠正规则认下、改读,不提校正/);
|
||||
// 原值:/按纠正规则认下、改读,不提校正/
|
||||
// 新值:/按改读规则认下、改读,不提校正/
|
||||
// 原因:BUG-1317,避免「生时纠正」被读成「用户在纠正解读」。
|
||||
assert.match(RECTIFICATION_OFFER_RULE, /只是不认同某个解释,按改读规则认下、改读,不提校正/);
|
||||
assert.match(RECTIFICATION_OFFER_RULE, /userAsked=true/);
|
||||
});
|
||||
|
||||
|
||||
@@ -36,6 +36,19 @@ test("an older body without chart fences keeps its charts, without the moved cav
|
||||
assert.doesNotMatch(markup, /出生时间敏感/);
|
||||
});
|
||||
|
||||
test("a full-data body that fences every calculation chart drops the trailing section", () => {
|
||||
const fullData = JSON.parse(readFileSync(new URL("./fixtures/report-full-data-chart-fences.json", import.meta.url), "utf8"));
|
||||
const fullCharts = assembleLongformCharts(fullData.packet, "ev-cover-longform");
|
||||
const drawn = fencedReportChartIds(fullData.markdown);
|
||||
assert.ok(fullCharts.length > 0);
|
||||
for (const chart of fullCharts) assert.ok(drawn.has(chart.id), chart.id);
|
||||
assert.equal(renderToStaticMarkup(React.createElement(ReportCalculationCharts, {
|
||||
charts: fullCharts,
|
||||
markdown: fullData.markdown,
|
||||
language: "zh",
|
||||
})), "");
|
||||
});
|
||||
|
||||
test("the page and the export skip the same charts", () => {
|
||||
const first = /```jyotish-chart\n[\s\S]*?```/.exec(fixture.markdown)![0];
|
||||
const drawn = [...fencedReportChartIds(first)];
|
||||
|
||||
@@ -107,7 +107,13 @@ def _sanitize_pl9_ai_density_markdown(markdown: str, title: str = "印度占星
|
||||
strip_han_plural_s,
|
||||
)
|
||||
|
||||
text = _sanitize_pl9_public_markdown(markdown)
|
||||
try:
|
||||
from pl9_language_terms import translate_zh_glued_phrases
|
||||
except ModuleNotFoundError: # pragma: no cover - package import compatibility
|
||||
from scripts.pl9_language_terms import translate_zh_glued_phrases
|
||||
# Whole phrases and glued planet+dignity tokens before shorter word swaps.
|
||||
# Chapters appended after the parity strip still pass through here.
|
||||
text = translate_zh_glued_phrases(_sanitize_pl9_public_markdown(markdown))
|
||||
replacements = {
|
||||
"# PL9 对标主册": f"# {title}",
|
||||
"出生资料与图盘(PL9 p1-p17)": "出生资料与图盘",
|
||||
@@ -1264,6 +1270,27 @@ def _sanitize_pl9_ai_density_markdown_en(markdown: str, title: str = "Vedic Astr
|
||||
|
||||
|
||||
|
||||
# Body residue (BUG-1312). Parenthetical Sanskrit names (nakshatra, yoga,
|
||||
# Dasha) and header abbreviations (SAV, BAV, KP, D1-D60) are separated from
|
||||
# Han by parentheses or spaces, so they are not "glued". They are not function
|
||||
# words. Chart fences are structured payload and are not part of the prose count.
|
||||
_ZH_GLUE_RE = re.compile(r"[\u4e00-\u9fff][A-Za-z]{3,}|[A-Za-z]{3,}[\u4e00-\u9fff]")
|
||||
_ZH_FUNCTION_WORD_RE = re.compile(
|
||||
r"(?<![A-Za-z])(?:of|in|the|and|by|from|with|listed|background|native|condition|met|satisfied|occupied)(?![A-Za-z])",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_ZH_CHART_FENCE_RE = re.compile(r"```jyotish-chart\n.*?\n```", re.DOTALL)
|
||||
|
||||
|
||||
def _zh_body_residue(markdown: str) -> dict[str, int]:
|
||||
body = (markdown or "").split("## 结构化资料附录", 1)[0]
|
||||
body = _ZH_CHART_FENCE_RE.sub("", body)
|
||||
return {
|
||||
"glued_latin": len(_ZH_GLUE_RE.findall(body)),
|
||||
"function_words": len(_ZH_FUNCTION_WORD_RE.findall(body)),
|
||||
}
|
||||
|
||||
|
||||
def _pl9_markdown_hygiene_receipt(markdown: str, *, language: str) -> dict[str, Any]:
|
||||
"""Check the Markdown before rendering user-facing PDF."""
|
||||
|
||||
@@ -1327,6 +1354,12 @@ def _pl9_markdown_hygiene_receipt(markdown: str, *, language: str) -> dict[str,
|
||||
chinese_chars = len(re.findall(r"[\u4e00-\u9fff]", markdown))
|
||||
if chinese_chars:
|
||||
violations.append(f"chinese_characters:{chinese_chars}")
|
||||
if language == 'zh':
|
||||
residue = _zh_body_residue(markdown)
|
||||
if residue["glued_latin"]:
|
||||
violations.append(f"glued_latin:{residue['glued_latin']}")
|
||||
if residue["function_words"]:
|
||||
violations.append(f"function_words:{residue['function_words']}")
|
||||
language_receipt = language_quality_receipt(markdown, language=language)
|
||||
violations.extend(language_receipt.get("violations") or [])
|
||||
orphan_lines = orphan_separator_lines(markdown)
|
||||
|
||||
@@ -864,11 +864,14 @@ def build_yoga_interpretation_pack(
|
||||
if name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
combination = str(raw.get("combination_en") or raw.get("combination") or raw.get("reason") or "")
|
||||
if language == "en":
|
||||
combination = str(raw.get("combination_en") or raw.get("name") or combination)
|
||||
combination = str(raw.get("combination_en") or raw.get("name") or raw.get("combination") or "")
|
||||
if re.search(r"[\u4e00-\u9fff]", combination):
|
||||
combination = "combination text is not listed separately"
|
||||
else:
|
||||
# Chinese pages keep the Chinese combination. combination_en is the
|
||||
# English twin and must not leak into the Chinese report.
|
||||
combination = str(raw.get("combination") or raw.get("reason") or "")
|
||||
planets = [str(item) for item in _list(raw.get("planets"))]
|
||||
overlap = sorted(set(planets) & set(active))
|
||||
if language == "zh":
|
||||
|
||||
@@ -11,6 +11,126 @@ import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
# Bilingual planet tokens from yoga combination text, longest first.
|
||||
# Dignity is glued on with no space ("木星Jupiterexalted"), so the long form
|
||||
# has to be translated before the short planet name.
|
||||
_BILINGUAL_PLANETS = (
|
||||
("南交点Ketu", "南交点"),
|
||||
("北交点Rahu", "北交点"),
|
||||
("水星Mercury", "水星"),
|
||||
("木星Jupiter", "木星"),
|
||||
("金星Venus", "金星"),
|
||||
("土星Saturn", "土星"),
|
||||
("火星Mars", "火星"),
|
||||
("月亮Moon", "月亮"),
|
||||
("太阳Sun", "太阳"),
|
||||
)
|
||||
_GLUED_DIGNITY = {
|
||||
"exalted": "入旺",
|
||||
"debilitated": "落陷",
|
||||
"moolatrikona": "本垣",
|
||||
"own": "入庙",
|
||||
"strong": "强",
|
||||
}
|
||||
_BILINGUAL_PLANET_RE = re.compile(
|
||||
"(" + "|".join(re.escape(raw) for raw, _public in _BILINGUAL_PLANETS) + ")"
|
||||
+ "(exalted|debilitated|moolatrikona|own|strong)?"
|
||||
)
|
||||
_BILINGUAL_PLANET_ZH = dict(_BILINGUAL_PLANETS)
|
||||
|
||||
# Whole formation sentences and status tokens. Longer phrases stay above the
|
||||
# short-word glossary so "of" / "in" are not left stuck to Chinese.
|
||||
_ZH_FORMATION_PHRASES = (
|
||||
(
|
||||
"| Bhava Number | Bhava Arambha / House beginning | Bhava Madhya / Middle of House | Bhava Antya / House ending |",
|
||||
"| 宫位编号 | 宫位起点 | 宫位中点 | 宫位终点 |",
|
||||
),
|
||||
(
|
||||
"Lord of sign occupied in navamsa by 2nd/5th/11th lord, exalted and joins the 9th lord",
|
||||
"第2、第5或第11宫主在九分盘所落星座的宫主入旺,并与第9宫主会合",
|
||||
),
|
||||
(
|
||||
"Lord of navamsa occupied by 4th lord in 12th",
|
||||
"第4宫主在九分盘所落星座的宫主位于第12宫",
|
||||
),
|
||||
("Learned, eloquent, wealthy from knowledge", "有学识,善表达,学问带来资源"),
|
||||
("Loss or destruction of property/house", "房屋或产业有损耗"),
|
||||
(
|
||||
"Lagna lord, his dispositor, the latter's dispositor in rasi and navamsa, all in kendras/trines/exaltation",
|
||||
"上升主星、它的定位星,以及定位星的定位星,在本命盘和九分盘都落在角宫、三方宫或入旺",
|
||||
),
|
||||
("Fulfills all desires, great king, supreme wealth", "愿望容易得到回应,传统上对应较高的地位与丰厚资源"),
|
||||
("Eloquent, skilled in debate, good communicator", "善表达,辩论清楚,沟通能力强"),
|
||||
("Enjoyment of food, pleasure in dining, hospitality", "饮食安稳,待客从容"),
|
||||
("Courage, initiative, status and practical drive", "有行动力,做事务实"),
|
||||
("Fear of deception, theft; deceptive nature", "容易担心受骗或失物,需要再核对事实"),
|
||||
(
|
||||
"Lagna malefic/Gulika in trine OR Gulika with Kendra/Trine lords OR L1 with Rahu/Sat/Ket",
|
||||
"凶星或吉利伽在三方宫,或吉利伽与角宫、三方宫主同在,或上升主与罗睺、土星、计都同在",
|
||||
),
|
||||
("或BVR精神扰动变体", "或(BVR)精神扰动变体"),
|
||||
("very_rare", "极少"),
|
||||
("Not supplied by source", "源数据未提供"),
|
||||
("Crux of Vedic Astrology", "《吠陀占星精要》"),
|
||||
("Viparita Raja Yoga", "颠倒王者格"),
|
||||
("Dhana Yoga", "财富瑜伽"),
|
||||
("Raj Yoga", "王者格"),
|
||||
("Custom condition met", "满足自定义条件"),
|
||||
("custom condition satisfied", "满足自定义条件"),
|
||||
("自定义条件满足", "满足自定义条件"),
|
||||
("hemmed in by malefics", "被凶星夹制"),
|
||||
("hemmed in by benefics", "被吉星夹制"),
|
||||
("occupied in navamsa", "在九分盘所落"),
|
||||
("occupied in navamsha", "在九分盘所落"),
|
||||
("Mutual aspect", "互照"),
|
||||
("mutual aspect", "互照"),
|
||||
("drig_rath_aspect_sequence_v1", "Drig 相位顺序 v1"),
|
||||
("Ashtakavarga八分法(BPHS/PVR书例校准v2.1)", "Ashtakavarga 八分法(BPHS / PVR 书例校准 v2.1)"),
|
||||
("Sade Sati阶段框架", "Sade Sati 阶段框架"),
|
||||
("灵魂星AK", "灵魂星(AK)"),
|
||||
("事业星AmK", "事业星(AmK)"),
|
||||
("兄弟星BK", "兄弟星(BK)"),
|
||||
("母亲星MK", "母亲星(MK)"),
|
||||
("子女星PK", "子女星(PK)"),
|
||||
("障碍星GK", "障碍星(GK)"),
|
||||
("配偶星DK", "配偶星(DK)"),
|
||||
("父亲星PiK", "父亲星(PiK)"),
|
||||
)
|
||||
|
||||
|
||||
def translate_zh_glued_phrases(text: str) -> str:
|
||||
"""Translate whole bilingual tokens before any shorter English word."""
|
||||
|
||||
if not text:
|
||||
return text
|
||||
|
||||
def planet(match: re.Match) -> str:
|
||||
name = _BILINGUAL_PLANET_ZH[match.group(1)]
|
||||
dignity = _GLUED_DIGNITY.get(match.group(2) or "", "")
|
||||
return name + dignity
|
||||
|
||||
text = _BILINGUAL_PLANET_RE.sub(planet, text)
|
||||
for raw, public in _ZH_FORMATION_PHRASES:
|
||||
text = text.replace(raw, public)
|
||||
# House-lord combinations are printed as H6_Lord(Venus) in H12. Translate
|
||||
# the whole pattern before the short word "in" is left beside Chinese.
|
||||
text = re.sub(
|
||||
r"H(\d+)_Lord\(([^)]+)\) in H(\d+)",
|
||||
r"第\1宫主(\2)在第\3宫",
|
||||
text,
|
||||
)
|
||||
text = re.sub(r"H(\d+)_Lord\(([^)]+)\)", r"第\1宫主(\2)", text)
|
||||
# Status tokens printed as bare English words. Longer phrases above already
|
||||
# consumed "not listed" / "Custom condition met", so these are the leftovers.
|
||||
for raw, public in (
|
||||
("listed", "已列出"),
|
||||
("background", "背景"),
|
||||
("native", "本命"),
|
||||
):
|
||||
text = re.sub(rf"(?<![A-Za-z]){raw}(?![A-Za-z])", public, text)
|
||||
return text
|
||||
|
||||
|
||||
def replace_natural_language_term(text: str, raw: str, public: str) -> str:
|
||||
"""Replace one glossary term in prose only.
|
||||
|
||||
|
||||
@@ -35,6 +35,29 @@ _PHRASES: dict[str, str] = {
|
||||
"特殊上升与派生点补充": "Special Lagnas and Points (supplement)",
|
||||
"敏感点与特殊落点": "Sensitive Points and Special Placements",
|
||||
"本命与分盘北印度图盘": "Natal and Divisional Charts (North Indian)",
|
||||
# Chart fences in the parity volume. The Chinese label is the key; the
|
||||
# English edition looks these up instead of keeping a second title list.
|
||||
"D1 本命盘": "D1 natal chart",
|
||||
"月亮盘": "Moon chart",
|
||||
"D2 财富分盘": "D2 wealth chart",
|
||||
"D3 手足分盘": "D3 siblings chart",
|
||||
"D4 居所分盘": "D4 home chart",
|
||||
"D5 声望分盘": "D5 reputation chart",
|
||||
"D6 压力分盘": "D6 pressure chart",
|
||||
"D7 子女分盘": "D7 children chart",
|
||||
"D8 变化分盘": "D8 change chart",
|
||||
"D9 九分盘": "D9 navamsha",
|
||||
"D10 事业分盘": "D10 career chart",
|
||||
"D11 收益分盘": "D11 gains chart",
|
||||
"D12 父母分盘": "D12 parents chart",
|
||||
"D16 享受分盘": "D16 comforts chart",
|
||||
"D20 灵修分盘": "D20 practice chart",
|
||||
"D24 学习分盘": "D24 learning chart",
|
||||
"D27 内在分盘": "D27 inner chart",
|
||||
"D30 困难分盘": "D30 difficulty chart",
|
||||
"D40 母系分盘": "D40 maternal chart",
|
||||
"D45 父系分盘": "D45 paternal chart",
|
||||
"D60 深层分盘": "D60 deep chart",
|
||||
"分盘对出生时间敏感,原始计算供核对,不单独增加结论的确定性。":
|
||||
"Divisional charts are sensitive to birth time. The raw calculations are here for checking and do not by themselves make a conclusion more certain.",
|
||||
" 数值位置表": " Position Table",
|
||||
|
||||
@@ -111,7 +111,11 @@ def _pl9_report_language(packet: dict) -> str:
|
||||
def _strip_pl9_user_engineering_markers(markdown: str) -> str:
|
||||
"""Remove internal audit/profile vocabulary from user-facing report text."""
|
||||
|
||||
text = markdown
|
||||
try:
|
||||
from scripts.pl9_language_terms import translate_zh_glued_phrases
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
|
||||
from pl9_language_terms import translate_zh_glued_phrases
|
||||
text = translate_zh_glued_phrases(markdown)
|
||||
replacements = {
|
||||
"parameter_sensitive / unverified": "参考",
|
||||
"parameter_sensitive": "参考",
|
||||
@@ -481,6 +485,149 @@ def _render_pl9_user_markdown(packet: dict) -> str:
|
||||
return cleaned
|
||||
|
||||
|
||||
# Drawable charts in the parity "north Indian charts" section. Bhava (Sripati)
|
||||
# has no chart id (its numbers stay in the Sripati table). Navamsha is not
|
||||
# listed again: it is the same id as D9. English titles live in
|
||||
# pl9_reader_english_terms, keyed by these Chinese labels.
|
||||
_PARITY_CHARTS = (
|
||||
("D1", "D1 本命盘", "d1"),
|
||||
("MOON", "月亮盘", "moon"),
|
||||
("D2", "D2 财富分盘", ("D2_Hora", "D2")),
|
||||
("D3", "D3 手足分盘", ("D3_Drekkana", "D3")),
|
||||
("D4", "D4 居所分盘", ("D4_Turyamsa", "D4_Chaturthamsa", "D4")),
|
||||
("D5", "D5 声望分盘", ("D5_Panchamsa", "D5")),
|
||||
("D6", "D6 压力分盘", ("D6_Shashthamsa", "D6")),
|
||||
("D7", "D7 子女分盘", ("D7_Saptamsa", "D7")),
|
||||
("D8", "D8 变化分盘", ("D8_Ashtamsa", "D8")),
|
||||
("D9", "D9 九分盘", ("D9_Navamsa", "D9")),
|
||||
("D10", "D10 事业分盘", ("D10_Dasamsa", "D10")),
|
||||
("D11", "D11 收益分盘", ("D11_Rudramsa", "D11")),
|
||||
("D12", "D12 父母分盘", ("D12_Dwadashamsa", "D12_Dvadasamsa", "D12")),
|
||||
("D16", "D16 享受分盘", ("D16_Shodasamsa", "D16")),
|
||||
("D20", "D20 灵修分盘", ("D20_Vimsamsa", "D20")),
|
||||
("D24", "D24 学习分盘", ("D24_Siddhamsa", "D24_Chaturvimsamsa", "D24")),
|
||||
("D27", "D27 内在分盘", ("D27_Bhamsa", "D27")),
|
||||
("D30", "D30 困难分盘", ("D30_Trimsamsa", "D30")),
|
||||
("D40", "D40 母系分盘", ("D40_Khavedamsa", "D40")),
|
||||
("D45", "D45 父系分盘", ("D45_Akshavedamsa", "D45")),
|
||||
("D60", "D60 深层分盘", ("D60_Shashtyamsa", "D60_Shashtiamsa", "D60")),
|
||||
)
|
||||
|
||||
|
||||
def _parity_chart_title(zh: str, language: str) -> str:
|
||||
if language != "en":
|
||||
return zh
|
||||
try:
|
||||
from scripts.pl9_reader_english_terms import PHRASES_EN
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
|
||||
from pl9_reader_english_terms import PHRASES_EN
|
||||
return dict(PHRASES_EN).get(zh, zh)
|
||||
|
||||
|
||||
def _parity_chart_source(kind, core_chart, worksheets, divisional, varga_full):
|
||||
if kind == "d1":
|
||||
planets = core_chart.get("planets") if isinstance(core_chart.get("planets"), dict) else {}
|
||||
ascendant = core_chart.get("ascendant") if isinstance(core_chart.get("ascendant"), dict) else {}
|
||||
return planets, ascendant
|
||||
if kind == "moon":
|
||||
d1_sheet = worksheets.get("d1_rasi_bhava") if isinstance(worksheets.get("d1_rasi_bhava"), dict) else {}
|
||||
moon = d1_sheet.get("moon_chart") if isinstance(d1_sheet.get("moon_chart"), dict) else {}
|
||||
if not moon and isinstance(divisional.get("moon_chart"), dict):
|
||||
moon = divisional["moon_chart"]
|
||||
raw = moon.get("raw") if isinstance(moon.get("raw"), dict) else {}
|
||||
if not raw and isinstance(moon.get("data"), dict):
|
||||
raw = moon["data"]
|
||||
if not raw:
|
||||
raw = moon
|
||||
if isinstance(raw.get("raw"), dict):
|
||||
raw = raw["raw"]
|
||||
ascendant = {}
|
||||
for key in ("Ascendant", "ascendant", "Asc"):
|
||||
if isinstance(raw.get(key), dict):
|
||||
ascendant = raw[key]
|
||||
break
|
||||
planets = raw.get("planets") if isinstance(raw.get("planets"), dict) else {}
|
||||
return planets, ascendant
|
||||
chart = {}
|
||||
for key in kind:
|
||||
candidate = varga_full.get(key) if isinstance(varga_full, dict) else None
|
||||
if isinstance(candidate, dict) and candidate:
|
||||
chart = candidate
|
||||
break
|
||||
ascendant = {}
|
||||
for key in ("Ascendant", "ascendant", "Asc"):
|
||||
if isinstance(chart.get(key), dict):
|
||||
ascendant = chart[key]
|
||||
break
|
||||
return chart, ascendant
|
||||
|
||||
|
||||
def _append_parity_chart_fences(lines, *, language, core_chart, worksheets, divisional, varga_full) -> None:
|
||||
"""One north-Indian fence per chart that has a sign to draw. No empty heading."""
|
||||
try:
|
||||
from scripts.report_chart_block import PLANET_ORDER, build_chart_block
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
|
||||
from report_chart_block import PLANET_ORDER, build_chart_block
|
||||
natal = core_chart.get("planets") if isinstance(core_chart.get("planets"), dict) else {}
|
||||
retrograde = {
|
||||
name: bool(row.get("retrograde")) if isinstance(row, dict) else False
|
||||
for name in PLANET_ORDER
|
||||
for row in [natal.get(name)]
|
||||
}
|
||||
seen: set[str] = set()
|
||||
for chart_id, title_zh, kind in _PARITY_CHARTS:
|
||||
if chart_id in seen:
|
||||
continue
|
||||
positions, ascendant = _parity_chart_source(kind, core_chart, worksheets, divisional, varga_full)
|
||||
title = _parity_chart_title(title_zh, language)
|
||||
try:
|
||||
block = build_chart_block(chart_id, title, positions, ascendant, retrograde)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
seen.add(chart_id)
|
||||
lines.extend([f"##### {title}", "", block, ""])
|
||||
|
||||
|
||||
def _ascendant_report_fields(asc: dict) -> dict:
|
||||
"""Longitude, nakshatra and lords for the report's ascendant row. No new dignity."""
|
||||
longitude = None
|
||||
for key in ("lon", "longitude", "degree_raw"):
|
||||
value = asc.get(key)
|
||||
if isinstance(value, (int, float)):
|
||||
longitude = float(value) % 360.0
|
||||
break
|
||||
if longitude is None:
|
||||
sign = asc.get("sign")
|
||||
degree = asc.get("degree_in_sign")
|
||||
if not isinstance(degree, (int, float)):
|
||||
degree = asc.get("degree")
|
||||
signs = globals().get("SIGNS") or []
|
||||
if sign in signs and isinstance(degree, (int, float)):
|
||||
longitude = signs.index(sign) * 30.0 + float(degree)
|
||||
fields = {}
|
||||
if longitude is not None:
|
||||
try:
|
||||
from scripts.jyotish_engine import _nakshatra_fields
|
||||
except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution
|
||||
from jyotish_engine import _nakshatra_fields
|
||||
fields = _nakshatra_fields(longitude)
|
||||
degree = asc.get("degree_in_sign")
|
||||
if not isinstance(degree, (int, float)):
|
||||
degree = asc.get("degree")
|
||||
pada = fields.get("nakshatra_pada") if fields else None
|
||||
if pada is None:
|
||||
pada = asc.get("nakshatra_pada") or asc.get("pada")
|
||||
return {
|
||||
"sign": asc.get("sign"),
|
||||
"degree": degree,
|
||||
"longitude": longitude,
|
||||
"nakshatra": fields.get("nakshatra") or asc.get("nakshatra"),
|
||||
"pada": pada,
|
||||
"nakshatra_lord": fields.get("nakshatra_lord") or asc.get("nakshatra_lord"),
|
||||
"sign_lord": asc.get("lord") or asc.get("rasi_lord"),
|
||||
}
|
||||
|
||||
|
||||
def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
"""Render only PL9 page-family material admitted to the parity main volume."""
|
||||
_bind_engine_symbols()
|
||||
@@ -544,6 +691,18 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
elif isinstance(value, (list, tuple)):
|
||||
value = ';'.join(str(item) for item in value if item not in (None, '', [], {}))
|
||||
text = str(value if value not in (None, '') else '-').replace('\n', ' ').strip()
|
||||
if _lang != 'en':
|
||||
# Whole status tokens. Longer phrases are handled in the glossary
|
||||
# before these short words, so a half-translated fragment is not
|
||||
# cleaned up by deleting letters here.
|
||||
text = {
|
||||
'active': '当前运行',
|
||||
'background': '背景',
|
||||
'listed': '已列出',
|
||||
'native': '本命',
|
||||
'Custom condition met': '满足自定义条件',
|
||||
'custom condition satisfied': '满足自定义条件',
|
||||
}.get(text, text)
|
||||
return text.replace('|', '/')
|
||||
|
||||
def _brief(items, limit=2) -> str:
|
||||
@@ -668,6 +827,10 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
def _public_status_note(value) -> str:
|
||||
zh_mapping = {
|
||||
'partial_verified': '已生成',
|
||||
'listed': '已列出',
|
||||
'active': '当前运行',
|
||||
'background': '背景',
|
||||
'native': '本命',
|
||||
'parameter_sensitive': '参数敏感',
|
||||
'blocked': '需补资料',
|
||||
'verified': '已核验',
|
||||
@@ -856,6 +1019,14 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
'| Planet | R/C | Sign | Degree | Longitude | House | Nakshatra | Pada | Speed |',
|
||||
'|--------|-----|------|--------|-----------|-------|-----------|------|-------|',
|
||||
])
|
||||
ascendant = _dict(core_chart.get('ascendant'))
|
||||
if ascendant:
|
||||
info = _ascendant_report_fields(ascendant)
|
||||
asc_label = 'Ascendant' if _lang == 'en' else '上升'
|
||||
lines.append(
|
||||
f"| {_cell(asc_label)} | - | {_cell(_token(info['sign']))} | {_cell(_degree(info['degree']))} | "
|
||||
f"{_cell(_degree(info['longitude']))} | 1 | {_cell(info['nakshatra'])} | {_cell(info['pada'])} | - |"
|
||||
)
|
||||
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'):
|
||||
row = _dict(planets.get(planet))
|
||||
if not row:
|
||||
@@ -875,6 +1046,14 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
'| 行星 | 星座主 | 星宿主 | 分主 | 分分主 | 尊贵状态 | 力量比 |',
|
||||
'|------|--------|--------|------|--------|----------|--------|',
|
||||
])
|
||||
if ascendant:
|
||||
info = _ascendant_report_fields(ascendant)
|
||||
asc_label = 'Ascendant' if _lang == 'en' else '上升'
|
||||
dignity = 'not applicable' if _lang == 'en' else '不适用'
|
||||
lines.append(
|
||||
f"| {_cell(asc_label)} | {_cell(_token(info['sign_lord']))} | {_cell(_token(info['nakshatra_lord']))} | "
|
||||
f"- | - | {_cell(dignity)} | - |"
|
||||
)
|
||||
for planet in ('Sun', 'Moon', 'Mars', 'Mercury', 'Jupiter', 'Venus', 'Saturn', 'Rahu', 'Ketu'):
|
||||
row = _dict(planets.get(planet))
|
||||
if not row:
|
||||
@@ -1078,8 +1257,14 @@ def render_pl9_parity_markdown(packet: dict) -> str:
|
||||
ready_natal_specs = [spec for spec in natal_specs if spec[2]]
|
||||
if ready_natal_specs:
|
||||
lines.extend(['### p6-p17 本命与分盘北印度图盘', '', '分盘对出生时间敏感,原始计算供核对,不单独增加结论的确定性。', ''])
|
||||
for page, label, _ready in ready_natal_specs:
|
||||
lines.extend([f'##### p{page} · {label}', ''])
|
||||
_append_parity_chart_fences(
|
||||
lines,
|
||||
language=_lang,
|
||||
core_chart=core_chart,
|
||||
worksheets=worksheets,
|
||||
divisional=divisional,
|
||||
varga_full=varga_full,
|
||||
)
|
||||
# The original PL9 charts are visual, but their sign placements are
|
||||
# still part of the visible evidence. Keep the chart drawing and add
|
||||
# a compact numeric transcription so an AI reader can recover the
|
||||
|
||||
@@ -62,6 +62,7 @@ def test_reader_report_karaka_table_is_in_bphs_order() -> None:
|
||||
import copy
|
||||
|
||||
from scripts.jaimini import KARAKA_CN
|
||||
from scripts.pl9_language_terms import translate_zh_glued_phrases
|
||||
from scripts.pl9_reader_export import _pl9_export_markdown_for_edition
|
||||
from tests.test_report_english_edition import CASES, _packet
|
||||
|
||||
@@ -70,4 +71,9 @@ def test_reader_report_karaka_table_is_in_bphs_order() -> None:
|
||||
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]]
|
||||
assert [row.split("|")[1].strip() for row in rows] == [KARAKA_CN[role] for role in BPHS_8]
|
||||
# 原值:[KARAKA_CN[role] for role in BPHS_8](灵魂星AK、事业星AmK、兄弟星BK、母亲星MK、父亲星PiK、子女星PK、障碍星GK、配偶星DK)
|
||||
# 新值:[translate_zh_glued_phrases(KARAKA_CN[role]) for role in BPHS_8](灵魂星(AK)、事业星(AmK)、兄弟星(BK)、母亲星(MK)、父亲星(PiK)、子女星(PK)、障碍星(GK)、配偶星(DK))
|
||||
# 原因:完整数据版正文会给这八个星位名的缩写加括号,避免三字母缩写被粘连检查判中。KARAKA_CN 仍是原字面值,咨询和星盘页继续用它。
|
||||
assert [row.split("|")[1].strip() for row in rows] == [
|
||||
translate_zh_glued_phrases(KARAKA_CN[role]) for role in BPHS_8
|
||||
]
|
||||
|
||||
@@ -150,6 +150,114 @@ def test_ashtakavarga_house_table_never_labels_the_total_as_sav(editions) -> Non
|
||||
assert row[5].isdigit()
|
||||
|
||||
|
||||
_CALCULATION_CHART_IDS = {
|
||||
"D1", "D2", "D3", "D4", "D7", "D9", "D10", "D11", "D12",
|
||||
"D16", "D20", "D24", "D27", "D30", "D40", "D60",
|
||||
}
|
||||
|
||||
|
||||
def _section_until_next_h2(markdown: str, heading: str) -> str:
|
||||
start = markdown.index(heading)
|
||||
lines = [heading]
|
||||
for line in markdown[start + len(heading):].splitlines():
|
||||
if line.startswith("## "):
|
||||
break
|
||||
lines.append(line)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def test_full_data_chart_section_draws_each_ready_chart_once(editions) -> None:
|
||||
from scripts.report_chart_block import FENCE_RE
|
||||
zh, en, _packet = editions
|
||||
for markdown, heading in (
|
||||
(zh, "### 本命与分盘北印度图盘"),
|
||||
(en, "### Natal and Divisional North Indian Charts"),
|
||||
):
|
||||
section = _section_until_next_h2(markdown, heading)
|
||||
blocks = []
|
||||
for match in FENCE_RE.finditer(section):
|
||||
block = json.loads(match.group(1))
|
||||
assert block["layout"] == "north"
|
||||
blocks.append(block)
|
||||
ids = [block["id"] for block in blocks]
|
||||
assert ids
|
||||
assert len(ids) == len(set(ids))
|
||||
assert _CALCULATION_CHART_IDS <= set(ids)
|
||||
assert "MOON" in ids
|
||||
assert not re.search(r"(?m)^##### .+\n\n+##### ", section)
|
||||
assert not re.search(r"(?m)^##### .*Bhava", section)
|
||||
assert "##### Navamsha" not in section and "##### p7 · Navamsha" not in section
|
||||
|
||||
|
||||
def _heading_table(markdown: str, heading: str) -> list[list[str]]:
|
||||
"""Like _table_after, but the lord table is a #### heading, not ###."""
|
||||
lines = markdown.splitlines()
|
||||
start = next(i for i, line in enumerate(lines) if line.startswith("#") and heading in line)
|
||||
rows = []
|
||||
for line in lines[start + 1:]:
|
||||
if line.startswith("#") or (rows and not line.startswith("|")):
|
||||
break
|
||||
if line.startswith("|") and not set(line) <= set("|- "):
|
||||
rows.append([cell.strip() for cell in line.strip("|").split("|")])
|
||||
return rows
|
||||
|
||||
|
||||
def test_birth_position_table_starts_with_the_ascendant(editions) -> None:
|
||||
zh, en, packet = editions
|
||||
asc = packet["core_chart"]["ascendant"]
|
||||
longitude = asc.get("lon") if isinstance(asc.get("lon"), (int, float)) else asc.get("degree_raw")
|
||||
fields = _nakshatra_fields(float(longitude))
|
||||
lord_zh = {
|
||||
"Sun": "太阳", "Moon": "月亮", "Mars": "火星", "Mercury": "水星",
|
||||
"Jupiter": "木星", "Venus": "金星", "Saturn": "土星",
|
||||
"Rahu": "北交点", "Ketu": "南交点",
|
||||
}
|
||||
zh_rows = _heading_table(zh, "行星原始位置")
|
||||
assert zh_rows[1][0] == "上升"
|
||||
assert fields["nakshatra"] in zh_rows[1][6]
|
||||
assert zh_rows[1][7] == str(fields["nakshatra_pada"])
|
||||
assert zh_rows[1][5] == "1"
|
||||
lords = _heading_table(zh, "行星主星与状态")
|
||||
assert lords[1][0] == "上升"
|
||||
assert lords[1][5] == "不适用"
|
||||
assert lords[1][2] in {fields["nakshatra_lord"], lord_zh.get(fields["nakshatra_lord"])}
|
||||
en_rows = _heading_table(en, "Planetary Raw Positions")
|
||||
assert en_rows[1][0] in {"Ascendant", "Lagna", "上升"}
|
||||
assert fields["nakshatra"] in en_rows[1][6]
|
||||
assert en_rows[1][7] == str(fields["nakshatra_pada"])
|
||||
|
||||
|
||||
def test_zh_hygiene_rejects_glued_english(editions) -> None:
|
||||
from scripts.pl9_full_data_export import _pl9_markdown_hygiene_receipt, _zh_body_residue
|
||||
injected = _pl9_markdown_hygiene_receipt("木星Jupiterexalted在巨蟹座\n", language="zh")
|
||||
assert injected["status"] == "fail"
|
||||
assert any(str(item).startswith("glued_latin:") for item in injected["violations"])
|
||||
residue = _zh_body_residue(editions[0])
|
||||
assert residue == {"glued_latin": 0, "function_words": 0}
|
||||
|
||||
|
||||
def test_zh_hygiene_is_clean_on_night_and_a_public_chart() -> None:
|
||||
from scripts.pl9_full_data_export import _zh_body_residue
|
||||
# Public birth used by tests/smoke_test_runner.py. Not a private user.
|
||||
public = {
|
||||
**CASES["day"],
|
||||
"name": "Albert Einstein",
|
||||
"year": 1879,
|
||||
"month": 3,
|
||||
"day": 14,
|
||||
"hour": 11,
|
||||
"minute": 30,
|
||||
"lat": 48.4,
|
||||
"lon": 9.98,
|
||||
"tz": 0.89,
|
||||
"age": 76,
|
||||
}
|
||||
for case in (CASES["night"], public):
|
||||
packet = _packet(case)
|
||||
markdown = _pl9_export_markdown_for_edition(copy.deepcopy(packet), "full_data")
|
||||
assert _zh_body_residue(markdown) == {"glued_latin": 0, "function_words": 0}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -10,7 +10,7 @@ Claude(开发机,Linux)负责规划与验收,Grok Build(产品负责
|
||||
| --- | --- | --- |
|
||||
| `agent/待规划` | Claude:诊断 → 任务书推 staging | `agent/待执行` 或 `agent/需拍板` |
|
||||
| `agent/待执行` | Grok:按任务书在 `codex/*` 实现、自测、推分支 | `agent/待验收` 或 `agent/需拍板` |
|
||||
| `agent/待验收` | Claude:只验 Grok 回报的 SHA | 通过 → 推 staging → `agent/部署中`;不通过 → 修复单 → `agent/待执行`(打回 2 次 → `agent/需拍板`) |
|
||||
| `agent/待验收` | Claude:只验 Grok 回报的 SHA | 通过 → 推 staging → `agent/部署中`;不通过 → 修复单 → `agent/待执行`(打回 2 次 → `agent/需拍板`);只是落后于 staging 且合并只碰文档时,Claude 自己合并,不打回、不计次数(2026-10-13 产品决定) |
|
||||
| `agent/部署中` | Claude 脚本(不用模型):核对 `/api/health` | `agent/待真机`;3 小时未部署 → `agent/需拍板` |
|
||||
| `agent/待真机` | 产品负责人 | 关 issue,或评论问题并改回 `agent/待规划` |
|
||||
| `agent/需拍板` | 产品负责人 | 回复后改回原状态 |
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## 要做的
|
||||
|
||||
1. `git fetch origin`,`git checkout --detach <extra.sha>`。确认 `git merge-base --is-ancestor origin/staging HEAD` 成立(分支基于最新 staging 可快进);不成立则判 `fail`,修复单要求 Grok 在最新 staging 上重做。
|
||||
1. `git fetch origin`,`git checkout --detach <extra.sha>`。确认 `git merge-base --is-ancestor origin/staging HEAD` 成立(分支基于最新 staging 可快进);不成立时先试 `git merge --no-ff origin/staging`:若无冲突,或冲突**只在 `docs/**`、`CHANGELOG*.md` 等纯文档里**(如两单都往 `docs/BUG_HISTORY.md` 末尾加条目),由你解决冲突、提交合并(提交信息写明「只碰文档」),核对 `scripts/`、`tests/`、`frontend/` 等代码路径与被验 SHA 的树完全相同,再按下面验收并推送,**不打回、不计打回次数**(2026-10-13 产品负责人在 #53 选定方案 A)。冲突碰到任何代码文件则判 `fail`,修复单要求 Grok 在最新 staging 上重做(Grok 不能改写已推分支,用 merge 而不是 rebase)。
|
||||
2. 对照任务书逐条给出 **通过 / 未通过 / 环境缺口**。验收命令按 `CLAUDE.md`「验收口径」与 `AGENTS.md` §7、§10 跑,前端全量 `npm test` 与 Python 快速门以你这里为准。依赖缺失时先在本工作目录 `npm ci --prefix frontend`;Python 用 `.venv/bin/python`,没有就用 `python3`。
|
||||
3. `test:db` 你这里跑不了:以 Grok 回报为准,但要核对它贴的失败清单与 `tools/agent-loop/windows-known-failures.txt` 逐条一致;对不上即未通过。
|
||||
4. 检查越界:diff 里有没有任务书没要求的文件、`.gitea/**`、依赖升级、冻结文件增长、弱化断言(缺"原值 / 新值 / 原因"三栏)、隐私数据。有就 `fail`。
|
||||
|
||||
Reference in New Issue
Block a user