Compare commits

...
Author SHA1 Message Date
jesse-uxandGrok (agent loop) fc2283693d fix(chart): span natal and moon planet tables across the row
VedicLayout now owns the third grid item, so a wrapper around the table cannot drop the natal or Moon table into the left column. Desktop grid-column targets that row. BUG-1421.

Self-test: tsc exit 0, lint 0 errors, 49 targeted tests, next build with / static.

Co-Authored-By: Grok (agent loop)
2026-10-23 01:22:46 +08:00
Jesse_ChenandClaude Opus 5.5 d72adb5754 docs(tasks): rectification explicit open brief (agent loop, issue #103)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
2026-10-23 00:30:54 +08:00
Jesse_ChenandClaude Opus 5.5 dd5ae8d032 docs(tasks): chart table span brief (agent loop, issue #102)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
2026-10-23 00:26:37 +08:00
Jesse_ChenandClaude Opus 5.5 9049b5e997 docs(tasks): ui kit prototype v5 confirmed by product
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
2026-10-23 00:23:31 +08:00
Jesse_ChenandClaude Opus 5.5 052a4bf70c docs(tasks): ui kit — synastry start button shows cost in a confirm dialog
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
2026-10-23 00:22:22 +08:00
Jesse_ChenandClaude Opus 5.5 23f1b31787 docs(tasks): ui kit — segmented Tabs for chart-type, language and theme switches
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
2026-10-23 00:17:30 +08:00
Jesse_ChenandClaude Opus 5.5 01ec3d229b docs: ui kit migration brief (shadcn/lucide/motion/resizable/dnd-kit/zustand); component-library rule in AGENTS
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
2026-10-23 00:15:44 +08:00
Jesse_ChenandClaude Opus 5.5 3632b0eabd docs(agents): fix bugs at the cause; cut redundant UI copy but keep accessible names
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
2026-10-23 00:11:56 +08:00
15 changed files with 653 additions and 10 deletions
+7 -1
View File
@@ -92,6 +92,8 @@
这份文件是产品与代码 Bug 的长期历史入口;`docs/research/pre_work_error_ledger.md` 负责基础设施、外部引擎、碎片目录与预检风险,两者不得互相替代。
**修在根因上(2026-10-23 产品定)**:Fix a bug at its cause, reasoning from first principles and established best practice. Do not patch over the symptom.
1. 修复前必须用报错原文、接口路径、状态码、模块名和用户操作搜索历史记录,优先检查相同模块的根因与防复发措施。
2. 若历史问题复发,必须关联原 `BUG-NNN`,说明旧测试、约束或发布门禁为何未拦住;不得把复发伪装成无关的新问题。
3. Bug 修复必须在同一变更中更新 `docs/BUG_HISTORY.md`:补充现有记录或新增连续编号,并记录状态、现象、触发条件、根因、修复、验证、防复发、关联记录和修复版本。
@@ -110,13 +112,17 @@
## 7. 前端红线
1. `./node_modules/.bin/tsc --noEmit` 通过;`npm run lint` **0 error**(react-hooks 编译器规则会拦 effect 内同步 setState、render 写 ref 等)。
2. `next build` 后 `/` 保持 `○ Static`;首屏 gzip 变化在 ±2% 内,超出要在进度记录里给出原因。
2. `next build` 后 `/` 保持 `○ Static`;首屏 gzip 变化在 ±2% 内,超出要在进度记录里给出原因。动效、拖拽、可拖宽布局这类库(Motion、dnd-kit、react-resizable-panels)一律按需加载,不进首屏;±2% 不因引入它们放宽(2026-10-23 产品定)。
3. 测试总数不得低于开工时 `origin/staging` 的实测;改任何既有断言必须写"原值 / 新值 / 原因"三栏说明,不得静默弱化。
4. 合同测试的 fixture 必须来自真实引擎响应(golden),不得手造形状。
5. 改 UI 的提交同时更新 `frontend/DESIGN.md`;新文案对照 `frontend/docs/VOICE.md`。
6. 不改数据库结构的轮次不得顺带动迁移;动表的轮次必须真跑 `npm run test:db`。staging 迁移在部署之前自动应用,因此必须对当前已部署的那一版代码向后兼容:加列、加表、加索引、加触发器、加函数可以同轮;删列、删表、重命名、收紧 `NOT NULL` / `CHECK`、改类型必须拆成两轮——先加新的并部署代码,再删旧的。自托管 PostgreSQL 兼容层的 stub 测不到真实查询语法,新用到的查询写法必须 `test:db` 真查或部署后 smoke(BUG-990:未实现的 `not(col,"is",null)` 让归档列表 500 了两周)。
7. 不得顺手升级依赖、不得顺手修不在任务书里的 warning;发现了写进 `BLOCKED.md` 或进度记录。
8. 移动、删除或改写组件与语句前,先在 `frontend/tests/` 和 `tests/` 里 grep 该文件路径、要删的类名与语句,找出按源码文本断言的合同测试,同一轮跟着改(按第 3 条写三栏)。漏改会让门禁红而业务代码是对的,已发生多次:BUG-933 / 934 / 939 / 992 / 1014。
9. Avoid unnecessary secondary labels and descriptions. Let the layout do the explaining.(2026-10-23 产品定)
10. Avoid unnecessary supporting text, descriptive copy, and redundant UI microcopy.(2026-10-23 产品定)
11. 删掉重复解释布局的文案,但保留必要的表单标签和控件名称。比如搜索框旁边已有「搜索」按钮,视觉上可以省掉重复标签,代码里的可访问名称却不能一起删。界面清爽和读屏能用不冲突。(2026-10-23 产品定)
12. **已有依赖能正确表达的组件和行为,严禁自行设计实现**(2026-10-23 产品定)。指定依赖:组件用 shadcn/ui(`base-nova`,见 `frontend/components.json`,缺的组件用 shadcn CLI 加,不手抄改写);图标只用 Lucide;动效只用 Motion;可拖宽 / 可收起的分栏用 react-resizable-panels(经 shadcn `Resizable`);拖拽排序用 dnd-kit;跨组件状态用 Zustand。GSAP 不再使用。新增手写的按钮、弹窗、菜单、下拉、页签、抽屉、提示框由 lint 拦截;迁移完成前存量文件列在白名单里,白名单只许减少。占星专有的画法(星盘、星图、报告排版)不在此列。
## 8. 隐私与安全
+5
View File
@@ -1,5 +1,10 @@
# 印度占星 Skill 更新日志
## 2026-10-23 — 星盘页本命和月亮的行星表重新占满整行
- 宽屏下,本命和月亮的行星表回到参数卡旁边那一整行,宿主、pada、状态不用横拖就能看见。分盘、Bhava、行运仍是整行。
- Skill 版本不变(BUG-1421)。
## 2026-10-22 — 星盘大运右栏可以切到 Yogini
- 大运页左栏仍是 Vimshottari。右栏默认 Chara,可以切到 Yogini。点开当前大运能看到小运。Yogini 的小运下面写「小运未经外部软件核对。」
+16
View File
@@ -19253,3 +19253,19 @@
- 相关记录:无
- 复发自:无
- 修复版本:本提交
## BUG-1421 | 星盘页本命和月亮的行星表被挤进左栏
- 状态:resolved
- 首次发现:2026-10-22
- 最近更新:2026-10-23
- 影响面:星盘页桌面宽度下的本命表和月亮盘表
- 用户现象:宽屏下选「本命」或「月亮」时,行星表只占左栏。宿主、pada、状态三列要横拖才看得到,右栏在参数卡下面空着。分盘、Bhava、行运的表正常。
- 触发条件:桌面宽度(≥1024px)打开星盘页的本命或月亮。来自 #87 给这两张表包了一层符号说明。
- 根因:跨栏按类名而非结构。桌面规则只认表格外层的两个类名。本命和月亮的直接子项变成了符号说明那一层,规则匹配不上,表就排进左栏。
- 修复:网格自己用固定的整行容器装表格,跨栏只认这个容器。表格内容、列数、列宽不变。
- 验证:`frontend/tests/chart-table-span-20261023.test.tsx`。五种盘型的网格都是三个直接子项,第三个是整行容器;桌面样式只让这个容器跨栏。
- 防复发:上面两条断言。再往表外包一层,结构断言会失败;跨栏不再跟着表格类名走。
- 相关记录:BUG-1083(窄屏仍只能在表自己的盒子里横滚)。issue #102。回归来源 #87。
- 复发自:无
- 修复版本:`codex/chart-table-span-20261023`
@@ -0,0 +1,30 @@
# PROGRESS · 星盘页行星表整行(2026-10-23)
Issue #102。BUG-1421。
## 基线
- 开工时 `origin/staging` = `d72adb5754eab4d66ac3a47c4edcb12704e29815`(任务书写的 `9049b5e99` 已过期)。
- 本文件最大编号是 BUG-1420,本单用 BUG-1421。
## 做了什么
- 星盘页网格的第三格改成固定的整行容器。本命、月亮、分盘、Bhava、行运的表都放在里面。
- 桌面跨栏只认这个容器。表格自己的类名不再负责跨栏。
- 容器可以缩到比内容窄,窄屏时表仍在自己的框里横滑。
- 表格的字、列、列宽没改。符号说明的气泡还在原来的位置。
## 自测
| 检查 | 命令 | 结果 |
| --- | --- | --- |
| 类型 | `frontend\node_modules\.bin\tsc --noEmit -p frontend` | 退出码 0 |
| lint | `frontend` 下 `npm run lint` | 退出码 0。0 error,122 条既有 warning,本轮不改 |
| 定向测试 | `node node_modules/tsx/dist/cli.mjs --test` 跑新结构测试,加上 `chart-page-view.test.tsx`、`chart-varga-table.test.tsx`、`chart-page-mobile-width-20260929.test.ts` | 49 条通过,0 失败 |
| 构建 | `frontend` 下 `npm run build` | 退出码 0。路由表 `/` 为 `○ Static`。预渲染 `index.html` 引用 33 个 JS、3 个 CSS,gzip-9 合计 646,234 字节(JS 575,154,CSS 71,080)。没有用改动前的代码再构建一次,所以不写增减百分比 |
未跑前端全量 `npm test`(验收时跑)。没有登录,本机浏览器截图没做,留给 `docs/testing/chart-table-span-20261023.md`。
## 偏离
- 无。编号说明:本分支的 BUG-1421 与尚未合并的 #95 分支各自占用了同一个号。若 #95 先进入 staging,验收时需要给本条改号。
+4 -1
View File
@@ -37,8 +37,11 @@
| 任务书 | 进度 | 主题 | 状态 | 落点 |
| --- | --- | --- | --- | --- |
| `TASK-rectification-explicit-open-20261023.md` | — | **明说要校正只回一句话(issue #103)**:找会话直接读一张对服务角色撤权的表、必然失败被吞(真正进行中的也续不上);`candidate_accepted` 被算作进行中挡住入口和提议卡;兜底只给一句话。产品选 C;只改入口口径、经 RPC 找会话、删兜底,不改共享状态集合、不动表 | 待执行(循环) | 分支 `codex/rectification-explicit-open-20261023` |
| `TASK-chart-table-span-20261023.md` | `PROGRESS-chart-table-span-20261023.md` | **星盘页本命 / 月亮行星表挤进左栏(issue #102)**:#87 给表包了 `ChartSymbolHelp` 后跨栏规则失配;根因修法是 `VedicLayout` 用固定整行容器承载表格,跨栏只认容器;加结构断言防复发。BUG-1421 | 待验收 | 分支 `codex/chart-table-span-20261023` |
| `TASK-ui-kit-migration-20261023.md` | — | **界面统一换成组件库**:shadcn base-nova(补 `shadcn/tailwind.css`)· Lucide · Motion · react-resizable-panels · dnd-kit · Zustand;lint 拦手写控件(白名单只减不增);对话旁星盘面板;会话、档案拖动排序(动表);长相不变。产品 2026-10-23 拍板 D1~D8;8 个 issue(K0~K7)串行,取代全站动效 #98 #100 #101 | 待领取(循环) | 原型 https://claude.ai/artifact/PGnWsjHhu38KJseEfEMNWG |
| `TASK-onboarding-journey-20261022.md` | — | **新用户启程**:建档加「先看谁的盘」(自己 / 帮别人看,本人资料可后补)、人物关系字段、首问免费(服务端原子)、起步三问、启程清单(七项从数据推导,收费项标价,老用户可见)、八条一次性提示(含星盘四页签)、启程流程专用动效。产品 2026-10-22 拍板 D1~D6;动表经产品同意进循环,拆四个 issue 串行。BUG-1420 起 | 待领取(循环) | 原型 https://claude.ai/artifact/PcTwMVb9RD611R3qStxv3U |
| `TASK-site-motion-20261022.md` | — | **全站动效**:换页按侧栏方向滑入、选中块跟随、页签、盘型切换星体移宫、换人、翻日期、列表补位、弹窗从触发处长出、抽屉、顶部提示、点数滚动、中英交叉淡入;重写 DESIGN §6(推翻 180 ms 上限与 Spatial Instant),不引动画库,先验证 View Transitions。产品 2026-10-22 拍板;三个 issue,排在启程(三)之后、启程(四)之前 | 待领取(循环) | 原型 https://claude.ai/artifact/7u9goivnmTNSGTNWNeVMQd |
| `TASK-site-motion-20261022.md` | — | **全站动效**:换页按侧栏方向滑入、选中块跟随、页签、盘型切换星体移宫、换人、翻日期、列表补位、弹窗从触发处长出、抽屉、顶部提示、点数滚动、中英交叉淡入;重写 DESIGN §6(推翻 180 ms 上限与 Spatial Instant),不引动画库,先验证 View Transitions。产品 2026-10-22 拍板;三个 issue,排在启程(三)之后、启程(四)之前;**2026-10-23 被 ui-kit-migration 取代** | 已取代 | 原型 https://claude.ai/artifact/7u9goivnmTNSGTNWNeVMQd |
| `TASK-rectification-narrower-delivery-20261016.md` | `PROGRESS-rectification-narrower-delivery-fix-20261017.md` | **交付区间收窄**:显示改按活着的簇分段。第 1 次打回只补两处显示漏段(取整差 8 分、缺区间整段退回)。对半出题和门槛 8 改 6 仍不做。基线与对照各 924 条,12 格包含率都过红线。BUG-1367 起,本轮原记 BUG-1370,因与婚恋合盘保留号冲突改为 BUG-1392 | 待验收 | `codex/rectification-narrower-delivery-20261016` |
| —(子代理直接执行,无任务书) | `PROGRESS-bug-1060-d9-double-transit-20260927.md` | **双重过运 D9 目标错配(BUG-1060)**:D9 第 N 宫目标误用 D9 上升、D9 宫主目标误用 D1 度数,改为标签所写的 D9 星座;D1 / CL 层逐字节不变;数据卡 golden 重采集(一张盘跨层结论 9→10);另记 BUG-1061 跨层数字比对(未修) | 已验收(待部署) | `codex/bug-1060-d9-double-transit-20260927`(未推送) |
| —(子代理直接执行,无任务书;产品 2026-09-27 授权) | `PROGRESS-bug-1061-cross-layer-pairing-20260927.md` | **双重过运跨层配对按名字数字比对(BUG-1061)**:改为结构化身份配对(D1 第 N 宫↔D9 第 N 宫;D1 第 N 宫主↔同一颗星的 D9 目标);各层结果与 D1/CL golden 逐字节不变;48 样本跨层条目 31→38;数据卡 golden 重采集(乔布斯 6→9、泰勒 9→10) | 已验收(待部署) | `codex/bug-1061-cross-layer-pairing-20260927`(未推送) |
@@ -0,0 +1,80 @@
# TASK · 星盘页本命 / 月亮盘的行星表被挤进左栏(issue #102,2026-10-23)
## 基线
- `origin/staging`:写本单时为 `9049b5e99`。开工时以当时的 `origin/staging` 为准。
- 执行方:Grok(Windows + Docker)。分支 `codex/chart-table-span-20261023`,只推这一个分支。
- BUG 编号:开工时运行 `grep -oE "BUG-[0-9]+" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1`,从下一个号起(写本单时最大号是 BUG-1420)。
## 事故实证
- **现象**:桌面宽度(≥1024px)下,星盘页选「本命」或「月亮」时,行星表只占左栏,和盘一样宽(约 466px)。宿主、pada、状态三列要横向拖动才看得到;右栏在参数卡下面空着。D2–D60、Bhava、行运的表都正常。
- **布局规则**:`frontend/src/app/globals.css` 的桌面媒体段(`@media (min-width: 1024px)`,符号 `.chart-page-planet-table-wrap, .chart-page-varga-table-block { grid-column: 1 / -1; }`)把这两个类名排成跨两栏的整行。
- **网格结构**:`frontend/src/components/chart-page/chart-vedic-tab.tsx` 的 `VedicLayout` 网格下有三类直接子项:`.chart-page-vedic-stage`(盘)、`.chart-page-params`(参数卡)、`{children}`(表)。
- **出问题的地方**:同文件 `NatalTable` 在 #87(`d2edf8b7a`,「上升的状态格改成「/」」)里被外包了一层 `ChartSymbolHelp`,渲染出来是 `<div class="chart-page-symbol-help">`。网格的直接子项变成了这个 div,上面的跨栏规则匹配不上,表就被自动排进了左栏第二行。
- **为什么分盘没事**:`ChartTypeBody` 里分盘外面多了一层 `.chart-page-varga-table-block`,仍然命中跨栏规则。本命、月亮两处直接放 `NatalTable`,所以出事。
- **违反的设计**:`frontend/DESIGN.md` 写着「桌面(≥1024)盘面与参数表左右并排,行星表是跨两列的整行——它的列宽需要完整版心」。
- **为什么没拦住**:#87 的进度记录(`docs/tasks/PROGRESS-chart-ascendant-slash-20261021.md`)写明没跑全量 `npm test`、没跑 `next build`,也没登录后看页面;而且现有测试没有一条断言 `grid-column`。
## 根因
跨栏规则是按「表格外层恰好是某个类名」写的,网格自己并不掌握「第三行是整行」这件事。所以任何人往表外面再包一层,布局就会悄悄坏掉。这一次是 #87 包了一层 `ChartSymbolHelp`,下一次可能是别的。
## 决策记录
- 产品 2026-10-22 把本问题提成 issue #102,2026-10-23 要求先规划。
- 按 AGENTS.md §5「修在根因上」,**不采用**「在跨栏规则里再补一个 `.chart-page-symbol-help` 类名」这种对症补丁。改为让网格自己掌握结构:`VedicLayout` 用一个固定的整行容器装 `children`,跨栏规则只认这个容器。
- 不改表格的列数、列宽,不动 D1 的字节冻结。
## 硬红线
1. 不改表格内容、列、列宽,也不改 `ChartSymbolHelp` 的行为(「/」的说明气泡必须照常弹出,不能被裁掉)。
2. 手机端不得回归 BUG-1083:390px 宽时页面 `scrollWidth` 必须等于 390,表格只能在自己的盒子里横滚。
3. `chart-varga-table.test.tsx` 的 D1 字节冻结、`chart-page-mobile-width-20260929.test.ts` 的现有断言都要照常通过。若必须改,按「原值 / 新值 / 原因」三栏写。
4. 本单只修布局。组件库迁移 K3(#107)会整体改造这一页,本单不要提前换组件。
## 任务
### T1 网格自己掌握整行
- `VedicLayout` 里把 `{children}` 包进一个固定容器,比如 `<div className="chart-page-vedic-tables">`。这个容器永远是网格的第三个直接子项。
- `globals.css` 桌面段的跨栏规则改为针对这个容器(`grid-column: 1 / -1`)。原来那两个类名如果别处不再需要跨栏,就从规则里删掉;删之前先按 AGENTS.md §7 第 8 条 grep 源码文本合同测试。
- 容器加 `min-width: 0`,保证窄屏时表格只在自己的盒子里横滚。
- `ChartTypeBody` 里分盘、Bhava、行运的 `.chart-page-varga-table-block` 保持不变。它们现在落在新容器里面,不再负责跨栏。
**验收**:在 1024px、1280px 宽下,「本命」「月亮」「D9」「Bhava」「行运」五种盘型的表都占满整行,宿主、pada、状态三列不用横拖就看得到。
### T2 回归测试
- 新增测试:分别渲染 `ChartVedicTab` 的 D1、MOON、D9、BHAVA、TRANSIT,断言 `.chart-page-vedic-layout` 恰好有三个直接子项,并且第三个是 `.chart-page-vedic-tables`。
- 在同一个测试文件里,断言 `globals.css` 的桌面媒体段包含 `.chart-page-vedic-tables { grid-column: 1 / -1; }`(或等价写法)。
- 以后谁再往表外包东西,这两条断言就会失败。
### T3 文档
- `docs/BUG_HISTORY.md` 新增一条:回归来源 #87,根因写「跨栏按类名而非结构」,防复发写上面 T2 的两条断言。
- `frontend/DESIGN.md` 星盘页一节补一句:「表格区由 `VedicLayout` 的整行容器承载,不靠表格自身类名跨栏。」
- `CHANGELOG.md` 写一行。
## 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/chart-table-span-20261023 .worktrees/chart-table-span-20261023 origin/staging
cd .worktrees/chart-table-span-20261023 && git status -sb
cd frontend && npm ci && ./node_modules/.bin/tsc --noEmit && npm run lint
./node_modules/.bin/tsx --test tests/chart-page-view.test.tsx tests/chart-varga-table.test.tsx tests/chart-page-mobile-width-20260929.test.ts # 记下基线
```
## 验收分工
- **Grok 自跑**:
- `tsc --noEmit`;
- `npm run lint`;
- 上面三个测试文件加新测试;
- `npm run build`,记录 `/` 是否 Static 和首屏 gzip;
- 在本机浏览器 1280px 和 390px 宽下各截一张本命盘页面的图,附在回报里。
- **Claude 验收时跑**:
- 前端全量 `npm test`,和基线逐条对比(#87 当时就是漏了这一步);
- 复核截图。
- **真机清单**:`docs/testing/chart-table-span-20261023.md`,写两步:宽屏看本命和月亮的表是不是整行;手机上页面不横向滚动。
@@ -1,5 +1,7 @@
# TASK · 新用户启程:先看谁的盘、首问免费、起步三问、启程清单、一次性提示与动效(2026-10-22)
> **2026-10-23 更新:** 红线 8 和 T7 里的「用 CSS 和 Web Animations API、复用 `lib/motion/` 底座」改为「**用 Motion**,复用 `TASK-ui-kit-migration-20261023.md` K0 做好的 Motion 预设」。界面控件一律用 shadcn 组件(AGENTS.md §7 第 12 条)。文案按 AGENTS.md §7 第 9~11 条删减,可访问名称要保留。顺序也跟着调整:#96 等迁移单 K0 合入后再开工;#99 改为依赖 K0,不再依赖 #98。
设计原型(可点,含两条路径与全部动效):https://claude.ai/artifact/PcTwMVb9RD611R3qStxv3U 。原型与本任务书不一致时,以本任务书为准。原型里的名字、日期、星位都是虚构示例。
## 基线
@@ -0,0 +1,134 @@
# TASK · 说「帮我做生时纠正」只回一句「可在历史对话里接着做」(issue #103,2026-10-23)
## 基线
- 写本单时 `origin/staging` 为 `dd5ae8d03`。开工时以当时的 `origin/staging` 为准。
- 执行方:Grok(Windows + Docker),分支 `codex/rectification-explicit-open-20261023`,只推这个分支。
- BUG 编号:开工时用 `grep -oE "BUG-[0-9]+" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1` 核对,从下一个号起。写本单时的最大号是 BUG-1420。
- **本单不需要动表**(理由见决策记录 D3)。
## 事故实证
**现象**:产品负责人在 staging 对话里说「请你帮我做一下生时纠正」,只收到一句「可在历史对话里接着做。」,没有卡片、链接或按钮,校正进不去。该账户的出生时间显示「已采用」。
**链路**(文件、行号以开工时为准,按符号定位):
1. **识别**:模型调用 `offer-birth-time-rectification` 并传 `explicitRequest: true`(`frontend/src/mastra/consultation-tools.ts`)。
2. **判定**:`decideRectificationOffer`(`src/lib/rectification-in-chat-server.ts`)读取 `getRectificationEntrySummary(...).hasResumableCase`,再进入 `decideRectificationOfferWith`(`src/lib/rectification-in-chat.ts`),得到 `case_in_progress`。
3. **「可续做」的状态集合**:`RESUMABLE_CASE_STATUSES`(`src/lib/contracts/rectification-entry.ts`)与 SQL 函数 `agentic_rectification_resumable_statuses()`(`20260812010000_agentic_rectification_v9_runtime.sql`)都包含 `candidate_accepted`。
- 但确认门实际上永远不开(Bug 历史:`confirmation_allowed=false`,「本会话以代表性时间收口」),`candidate_accepted` 在现实里就是终点。
- 结果:**每个采用过时间的用户都会被永久判为「有进行中的校正」**。明说要校正进不去;普通对话里的校正提议卡也再不会出现。
4. **找那次校正的会话**:`finishExplicitRectificationRequest`(`consultation-tools.ts`)调用 `loadResumableSessionId`,实现在 `src/app/api/consult/route.ts`。它直接 `from("agentic_rectification_cases").select("session_id")` 读表。
- 而 `20260814010000_immutable_skill_registry.sql` 写了 `revoke all on table public.agentic_rectification_cases from service_role;`,之后没有任何迁移把权限授回。`20260904010000_read_report_candidate_range.sql` 的注释也写明:直接读表的权限保持撤销,只能走 RPC。
- 自托管环境下,admin 客户端以 `set local role service_role` 运行,所以这次查询**必然**报 permission denied。`if (error || !data) return null` 把错误吞掉,也没有日志。
- **结果:即使是真正没做完的校正,也永远续不上,必然落到那一句话。**
- 单测没拦住的原因:测试里的 `loadResumableSessionId` 是注入的假函数,从来没有碰过真库。
5. **兜底**:拿到 null 后写成 `kind: "refused"` + `RECTIFICATION_DIRECT_RESUME_ELSEWHERE_COPY`。`stream-agent-response.ts` 只在 `new` / `resume` 时下发 `rectificationDirectOpen`,所以前端只剩一句话。
## 根因
1. **找会话的方式错了**:绕开 RPC 直接读一张对服务角色撤销了权限的表,失败又被静默吞掉。
2. **「进行中」的口径错了**:把一个实际上的终点状态(`candidate_accepted`)算作「进行中」,这个口径同时挡住了明说入口和提议卡。
3. **兜底设计错了**:失败时只给一句话,没有任何能操作的出口。
## 决策记录
| # | 决定 | 来源 |
| --- | --- | --- |
| D1 | **已采用(`candidate_accepted`)的校正不算「进行中」**。用户明说要校正时,直接开一次新的;普通对话里的提议卡也按同一口径恢复出现 | 产品 2026-10-22 选 C |
| D2 | 删掉「可在历史对话里接着做」这个兜底:**说了要校正,就一定打开一个能操作的校正** | 产品 2026-10-22 |
| D3 | **只改「入口判定」的口径,不改共享的 `RESUMABLE_CASE_STATUSES` 和 SQL 函数**。理由是那个集合还被采用后补证据、重新出卡、暂停恢复、Skill 升级(不在集合内会被当成 `case_terminal`)、改资料触发器(置 `needs_rebaseline`)、以 `session` 打开时的 resumed / readonly 判定共同使用;从里面删掉 `candidate_accepted` 会让旧会话里的采用后核对题变成只读、Skill 升级报错。按 AGENTS.md §5「修在根因上」,根因是入口口径,不是状态机。因此本单**不动表** | Claude 规划 |
| D4 | 真正没做完的校正(`draft` / `collecting_evidence` / `candidate_ready` / `needs_rebaseline` / `paused`)仍然续做,但改为**经 RPC** 拿到会话号(现成的 `getRectificationCase(accounting, userId, caseId)`,`src/lib/rectification-agentic/v9/case-service.ts`,返回值带 `sessionId`)。RPC 失败时打日志,并改为直接新开一次。数据库早已允许同一用户并行多个校正(BUG-177 删了唯一索引,`rectification-v9-database.test.ts` 已验证) | Claude 规划 |
**推翻的旧决策**:
- BUG-1306 修复段原话「进行中的 case 不新开:查到 `agentic_rectification_cases.session_id` 就打开那次,查不到就说可以在历史对话里接着做」:本单推翻「查不到就说去历史接着做」,并改为经 RPC 查找。
- BUG-1306 10-12 决策日志「服务端本人 / 开关 / 进行中 case / 价格判定不变」中的「进行中 case」:口径改为不含 `candidate_accepted`。
- BUG-1248 提议卡条件「无进行中 case」:沿用,但口径同上。
- 与 BUG-177「不得用存在 resumable Case 阻止新 Case」一致,本单是在对话入口上重申这一点。
## 硬红线
1. 不改 `RESUMABLE_CASE_STATUSES`、`agentic_rectification_resumable_statuses()` 及其它 SQL;不新增迁移。
2. 非本人、开关关闭、没有价格这三种拒绝,行为不变。
3. 扣点规则不变:开始答第一题时才扣。
4. 日志不得含任何个人资料,只记 `requestId`、错误类别和 caseId 是否存在。
5. 旧会话里已采用的那次校正,采用后核对题在**新校正还没采用前**必须照常能答。新校正采用了不同的时间之后,旧那次的核对题不得报错或弹出错误提示,按只读处理即可(`segmentSelection` 为 null 时的界面状态)。
6. 改既有断言必须写「原值 / 新值 / 原因」三栏。按 AGENTS.md §7 第 8 条,先 grep 源码文本合同测试,同一轮里跟着改。
## 任务
### T1 入口口径:已采用不算进行中
- 新增一个只给入口用的判定,名称执行方定,例如 `blocksNewRectification(status)`:命中 D4 列的五种状态返回 true,`candidate_accepted` 返回 false。
- `getRectificationEntrySummary` 返回的 `latestResumable.status` 如果是 `candidate_accepted`,入口就视为「没有进行中的校正」。
- `decideRectificationOffer`(明说入口和普通提议卡共用)改用这个判定。
- 已知边界:summary 只返回最近活动的一次可续做校正。如果最近一次是已采用的、更早还有一次没做完的,本单按「没有进行中的」处理,直接新开。数据库允许并行,用户在历史对话里仍能找到那次。在进度记录里写明这一点。
**验收**
- 单测:最近一次是 `candidate_accepted` → 明说要校正得到 `new`,提议卡可以出现;最近一次是 `collecting_evidence` → 得到 `resume`。
### T2 经 RPC 找会话,删掉兜底
- `loadResumableSessionId` 改为调用 `getRectificationCase(accounting, userId, caseId)`,取 `sessionId`。不再直接读表。
- `finishExplicitRectificationRequest` 的分支:
- 有进行中的校正且拿到 `sessionId` → `kind: "resume"`;
- 有进行中的校正,但 RPC 失败或拿不到 `sessionId` → 打日志(红线 4),然后 `kind: "new"`;
- 没有进行中的校正 → `kind: "new"`。
- 删除常量 `RECTIFICATION_DIRECT_RESUME_ELSEWHERE_COPY`,以及 `consultation-tools.ts` 里给模型的「去历史对话接着做」说明;同步删掉 `frontend/docs/VOICE.md` 里的对应口径。
- 清理 `case-status.ts` 里已经过期的 `MAX_RESUMABLE_CASES_PER_USER = 1`:注释说它「镜像 DB 部分唯一索引」,但索引已被 BUG-177 删除。先 grep 引用;删除前确认没有业务逻辑依赖,测试里的断言按三栏改。
**验收**
- 单测:RPC 抛错 → `new`,并且有一条日志调用(不含个人资料)。
- **真库测试**(`test:db`):用服务角色调 `loadResumableSessionId` 的新实现,能拿到一个进行中校正的 `sessionId`。旧实现在同样条件下会 permission denied,这条测试就是当初缺的防线。
### T3 旧会话不报错
- 准备一个已采用的旧校正,再开一个新校正,并让新校正采用不同的时间。然后打开旧会话:
- 采用后核对题显示为只读;
- 不弹错误;
- 控制台没有未处理的异常。
- 用交互测试或组件测试覆盖。
### T4 文档
- `docs/BUG_HISTORY.md` 新增一条:关联 BUG-1306、BUG-1248、BUG-177。根因写三条(读表权限、口径、兜底);防复发写 T2 的真库测试和 T1 的口径单测。
- `CHANGELOG.md` 写一行用户可感知的变化。
- `docs/testing/rectification-explicit-open-20261023.md` 真机清单:
1. 已采用过时间的账户,在对话里说「帮我做生时纠正」,应直接进入新的校正;
2. 一个答到一半的校正,从对话里说要校正,应接回那次;
3. 普通对话问到时机类问题时,提议卡会出现(已采用的账户)。
## 让步顺序
1. 先砍 T3 的自动化测试,改为写进真机清单。
2. T1、T2、真库测试都不可让。
## 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-explicit-open-20261023 .worktrees/rectification-explicit-open-20261023 origin/staging
cd .worktrees/rectification-explicit-open-20261023 && git status -sb
grep -oE "BUG-[0-9]+" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1
cd frontend && npm ci && ./node_modules/.bin/tsc --noEmit && npm run lint
./node_modules/.bin/tsx --test tests/rectification-explicit-entry-20261012.test.ts tests/rectification-in-chat-20261007.test.ts tests/rectification-v9-contracts.test.ts # 记基线
```
## 验收分工
| 谁 | 跑什么 |
| --- | --- |
| Grok 自跑 | `tsc --noEmit`;`npm run lint`;上面三个测试文件加新增测试;`npm run test:db`,与 `tools/agent-loop/windows-known-failures.txt` 逐条比对;`npm run build` |
| Claude 验收时跑 | 前端全量 `npm test`,与基线逐条对比;复核没有新迁移、`RESUMABLE_CASE_STATUSES` 和 SQL 函数未改;复核日志不含个人资料 |
| 环境缺口 | 真实模型会不会在用户明说时调用工具,Claude 这边没有模型凭据,留给真机清单第 1 步 |
现有会受影响的断言(改时按三栏写):
- `rectification-explicit-entry-20261012.test.ts`
- 断言 `RESUME_ELSEWHERE_COPY` 原文(约第 129–132 行);
- 「an in-progress case resumes that session, or says to continue in history」(约第 243 行),其中 loader 返回 null 或抛错时期望 `refused`;
- 普通调用 note 匹配 `/历史对话/`;
- 约第 695–698 行的源码文本合同(`openRectificationFromChat(...)` 恰好出现 1 次)。
- `rectification-in-chat-20261007.test.ts`:`hasResumableCase` 为 yes 时期望 `case_in_progress`(约第 107–115 行),需要补「最近一次是已采用」的分支。
- `rectification-v9-contracts.test.ts`:`MAX_RESUMABLE_CASES_PER_USER === 1`(T2 清理时)。
+2
View File
@@ -1,5 +1,7 @@
# TASK · 全站动效:换页、页签、盘型、列表、弹窗、抽屉、提示、数字(2026-10-22)
> **2026-10-23 更新:本单不再单独执行。** 产品推翻了下面的 D3「不引入动画库」,改为一律用 Motion(见 `TASK-ui-kit-migration-20261023.md` 的 D2)。本单的**动效规范表仍然有效**,由迁移单 K0 做成 Motion 预设;M0~M4 并入迁移单 K1~K5。issue #98、#100、#101 不再执行。
设计原型(可在「现在 / 改后」之间对比):https://claude.ai/artifact/7u9goivnmTNSGTNWNeVMQd 。原型和本任务书不一致时,以本任务书为准。
## 基线
@@ -0,0 +1,260 @@
# TASK · 界面统一换成组件库:shadcn/ui · Lucide · Motion · react-resizable-panels · dnd-kit · Zustand(2026-10-23)
可交互原型(保持现在的 Claude 风格,全部用下列依赖实现,带「显示组件来源」开关):https://claude.ai/artifact/PGnWsjHhu38KJseEfEMNWG 。原型和本任务书不一致时,以本任务书为准。**产品 2026-10-23 确认原型定稿(第 5 版)**,验收时以它为视觉参照。原型的数据是虚构的。
## 基线
- 写本单时的 `origin/staging` 是 `3632b0eab`。每个 issue 开工时,以当时的 `origin/staging` 为准。
- BUG 编号:开工时用 `grep -oE "BUG-[0-9]+" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1` 核对最大号,从下一个号开始(写本单时是 BUG-1420)。
- 本单拆成 8 个 issue,串行执行,见文末「循环交接」。其中 K1 和 K4 要动表(加排序列),产品已经同意。
## 现状实证(2026-10-23 在 staging 代码上实测)
1. **shadcn 装了,但几乎没用。**
- `frontend/components.json` 已经配成 `style: "base-nova"`、`iconLibrary: "lucide"`。
- 但 `src/components/ui/` 下只有 8 个组件:button、calendar、popover、select、sidebar、sonner、tabs、textarea。
- 业务代码(不含后台)里有手写 `<button>` 148 处、原生 `<dialog>` 8 个、自写 `role="dialog"` 弹层 3 个,还有 4 处自己 `createPortal`。
2. **样式全靠手写。** `src/app/globals.css` 有 7002 行、902 个自定义类、14 段 `@keyframes`。同一种控件各写一份,例如:
- 会话行菜单 `.session-actions` / `.session-action-item`,其实就是一个手写的 DropdownMenu;
- 账户入口 `.profile-trigger`;
- `.new-chat`、`.report-nav-button` 等。
3. **漏引 shadcn 的变体规则。** `globals.css` 没有 `@import "shadcn/tailwind.css"`。base-nova 组件用到的 `data-horizontal:`、`data-vertical:`、`data-active:` 等变体都定义在这个文件里,漏引时页签方向、分段按钮拼接、选中态都会错。原型第一版就踩过这个坑。
4. **动效分散。** 有 14 段手写关键帧;`chat-message-row.tsx` 里用了一处 GSAP。
5. **状态分散。** 首页 `Home()` 和各个 hook 里共有 171 处 `useState`、2 个 context,状态靠参数一层层往下传。
6. **没有拖拽,也没有可拖宽的分栏。** 侧栏宽度是写死的;想看星盘只能单独开一页。
7. 手写控件最多的文件(按钮、弹窗、菜单出现次数):
- `email-otp-login.tsx` 18
- `people/people-page.tsx` 12
- `report-export-drawer.tsx` 7
- `synastry-add-dialog.tsx` 6
- `chart-type-picker.tsx` 6
- `birth-time-choice-question.tsx` 6
- `onboarding-redeem-paywall.tsx` 5
- `birth-time-candidate-result.tsx` 5
- `app/(app)/page.tsx` 5
- 其余见各 issue 的文件清单。
## 根因
- 引入 shadcn 的时候只拷了几个组件,之后每个新功能都自己写控件和样式;没有规矩、也没有检查拦住手写。
- 动效、状态、拖拽都没有定好用哪个库,于是各写各的。
## 决策记录(产品负责人 2026-10-23 拍板,全部同意)
| # | 决定 | 推翻了什么 |
| --- | --- | --- |
| D1 | **加 4 个依赖、去掉 1 个**:加 `motion`、`react-resizable-panels`(v4,经 shadcn `Resizable`)、`@dnd-kit/core` + `@dnd-kit/sortable` + `@dnd-kit/utilities`、`zustand`;去掉 `gsap` 和 `@gsap/react`。版本固定到发布满两周的版本 | AGENTS.md §7.7「不得顺手升级依赖」:本单是产品点名授权的新增 |
| D2 | **动效一律用 Motion** | `TASK-site-motion-20261022.md` 的 D3「不引入动画库」,以及 `TASK-onboarding-journey-20261022.md` 红线 8 的「用 CSS 和 Web Animations API」。site-motion 的**动效规范表仍然有效**,按表里的时长和曲线做成 Motion 预设;它的 M0~M4 并入本单 |
| D3 | **已有依赖能正确表达的组件和行为,严禁自行设计实现**,并加 lint 自动拦截 | 已写进 AGENTS.md §7 第 12 条 |
| D4 | **首屏 ±2% 不放宽**。Motion、dnd-kit、react-resizable-panels 按需加载,不进首屏 | 已写进 AGENTS.md §7 第 2 条 |
| D5 | **新功能:在对话旁边拉出星盘面板**,可以拖宽、可以收起 | 无 |
| D6 | **会话和星盘档案可以拖动排序,顺序跨设备保留**。要加排序列,产品同意动表 | 无 |
| D7 | **长相不变**:配色、字体、圆角沿用现有的 Claude 风格设计令牌,映射成 shadcn 的 CSS 变量(`--background`、`--primary` 等)。本单只换实现,不换长相 | 无 |
| D9 | **二选一、多选一的切换一律用分段式 `Tabs`**(盘型、中英、主题等),不用描边的 `ToggleGroup`;`ToggleGroup` 只留给可多选或标签式的场景 | 无 |
| D8 | **文案规矩**按 AGENTS.md §7 第 9~11 条执行:删掉解释布局的副标题、说明句、重复标签,可访问名称保留 | 无 |
## 硬红线
1. 不得手抄或改写 shadcn 组件。缺什么组件就用 `npx shadcn@<固定版本> add <组件名>` 加。加进来之后只许改设计令牌和类名组合,不许改组件的行为。
2. `globals.css` 第一行之后必须 `@import "shadcn/tailwind.css";`。没有这一行,验收直接不通过。
3. 首屏:`next build` 后 `/` 仍为 `○ Static`,首屏 gzip ±2%。Motion 用 `LazyMotion` + `m` 组件,`domAnimation` 特性包异步加载;dnd-kit、react-resizable-panels 只在用到的页面里动态引入。
4. 揭幕后不得出现新的等待动画(AGENTS.md §6)。动效只回应用户操作;一律包在 `MotionConfig reducedMotion="user"` 里,尊重系统的减少动态效果设置。
5. 冻结合同:`Home()` 的 `useState` / `useRef` 数只许减少(状态迁进 Zustand 后应当明显下降)。`ChatComposer` 仍然是唯一的输入框,只是改为用 shadcn `InputGroup` 实现。
6. 不得改动业务行为和数据:同样的输入,接口请求、扣点、落库都必须和改前一致。每个 issue 都要对比改前改后的全量测试;改了既有断言要写「原值 / 新值 / 原因」三栏。
7. 删除手写类和关键帧之前,按 AGENTS.md §7 第 8 条先 grep 源码文本合同测试,同一轮跟着改。
8. 动表只加列(排序列可为空,有默认值),对已部署代码向后兼容;必须真跑 `npm run test:db`。
9. 星盘画法、星图、报告排版、天空过场这些占星专有的绘制保留手写,不算违规;但其中的按钮、菜单等控件仍要换成组件。
10. 文案按 D8 删减时,读屏名称不能跟着删:图标按钮要有 `aria-label`,表单控件要有可访问名称。
## 任务分解
### K0 底座与规矩
- 加 D1 列的依赖,去掉 gsap。`chat-message-row.tsx` 里的 GSAP 改用 Motion,效果不变。
- `globals.css` 加 `@import "shadcn/tailwind.css"`。把现有设计令牌映射到 shadcn 变量,深色、浅色都要映射(D7)。
- 用 shadcn CLI 加以下组件:
- dialog、alert-dialog、sheet、drawer
- dropdown-menu、tooltip、card、badge、table、separator、scroll-area、avatar
- input、input-group、label、field、toggle、toggle-group、resizable
- checkbox、radio-group、switch、collapsible、empty、kbd、skeleton
- 说明:skeleton 只给星盘页 BUG-1016 那一处例外用。
- `src/lib/motion/`:
- 把 `TASK-site-motion-20261022.md` 动效规范表里的每一档做成 Motion 预设(时长、曲线、方向);
- 包装好 `LazyMotion` 和 `MotionConfig`;
- 不另写任何动画逻辑。
- `src/stores/`:先建骨架,有会话、人物、账户、界面四个 store。状态在后面各个 issue 里逐步迁入。
- **lint 规则**:在 `src/components/ui/**` 以外:
- 出现 JSX 原生 `button`、`dialog`、`select`、`input type=checkbox|radio`,或 `role="dialog|menu|menuitem|tablist|tab|listbox"`,或 `createPortal`,就报错;
- 用字符充当图标(☰ ↑ × ⋯ ✓ ▾ 等出现在 JSX 文本里)也报错。
- **存量白名单**:迁移前已有的违规文件列进白名单,再加一条测试,断言白名单只能变短、不能变长。
- 星盘和星图里的 SVG 绘制不受图标规则限制,规则里写明豁免哪些路径。
**验收**
- `npm ls` 里能看到新依赖,看不到 gsap。
- 往 `src/components/` 下新加一个含 `<button>` 的文件,lint 报错。
- 首屏 gzip ±2%,`/` Static。
- 界面截图和改前一致:K0 只加底座,不换控件。
### K1 外壳:侧栏、会话列表、账户菜单、设置、页面切换
文件:`app-sidebar.tsx`、`sidebar-session-row.tsx`、`theme-preference-menu.tsx`、`account-dialog-overlay.tsx`、`profile-panel.tsx`、`billing-panel.tsx`、`account-deletion-section.tsx`、`account-deletion-gate.tsx`、`feedback-dialog.tsx`、`subject-switcher.tsx`,以及 `globals.css` 里对应的手写类。
- **侧栏**:
- 用 shadcn `Sidebar` 的菜单组件;
- 桌面上放进 `ResizablePanelGroup`,可以拖宽(200–360 px),拖到最左收起,宽度记在本机;
- 手机上用 `Sheet` 从左边滑出;
- 当前页的高亮底色用 Motion 的 `layoutId` 滑动。
- **会话行**:
- 更多菜单用 `DropdownMenu`(收藏、重命名、转发、删除);
- 删除前弹 `AlertDialog`;
- 拖动排序用 dnd-kit,键盘也能排;
- 顺序写进新加的排序列(D6,动表);收藏的会话仍然排在最前。
- **账户菜单**:
- 用 `DropdownMenu`,主题是 `DropdownMenuRadioGroup`;
- 触发按钮只显示头像和名字,点数不在这里重复显示(D8)。
- **设置弹窗**:
- 用 `Dialog` + `Tabs`,三个分区名不变;主题选择也用分段式 `Tabs`;
- 注销确认用 `AlertDialog`,反馈用 `Dialog`。
- **页面切换**:Motion 的 `AnimatePresence`,方向跟随侧栏顺序(site-motion 规范表的「页面切换」档)。
- **顶栏点数**:
- 改成正常大小的 `Button variant="outline"`,内容是 Lucide `Coins` 图标加数字;
- 「点」字只放在 `aria-label` 里;
- 数字变化时上下滚动。
- 现有的手写类 `.session-row`、`.session-actions`、`.profile-trigger`、`.new-chat`、`.report-nav-button` 等,迁移后删除,并从 lint 白名单里移出对应文件。
**验收**
- 拖动会话排序后刷新、换设备,顺序都保持(DB 测试加交互测试)。
- 快速连点侧栏 5 次,最终停在最后点的那一页,没有残留的旧页。
- 侧栏拖到最左能收起,宽度刷新后仍然记得。
- 手机 375 px 宽时侧栏是抽屉,页面不横向滚动。
### K2 对话页
文件:`chat-composer.tsx`、`chat-message-row.tsx`、`chat-message-actions.tsx`、`conversation-follow-ups.tsx`、`reply-down-reason.tsx`、`model-selector.tsx`、`onboarding-redeem-paywall.tsx`、`starter-home.tsx`、`app/(app)/page.tsx`、`jump-to-latest-button.tsx`。
- **输入框**:
- `ChatComposer` 改用 `InputGroup` + `InputGroupTextarea`;
- 底部 `InputGroupAddon align="block-end"` 放两样东西:左边是模型选择(`DropdownMenu`),右边是圆形发送按钮;
- 发送按钮和输入框同在一个框里,底边对齐;
- 模型选择从顶栏移进输入框,各模型的价格只在菜单里显示。
- **回答操作**:赞、踩、复制、重新生成用 `Button size="icon-sm"`,每个配 `Tooltip`;「不满意原因」用 `Dialog`。
- **追问**:用 `Button variant="outline" size="sm"`,依次浮出(Motion 的 stagger)。
- **兑换窗**:改用 `Dialog`,文案不变。
- **对话旁的星盘面板(D5)**:
- 顶栏加一个 `PanelRightOpen` / `PanelRightClose` 图标按钮,配 `Tooltip`,带 `aria-label`;
- 面板放在 `ResizablePanelGroup` 里,可以拖宽、可以收起,宽窄记在本机;
- 面板里是当前人物的本命盘(复用星盘页的绘制组件);
- 手机上不显示这个入口。
- **对话状态**:当前会话、消息、流式状态迁进 Zustand 的会话 store;`Home()` 里对应的 `useState` 删掉。
- **流式输出**:逐字释放的逻辑不改;消息进场用 Motion。
**验收**
- 回归原有的对话测试:流式、停止、排队、撤回、退点、寒暄免费。
- `Home()` 的 hook 数下降,写进进度记录。
- 星盘面板打开、拖宽、收起、刷新后仍然记得状态。
### K3 星盘页
文件:`chart-page/*`(`chart-page-view.tsx`、`chart-type-picker.tsx`、`chart-vedic-tab.tsx`、`chart-dasha-tab.tsx`、`chart-basics-tab.tsx`、`chart-western-tab.tsx`、`chart-birth-editor.tsx`、`chart-param-table.tsx`、`chart-symbol-help.tsx`)。
- **四个页签**:用 `Tabs`,`variant="line"`。
- **盘型条**(本命、D9、D10、月亮…):用分段式 `Tabs`(`TabsList` 默认变体:灰底槽,选中的一格浮起亮底),盘型多时用 `ScrollArea` 横向滚动;不用描边的 `ToggleGroup`(产品 10-23 看过,嫌丑)。「星盘类型」面板用 `Popover` 或 `Sheet`。
- **盘与参数**:桌面上两栏放在 `ResizablePanelGroup` 里,可以拖宽;手机上上下排。
- **行星表**:
- 用 `Table`,**跨满整行**(继承 #102 的修复,不得回归);
- 状态列统一用 `Badge`:落陷、敌用 `destructive`,其余用 `secondary`,没有状态就写「—」。同一列不得出现两种徽标形状。
- **出生资料就地编辑**:用 `Popover` 或 `Dialog`,表单用 `Field` 和 `Input`。
- **Motion**:换盘型时星体从旧宫位移到新宫位;换人时格线重画;大运展开用 `Collapsible` 加 Motion。
- 星盘 SVG 的绘制保留手写(红线 9)。
**验收**
- 切盘型后星体最终坐标和直接渲染一致(坐标断言)。
- D1 字节冻结测试仍然通过。
- 1024、1280、390 三种宽度下截图都正常。
### K4 星盘档案与婚恋合盘
文件:`people/people-page.tsx`、`people-home-links.tsx`、`chart-profile-form.tsx`、`profile-gender-field.tsx`、`synastry/*`(`synastry-add-dialog.tsx`、`synastry-picker.tsx`、`synastry-report-view.tsx` 等)。
- **人物卡**:用 `Card`,加 `CardAction`,更多菜单用 `DropdownMenu`;编辑用 `Dialog`;删除用 `AlertDialog`。
- **拖动排序**:用 dnd-kit 的网格排序,顺序写进 `chart_profiles` 的新排序列(D6,动表);本人始终排在第一。
- **关系选择**:用 `ToggleGroup`,表单的标签和读屏名称都要保留(D8)。
- **合盘选人页**:
- 去掉页内重复的标题和副标题(顶栏已经有页名);
- 两个 `Select` 左右并排,中间一个 Lucide `X`(`aria-hidden`);
- 下方正中放「开始合盘」按钮,图标用 Lucide `HeartHandshake`(和侧栏一致,不用 `Sparkles`);按钮上不写价格。点了弹 `AlertDialog` 确认:「会用掉 {价格} 点,余额 {余额} 点。」,按钮是「取消 / 开始」;余额不够时说「需要 {价格} 点,余额只有 {余额} 点。」,主按钮换成「去补点数」(产品 10-23 定);
- 手机上两个选择框上下排;
- 两个 `Select` 都有可访问名称「第一个人」「第二个人」。
**验收**
- 拖动排序后刷新、换设备,顺序都保持。
- 删人后,相关的合盘和报告照原逻辑处理。
- 合盘按钮在桌面和手机上都居中。
### K5 报告、星历、登录与其余页面
文件:
- `personal-report/*`:`report-actions.tsx`、`report-export-drawer.tsx`、`personal-report-center.tsx`、`report-edge-jump.tsx`、`report-row-delete.tsx`
- `ephemeris/*`
- `email-otp-login.tsx`、`legal/*`、`app/global-error.tsx`、`markdown-code-block.tsx`、`birth-sky/birth-sky-replay.tsx` 里的控件
具体改法:
- **中英切换**:用分段式 `Tabs`(同盘型条),带 `aria-label="报告语言"`;不用描边的 `ToggleGroup`。
- **导出**:用 `Sheet`(桌面从右侧滑出,手机从底部);删掉说明句和文件大小(D8)。
- **星历翻日期**:用 `Button` 加 Lucide 箭头,内容按方向翻页(Motion)。卡片不得被翻页容器裁掉边框,原型第一版就出过这个问题。
- **登录页**:输入框和按钮换成组件;验证码输入用 shadcn `InputOTP`。
**验收**
- BUG-1126 回归测试通过(中英切换后不残留旧语言节点)。
- 登录页 E2E(或交互测试)全过。
### K6 生时校正界面
文件:`birth-time-*`、`rectification-*` 系列组件(`birth-time-choice-question.tsx`、`birth-time-candidate-result.tsx`、`birth-time-rectification.tsx`、`rectification-choice-card.tsx`、`rectification-board.tsx` 等)。
- 只换控件和动效,**题目逻辑、计分、落库一概不动**。
- 选项卡片改用 `ToggleGroup` 或 `RadioGroup`。
- 排在 #103(校正入口修复)合入之后。
**验收**:校正的全量前端测试、Python `test_rectification_*`,改前改后逐条一致。
### K7 收尾
- 删掉 `globals.css` 里已经没人用的手写类和 `@keyframes`,进度记录里写清改前改后的行数。
- lint 白名单清空(占星绘制的豁免除外)。
- 重写 `frontend/DESIGN.md`:
- 第一节改为「组件一律用 shadcn base-nova,令牌如下」,动效节改为 Motion 预设表;
- 旧的 `CLAUDE_DESIGN.md` 改编说明降为参考。
- 更新 `frontend/docs/VOICE.md`:按 D8 删掉已不存在的说明句。
## 让步顺序
做不完时,按下面的顺序先砍:
1. K7 里 `globals.css` 的彻底清理(白名单仍然只减不增)
2. 星盘面板宽窄的本机记忆
3. 换人时的格线重画
4. 拖动排序的键盘操作(鼠标拖动不让)
D3 的 lint 规则、`shadcn/tailwind.css`、首屏体积、文案规矩、读屏名称都不能让。
## 循环交接
- **执行方**:Grok(Windows + Docker)。每个 issue 一个分支,只推自己的分支。
- **Grok 自跑**:`tsc --noEmit`、`npm run lint`、相关测试文件、`npm run build`(记录 `/` 是否 Static 和首屏 gzip)。动表的 issue 还要跑 `npm run test:db`,与 `tools/agent-loop/windows-known-failures.txt` 逐条比对。
- **Claude 验收时跑**:前端全量 `npm test`,逐条对比基线;复核首屏 gzip;对照原型截图检查。
- **真机清单**:`docs/testing/ui-kit-migration-20261023.md`,由 K1 创建,后面的 issue 往里追加。
- 每个 issue 在同一变更里更新 `docs/BUG_HISTORY.md`(体验改进类记 `enhancement`)、`CHANGELOG.md` 和 `frontend/DESIGN.md`。
| issue | 内容 | 分支 | 前置 |
| --- | --- | --- | --- |
| 组件库(K0) | 底座与规矩 | `codex/ui-kit-0-base-20261023` | #95 合入 |
| 组件库(K1) | 外壳 | `codex/ui-kit-1-shell-20261023` | #97 合入 |
| 组件库(K2) | 对话页 | `codex/ui-kit-2-chat-20261023` | K1 合入 |
| 组件库(K3) | 星盘页 | `codex/ui-kit-3-chart-20261023` | K2、#102 合入 |
| 组件库(K4) | 档案与合盘 | `codex/ui-kit-4-people-20261023` | K3 合入 |
| 组件库(K5) | 报告、星历、登录 | `codex/ui-kit-5-pages-20261023` | K4 合入 |
| 组件库(K6) | 生时校正界面 | `codex/ui-kit-6-rectification-20261023` | K5、#103 合入 |
| 组件库(K7) | 收尾 | `codex/ui-kit-7-cleanup-20261023` | K6 合入 |
新用户启程的几单也跟着调整:#96 改为等 K0 合入后再开工,新界面直接用组件库;#99(启程动效)改为依赖 K0 的 Motion 预设,不再依赖 #98。#98、#100、#101(全站动效一、二、三)并入 K1~K5,不再单独执行。
@@ -0,0 +1,6 @@
# 星盘页行星表整行(2026-10-23)
登录后打开星盘页。不要用真实出生资料做记录,用站内已有的示例盘即可。
1. 把窗口拉到大约 1280 宽。依次看「本命」「月亮」「D9」「Bhava」「行运」。每张表都占满盘和参数卡下面的整行,宿主、pada、状态不用横拖就能看见。
2. 把窗口收到大约 390 宽(或手机)。页面本身不左右滚动。表如果放不下,只在表自己的框里横滑。
+1 -1
View File
@@ -1136,7 +1136,7 @@ D40 / D45 / D60 的表格上方,出生时间不是「已校正」时多一句
**不再按断点复制参数。** 原本同一组参数有三种画法:中宫叠一张 `.chart-page-center-card`(手机两行、桌面六行),盘下方再来一张 `.chart-page-params-below` 重复那六行,由 media query 三选一。同样的事实画三遍、靠断点开关,改一处要改三处。键值表让每个字段只出现一次,`.chart-page-center-card` / `-compact` / `-extra` / `.chart-page-params-below` / `.chart-page-boundary-desktop` 五个类连同它们的 767/768 开关块一并删除,由 `chart-page-view.test.tsx` 守着不许回来。
桌面(≥1024)盘面与参数表左右并排,行星表是跨两列的整行——它的列宽需要完整版心。2026-09-29 起桌面印度盘宽 440px;当晚(BUG-1100,产品「框太丑」)上限从绘图区移到外框 `.chart-page-vedic-stage`(440px + 两侧 `--space-3` 内边距 + 边框),外框贴合星图,不再是一张宽卡片中间放一张窄盘;手机端外框不设上限;西洋桌面绘图区上限 480px。低于此断点的结果盘不收紧,参数区和表格不缩。等待盘与结果盘复用同一桌面列宽与绘图容器,不再使用独立 400px 等待盘上限;印度等待绘图容器以 400/452 比例预留结果图例带,内部空 SVG 仍为 400×400,避免揭幕时外框和说明区纵向跳动,不改共享绘图。只有盘位动(2026-09-30 起为宫位依次点亮,不再整张呼吸),失败后退出,参数表不新增骨架。
桌面(≥1024)盘面与参数表左右并排,行星表是跨两列的整行——它的列宽需要完整版心。表格区由 `VedicLayout` 的整行容器承载,不靠表格自身类名跨栏。2026-09-29 起桌面印度盘宽 440px;当晚(BUG-1100,产品「框太丑」)上限从绘图区移到外框 `.chart-page-vedic-stage`(440px + 两侧 `--space-3` 内边距 + 边框),外框贴合星图,不再是一张宽卡片中间放一张窄盘;手机端外框不设上限;西洋桌面绘图区上限 480px。低于此断点的结果盘不收紧,参数区和表格不缩。等待盘与结果盘复用同一桌面列宽与绘图容器,不再使用独立 400px 等待盘上限;印度等待绘图容器以 400/452 比例预留结果图例带,内部空 SVG 仍为 400×400,避免揭幕时外框和说明区纵向跳动,不改共享绘图。只有盘位动(2026-09-30 起为宫位依次点亮,不再整张呼吸),失败后退出,参数表不新增骨架。
**符号说明(2026-09-29;当晚改为气泡,BUG-1101)。** `/chart` 开启共享 SVG 的可选交互模式:说明是标记旁的小气泡(`.chart-symbol-bubble`,最宽 220px,`--color-canvas` 底 + 细边 + `--shadow-elevated`),默认在标记上方 8px,离外框顶不足 48px 时翻到下方,左右被夹在外框内;气泡不接收指针,不会挡住旁边的标记。电脑:鼠标悬停出现、移开消失,点击与键盘聚焦同样出现。手机:轻点标记出现,触摸产生的离开事件不关它,点盘外任意处或 3 秒后自动消失。盘下不再有「停在或轻点……」提示行和说明行;读屏器仍从一个隐藏的 `role="status"` 读到同一句话;Esc、离开、失焦可关闭,切盘/人物/数据身份变化清理旧说明。行星与其逆行短线共用一个目标;西洋 caption 和命中圆用一致的行星解析,原点击过滤相位及再次点击取消保留。报告、校正、静态导出默认不启用该模式;纯绘图层不持有 hooks,静态 SVG 白名单不扩。
+6 -6
View File
@@ -5808,8 +5808,9 @@ button.synastry-dd-add { border-radius: 0 0 var(--radius-md) var(--radius-md); }
@media print { .chart-page-type-sheet { display: none !important; } }
.chart-page-precision-hint,
.chart-page-table-note { margin: 0 0 var(--space-2); }
/* A note + table pair is one grid item, so the table keeps its own scroll box. */
.chart-page-varga-table-block { min-width: 0; }
/* The table row is one grid item. A note and its table stay in one scroll box. */
.chart-page-varga-table-block,
.chart-page-vedic-tables { min-width: 0; }
.chart-page-varga-table-block > .chart-page-planet-table-wrap + .chart-page-planet-table-wrap { margin-top: var(--space-4); }
/* A body the Bhava boundaries move to another house. */
.chart-page-planet-table tr.is-shifted > td,
@@ -6341,10 +6342,9 @@ button.synastry-dd-add { border-radius: 0 0 var(--radius-md) var(--radius-md); }
card with empty sides around a 440px chart. 440 + the card's padding. */
.chart-page-vedic-stage { width: 100%; max-width: calc(440px + 2 * var(--space-3) + 2px); }
.chart-page-western-wheel { max-width: 480px; }
/* Chart and parameters sit side by side; the planet table is a full-width
row under both, because its columns need the whole measure. */
.chart-page-planet-table-wrap,
.chart-page-varga-table-block { grid-column: 1 / -1; }
/* Chart and parameters sit side by side. The third grid item is the table
row, so a wrapper around a table cannot drop it back into the left column. */
.chart-page-vedic-tables { grid-column: 1 / -1; }
.chart-page-basics-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@@ -350,7 +350,9 @@ function VedicLayout({ view, chart, ariaLabel, ascendantHouse, children }: {
chart can and cannot be read for. */}
<p className="chart-page-boundary">{view.vedic.boundary}</p>
</div>
{children}
<div className="chart-page-vedic-tables">
{children}
</div>
</div>
);
}
@@ -0,0 +1,97 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import test from "node:test";
import { ChartVedicTab } from "../src/components/chart-page/chart-vedic-tab.tsx";
import { assembleChartView } from "../src/lib/chart-view-load.ts";
import { chartViewOkSchema, type ChartViewOk } from "../src/lib/chart-view-contract.ts";
Object.assign(globalThis, { React });
const golden = JSON.parse(readFileSync(new URL("./fixtures/chart-view-golden.json", import.meta.url), "utf8"));
const bhavaGolden = JSON.parse(readFileSync(new URL("./fixtures/chart-view-bhava-golden.json", import.meta.url), "utf8"));
const transitPacket = JSON.parse(readFileSync(new URL("./fixtures/ephemeris-chart-transit-2026-09-15.golden.json", import.meta.url), "utf8"));
const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const profile = {
name: "示例",
date: "1990-06-15",
time: "12:00",
placeLabel: "北京",
latitude: 39.9042,
longitude: 116.4074,
timezoneOffset: 8,
timezoneId: "Asia/Shanghai",
ayanamsa: "raman",
birthTimeStatus: "confirmed",
};
function directDivClasses(html: string, parentClass: string): string[] {
const needle = `<div class="${parentClass}">`;
const start = html.indexOf(needle);
assert.ok(start >= 0, parentClass);
let index = start + needle.length;
let depth = 1;
const classes: string[] = [];
while (index < html.length && depth > 0) {
const nextOpen = html.indexOf("<div", index);
const nextClose = html.indexOf("</div>", index);
assert.ok(nextClose >= 0);
if (nextOpen >= 0 && nextOpen < nextClose) {
if (depth === 1) {
const tagEnd = html.indexOf(">", nextOpen);
const tag = html.slice(nextOpen, tagEnd + 1);
classes.push(tag.match(/class="([^"]*)"/)?.[1] ?? "");
}
depth += 1;
index = html.indexOf(">", nextOpen) + 1;
} else {
depth -= 1;
index = nextClose + "</div>".length;
}
}
return classes;
}
async function fullView(): Promise<ChartViewOk> {
const result = await assembleChartView({
userId: "user-1",
profile,
asOf: "2026-09-15",
layers: ["varga", "bhava", "transit"],
postEngine: async (path, _body, cacheScope) => {
if (path === "/api/chart" && cacheScope?.startsWith("transit:")) return { status: "ok", payload: transitPacket };
if (path === "/api/chart") return { status: "ok", payload: golden.chart };
if (path === "/api/varga_full") return { status: "ok", payload: golden.varga_full };
if (path === "/api/bhava_chalit") return { status: "ok", payload: bhavaGolden.bhava_chalit };
return { status: "http_error", path, elapsedMs: 1, httpStatus: 500 };
},
});
assert.equal(result.body.status, "ok");
return chartViewOkSchema.parse(result.body);
}
test("every Vedic chart type puts its table in the layout's full-width row", async () => {
const view = await fullView();
for (const vargaId of ["D1", "MOON", "D9", "BHAVA", "TRANSIT"]) {
const markup = renderToStaticMarkup(React.createElement(ChartVedicTab, { view, vargaId, onSelectVarga: () => {} }));
const children = directDivClasses(markup, "chart-page-vedic-layout");
assert.deepEqual(children, [
"chart-page-vedic-stage",
"chart-page-params",
"chart-page-vedic-tables",
], vargaId);
}
});
test("the desktop chart grid spans the table row, not a table's own class", () => {
const desktopStart = globalStyles.indexOf("@media (min-width: 1024px) {\n .chart-page-vedic-layout,");
assert.ok(desktopStart >= 0);
const desktopEnd = globalStyles.indexOf("\n}", desktopStart);
const desktop = globalStyles.slice(desktopStart, desktopEnd);
assert.match(desktop, /\.chart-page-vedic-tables \{ grid-column: 1 \/ -1; \}/);
assert.doesNotMatch(desktop, /\.chart-page-planet-table-wrap[\s\S]*grid-column/);
assert.doesNotMatch(desktop, /\.chart-page-varga-table-block[\s\S]*grid-column/);
});