From 9308382ef8009b3002ad9fe57b4558a83087d4bb Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 15 Oct 2026 08:45:34 +0800 Subject: [PATCH 1/5] docs(tasks): api chart subprocess gate fix brief (agent loop, issue #64, bounce 1) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LutgzyJqNhPpZy1nFsJtN3 --- docs/tasks/README.md | 1 + ...-api-chart-subprocess-gate-fix-20261015.md | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 docs/tasks/TASK-api-chart-subprocess-gate-fix-20261015.md diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 3c8a8cdf..594c60d0 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -483,6 +483,7 @@ | `TASK-frontend-dead-code-20261013.md` | `PROGRESS-frontend-dead-code-20261013.md` | 前端死代码 17 文件与无调用方接口删除,含未登录重计算接口(BUG-1342) | 待验收 | `codex/frontend-dead-code-20261013` | | `TASK-frontend-dead-code-fix-20261014.md` | `PROGRESS-frontend-dead-code-20261013.md` | **#63 第 1 次打回**:类名白名单去掉已删组件留下的条目 | 待验收 | `codex/frontend-dead-code-20261013` | | `TASK-api-chart-subprocess-gate-20261013.md` | — | 合盘不带 skip 串行排盘、报告无效子进程、名额轻重混用(只模拟)(BUG-1333~1334,A/B) | 待领取(排队:api-disk-cache-retention + report-reference-perf) | `codex/api-chart-subprocess-gate-20261013` | +| `TASK-api-chart-subprocess-gate-fix-20261015.md` | — | **#64 第 1 次打回**:`336286e4` 内容通过(合盘 1.19→0.015 s、正文逐字节一致);Python 合同按源码检查合盘路由旧写法未跟改;A/B 脚本默认覆盖入库记录 | 待领取 | `codex/api-chart-subprocess-gate-20261013` | | `TASK-answer-effort-ab-20261013.md` | — | 答题步推理强度开关 + A/B(需模型 key) | 待领取(排队:consult-prep-parallel) | `codex/answer-effort-ab-20261013` | | `TASK-consult-route-split-20261013.md` | — | consult POST 拆分第一阶段:开流前加载 + 计费结算 | 待领取(排队:consult-prep-parallel、answer-effort-ab) | `codex/consult-route-split-20261013` | | `TASK-pl9-render-split-20261013.md` | — | render_pl9_markdown 第一阶段:搬出无依赖内部函数 | 待领取(排队:api-chart-subprocess-gate) | `codex/pl9-render-split-20261013` | diff --git a/docs/tasks/TASK-api-chart-subprocess-gate-fix-20261015.md b/docs/tasks/TASK-api-chart-subprocess-gate-fix-20261015.md new file mode 100644 index 00000000..061419d3 --- /dev/null +++ b/docs/tasks/TASK-api-chart-subprocess-gate-fix-20261015.md @@ -0,0 +1,37 @@ +# TASK · 合盘与报告子进程修复单(issue #64,第 1 次打回) + +- 日期:2026-10-15 +- 原任务书:`docs/tasks/TASK-api-chart-subprocess-gate-20261013.md` +- 被验 SHA:`336286e428c7cb43cac71eae5286e112c0b500b7`(`codex/api-chart-subprocess-gate-20261013`) +- 执行方:Grok,同一分支续做(先 merge 最新 `origin/staging`,不改写已推历史) +- BUG 编号:沿用本单编号 + +## 1. 验收结论:内容通过,Python 全量多出 1 条失败 + +| 检查 | 结果 | 证据 | +| --- | --- | --- | +| 合盘带 skip、两两并发 | 通过 | Claude 在 Linux 复跑 `scripts/research/api_chart_subprocess_ab.py`:串行 1.19 s / 4 个子进程 → 并发带 skip 0.015 s / 0 个,摘要逐字节一致 | +| 报告正文 | 通过 | 同脚本 `report_bytes_match: true`(本机装了 PyJHora,短路不触发,子进程数不变;生产不装,以 Grok 回报为准) | +| 进程内缓存 | 通过 | 只缓存「PyJHora 是否可导入」「外部解释器路径」「静态 oracle 文件校验(键含路径 + mtime + 大小)」,不含出生资料(BUG-721) | +| 主文件 | 通过 | 类方法数不变,行数减少 | +| 前端全量 / tsc / lint | 通过 | 失败清单与 staging 基线相同 | +| **Python 全量(与 staging 逐条比对)** | **未通过:新增 1 条** | `tests/test_supabase_user_data_contract.py::test_synastry_route_orchestrates_python_chart_and_ashtakoot`:`assert 'postPython("/api/chart", selfPayload)' in route` | + +**根因**:这条 Python 合同按源码文本检查 `frontend/src/app/api/synastry/route.ts` 里的 `postPython("/api/chart", selfPayload)`。本单把它改成了 `Promise.all` 里的 `postPython("/api/chart", { ...selfPayload, skip_vedastro_main_entry_overview: true })`,合同没跟着改(AGENTS.md §7.8;Python 测试也会读前端源码,所以前端改动要 grep `tests/` 和 `frontend/tests/` 两处)。 + +另:本机复跑时 `api_chart_subprocess_ab.py` 会**覆盖仓库里的** `docs/testing/api-chart-subprocess-gate-ab-20261013.json`。研究脚本默认不应写已入库的记录文件:改为默认写到临时目录或 `artifacts/`,只有显式 `--out <路径>` 时才写指定文件。 + +## 2. 修法要求 + +1. 改 `tests/test_supabase_user_data_contract.py` 那条断言,按 §7.3 写「原值 / 新值 / 原因」三栏,**意图不放宽**:仍要锁住「合盘路由向 Python `/api/chart` 请求本人和对方两张盘」,新断言要同时锁住带 `skip_vedastro_main_entry_overview: true`;该测试里其它 token(如 ashtakoot 调用)不动。 +2. `grep -rn "synastry/route\|postPython(\"/api/chart\"\|postPython(\"/api/varga_full\"" tests frontend/tests`,其它按源码文本断言的同样处理。 +3. `api_chart_subprocess_ab.py` 默认不覆盖入库文件(见上)。 +4. 业务代码不改。PROGRESS 末尾记一行。 + +## 3. 验收标准 + +Claude 全量 pytest 与 staging 基线逐条比对无新增;前端全量无新增;`git diff 336286e4..<新 SHA> -- frontend/src scripts/*.py` 只有第 3 条的脚本改动。 + +## 4. 让步顺序 + +无。第 1 次打回。 From 10d0c4bba64a7c9a42a34df686a80c81d34c6fd4 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 15 Oct 2026 09:03:35 +0800 Subject: [PATCH 2/5] docs(tasks): answer effort switch fix brief (agent loop, issue #65, bounce 1) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LutgzyJqNhPpZy1nFsJtN3 --- docs/tasks/README.md | 1 + .../TASK-answer-effort-ab-fix-20261015.md | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 docs/tasks/TASK-answer-effort-ab-fix-20261015.md diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 594c60d0..ea0d1e47 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -485,6 +485,7 @@ | `TASK-api-chart-subprocess-gate-20261013.md` | — | 合盘不带 skip 串行排盘、报告无效子进程、名额轻重混用(只模拟)(BUG-1333~1334,A/B) | 待领取(排队:api-disk-cache-retention + report-reference-perf) | `codex/api-chart-subprocess-gate-20261013` | | `TASK-api-chart-subprocess-gate-fix-20261015.md` | — | **#64 第 1 次打回**:`336286e4` 内容通过(合盘 1.19→0.015 s、正文逐字节一致);Python 合同按源码检查合盘路由旧写法未跟改;A/B 脚本默认覆盖入库记录 | 待领取 | `codex/api-chart-subprocess-gate-20261013` | | `TASK-answer-effort-ab-20261013.md` | — | 答题步推理强度开关 + A/B(需模型 key) | 待领取(排队:consult-prep-parallel) | `codex/answer-effort-ab-20261013` | +| `TASK-answer-effort-ab-fix-20261015.md` | — | **#65 第 1 次打回**:`ab7f66be` 开关逻辑通过(默认请求体不变);研究脚本从 `frontend/tests` 读文件(BUG-1140) | 待领取 | `codex/answer-effort-ab-20261013` | | `TASK-consult-route-split-20261013.md` | — | consult POST 拆分第一阶段:开流前加载 + 计费结算 | 待领取(排队:consult-prep-parallel、answer-effort-ab) | `codex/consult-route-split-20261013` | | `TASK-pl9-render-split-20261013.md` | — | render_pl9_markdown 第一阶段:搬出无依赖内部函数 | 待领取(排队:api-chart-subprocess-gate) | `codex/pl9-render-split-20261013` | | `TASK-python-hygiene-20261013.md` | — | 免费层排队开关报反、静默吞异常、功能吉凶兜底合一、测试专用脚本归位(BUG-1340) | 待领取(排队:pl9-render-split) | `codex/python-hygiene-20261013` | diff --git a/docs/tasks/TASK-answer-effort-ab-fix-20261015.md b/docs/tasks/TASK-answer-effort-ab-fix-20261015.md new file mode 100644 index 00000000..c8ef3b42 --- /dev/null +++ b/docs/tasks/TASK-answer-effort-ab-fix-20261015.md @@ -0,0 +1,36 @@ +# TASK · 答题推理强度开关修复单(issue #65,第 1 次打回) + +- 日期:2026-10-15 +- 原任务书:`docs/tasks/TASK-answer-effort-ab-20261013.md` +- 被验 SHA:`ab7f66be14d93a5ccbc3433f3c44e134a65b1e21`(`codex/answer-effort-ab-20261013`) +- 执行方:Grok,同一分支续做(先 merge 最新 `origin/staging`,不改写已推历史) + +## 1. 验收结论:开关逻辑通过,前端全量多出 1 条失败 + +| 检查 | 结果 | 证据 | +| --- | --- | --- | +| 开关默认关时请求体不变 | 通过 | `CONSULT_ANSWER_REASONING_EFFORT` 未设或为空时 `answerEffortOptions` 返回 `{}`;非法值只警告一次按未设处理 | +| 只在答题步加强度 | 通过 | 主循环第 0 步 `answerReady=false`;公开日历从第一步即答题 | +| 改既有断言 | 通过 | `consult-answer-clock-20261001.test.ts` 三栏齐全 | +| tsc / lint / Python 全量 | 通过 | Python 与 staging 逐条一致(一条 prashna 族测试并发下抖动,单跑通过) | +| **前端全量 `npm test`** | **未通过:新增 1 条** | `frontend/tests/runtime-sources-no-test-imports.test.ts`「files copied into the web image never import from frontend/tests」 | + +**根因**:`frontend/scripts/research/answer-effort-probe.mts` 约 186 行 `new URL("../../tests/fixtures/answer-effort-switch-20261013.json", import.meta.url)`。web 镜像只复制 `frontend/src` 与 `frontend/scripts`,`next build` 会类型检查它们——推上 staging 后镜像构建会失败(BUG-1140;#58、#62 已因同一形状各打回一次)。 + +## 2. 修法要求 + +1. 把 `answer-effort-probe.mts` 挪到 `frontend/tests/answer-effort-probe.mts`(文件名不得以 `.test.ts` / `.test.tsx` 结尾),路径改相对;同步改 PROGRESS 与 `docs/testing/answer-effort-ab-20261013.md` 里的命令。 +2. **交回前必须本机跑一次** `npx tsx --test tests/runtime-sources-no-test-imports.test.ts`,fail 与 cancelled 为 0,贴进回报。 +3. 业务代码不改;开关默认值不改。 + +## 3. 非阻断提醒(不要求本轮改,写进 PROGRESS 即可) + +开关打开时,`prepareStep` 返回的 `providerOptions` 来自 `agentGenerationSettings(model, { thinking: "enabled", ... })`。确认它不会把基础流选项里原有的 `providerOptions`(思考预算、缓存等)整体覆盖掉;若会覆盖,在 BLOCKED.md 记下,等有模型 key 的 A/B 一并核对。 + +## 4. 验收标准 + +前端全量 `npm test` 失败清单与 staging 基线逐名相同;`git diff ab7f66be..<新 SHA> -- frontend/src` 为空。 + +## 5. 让步顺序 + +无。第 1 次打回。 From 2b7112a6010acbcc0fec809b6b27e2375293dd5c Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Thu, 15 Oct 2026 09:19:35 +0800 Subject: [PATCH 3/5] docs(tasks): chart page ascendant row brief (agent loop, issue #53 round 2) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LutgzyJqNhPpZy1nFsJtN3 --- docs/tasks/README.md | 1 + .../TASK-chart-ascendant-row-20261015.md | 102 ++++++++++++++++++ docs/testing/chart-ascendant-row-20261015.md | 9 ++ 3 files changed, 112 insertions(+) create mode 100644 docs/tasks/TASK-chart-ascendant-row-20261015.md create mode 100644 docs/testing/chart-ascendant-row-20261015.md diff --git a/docs/tasks/README.md b/docs/tasks/README.md index ea0d1e47..6a6c77eb 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -213,6 +213,7 @@ | --- | --- | --- | --- | --- | | `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-chart-ascendant-row-20261015.md` | — | **星盘页上升行无星宿 / 表头错位 / 大运卡左边线(issue #53 第二轮)**:`/api/chart` 重建上升时丢了星宿三字段,BUG-1201 从未打通(golden 取自引擎内部);状态改「不适用」(推翻 DESIGN 原口径);居中列表头没居中;当前大运 inset 左线去掉。执行方 Grok | 待领取 | `codex/chart-ascendant-row-20261015` | | `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) | diff --git a/docs/tasks/TASK-chart-ascendant-row-20261015.md b/docs/tasks/TASK-chart-ascendant-row-20261015.md new file mode 100644 index 00000000..c18a5f7e --- /dev/null +++ b/docs/tasks/TASK-chart-ascendant-row-20261015.md @@ -0,0 +1,102 @@ +# TASK · 星盘页上升行无星宿、表头错位、大运卡左边线(issue #53 第二轮) + +- 日期:2026-10-15 +- 来源:issue #53,产品负责人 10-15 在 staging 星盘页反馈:D1 本命与月亮盘的上升行「星宿 / 宿主 / pada / 状态」全是「—」;分盘表「庙旺」表头与内容不对齐;大运卡左侧那条线要去掉 +- 执行方:**Grok(Windows + Docker)**,分支 `codex/chart-ascendant-row-20261015`,只推这个分支 +- 验收:Claude(Linux) +- BUG 编号:开工时核对 `docs/BUG_HISTORY.md` 最大号(写作时为 BUG-1350);**BUG-1351 已被 issue #65 的分支占用,本单从 BUG-1352 起** + +## 1. 基线 + +`origin/staging` `10d0c4bba`。 + +## 2. 事故实证与根因 + +### A · 上升行星宿 / 宿主 / pada 为空(D1 与月亮盘同因)——BUG-1201 修复从未打通 + +- 引擎正确:`scripts/jyotish_engine.py::compute_chart_data`(约 12378 行)给上升加了 `**_nakshatra_fields(asc_deg)`;`scripts/domain_calculation_service.py::compute_chart`(约 242 行)原样传出。 +- **`/api/chart` 把它们丢了**:`scripts/jyotish_api_server.py::_compute_chart_sync` 约 6660–6666 行重建 `result['ascendant']`,只留 `sign / sign_idx / degree / degree_in_sign / lon`;`result['modules']['chart']['ascendant']` 用的是同一个字典。该段来自 06-24 的 `494e3870`,BUG-1201 的 `ae18952e` 没碰它。虚构盘(1990-06-15 12:00 北京)实测 `_compute_chart_sync` 返回的上升没有任何星宿键。 +- 前端映射没问题:`frontend/src/lib/chart-view-mapper.ts::ascendantRow`(约 390–409 行)读 `nakshatra / nakshatra_pada / nakshatra_lord`,缺失时按设计回落「—」。月亮盘复用同一个 `view.vedic.ascendant`(`chart-vedic-tab.tsx` 约 351–355 行)。 +- **不是缓存问题**:服务器缓存 900 s、前端 5 分钟,`cache_schema_version` 已是 6;BUG-1201 那次把 4→5 也没用,因为接口输出根本没变。 +- **为什么没拦住**:`tests/test_report_chart_blank_columns.py` 只调 `compute_chart_data`;前端 golden `frontend/tests/fixtures/chart-view-golden.json` 取自 `domain_calculation_service.compute_chart` 而不是接口真实响应,又手工补了 `_nakshatra_fields`——违反 AGENTS.md §7.4「合同 fixture 必须来自真实引擎响应」,所以丢字段的那一层从没被测到。 + +### B · 上升行「状态」写「—」 + +`chart-vedic-tab.tsx::AscendantRow` 约 99 行写死 `—`,注释与 `frontend/DESIGN.md` 约 1181 行写「上升没有尊贵,状态格写「—」」——这是当时的有意设计,不是回归。产品负责人这次把它算作「没数据」。 + +### C · 表头与内容不对齐 + +`frontend/src/app/globals.css` 约 5619–5623 行把 `.chart-page-planet-table` 的所有 `th / td` 设为左对齐;约 5658 行只把 `td.chart-page-planet-mid` 居中;对应的 `` 没有这个类,于是表头靠左、内容居中。所有 `.chart-page-planet-table` 都有:D1 / 月亮盘的「速度、宿主、pada」,分盘的「庙旺、同位」,行运的「速度」,Bhava 的「换宫」。 + +### D · 大运卡左边线 + +`globals.css` 约 5774–5778 行 `.chart-page-dasha-period.is-current { …; box-shadow: inset 2px 0 0 var(--color-focus); }`(`--color-focus` 是陶土色),挂在当前大运上(默认展开的那一张,`chart-dasha-tab.tsx` 约 115 行)。来自 `1e3570b4`;`frontend/DESIGN.md`「大运 Tab」一节没写,也没有测试引用。 + +## 3. 决策记录 + +- 产品负责人在 issue #53 授权修复上面四项。 +- **B(按推荐先定,产品不同意回一句即改)**:上升行「状态」改写「不适用」,与分盘表上升「庙旺」格(BUG-1099)、报告上升行(BUG-1314)同一口径。**这推翻 `frontend/DESIGN.md` 约 1181 行「状态格写「—」」**,DESIGN.md 同步改。 +- **D(按推荐先定)**:只去掉左边那条线,当前大运的浅底与细边框保留,用来标「现在走的是这一段」。 +- 不推翻 BUG-1201 的设计(上升行要有星宿 / 宿主 / pada),这次是把它真正打通;记为新 BUG「复发自 BUG-1201(修复未打通接口层)」,并在 BUG-1201 记录里补一行更正其验证结论。 + +## 4. 硬红线 + +1. `scripts/jyotish_api_server.py`:类方法数与 `JyotishAPIHandler.__new__` 伪造点不得增长(`tests/test_api_server_growth_contract.py`;注释里写这个词也算);只在 `_compute_chart_sync` 里补字段。 +2. 不改引擎数值;不改 `_nakshatra_fields`、`compute_chart_data`。 +3. 合同 fixture 必须来自**接口真实响应**(§7.4);不得手工补字段。 +4. 改既有断言写「原值 / 新值 / 原因」三栏;开工先 grep(§7.8,Python 测试也会读前端源码): + `grep -rn "chart-page-planet-mid\|AscendantRow\|上升没有尊贵\|is-current\|chart-view-golden\|ascendant\b" frontend/tests tests | grep -v node_modules` + 已知:`frontend/tests/chart-varga-table.test.tsx` 约 101–103 行断言上升行末尾 `—`;`frontend/tests/fixtures/chart-d1-table-baseline.html`;`chart-page-view.test.tsx`、`chart-page-hook.test.ts`。 +5. `frontend/scripts/**`、`frontend/src/**` 不得从 `frontend/tests/**` 导入或读文件(BUG-1140,近期已 5 次打回)。交回前必须本机跑 `npx tsx --test tests/runtime-sources-no-test-imports.test.ts`。 +6. 不碰 `frontend/src/app/(app)/page.tsx`;不改 `.gitea/**`;不动数据库;不升级依赖。 + +## 5. 任务分解 + +### T1 · 接口带出上升的星宿字段(新 BUG,复发自 BUG-1201) + +- `_compute_chart_sync` 重建 `result['ascendant']` 时,从 `ascendant_data` 带上 `nakshatra`、`nakshatra_pada`、`nakshatra_lord`(引擎已有就原样带;没有就调 `_nakshatra_fields(lon)`);`cache_schema_version` 6 → 7。 +- 验收: + - 新回归测试**经过 `/api/chart` 处理函数**(用现有测试里已有的调用方式,不得新增 `JyotishAPIHandler.__new__`):虚构盘的响应 `ascendant` 有三个字段,且等于 `_nakshatra_fields(lon)`;`modules.chart.ascendant` 同样有。 + - 用这次的**接口真实响应**重新生成 `chart-view-golden.json` 的上升部分,`fixtureProvenance` 写明命令;删掉手工补字段的那一步。 + - 前端:D1 与月亮盘上升行渲染出星宿、宿主、pada(测试断言具体值,不是只断言「不是 —」)。 + +### T2 · 上升行状态「不适用」 + +- `AscendantRow` 状态格改「不适用」;DESIGN.md 约 1181 行同步改;`chart-varga-table.test.tsx` 与 baseline html 按三栏改。 + +### T3 · 表头对齐 + +- 给居中列对应的 `` 加 `chart-page-planet-mid`(或用列选择器),让表头与内容同为居中;涉及 D1 / 月亮盘、分盘、行运、Bhava 四类表。8 列表的 markup 若被 DESIGN.md 约 1195 行写成「冻结」,改用 CSS 选择器、不改 markup,并在 DESIGN.md 记一句。 +- 验收:渲染测试断言每张表里居中列的 `th` 与 `td` 同为居中(类名或计算后的选择器);DESIGN.md 更新。 + +### T4 · 去掉大运卡左边线 + +- 删 `.chart-page-dasha-period.is-current` 的 `box-shadow` 那一行,浅底与边框保留;DESIGN.md「大运 Tab」一节加一句「当前大运用浅底和细边框标出,不画左侧强调线」。 +- 验收:一条 CSS 合同测试断言 `.chart-page-dasha-period.is-current` 不含 `box-shadow`。 + +### T5 · 记录 + +- `docs/BUG_HISTORY.md`:新 BUG(上升星宿字段被 `/api/chart` 丢弃,复发自 BUG-1201,写清为何 golden 没拦住);BUG-1201 记录补一行更正;表头错位、左边线各一条或合为一条 UI 记录。 +- `docs/tasks/PROGRESS-chart-ascendant-row-20261015.md`、`CHANGELOG.md`、`frontend/DESIGN.md`。 +- 真机清单 `docs/testing/chart-ascendant-row-20261015.md`。 + +## 6. 让步顺序 + +T1 → T3 → T2 → T4。T1 不可让。 + +## 7. 验收命令分工 + +| 谁 | 命令 | +| --- | --- | +| Grok 自跑 | `python -m pytest tests/test_report_chart_blank_columns.py tests/test_api_server_growth_contract.py <新测试> -q`;前端 `tsc --noEmit`、`npm run lint`、改到 / 新增的前端测试单文件,**加** `tests/runtime-sources-no-test-imports.test.ts`(fail 与 cancelled 为 0)。前端全量与 Python 快速门不跑 | +| Claude 验收 | 上述 + Python 全量与基线逐条比对 + 快速门 + 前端全量 `npm test` + `next build`(`/` 仍 Static,首屏 gzip ±2%) | +| 不需要 | `test:db` | + +## 8. 开工前置 + +```bash +git fetch origin --prune +git worktree add -b codex/chart-ascendant-row-20261015 .worktrees/chart-ascendant-row-20261015 origin/staging +grep -n "^## BUG-" docs/BUG_HISTORY.md | tail -3 # BUG-1351 已被 #65 占用 +grep -rn "chart-page-planet-mid\|AscendantRow\|上升没有尊贵\|is-current\|chart-view-golden" frontend/tests tests +``` diff --git a/docs/testing/chart-ascendant-row-20261015.md b/docs/testing/chart-ascendant-row-20261015.md new file mode 100644 index 00000000..5a8d9746 --- /dev/null +++ b/docs/testing/chart-ascendant-row-20261015.md @@ -0,0 +1,9 @@ +# 真机清单 · 星盘页上升行、表头、大运卡(issue #53 第二轮) + +部署到 staging 后,打开本人星盘页(手机和电脑各一次): + +1. D1 本命表第一行「上升」:星宿、宿主、pada 都有值;状态写「不适用」。 +2. 切到月亮盘:上升那一行同样有星宿、宿主、pada,状态「不适用」。 +3. 切到任一分盘(如 D9):「庙旺」「同位」表头和下面的内容都居中对齐。 +4. D1 表的「速度、宿主、pada」,行运表的「速度」,Bhava 表的「换宫」:表头和内容对齐。 +5. 大运 Tab:当前那段大运没有左侧那条彩色竖线,仍有浅色底能看出是「现在这一段」。 From b184448871df2fc579a338959ee3522abe23c5b5 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Thu, 15 Oct 2026 09:21:14 +0800 Subject: [PATCH 4/5] fix(db): check callers against the adapter that actually runs The self-hosted client was typed as a full Supabase client, so an old scoring poll called .single() on a plain promise and failed. The new DataClient only lists methods the adapter implements. Co-Authored-By: Grok (agent loop) --- CHANGELOG.md | 4 + docs/BUG_HISTORY.md | 17 ++ .../PROGRESS-data-layer-types-20261013.md | 92 ++++++++++ docs/tasks/README.md | 2 +- frontend/src/app/api/chart-profiles/route.ts | 2 +- .../src/lib/birth-time-journey-case-loader.ts | 4 +- frontend/src/lib/birth-time-journey-store.ts | 4 +- .../birth-time-journey-turn-persistence.ts | 4 +- .../src/lib/birth-time-scoring-job-store.ts | 25 ++- frontend/src/lib/db/data-client.ts | 46 +++++ .../src/lib/db/local-postgres-client-core.ts | 5 +- .../rectification-agentic/v9/case-service.ts | 4 +- .../v9/product-domain.ts | 4 +- .../rectification-agentic/v9/tool-service.ts | 2 +- .../src/lib/rectification-in-chat-server.ts | 18 +- .../src/lib/supabase/admin-client-core.ts | 10 +- frontend/src/lib/supabase/server.ts | 9 +- frontend/src/mastra/rectification-v9-tools.ts | 4 +- .../database-scoring-job-claim-shape.test.ts | 157 ++++++++++++++++++ 19 files changed, 366 insertions(+), 47 deletions(-) create mode 100644 docs/tasks/PROGRESS-data-layer-types-20261013.md create mode 100644 frontend/src/lib/db/data-client.ts create mode 100644 frontend/tests/database-scoring-job-claim-shape.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cac04799..fb876551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 印度占星 Skill 更新日志 +## 2026-10-15 — 旧版校正评分轮询不再中途报错(待合入 staging) + +- 旧版生时校正在查询评分进度时,会因为多读了一步数据库结果而直接失败。现在按数据库实际返回的那一行读取。其他查询行为不变。Skill 版本不变(BUG-1351)。 + ## 2026-10-14 — 生产可以在测试环境后面只多了文档时照常发布(待合入 staging) - 生产发布用最近一次测过的代码版本。测试环境后面如果只多了文档,不再挡住发布。生产环境拒绝测试用验证码,网址末尾多一个斜杠也拒绝。迁移演练在主机上不再依赖本机 Node;演练库删不掉会明确失败。只多了文档时,回退不再被拒绝。Skill 版本不变(BUG-1348、BUG-1349)。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index deb92593..2a6cea06 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -36131,3 +36131,20 @@ - 相关记录:BUG-203(锁文件,不是这一条)、BUG-1063(人物表列级读取权)、BUG-1348。 - 复发自:无。 - 修复版本:分支 `codex/release-path-production-20261014`。编号接在 BUG-1349 之后。 + +## BUG-1351 | 自托管下旧版校正评分轮询 rpc 结果多读一步,必抛 TypeError + +- 状态:resolved +- 首次发现:2026-10-13 +- 最近更新:2026-10-15 +- 影响面:旧版生时校正(legacy-guided-v1)的评分轮询,`POST /api/birth-time-journey` 且类型为 `poll_scoring` +- 用户现象:这类存量校正在查看评分进度时直接失败,页面拿不到「已领取 / 处理中 / 已完成」。 +- 触发条件:自托管数据库(当前 staging 与生产都是这条路径)上,对 `claim_birth_time_scoring_job` 的返回值再调用 `.single()`。该函数返回的是表格里的一行,本地适配层把整表收成数组,没有 `.single`。 +- 根因:数据访问层被标成完整的 Supabase 客户端,编译器看不出适配层没有 `.single`。原有测试用内存仓库,没有走过真实适配层。 +- 修复:去掉 `.single()`。返回值如果是数组就取第一行,如果已是单行对象就直接用,再做原来的字段校验。Supabase 回滚分支不带 `.single()` 时同样返回数组,行为一致。 +- 验证:修复前两条单元测试都抛出 `supabase.rpc(...).single is not a function`。修复后这两条通过。另有一条真实迁移测试:函数返回值是数组,认领结果为已领取,且不抛错。`tsc --noEmit` 0 错。 +- 防复发:适配层 `implements` 自己的 `DataClient`。新增调用如果用了适配层没有的方法,编译直接失败。评分认领有数据库级用例。 +- 相关记录:BUG-124、BUG-130、BUG-576、BUG-926 +- 复发自:无 +- 修复版本:分支 `codex/data-layer-types-20261013`。任务书预留 BUG-1343;开工时本文件最大号已是 BUG-1350,1343 没有条目。按「从当前最大号往后」编为 BUG-1351。 + diff --git a/docs/tasks/PROGRESS-data-layer-types-20261013.md b/docs/tasks/PROGRESS-data-layer-types-20261013.md new file mode 100644 index 00000000..3fbf0f66 --- /dev/null +++ b/docs/tasks/PROGRESS-data-layer-types-20261013.md @@ -0,0 +1,92 @@ +# PROGRESS · 数据访问层 DataClient 类型(第一阶段) + +- 分支:`codex/data-layer-types-20261013` +- 基线:`origin/staging` `9274491681f6fcacbc87e3797441d2c5221de874` +- 前置:`origin/codex/node-runtime-resources-20261013` 已是 `origin/staging` 的祖先(`merge-base --is-ancestor` 退出码 0)。`local-postgres-client-core.ts` 只加了 `import type` 和两处 `implements`。 +- `frontend/src/lib/rectification-agentic/session.ts` 已不在,本单没有改它。 +- 编号:任务书预留 BUG-1343。开工时 `docs/BUG_HISTORY.md` 最大号是 BUG-1350,1343 没有条目。按台账规则从最大号往后,记为 **BUG-1351**。 + +## T1 · 编译器暴露的 2 条 + +`DataClient` 换上之后、缺陷尚未修时,`tsc --noEmit` 退出码 2,恰好 2 条: + +``` +src/app/api/chart-profiles/route.ts(59,40): error TS7006: Parameter 'row' implicitly has an 'any' type. +src/lib/birth-time-scoring-job-store.ts(103,10): error TS2339: Property 'single' does not exist on type 'Promise | null; count?: number | null | undefined; }>>'. +``` + +`grep`:`as unknown as SupabaseClient` 在 `frontend/src` 无结果。`@supabase/supabase-js` 只剩 `admin-client-core.ts` 的 `createClient` 回滚分支。 + +## T2 · 缺陷修复红转绿 + +修复前,`claimScoringJob` 对返回数组的假 `rpc` 调用 `.single()`: + +- `claimScoringJob reads the first row when rpc returns an array`:失败,`TypeError: supabase.rpc(...).single is not a function` +- `claimScoringJob also accepts the single-object rpc shape`:同样失败 + +去掉 `.single()` 并兼容数组与单对象之后,这两条通过。真实迁移用例 `claim_birth_time_scoring_job returns an array and claimScoringJob accepts it` 通过:`data` 是数组,第一行 `claim_state` 为 `claimed`,store 返回 `{ kind: "claimed", algorithmVersion }`。第一次启动测试库时迁移进程只留下笼统的 `database migration failed`(脚本会把非预期异常收成这一句),容器未到 Healthy。同一条用例在容器 Healthy 后重跑通过。 + +`chart-profiles/route.ts` 的 `row` 标为 `unknown`。 + +修完后 `tsc --noEmit` 退出码 0。`npm run lint` 退出码 0,0 error,123 warning(与改动前的存量警告数量一致,本单没有新增 warning)。 + +## T3 · 转译差分 + +类型层改动用 esbuild 0.28.1、`--format=esm`、`--tsconfig=frontend/tsconfig.json`,旧文件与新文件都按这份 tsconfig 转译。除 T2 的两个文件和新建的 `data-client.ts` 外,全部 `SAME`: + +- `src/lib/birth-time-journey-case-loader.ts` +- `src/lib/birth-time-journey-store.ts` +- `src/lib/birth-time-journey-turn-persistence.ts` +- `src/lib/db/local-postgres-client-core.ts` +- `src/lib/rectification-agentic/v9/case-service.ts` +- `src/lib/rectification-agentic/v9/product-domain.ts` +- `src/lib/rectification-agentic/v9/tool-service.ts`(只改了一句注释;转译后注释被去掉,产物相同) +- `src/lib/rectification-in-chat-server.ts` +- `src/lib/supabase/admin-client-core.ts` +- `src/lib/supabase/server.ts` +- `src/mastra/rectification-v9-tools.ts` + +任务书把旧文件写到系统临时目录再转译。esbuild 0.28.1 会沿文件所在目录寻找 `tsconfig.json`。临时目录找不到本仓库的 `tsconfig.json`(`target` 为 ES2017),类字段的转译结果会和仓库内文件不一致,即使源码只有类型或注释差别也会报 DIFF。两边都带上同一份 `--tsconfig` 后为 SAME。这不是运行时变化。 + +## T4 · 把 data 收成 unknown 的错误清单 + +临时把 `UntypedData` 改成 `unknown` 后 `tsc --noEmit` 退出码 2,共 93 条、12 个文件。随后已改回 `any`。 + +| 条数 | 文件 | +| ---: | --- | +| 24 | `src/app/api/account/route.ts` | +| 17 | `src/app/api/consult/route.ts`(其中 1 条是 `DataClient` 不能赋给 `GuardedTitleClient`,`consult/route.ts` 约 1055 行) | +| 16 | `src/app/api/consult/status/route.ts` | +| 12 | `src/app/api/payment/epay/status/route.ts` | +| 7 | `src/lib/epay/availability.ts` | +| 5 | `src/lib/rectification-agentic/v9/product-domain.ts` | +| 4 | `src/app/api/sessions/route.ts` | +| 3 | `src/lib/rectification-agentic/v9/agent-route-request.ts` | +| 2 | `src/app/api/chart-profiles/[id]/route.ts` | +| 1 | `src/app/api/account/avatar/route.ts` | +| 1 | `src/app/api/chart-profiles/route.ts` | +| 1 | `src/app/api/sessions/[id]/route.ts` | + +和任务书写作时的 122 条相比,`consult/route.ts` 少了 10 条,`chart-profiles/route.ts` 少了 1 条(本单已把 `row` 标成 `unknown`)。没有单独再出现一条 `session-title-guard.ts` 的错误,`GuardedTitleClient` 那条记在 `consult/route.ts` 里。这些留给第二阶段,本单没有改那些业务文件。 + +## test:db + +`npm run test:db`(`tsx --test --test-concurrency=1 tests/database-*.test.ts`)约 19 分钟,退出码 1。汇总:85 条,通过 72,失败 13,取消 0,跳过 0。 + +失败的 13 条与 `tools/agent-loop/windows-known-failures.txt` 逐条一致,没有多出来的失败: + +- staging backups are encrypted, atomic, private, and retain the newest three +- rejects destructive backup directory aliases and symlink components before mutation +- rejects unsafe writable backup parents before creating the target +- rejects a direct canonical sticky shared backup directory before chmod +- creates every absent backup path component privately despite a permissive caller umask +- same-second backups publish once without overwriting the completed archive +- find enumeration failures preserve existing backups and do not report completion +- refuses full disks and removes a failed-pipeline partial file +- database env validator accepts punctuated literal required secrets +- database env validator enforces an explicit staging owner uid without printing values +- database env validator accepts a private valid file without printing values +- personal report job migration is atomic, lease-bound, recoverable, and owner read-only +- personal report sections enforce owner-read RLS and service-owned durable transitions + +本单新增的 3 条(两条假客户端、一条真实迁移认领)都在通过的 72 条里。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 3c8a8cdf..38d4fcf7 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -489,5 +489,5 @@ | `TASK-python-hygiene-20261013.md` | — | 免费层排队开关报反、静默吞异常、功能吉凶兜底合一、测试专用脚本归位(BUG-1340) | 待领取(排队:pl9-render-split) | `codex/python-hygiene-20261013` | | `TASK-dasha-unify-20261013.md` | — | /api/dasha 改走引擎同一套实现、删占位注册表、Ashtottari 出生余额(BUG-1341、1344) | 待领取(排队:python-hygiene) | `codex/dasha-unify-20261013` | | `TASK-api-server-service-extract-20261013.md` | — | API 单体第一阶段:拆出不带 HTTP 的计算类,生产伪造 handler 归零 | 待领取(排队:dasha-unify) | `codex/api-server-service-extract-20261013` | -| `TASK-data-layer-types-20261013.md` | — | 数据访问层改用自己的 DataClient 类型第一阶段(BUG-1343) | 待领取(排队:node-runtime-resources) | `codex/data-layer-types-20261013` | +| `TASK-data-layer-types-20261013.md` | `PROGRESS-data-layer-types-20261013.md` | 数据访问层改用自己的 DataClient 类型第一阶段(BUG-1351;任务书预留的 1343 已被更大编号越过) | 待验收 | `codex/data-layer-types-20261013` | | `TASK-contracts-helpers-dedupe-20261013.md` | — | 前后端契约与小工具去重第一阶段 | 待领取(排队:frontend-dead-code、consult-route-split) | `codex/contracts-helpers-dedupe-20261013` | diff --git a/frontend/src/app/api/chart-profiles/route.ts b/frontend/src/app/api/chart-profiles/route.ts index 24eefec2..5df64576 100644 --- a/frontend/src/app/api/chart-profiles/route.ts +++ b/frontend/src/app/api/chart-profiles/route.ts @@ -56,7 +56,7 @@ export async function GET() { .order("updated_at", { ascending: false }); if (error) throw error; - const profiles = (data ?? []).map((row) => toClientProfile(row as Record)); + const profiles = (data ?? []).map((row: unknown) => toClientProfile(row as Record)); return NextResponse.json({ profiles, limit: MAX_SAVED_SUBJECTS, diff --git a/frontend/src/lib/birth-time-journey-case-loader.ts b/frontend/src/lib/birth-time-journey-case-loader.ts index b5b32463..9621ac50 100644 --- a/frontend/src/lib/birth-time-journey-case-loader.ts +++ b/frontend/src/lib/birth-time-journey-case-loader.ts @@ -1,4 +1,4 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; +import type { DataClient } from "@/lib/db/data-client"; import { z } from "zod"; import { parseRectificationQuestionnaire, @@ -34,7 +34,7 @@ export type JourneyLoadClient = { from(table: string): { select(columns: string): JourneyLoadQuery }; }; -export function createJourneyLoadClient(supabase: SupabaseClient): JourneyLoadClient { +export function createJourneyLoadClient(supabase: DataClient): JourneyLoadClient { return { from(table) { return { diff --git a/frontend/src/lib/birth-time-journey-store.ts b/frontend/src/lib/birth-time-journey-store.ts index 1f5571c8..44370ec1 100644 --- a/frontend/src/lib/birth-time-journey-store.ts +++ b/frontend/src/lib/birth-time-journey-store.ts @@ -1,6 +1,6 @@ import "server-only"; -import type { SupabaseClient } from "@supabase/supabase-js"; +import type { DataClient } from "@/lib/db/data-client"; import type { BirthTimeJourneyStore, } from "./birth-time-journey-service.ts"; @@ -24,7 +24,7 @@ import { createDynamicScoringJobStore } from "./birth-time-dynamic-scoring-job-s export { BirthTimeJourneyStoreError } from "./birth-time-journey-turn-persistence.ts"; export function createSupabaseBirthTimeJourneyStore( - supabase: SupabaseClient, + supabase: DataClient, now: () => Date = () => new Date(), ): BirthTimeJourneyStore { const loadClient = createJourneyLoadClient(supabase); diff --git a/frontend/src/lib/birth-time-journey-turn-persistence.ts b/frontend/src/lib/birth-time-journey-turn-persistence.ts index 3ca50096..588a105a 100644 --- a/frontend/src/lib/birth-time-journey-turn-persistence.ts +++ b/frontend/src/lib/birth-time-journey-turn-persistence.ts @@ -1,4 +1,4 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; +import type { DataClient } from "@/lib/db/data-client"; import { z } from "zod"; import type { JourneySnapshot } from "./birth-time-journey.ts"; import type { LegacyStoredRectificationCase, StoredRectificationCase } from "./birth-time-journey-service.ts"; @@ -56,7 +56,7 @@ function canonicalAskedDomains(input: readonly EvidenceDomain[]): readonly Evide } export function createJourneyTurnPersistence( - supabase: SupabaseClient, + supabase: DataClient, loadCase: (userId: string, caseId: string) => Promise, ) { return { diff --git a/frontend/src/lib/birth-time-scoring-job-store.ts b/frontend/src/lib/birth-time-scoring-job-store.ts index 8fb36d79..3d13559c 100644 --- a/frontend/src/lib/birth-time-scoring-job-store.ts +++ b/frontend/src/lib/birth-time-scoring-job-store.ts @@ -1,4 +1,4 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; +import type { DataClient } from "@/lib/db/data-client"; import { z } from "zod"; import { BirthTimeScoringJobError } from "./birth-time-scoring-job.ts"; import type { @@ -58,7 +58,7 @@ async function requireReloaded( } export function createSupabaseScoringJobStore( - supabase: SupabaseClient, + supabase: DataClient, loadCase: (userId: string, caseId: string) => Promise, ): ScoringStoreMethods { return { @@ -91,18 +91,17 @@ export function createSupabaseScoringJobStore( }, async claimScoringJob(identity) { - const { data, error } = await supabase - .rpc("claim_birth_time_scoring_job", { - p_user_id: identity.userId, - p_case_id: identity.caseId, - p_job_id: identity.jobId, - p_evidence_fingerprint: identity.evidenceFingerprint, - p_algorithm_version: identity.algorithmVersion, - p_now: identity.now, - }) - .single(); + const { data, error } = await supabase.rpc("claim_birth_time_scoring_job", { + p_user_id: identity.userId, + p_case_id: identity.caseId, + p_job_id: identity.jobId, + p_evidence_fingerprint: identity.evidenceFingerprint, + p_algorithm_version: identity.algorithmVersion, + p_now: identity.now, + }); if (error) throw mapScoringRpcError(error.message, error.code); - const parsed = claimSchema.safeParse(data); + const row = Array.isArray(data) ? data[0] ?? null : data; + const parsed = claimSchema.safeParse(row); if (!parsed.success) throw new BirthTimeJourneyStoreError("load_case"); return { kind: parsed.data.claim_state, diff --git a/frontend/src/lib/db/data-client.ts b/frontend/src/lib/db/data-client.ts new file mode 100644 index 00000000..6b65cc63 --- /dev/null +++ b/frontend/src/lib/db/data-client.ts @@ -0,0 +1,46 @@ +export type DataQueryError = Readonly<{ message: string; code?: string }>; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- phase 1: equals SupabaseClient; phase 2 narrows per table +type UntypedData = any; + +export type DataQueryResult = Readonly<{ + data: UntypedData; + error: DataQueryError | null; + count?: number | null; +}>; + +export interface DataQueryBuilder extends PromiseLike { + select(columns?: string): DataQueryBuilder; + insert(value: Record | readonly Record[]): DataQueryBuilder; + upsert( + value: Record | readonly Record[], + options?: { onConflict?: string }, + ): DataQueryBuilder; + update(values: Record): DataQueryBuilder; + delete(options?: { count?: string }): DataQueryBuilder; + eq(column: string, value: unknown): DataQueryBuilder; + neq(column: string, value: unknown): DataQueryBuilder; + gt(column: string, value: unknown): DataQueryBuilder; + lte(column: string, value: unknown): DataQueryBuilder; + like(column: string, value: unknown): DataQueryBuilder; + in(column: string, value: readonly unknown[]): DataQueryBuilder; + is(column: string, value: unknown): DataQueryBuilder; + not(column: string, operator: string, value: unknown): DataQueryBuilder; + or(expression: string): DataQueryBuilder; + order(column: string, options?: { ascending?: boolean }): DataQueryBuilder; + limit(value: number): DataQueryBuilder; + abortSignal(signal: AbortSignal): DataQueryBuilder; + single(): Promise; + maybeSingle(): Promise; +} + +export interface DataClient { + readonly auth: Readonly<{ + getUser: () => Promise<{ + data: { user: { id: string; email?: string | null } | null }; + error: unknown; + }>; + }>; + from(table: string): DataQueryBuilder; + rpc(fn: string, args?: Readonly>): Promise; +} diff --git a/frontend/src/lib/db/local-postgres-client-core.ts b/frontend/src/lib/db/local-postgres-client-core.ts index a0c2b5a9..4439cb81 100644 --- a/frontend/src/lib/db/local-postgres-client-core.ts +++ b/frontend/src/lib/db/local-postgres-client-core.ts @@ -1,4 +1,5 @@ import { Pool, type PoolClient } from "pg"; +import type { DataClient, DataQueryBuilder } from "./data-client"; type LocalIdentity = Readonly<{ id: string; email: string | null }> | null; export type LocalDatabaseRole = "authenticated" | "service_role"; @@ -457,7 +458,7 @@ export function queryValue(type: string | undefined, value: unknown): unknown { return value; } -class LocalPostgresQueryBuilder implements PromiseLike { +class LocalPostgresQueryBuilder implements DataQueryBuilder { private selectedColumns: string[] | null = null; private mutation: Mutation | null = null; private readonly filters: Filter[] = []; @@ -863,7 +864,7 @@ function castType(value: string): string { return value; } -export class LocalPostgresDataClient { +export class LocalPostgresDataClient implements DataClient { readonly auth: Readonly<{ getUser: () => Promise< Readonly<{ diff --git a/frontend/src/lib/rectification-agentic/v9/case-service.ts b/frontend/src/lib/rectification-agentic/v9/case-service.ts index db9daa26..37559fba 100644 --- a/frontend/src/lib/rectification-agentic/v9/case-service.ts +++ b/frontend/src/lib/rectification-agentic/v9/case-service.ts @@ -8,7 +8,7 @@ */ import { createHash } from "node:crypto"; import { requestCandidateIntervals, type DatedCandidateRange } from "../core/candidate-window.ts"; -import type { SupabaseClient } from "@supabase/supabase-js"; +import type { DataClient } from "@/lib/db/data-client"; import { resolveMissingBirthTimezoneOffset } from "../../birth-profile-timezone.ts"; import { declaredClockRange } from "../../declared-birth-window.ts"; import { @@ -43,7 +43,7 @@ import { type RectificationEntrySummary, } from "./open-request.ts"; -type AccountingClient = SupabaseClient; +type AccountingClient = DataClient; export type V9BaselineSnapshot = Readonly<{ birth_date: string; diff --git a/frontend/src/lib/rectification-agentic/v9/product-domain.ts b/frontend/src/lib/rectification-agentic/v9/product-domain.ts index 60e10495..d151cd4f 100644 --- a/frontend/src/lib/rectification-agentic/v9/product-domain.ts +++ b/frontend/src/lib/rectification-agentic/v9/product-domain.ts @@ -1,4 +1,4 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; +import type { DataClient } from "@/lib/db/data-client"; import { RectificationCaseServiceError } from "./case-service.ts"; import type { OpenRectificationCaseRequest } from "./open-request.ts"; @@ -17,7 +17,7 @@ const domainsBySessionTheme: Readonly> = { /** Only an owned persisted consultation is a product-domain source. */ export async function resolveRectificationProductDomain( - client: SupabaseClient, + client: DataClient, userId: string, request: OpenRectificationCaseRequest, ): Promise { diff --git a/frontend/src/lib/rectification-agentic/v9/tool-service.ts b/frontend/src/lib/rectification-agentic/v9/tool-service.ts index 0e62ff60..92a4e5ff 100644 --- a/frontend/src/lib/rectification-agentic/v9/tool-service.ts +++ b/frontend/src/lib/rectification-agentic/v9/tool-service.ts @@ -44,7 +44,7 @@ import { decisionStateFingerprint } from "../core/decision-fingerprint"; const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; /** - * Minimal structural RPC client. SupabaseClient satisfies this; tests can + * Minimal structural RPC client. DataClient satisfies this; tests can * pass a fake with a single rpc() method. */ export type RectificationRpcClient = { diff --git a/frontend/src/lib/rectification-in-chat-server.ts b/frontend/src/lib/rectification-in-chat-server.ts index 4ec8d784..2414a299 100644 --- a/frontend/src/lib/rectification-in-chat-server.ts +++ b/frontend/src/lib/rectification-in-chat-server.ts @@ -1,6 +1,6 @@ import "server-only"; -import type { SupabaseClient } from "@supabase/supabase-js"; +import type { DataClient } from "@/lib/db/data-client"; import { resolveFeaturePricing } from "@/lib/feature-pricing"; import { isProductEnabled } from "@/lib/product-access"; import { getRectificationEntrySummary } from "@/lib/rectification-agentic/v9/case-service"; @@ -27,7 +27,7 @@ import { /** C3: whether the offer card may be shown, decided by the server and never by the model. */ export async function decideRectificationOffer(input: { - accounting: SupabaseClient; + accounting: DataClient; userId: string; subjectRole: "self" | "other"; modelId: string; @@ -42,7 +42,7 @@ export async function decideRectificationOffer(input: { type SessionRow = { id: string; messages: unknown; session_type?: string | null }; -async function readConsultationSession(client: SupabaseClient, userId: string, sessionId: string): Promise { +async function readConsultationSession(client: DataClient, userId: string, sessionId: string): Promise { const { data, error } = await client .from("chat_sessions") .select("id,messages,session_type") @@ -54,7 +54,7 @@ async function readConsultationSession(client: SupabaseClient, userId: string, s return (row.session_type ?? "consultation") === "consultation" ? row : null; } -async function writeMessages(client: SupabaseClient, userId: string, sessionId: string, messages: unknown[]): Promise { +async function writeMessages(client: DataClient, userId: string, sessionId: string, messages: unknown[]): Promise { const { error } = await client .from("chat_sessions") .update({ messages }) @@ -71,7 +71,7 @@ async function writeMessages(client: SupabaseClient, userId: string, sessionId: * Does not touch consultation_requests. */ export async function persistOpenedDirectOffer(input: { - client: SupabaseClient; + client: DataClient; userId: string; sessionId: string; sentence: string; @@ -96,7 +96,7 @@ export async function persistOpenedDirectOffer(input: { /** C4: after a case opens from a consultation, bind the offer card to it. Best effort. */ export async function bindRectificationSource(input: { - client: SupabaseClient; + client: DataClient; userId: string; sourceSessionId: string; caseId: string; @@ -118,7 +118,7 @@ const SOURCE_SEARCH_LIMIT = 20; * from history has none. */ export async function findRectificationSource( - client: SupabaseClient, + client: DataClient, userId: string, caseId: string, ): Promise { @@ -139,8 +139,8 @@ export async function findRectificationSource( * never from the browser. */ export async function returnToRectificationSource(input: { - client: SupabaseClient; - accounting: SupabaseClient; + client: DataClient; + accounting: DataClient; userId: string; caseId: string; now?: () => Date; diff --git a/frontend/src/lib/supabase/admin-client-core.ts b/frontend/src/lib/supabase/admin-client-core.ts index 2a462b90..d30291ba 100644 --- a/frontend/src/lib/supabase/admin-client-core.ts +++ b/frontend/src/lib/supabase/admin-client-core.ts @@ -1,11 +1,12 @@ -import { createClient, type SupabaseClient } from "@supabase/supabase-js"; +import { createClient } from "@supabase/supabase-js"; +import type { DataClient } from "@/lib/db/data-client"; import { createLocalPostgresDataClient } from "@/lib/db/local-postgres-client-core"; import { getSupabaseUrl, SupabaseConfigurationError, } from "./config"; -export function createAdminSupabaseClient() { +export function createAdminSupabaseClient(): DataClient { if (process.env.AUTH_PROVIDER?.trim() === "self-hosted") { const connectionString = process.env.SERVICE_DATABASE_URL?.trim(); if (!connectionString) { @@ -18,7 +19,7 @@ export function createAdminSupabaseClient() { connectionString, null, "service_role", - ) as unknown as SupabaseClient; + ) as DataClient; } const url = getSupabaseUrl(); const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY; @@ -26,9 +27,10 @@ export function createAdminSupabaseClient() { throw new SupabaseConfigurationError(["SUPABASE_SERVICE_ROLE_KEY"]); } + // 回滚分支,运行时形状是超集 return createClient(url, serviceRoleKey, { auth: { autoRefreshToken: false, persistSession: false }, - }); + }) as unknown as DataClient; } export function isAdminEmail(email: string | null | undefined) { diff --git a/frontend/src/lib/supabase/server.ts b/frontend/src/lib/supabase/server.ts index 351603bd..c9c3fc38 100644 --- a/frontend/src/lib/supabase/server.ts +++ b/frontend/src/lib/supabase/server.ts @@ -1,8 +1,8 @@ import "server-only"; import { createServerClient } from "@supabase/ssr"; -import type { SupabaseClient } from "@supabase/supabase-js"; import { cookies, headers } from "next/headers"; +import type { DataClient } from "@/lib/db/data-client"; import { createLocalPostgresDataClient } from "@/lib/db/local-postgres-client"; import { readDatabaseUrl } from "@/lib/db/config"; import { getIdentityAuthServices } from "@/modules/identity/auth"; @@ -11,7 +11,7 @@ import { getSupabasePublicConfig } from "./config"; export async function createServerSupabaseClient(options?: { readonly disableCookieCache?: boolean; -}) { +}): Promise { if (process.env.AUTH_PROVIDER?.trim() === "self-hosted") { const requestHeaders = new Headers(await headers()); const session = await readIdentitySession( @@ -22,11 +22,12 @@ export async function createServerSupabaseClient(options?: { return createLocalPostgresDataClient( readDatabaseUrl(process.env, "APP_DATABASE_URL"), session ? { id: session.user.id, email: session.user.email } : null, - ) as unknown as SupabaseClient; + ) as DataClient; } const { url, anonKey } = getSupabasePublicConfig(); const cookieStore = await cookies(); + // 回滚分支,运行时形状是超集 return createServerClient(url, anonKey, { cookies: { getAll: () => cookieStore.getAll(), @@ -40,5 +41,5 @@ export async function createServerSupabaseClient(options?: { } }, }, - }); + }) as unknown as DataClient; } diff --git a/frontend/src/mastra/rectification-v9-tools.ts b/frontend/src/mastra/rectification-v9-tools.ts index 41a66610..315c4aa9 100644 --- a/frontend/src/mastra/rectification-v9-tools.ts +++ b/frontend/src/mastra/rectification-v9-tools.ts @@ -12,7 +12,7 @@ import { windowIntervalsWidth } from "../lib/rectification-agentic/core/candidate-window.ts"; import { createTool } from "@mastra/core/tools"; import { z } from "zod"; -import type { SupabaseClient } from "@supabase/supabase-js"; +import type { DataClient } from "@/lib/db/data-client"; import type { PublicRectificationMethod } from "@/lib/rectification-agentic/v9/public-receipt"; import { canonicalToolInputFingerprint, @@ -196,7 +196,7 @@ export type RectificationV9Context = Readonly<{ turnId: string; attemptId?: string; userMessage?: string | null; - accounting: SupabaseClient; + accounting: DataClient; engineBase?: string; decision?: RectificationDecision; productDelivery?: import("../lib/rectification-agentic/v9/product-delivery.ts").ProductDelivery | null; diff --git a/frontend/tests/database-scoring-job-claim-shape.test.ts b/frontend/tests/database-scoring-job-claim-shape.test.ts new file mode 100644 index 00000000..367a7bf3 --- /dev/null +++ b/frontend/tests/database-scoring-job-claim-shape.test.ts @@ -0,0 +1,157 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +import { + closeLocalPostgresDataPools, + createLocalPostgresDataClient, +} from "../src/lib/db/local-postgres-client-core.ts"; +import type { DataClient } from "../src/lib/db/data-client.ts"; +import { birthTimeScoringAlgorithmVersion } from "../src/lib/birth-time-scoring-job.ts"; +import { createSupabaseScoringJobStore } from "../src/lib/birth-time-scoring-job-store.ts"; +import { startPostgresFixture } from "./helpers/postgres-fixture.ts"; + +const runnerPath = fileURLToPath(new URL("../scripts/db-migrate.mjs", import.meta.url)); + +function dockerAvailable(): boolean { + return spawnSync("docker", ["version", "--format", "{{.Server.Version}}"], { + encoding: "utf8", + stdio: "ignore", + }).status === 0; +} + +const skipWithoutDocker = dockerAvailable() ? false : "docker unavailable on this host"; + +const ids = { + user: "e1111111-1111-4111-8111-111111111111", + caseId: "e2222222-2222-4222-8222-222222222222", + job: "e3333333-3333-4333-8333-333333333333", + shapeJob: "e4444444-4444-4444-8444-444444444444", +}; + +const fingerprint = "claim-shape-fingerprint"; + +function sqlLiteral(value: string): string { + return `'${value.replaceAll("'", "''")}'`; +} + +const identity = { + userId: ids.user, + caseId: ids.caseId, + jobId: ids.job, + evidenceFingerprint: fingerprint, + algorithmVersion: birthTimeScoringAlgorithmVersion, + now: "2026-10-15T00:00:00.000Z", +}; + +test("claimScoringJob reads the first row when rpc returns an array", async () => { + const rpc = async () => ({ + data: [{ claim_state: "claimed", algorithm_version: birthTimeScoringAlgorithmVersion }], + error: null, + }); + const store = createSupabaseScoringJobStore({ rpc } as unknown as DataClient, async () => null); + const claim = await store.claimScoringJob(identity); + assert.deepEqual(claim, { + kind: "claimed", + algorithmVersion: birthTimeScoringAlgorithmVersion, + }); +}); + +test("claimScoringJob also accepts the single-object rpc shape", async () => { + const rpc = async () => ({ + data: { claim_state: "processing", algorithm_version: birthTimeScoringAlgorithmVersion }, + error: null, + }); + const store = createSupabaseScoringJobStore({ rpc } as unknown as DataClient, async () => null); + const claim = await store.claimScoringJob(identity); + assert.deepEqual(claim, { + kind: "processing", + algorithmVersion: birthTimeScoringAlgorithmVersion, + }); +}); + +test("claim_birth_time_scoring_job returns an array and claimScoringJob accepts it", { skip: skipWithoutDocker }, async () => { + const fixture = startPostgresFixture(); + const schemaUrl = fixture.connectionUrl("schema_owner", "schema-owner-test-password"); + try { + const migration = spawnSync(process.execPath, [runnerPath], { + encoding: "utf8", + env: { ...process.env, SCHEMA_DATABASE_URL: schemaUrl }, + }); + assert.equal( + migration.status, + 0, + JSON.stringify({ + status: migration.status, + stderr: migration.stderr, + stdoutTail: String(migration.stdout).slice(-1500), + }), + ); + + fixture.psqlAs( + "identity_runtime", + "identity-runtime-test-password", + `insert into identity.users (id, name, email, email_verified, email_verified_at) + values (${sqlLiteral(ids.user)}, 'Claim Shape', 'claim-shape@example.com', true, now());`, + ); + fixture.psql(` + insert into public.birth_time_rectification_cases ( + id, user_id, status, reported_date, source + ) values ( + ${sqlLiteral(ids.caseId)}, ${sqlLiteral(ids.user)}, 'rectifying', '1990-01-01', 'family_exact' + ); + insert into public.birth_time_rectification_scoring_jobs ( + id, case_id, user_id, evidence_fingerprint, algorithm_version, status, expires_at + ) values ( + ${sqlLiteral(ids.job)}, ${sqlLiteral(ids.caseId)}, ${sqlLiteral(ids.user)}, + ${sqlLiteral(fingerprint)}, ${sqlLiteral(birthTimeScoringAlgorithmVersion)}, + 'pending', '2026-10-15T01:00:00Z' + ), ( + ${sqlLiteral(ids.shapeJob)}, ${sqlLiteral(ids.caseId)}, ${sqlLiteral(ids.user)}, + ${sqlLiteral(`${fingerprint}-shape`)}, ${sqlLiteral(birthTimeScoringAlgorithmVersion)}, + 'pending', '2026-10-15T01:00:00Z' + ); + update public.birth_time_rectification_cases + set journey_protocol = 'legacy-guided-v1', + turn_state = ${sqlLiteral(JSON.stringify({ + nextAction: { kind: "score_pending", jobId: ids.shapeJob }, + }))}::jsonb + where id = ${sqlLiteral(ids.caseId)}; + `); + + const client = createLocalPostgresDataClient( + fixture.connectionUrl("service_runtime", "service-runtime-test-password"), + null, + "service_role", + ); + const direct = await client.rpc("claim_birth_time_scoring_job", { + p_user_id: ids.user, + p_case_id: ids.caseId, + p_job_id: ids.shapeJob, + p_evidence_fingerprint: `${fingerprint}-shape`, + p_algorithm_version: birthTimeScoringAlgorithmVersion, + p_now: identity.now, + }); + assert.equal(direct.error, null, direct.error?.message); + assert.equal(Array.isArray(direct.data), true); + assert.equal(direct.data[0]?.claim_state, "claimed"); + + fixture.psql(` + update public.birth_time_rectification_cases + set turn_state = ${sqlLiteral(JSON.stringify({ + nextAction: { kind: "score_pending", jobId: ids.job }, + }))}::jsonb + where id = ${sqlLiteral(ids.caseId)}; + `); + const store = createSupabaseScoringJobStore(client, async () => null); + const claim = await store.claimScoringJob(identity); + assert.deepEqual(claim, { + kind: "claimed", + algorithmVersion: birthTimeScoringAlgorithmVersion, + }); + } finally { + await closeLocalPostgresDataPools(); + fixture.stop(); + } +}); From 454951de2776c1f68808fb2b8e00b35dddb929d0 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Thu, 15 Oct 2026 09:46:37 +0800 Subject: [PATCH 5/5] fix(chart): keep the ascendant nakshatra the page already renders The chart API rebuilt the ascendant without nakshatra, lord, and pada, so those cells stayed blank. Cache schema is now 7. Centered column headers line up with their cells, and the current dasha card keeps its tint without the left accent. pytest 34 passed. tsc 0 errors. lint 0 errors and 123 existing warnings. Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor --- CHANGELOG.md | 4 ++ docs/BUG_HISTORY.md | 35 ++++++++++++ .../PROGRESS-chart-ascendant-row-20261015.md | 33 +++++++++++ docs/tasks/README.md | 2 +- frontend/DESIGN.md | 6 +- frontend/src/app/globals.css | 10 +++- .../components/chart-page/chart-vedic-tab.tsx | 12 ++-- frontend/tests/chart-bhava.test.tsx | 1 + frontend/tests/chart-moon-transit.test.tsx | 9 ++- .../chart-page-mobile-width-20260929.test.ts | 14 +++++ frontend/tests/chart-varga-table.test.tsx | 9 ++- .../tests/fixtures/chart-view-golden.json | 2 +- scripts/jyotish_api_server.py | 15 ++++- tests/test_api_chart_ascendant_nakshatra.py | 56 +++++++++++++++++++ tests/test_api_scratch_retention.py | 5 +- 15 files changed, 196 insertions(+), 17 deletions(-) create mode 100644 docs/tasks/PROGRESS-chart-ascendant-row-20261015.md create mode 100644 tests/test_api_chart_ascendant_nakshatra.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cac04799..bdd1512c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 印度占星 Skill 更新日志 +## 2026-10-15 — 星盘页上升行补上星宿,表头对齐(待合入 staging) + +- 本命盘和月亮盘的上升行现在有星宿、宿主和 pada。状态写「不适用」,不再是空横杠。分盘等表里居中的那几列,表头也居中。当前大运不再画左侧那条线,浅色底还在。已经打开过的星盘会重新计算。Skill 版本不变(BUG-1352、BUG-1353)。 + ## 2026-10-14 — 生产可以在测试环境后面只多了文档时照常发布(待合入 staging) - 生产发布用最近一次测过的代码版本。测试环境后面如果只多了文档,不再挡住发布。生产环境拒绝测试用验证码,网址末尾多一个斜杠也拒绝。迁移演练在主机上不再依赖本机 Node;演练库删不掉会明确失败。只多了文档时,回退不再被拒绝。Skill 版本不变(BUG-1348、BUG-1349)。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index deb92593..90582d0f 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -16174,6 +16174,7 @@ - 根因:`compute_chart_data` 给九曜按 `nak_span` 算了 `nakshatra / nakshatra_pada / nakshatra_lord`,`result["ascendant"]` 漏了;前端按「引擎不给就不推」留空。 - 修复:抽 `_nakshatra_fields(sidereal_lon)`,九曜、计都、上升共用;`/api/chart` 缓存 `cache_schema_version` 4 → 5(缓存 TTL 900 s,改版本只影响查找键;指纹 / 绑定只用出生资料,不受影响)。前端只改注释。 - 验证:`tests/test_report_chart_blank_columns.py`(边界 0°、13°20′ 两侧、360°;golden 盘上升 Uttara Phalguni 1 足、九曜与 golden 逐星一致);`frontend/tests/chart-varga-table.test.tsx` 上升行断言;旧缓存无字段仍渲染「—」。golden `chart-view-golden.json` 用 `_nakshatra_fields` 重录(命令写在 `sourceCommand`)。 +- 2026-10-15 更正:上面的验证只打到引擎 `compute_chart_data`,前端 golden 的星宿三项是事后补上去的,没有经过 `/api/chart` 重建上升的那一层。接口仍把三项丢掉,星盘页继续显示「—」。见 BUG-1352。 - 防复发:同一个量(星宿)只有一个计算函数,新增盘点直接复用。 - 相关记录:BUG-1076(D1 表逐字节基线:本次放开上升三格,见进度记录三栏)。 - 复发自:无 @@ -34241,6 +34242,7 @@ - 根因:`compute_chart_data` 给九曜按 `nak_span` 算了 `nakshatra / nakshatra_pada / nakshatra_lord`,`result["ascendant"]` 漏了;前端按「引擎不给就不推」留空。 - 修复:抽 `_nakshatra_fields(sidereal_lon)`,九曜、计都、上升共用;`/api/chart` 缓存 `cache_schema_version` 4 → 5(缓存 TTL 900 s,改版本只影响查找键;指纹 / 绑定只用出生资料,不受影响)。前端只改注释。 - 验证:`tests/test_report_chart_blank_columns.py`(边界 0°、13°20′ 两侧、360°;golden 盘上升 Uttara Phalguni 1 足、九曜与 golden 逐星一致);`frontend/tests/chart-varga-table.test.tsx` 上升行断言;旧缓存无字段仍渲染「—」。golden `chart-view-golden.json` 用 `_nakshatra_fields` 重录(命令写在 `sourceCommand`)。 +- 2026-10-15 更正:上面的验证只打到引擎 `compute_chart_data`,前端 golden 的星宿三项是事后补上去的,没有经过 `/api/chart` 重建上升的那一层。接口仍把三项丢掉,星盘页继续显示「—」。见 BUG-1352。 - 防复发:同一个量(星宿)只有一个计算函数,新增盘点直接复用。 - 相关记录:BUG-1076(D1 表逐字节基线:本次放开上升三格,见进度记录三栏)。 - 复发自:无 @@ -36131,3 +36133,36 @@ - 相关记录:BUG-203(锁文件,不是这一条)、BUG-1063(人物表列级读取权)、BUG-1348。 - 复发自:无。 - 修复版本:分支 `codex/release-path-production-20261014`。编号接在 BUG-1349 之后。 + +## BUG-1352 | 星盘页上升行的星宿三项被排盘接口丢掉 + +- 状态:resolved +- 首次发现:2026-10-03 +- 最近更新:2026-10-15 +- 影响面:星盘页本命盘和月亮盘的上升行(星宿、宿主、pada) +- 用户现象:这两行的星宿、宿主、pada、状态都是「—」,像没算出来。九曜那几行是有的。 +- 触发条件:打开星盘页的本命盘或月亮盘。上升经度其实已经算好。 +- 根因:引擎给上升写了星宿三项,排盘接口在重组结果时只留了星座和度数,把这三项丢了。月亮盘用的是同一份上升。上次修复只测了引擎,前端样例又是事后补上这三项的,所以接口这一层一直没被发现。 +- 修复:接口重组上升时原样带上这三项;引擎没给时按同一套星宿算法补上。排盘缓存版本 6 改为 7,避免继续用没有这三项的旧结果。 +- 验证:虚构盘(1990-06-15 12:00 北京)走排盘处理函数,上升和 `modules.chart.ascendant` 的三项与星宿算法一致,为 Uttara Phalguni、1、Sun。本命表和月亮盘表都渲染出这三项。 +- 防复发:回归测试必须经过排盘处理函数,不能只测引擎,也不能手工往样例里补字段。 +- 相关记录:BUG-1201、BUG-1314 +- 复发自:BUG-1201(修复没有打通接口层) +- 修复版本:分支 `codex/chart-ascendant-row-20261015`。任务书写明 BUG-1351 已被另一单占用,本单从 BUG-1352 起。 + +## BUG-1353 | 星盘表居中列的表头靠左,当前大运卡片左边多一条线 + +- 状态:resolved +- 首次发现:2026-10-15 +- 最近更新:2026-10-15 +- 影响面:星盘页行星表的表头,以及大运列表里当前这一段 +- 用户现象:分盘「庙旺」的表头靠左、格子里的字居中,对不齐。当前大运卡片左边有一条竖线。 +- 触发条件:打开星盘页的分盘表或大运列表。 +- 根因:居中只加在内容格子上,表头没有。八列表的写法是冻结的,不能改标记。大运当前段用内阴影画了左侧强调线,设计说明里没写,也没有测试盯着。 +- 修复:表头与内容一起居中。本命和月亮盘的八列表不改标记,用列选择器把速度、宿主、pada 的表头居中,宫位仍靠左。分盘的庙旺和同位、行运的速度、Bhava 的换宫,表头加上与内容相同的居中标记。当前大运去掉左侧那条线,浅底和细边框留着。 +- 验证:样式合同锁定八列表对应表头居中、宫位列仍靠左,以及当前大运规则里没有 box-shadow。分盘、行运、Bhava 的渲染测试锁定表头带居中标记。 +- 防复发:上述样式合同和渲染测试。 +- 相关记录:BUG-1076、BUG-1352 +- 复发自:无 +- 修复版本:分支 `codex/chart-ascendant-row-20261015`。 + diff --git a/docs/tasks/PROGRESS-chart-ascendant-row-20261015.md b/docs/tasks/PROGRESS-chart-ascendant-row-20261015.md new file mode 100644 index 00000000..54bf0568 --- /dev/null +++ b/docs/tasks/PROGRESS-chart-ascendant-row-20261015.md @@ -0,0 +1,33 @@ +# PROGRESS · 星盘页上升行(issue #53 第二轮) + +- 分支:`codex/chart-ascendant-row-20261015` +- 基线:`origin/staging` `2b7112a6010acbcc0fec809b6b27e2375293dd5c` +- 编号:本文件最大号是 BUG-1350。任务书说 BUG-1351 已被 #65 的分支占用,本单用 BUG-1352(上升星宿被接口丢掉,复发自 BUG-1201)和 BUG-1353(表头错位与大运左边线)。 + +## 做了什么 + +- `/api/chart` 重组上升时带上 `nakshatra`、`nakshatra_pada`、`nakshatra_lord`。引擎已经有就原样带上;没有才按同一套星宿算法补。`cache_schema_version` 6 → 7。 +- 上升行状态从「—」改为「不适用」。本命、月亮盘共用这一行。 +- 八列表的标记没改。速度、宿主、pada 的表头用列选择器居中,宫位仍靠左。分盘的庙旺 / 同位、行运的速度、Bhava 的换宫,表头加上与内容相同的居中标记。 +- 当前大运去掉 `box-shadow`,浅底和细边框还在。 + +## 样例 + +`chart-view-golden.json` 里上升的三项本来就是 Uttara Phalguni、1、Sun,和这次接口真实返回一致,数值没有手改。`sourceCommand` 不再写「用 `_nakshatra_fields` 事后补上」,改为写明来自 `_compute_chart_sync`。 + +## 断言改动 + +| 位置 | 原值 | 新值 | 原因 | +| --- | --- | --- | --- | +| `chart-varga-table.test.tsx` 上升行状态格 | `—` | `不适用` | 产品把横杠看成没数据 | +| `test_api_scratch_retention.py` 缓存版本 | `6` | `7` | 旧缓存没有星宿三项,必须换键 | +| 基线 html | 仍是改前的整张表 | 不改文件 | 测试用替换得到新表,原值留在基线里 | + +## 自测 + +- `pytest tests/test_api_chart_ascendant_nakshatra.py tests/test_report_chart_blank_columns.py tests/test_api_server_growth_contract.py tests/test_api_scratch_retention.py -q`:34 通过,退出码 0。没有新增 `JyotishAPIHandler.__new__`。 +- 前端:`chart-varga-table`、`chart-moon-transit`、`chart-bhava`、`chart-page-mobile-width-20260929`、`runtime-sources-no-test-imports`。月亮盘那条第一次用整页去配「上升」,配到了图上的标记;改成只看月亮盘表格后通过。 +- `tsc --noEmit -p .`:0 个错误,退出码 0。 +- `npm run lint`:0 个错误、123 条原有警告,退出码 0。 +- `chart-page-view` 与 `chart-page-hook`:46 通过,退出码 0。 +- 真机清单沿用任务书里已有的 `docs/testing/chart-ascendant-row-20261015.md`。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 6a6c77eb..bd829596 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -213,7 +213,7 @@ | --- | --- | --- | --- | --- | | `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-chart-ascendant-row-20261015.md` | — | **星盘页上升行无星宿 / 表头错位 / 大运卡左边线(issue #53 第二轮)**:`/api/chart` 重建上升时丢了星宿三字段,BUG-1201 从未打通(golden 取自引擎内部);状态改「不适用」(推翻 DESIGN 原口径);居中列表头没居中;当前大运 inset 左线去掉。执行方 Grok | 待领取 | `codex/chart-ascendant-row-20261015` | +| `TASK-chart-ascendant-row-20261015.md` | `PROGRESS-chart-ascendant-row-20261015.md` | **星盘页上升行无星宿 / 表头错位 / 大运卡左边线(issue #53 第二轮)**:`/api/chart` 重建上升时丢了星宿三字段(BUG-1352,复发自 BUG-1201);状态改「不适用」;居中列表头对齐;当前大运去掉左边线(BUG-1353) | 待验收 | `codex/chart-ascendant-row-20261015` | | `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) | diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index ba81ed57..74d2c6f0 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -1178,7 +1178,7 @@ D40 / D45 / D60 的表格上方,出生时间不是「已校正」时多一句 | pada | 1–4,居中 | | 状态 | 尊贵 / 庙旺等,原样保留,只是挪到最后一列 | -第一行是上升。星宿 / 宿主 / pada 用引擎按上升经度算出的值,和九曜同一个算法(2026-10-03,BUG-1201;之前引擎漏算,这三格一直是「—」);旧缓存里没有这三项时仍写「—」,页面不自己推算。上升没有尊贵,状态格写「—」。数字列 `tabular-nums`,符号列与宿主列居中。 +第一行是上升。星宿 / 宿主 / pada 用引擎按上升经度算出的值,和九曜同一个算法(2026-10-03,BUG-1201;之前引擎漏算,这三格一直是「—」);旧缓存里没有这三项时仍写「—」,页面不自己推算。上升没有尊贵,状态格写「不适用」(2026-10-15,与分盘表、报告同一口径;不再写「—」)。数字列 `tabular-nums`,符号列与宿主列居中。 本轮不新增第二层页签:参照里「宫位 / 大运 / 星力 / 瑜伽」等页签的内容后续另立单。 @@ -1192,7 +1192,7 @@ D40 / D45 / D60 的表格上方,出生时间不是「已校正」时多一句 | Bhava | 行星 · 黄道经度 · 星座宫 · Bhava 宫 · 换宫;另一张「宫 · 宫头 · 起点(sandhi)」12 行 | Sripati。换宫的行用 `--color-selected` 浅底。拿不到真实 MC 时这一栏走失败句,不画等宫(BUG-1077) | | 行运 | 行星 · 今天的位置 · 速度 · 落本命 · 本命同星 | 表头写计算时刻「行运 · YYYY-MM-DD 12:00(时区)」,上方一句「以本命上升为第 1 宫,放的是这一天中午的行星位置。岁差和交点与本命相同。」 | -说明句 + 表格包在 `.chart-page-varga-table-block` 里,作为一个 grid 项在 ≥1024 时跨两列。宫位表头与对应数据格统一左对齐、相同 padding;包括本命/月亮、分盘、Bhava、行运。宿主、pada、速度、换宫等原本居中列不随之变化。本命/月亮八列 markup 冻结,通过限定八列表头的第四列 CSS 修正宫位,不能全局覆盖 `chart-page-planet-mid`。 +说明句 + 表格包在 `.chart-page-varga-table-block` 里,作为一个 grid 项在 ≥1024 时跨两列。宫位表头与对应数据格统一左对齐、相同 padding;包括本命/月亮、分盘、Bhava、行运。宿主、pada、速度、换宫等原本居中列不随之变化:表头与内容同为居中。本命/月亮八列 markup 冻结,速度、宿主、pada 的表头用列选择器居中,不改 markup,也不把宫位那一列居中。分盘的庙旺 / 同位、行运的速度、Bhava 的换宫,表头带 `chart-page-planet-mid`。不能全局覆盖 `chart-page-planet-mid` 去改宫位列。 ### 三套坐标系 @@ -1214,7 +1214,7 @@ D40 / D45 / D60 的表格上方,出生时间不是「已校正」时多一句 时间条只画时长大于 0 的段,宽度按时长。当前段用主文字色内描边,一条竖线标 `asOf`。两端是 `YYYY.MM`。0 年段不进这条轴。段色轮换现有的浅底 token(`--color-action-soft`、`--color-success-muted`、`--color-canvas-strong`、`--color-danger-muted`、`--color-selected`、`--color-canvas-muted`),不新造一套色。 -列表用原生 `
` / ``。只有当前段带 `open`。子运一行是「名称 · YYYY.MM – YYYY.MM」,日期 `tabular-nums` 靠右,当前子运用主文字色。`summary` 与子运行高 44px。未展开的子运列表用 `details:not([open]) > ul { display: none }` 收起。0 年段不是 `
`,文字用 `--color-ink-tertiary`,只写「0 年」和「本段时长为 0,不展开」。 +列表用原生 `
` / ``。只有当前段带 `open`。子运一行是「名称 · YYYY.MM – YYYY.MM」,日期 `tabular-nums` 靠右,当前子运用主文字色。`summary` 与子运行高 44px。未展开的子运列表用 `details:not([open]) > ul { display: none }` 收起。0 年段不是 `
`,文字用 `--color-ink-tertiary`,只写「0 年」和「本段时长为 0,不展开」。当前大运用浅底和细边框标出,不画左侧强调线。 ### 西洋盘 Tab(2026-09-25) diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 17afe73f..36fcfcb4 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -5655,11 +5655,18 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class .chart-page-planet-glyph.is-node, .ephemeris-transit-glyph.is-node { color: var(--color-planet-node); } .chart-page-planet-glyph.is-ascendant { color: var(--color-planet-ascendant); font-weight: 700; } +.chart-page-planet-table th.chart-page-planet-mid, .chart-page-planet-table td.chart-page-planet-mid { text-align: center; } -/* The natal/Moon eight-column markup stays frozen; only its house column changes. */ +/* The natal/Moon eight-column markup stays frozen; only its house column changes. + Speed, nakshatra lord and pada stay centered, including their headers. */ .chart-page-planet-table:has(> thead > tr > th:nth-child(8)) > tbody > tr > td:nth-child(4) { text-align: left; } +.chart-page-planet-table:has(> thead > tr > th:nth-child(8)) > thead > tr > th:nth-child(3), +.chart-page-planet-table:has(> thead > tr > th:nth-child(8)) > thead > tr > th:nth-child(6), +.chart-page-planet-table:has(> thead > tr > th:nth-child(8)) > thead > tr > th:nth-child(7) { + text-align: center; +} /* Figures line up column-wise instead of drifting with glyph width. */ .chart-page-planet-num { font-variant-numeric: tabular-nums; } .chart-page-boundary { @@ -5774,7 +5781,6 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class .chart-page-dasha-period.is-current { background: var(--color-selected); border-color: var(--color-border-strong); - box-shadow: inset 2px 0 0 var(--color-focus); } .chart-page-dasha-period.is-zero { color: var(--color-ink-tertiary); } .chart-page-dasha-period.is-zero span { margin-left: auto; } diff --git a/frontend/src/components/chart-page/chart-vedic-tab.tsx b/frontend/src/components/chart-page/chart-vedic-tab.tsx index 3a768db5..596bbdb9 100644 --- a/frontend/src/components/chart-page/chart-vedic-tab.tsx +++ b/frontend/src/components/chart-page/chart-vedic-tab.tsx @@ -79,7 +79,7 @@ function PlanetRow({ row }: { row: ChartViewPlanetRow }) { /** * The ascendant is a chart point: nakshatra, lord and pada come from the - * engine (BUG-1201); it has no dignity, so the status cell stays empty. + * engine (BUG-1201); it has no dignity, so the status cell says 不适用. * `house` is 1 except on the Moon chart, which counts from the Moon. */ function AscendantRow({ row, house = row.house }: { row: ChartViewAscendantRow; house?: number }) { @@ -96,7 +96,7 @@ function AscendantRow({ row, house = row.house }: { row: ChartViewAscendantRow; {row.nakshatraLord ? : "—"} {row.pada ?? "—"} - — + 不适用 ); } @@ -192,8 +192,8 @@ export function VargaTable({ varga }: { varga: ChartViewVarga }) { 分盘星座 宫位 本命星座 - {columns.supportsDignity && 庙旺} - {columns.supportsVargottama && 同位} + {columns.supportsDignity && 庙旺} + {columns.supportsVargottama && 同位} @@ -219,7 +219,7 @@ function TransitTable({ transit }: { transit: ChartViewTransitOk }) { 行星 今天的位置 - 速度 + 速度 落本命 本命同星 @@ -257,7 +257,7 @@ function BhavaTables({ bhava }: { bhava: ChartViewBhavaOk }) { 黄道经度 星座宫 Bhava 宫 - 换宫 + 换宫 diff --git a/frontend/tests/chart-bhava.test.tsx b/frontend/tests/chart-bhava.test.tsx index c90b870d..13cf1b05 100644 --- a/frontend/tests/chart-bhava.test.tsx +++ b/frontend/tests/chart-bhava.test.tsx @@ -90,6 +90,7 @@ test("Sripati rows, cusps and the drawn houses follow the engine's Bhava houses" assert.match(markup, /
交点<\/dt>/); assert.doesNotMatch(markup, /Swiss Ephemeris|>引擎Bhava · Sripati<\/caption>/); + assert.match(markup, /换宫<\/th>/); assert.match(markup, /宫头与宫界<\/caption>/); assert.equal((markup.match(/class="is-shifted"/g) ?? []).length, shifted.length); assert.equal((markup.match(/是<\/td>/g) ?? []).length, shifted.length); diff --git a/frontend/tests/chart-moon-transit.test.tsx b/frontend/tests/chart-moon-transit.test.tsx index 79237cb0..283b9014 100644 --- a/frontend/tests/chart-moon-transit.test.tsx +++ b/frontend/tests/chart-moon-transit.test.tsx @@ -87,6 +87,12 @@ test("the Moon chart is the D1 counted from the Moon's sign, projected without a const markup = renderToStaticMarkup(React.createElement(ChartVedicTab, { view, vargaId: "MOON", onSelectVarga: () => {} })); assert.match(markup, /月亮盘 · 以月亮所在星座([^)]+)为第 1 宫<\/caption>/); assert.deepEqual(ascendantMarkHouses(markup), [moon.ascendantHouse], "the lagna mark sits in the natal ascendant's house"); + const table = markup.slice(markup.indexOf("月亮盘")); + const ascendantRow = table.match(/[\s\S]*?上升[\s\S]*?<\/tr>/)?.[0] ?? ""; + assert.match(ascendantRow, /Uttara Phalguni<\/td>/); + assert.match(ascendantRow, /is-sun/); + assert.match(ascendantRow, />1不适用 { @@ -125,7 +131,8 @@ test("the transit layer posts today's noon with the natal ayanamsa and node mode const markup = renderToStaticMarkup(React.createElement(ChartVedicTab, { view, vargaId: "TRANSIT", onSelectVarga: () => {} })); assert.match(markup, /行运 · 2026-09-15 12:00(Asia\/Shanghai)<\/caption>/); - for (const column of ["行星", "今天的位置", "速度", "落本命", "本命同星"]) assert.match(markup, new RegExp(`${column}`)); + for (const column of ["行星", "今天的位置", "落本命", "本命同星"]) assert.match(markup, new RegExp(`${column}`)); + assert.match(markup, /速度<\/th>/); assert.ok(markup.includes(CHART_VIEW_COPY.transitNote)); assert.match(markup, /
/); assert.match(markup, /
岁差<\/dt>/); diff --git a/frontend/tests/chart-page-mobile-width-20260929.test.ts b/frontend/tests/chart-page-mobile-width-20260929.test.ts index 24b4742b..3ea5335d 100644 --- a/frontend/tests/chart-page-mobile-width-20260929.test.ts +++ b/frontend/tests/chart-page-mobile-width-20260929.test.ts @@ -48,7 +48,21 @@ test("the Vedic waiting slot reserves the result legend band without changing th test("only the house column in frozen natal and Moon tables overrides centering", () => { assert.match(cssDeclarations(".chart-page-planet-table td.chart-page-planet-mid", globalStyles), /text-align:\s*center/); + assert.match(cssDeclarations(".chart-page-planet-table th.chart-page-planet-mid", globalStyles), /text-align:\s*center/); assert.match(cssDeclarations(".chart-page-planet-table:has(> thead > tr > th:nth-child(8)) > tbody > tr > td:nth-child(4)", globalStyles), /text-align:\s*left/); + for (const child of [3, 6, 7]) { + assert.match( + cssDeclarations(`.chart-page-planet-table:has(> thead > tr > th:nth-child(8)) > thead > tr > th:nth-child(${child})`, globalStyles), + /text-align:\s*center/, + ); + } +}); + +test("the current dasha card keeps its tint and drops the left accent line", () => { + const current = cssDeclarations(".chart-page-dasha-period.is-current", globalStyles); + assert.match(current, /background:\s*var\(--color-selected\)/); + assert.match(current, /border-color:\s*var\(--color-border-strong\)/); + assert.doesNotMatch(current, /box-shadow/); }); test("wide chart-page content still scrolls inside its own box instead of widening the page", () => { diff --git a/frontend/tests/chart-varga-table.test.tsx b/frontend/tests/chart-varga-table.test.tsx index 12451038..cefabcf5 100644 --- a/frontend/tests/chart-varga-table.test.tsx +++ b/frontend/tests/chart-varga-table.test.tsx @@ -99,8 +99,11 @@ test("the D1 table is byte-for-byte the table the page drew before BUG-1076, asc // (Leo 29°23′ = Uttara Phalguni pada 1, lord Sun), where the baseline had dashes. const baseline = readFileSync(new URL("./fixtures/chart-d1-table-baseline.html", import.meta.url), "utf8"); const dashes = '————'; + // 原值:上升行状态格 — + // 新值:不适用 + // 原因:产品把「—」看成没数据。上升没有尊贵,与分盘表、报告同一口径写「不适用」。基线文件仍是改前的表。 const filled = 'Uttara Phalguni☉' - + '1—'; + + '1不适用'; assert.equal(baseline.split(dashes).length, 2, "the baseline's ascendant row is the only all-dash row"); assert.equal(table, baseline.replace(dashes, filled)); }); @@ -113,6 +116,8 @@ test("the D1 ascendant row shows the engine's nakshatra, lord and pada", async ( const ascendant = row(render(view, "D1"), "上升"); assert.match(ascendant, /Uttara Phalguni<\/td>/); assert.match(ascendant, /is-sun/, "Uttara Phalguni's lord is the Sun"); + assert.match(ascendant, />1不适用)[key]; @@ -136,6 +141,8 @@ test("divisional rows carry engine dignity and vargottama, and drop nakshatra co const markup = render(view, "D9"); const header = markup.match(/([\s\S]*?)<\/thead>/)?.[1] ?? ""; for (const column of ["行星", "分盘星座", "宫位", "本命星座", "庙旺", "同位"]) assert.match(header, new RegExp(`>${column}<`)); + assert.match(header, /庙旺<\/th>/); + assert.match(header, /同位<\/th>/); assert.doesNotMatch(header, /星宿|宿主|pada/); // Same wording as the D1 status column (BUG-1202), friendship levels included. // 原值:同位行星单元格是「同位」,非同位是「—」 diff --git a/frontend/tests/fixtures/chart-view-golden.json b/frontend/tests/fixtures/chart-view-golden.json index 8d47e425..a1dfc734 100644 --- a/frontend/tests/fixtures/chart-view-golden.json +++ b/frontend/tests/fixtures/chart-view-golden.json @@ -1,5 +1,5 @@ { - "sourceCommand": "anaconda python: domain_calculation_service.compute_chart + compute_vimshottari_timeline; divisional_charts_extended.DivisionalChartsCalculator; jaimini.calc_chara_dasha_with_antardasha (pratyantar_dashas stripped; unused by the page); western_chart_engine.build_tropical_natal_chart. Fictional 1990-06-15 12:00 Beijing 39.9042N 116.4074E tz+8 Raman mean node. D40/D45/D60 appended 2026-09-28 from the same chart.planets[*].lon and ascendant.lon via DivisionalChartsCalculator._calculate_single_varga (recompute of D9/D10/D30 from those inputs equals the stored ones). 2026-10-03 (BUG-1201/1202): chart.ascendant nakshatra fields from jyotish_engine._nakshatra_fields(ascendant.lon) and varga_full.result[*].planets[*].dignity_level from scripts/varga_dignity.annotate_varga_dignity on the stored positions (the same calls /api/chart and /api/varga_full now make).", + "sourceCommand": "anaconda python: domain_calculation_service.compute_chart + compute_vimshottari_timeline; divisional_charts_extended.DivisionalChartsCalculator; jaimini.calc_chara_dasha_with_antardasha (pratyantar_dashas stripped; unused by the page); western_chart_engine.build_tropical_natal_chart. Fictional 1990-06-15 12:00 Beijing 39.9042N 116.4074E tz+8 Raman mean node. D40/D45/D60 appended 2026-09-28 from the same chart.planets[*].lon and ascendant.lon via DivisionalChartsCalculator._calculate_single_varga (recompute of D9/D10/D30 from those inputs equals the stored ones). 2026-10-03 (BUG-1202): varga_full.result[*].planets[*].dignity_level from scripts/varga_dignity.annotate_varga_dignity on the stored positions. 2026-10-15 (BUG-1352): chart.ascendant nakshatra, nakshatra_pada and nakshatra_lord are the fields returned by JyotishAPIHandler._compute_chart_sync for this birth (tests/test_api_chart_ascendant_nakshatra.py); they are not filled in by hand.", "chart": { "success": true, "birth": { diff --git a/scripts/jyotish_api_server.py b/scripts/jyotish_api_server.py index ca07c6cd..432467e5 100644 --- a/scripts/jyotish_api_server.py +++ b/scripts/jyotish_api_server.py @@ -2589,12 +2589,24 @@ def _vedastro_runtime_fingerprint() -> dict: } +def _chart_ascendant_nakshatra(ascendant_data: dict, lon: float) -> dict: + """Keep the engine's nakshatra fields when /api/chart rebuilds the ascendant.""" + fields = { + key: ascendant_data[key] + for key in ('nakshatra', 'nakshatra_pada', 'nakshatra_lord') + if ascendant_data.get(key) not in (None, '') + } + if 'nakshatra' in fields: + return fields + return _load_local_module('jyotish_engine')._nakshatra_fields(lon) + + def _build_api_chart_cache_payload(body: dict) -> dict: import api_scratch_retention as retention today = body.get('today') or body.get('current_date') return { - 'cache_schema_version': 6, + 'cache_schema_version': 7, 'birth': retention.normalize_chart_cache_birth(body), 'calculation': { 'ayanamsa': _request_ayanamsa(body), @@ -6663,6 +6675,7 @@ class JyotishAPIHandler(BaseHTTPRequestHandler, VedastroEvidenceMixin, SynastryM 'degree': round(asc_lon % 30, 2), 'degree_in_sign': round(asc_lon % 30, 2), 'lon': round(asc_lon, 4), + **_chart_ascendant_nakshatra(ascendant_data, asc_lon), }, 'planets': planets_data, 'houses': houses, 'shadbala': shadbala_summary, 'dasha': { diff --git a/tests/test_api_chart_ascendant_nakshatra.py b/tests/test_api_chart_ascendant_nakshatra.py new file mode 100644 index 00000000..8232dc87 --- /dev/null +++ b/tests/test_api_chart_ascendant_nakshatra.py @@ -0,0 +1,56 @@ +"""The chart API must keep the ascendant nakshatra the engine already computed.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / "scripts" +TESTS = ROOT / "tests" +for path in (SCRIPTS, TESTS): + if str(path) not in sys.path: + sys.path.insert(0, str(path)) + +import jyotish_api_server # noqa: E402 +from test_api_server_security import _handler # noqa: E402 + +BEIJING = { + "year": 1990, + "month": 6, + "day": 15, + "hour": 12, + "minute": 0, + "second": 0, + "lat": 39.9042, + "lon": 116.4074, + "tz": 8, + "ayanamsa": "raman", + "node_mode": "mean", + "transit_date": "2026-10-15", + "skip_vedastro_main_entry_overview": True, +} + + +def test_api_chart_keeps_ascendant_nakshatra_from_the_engine(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("JYOTISH_API_CHART_CACHE_TTL_SECONDS", "0") + monkeypatch.setenv("VEDASTRO_ENABLE_NETWORK", "0") + monkeypatch.setattr( + jyotish_api_server, + "_attach_vedastro_main_entry_overview", + lambda result, _birth: result, + ) + engine = jyotish_api_server._load_local_module("jyotish_engine") + result = _handler()._compute_chart_sync(BEIJING) + lon = result["ascendant"]["lon"] + expected = engine._nakshatra_fields(lon) + + for packet in (result["ascendant"], result["modules"]["chart"]["ascendant"]): + assert packet["nakshatra"] == expected["nakshatra"] + assert packet["nakshatra_pada"] == expected["nakshatra_pada"] + assert packet["nakshatra_lord"] == expected["nakshatra_lord"] + assert result["ascendant"]["nakshatra"] == "Uttara Phalguni" + assert result["ascendant"]["nakshatra_pada"] == 1 + assert result["ascendant"]["nakshatra_lord"] == "Sun" diff --git a/tests/test_api_scratch_retention.py b/tests/test_api_scratch_retention.py index 65dab310..9c401d36 100644 --- a/tests/test_api_scratch_retention.py +++ b/tests/test_api_scratch_retention.py @@ -139,7 +139,10 @@ def test_numeric_birth_spellings_share_one_chart_cache_key(): assert _chart_key(dict(base, hour=12, tz="Asia/Kolkata")) not in keys skipped = _chart_key(dict(base, hour=12, skip_vedastro_main_entry_overview=True)) assert skipped not in keys - assert api._build_api_chart_cache_payload(base)["cache_schema_version"] == 6 + # 原值:cache_schema_version == 6 + # 新值:7 + # 原因:/api/chart 的上升补上星宿三项,旧缓存里没有这三项,必须换键。 + assert api._build_api_chart_cache_payload(base)["cache_schema_version"] == 7 def test_audit_artifact_default_off_and_compact_when_on(monkeypatch, tmp_path):