合并 origin/staging。Yogi 记录改记为 BUG-1421,避开已经占用的 BUG-1420。

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-23 10:18:13 +08:00
co-authored by Grok (agent loop)
8 changed files with 1027 additions and 18042 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. 隐私与安全
+19 -18041
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -37,6 +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` | — | **星盘页本命 / 月亮行星表挤进左栏(issue #102)**:#87 给表包了 `ChartSymbolHelp` 后跨栏规则失配;根因修法是 `VedicLayout` 用固定整行容器承载表格,跨栏只认容器;加结构断言防复发 | 待执行(循环) | 分支 `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,排在启程(三)之后、启程(四)之前;**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`,写两步:宽屏看本命和月亮的表是不是整行;手机上页面不横向滚动。
@@ -0,0 +1,341 @@
# 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 。原型与本任务书不一致时,以本任务书为准。原型里的名字、日期、星位都是虚构示例。
## 基线
- `origin/staging` = `c1aa72fcb`(写本单时)。开工时以当时的 `origin/staging` 为准,在进度记录里写下实际基线 SHA。
- BUG 编号起点:**BUG-1420**。开工时运行 `grep -oE "BUG-[0-9]+" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1` 核对最大号;若已被占用就顺延,并在进度记录里写明。
- 本单要动表。产品负责人 2026-10-22 明确要求把它交给 agent 循环执行,并同意其中的动表改动。执行方按文末「循环交接」拆成四个 issue,串行完成;**下面「开工前置命令」里的分支名以「循环交接」表为准**。
## 事故实证(行号按符号定位,以开工时为准)
1. **新账户第一次提问必定被拦。**
- 新账户点数默认是 0(`frontend/supabase/migrations/20260715000000_account_credits.sql` 中 `credits` 的默认值)。
- 前端:`hooks/use-consultation-run.ts` 发送前判断 `account.credits <= 0 && !account.activeSubscription`,成立就调 `openRedeemPaywallOnce()` 弹兑换窗。
- 服务端:`lib/consult/settlement.ts` 的 `reserveCredit` 按 `chat.standard` 预扣,余额不足返回 402。
- 结果:用户建完档、看完「那一刻的天空」,第一次提问就撞上付费。
2. **过场之后没有下一步。** `components/birth-sky/birth-sky-reveal.tsx` 播完后落到 `components/starter-home.tsx` 的空白首页,只有一行问候、输入框和「今日星语」。新用户不知道能问什么。
3. **没有任何功能介绍。** 星盘页(四个页签)、星盘档案、婚恋合盘、个人报告、星历只出现在侧栏,没有任何地方告诉新用户它们存在。仓库里也没有引导组件或引导库。
4. **只能先排自己的盘。** 建档只认本人:`lib/home-profile.ts` 的 `missingProfileStep()` 检查 `profiles` 行,资料不全就进不了首页。想先给伴侣、孩子看的用户必须先填自己。下列文件都依赖这个判断,T3 要逐个核对:
- `hooks/use-profile-onboarding.ts`、`lib/home-bootstrap-run.ts`、`lib/home-warm-start.ts`、`lib/session-list-context.tsx`
- `hooks/use-consultation-run.ts`、`hooks/use-rectification-surface.ts`、`lib/rectification-session-composer-guard.ts`
- `components/daily-starlanguage-binder.tsx`、`components/home-onboarding-shell.tsx`、`app/(app)/page.tsx`
5. **档案没有关系。** 人物只分 `self` / `other`:`lib/subject-birth.ts` 的 `role`,以及 `chat_sessions.chart_profile_role` 的 check 约束(`20260830010000_chat_session_chart_profile_binding.sql`)。`chart_profiles` 上没有伴侣、孩子、父母、朋友之分。
6. **残留死代码。**
- `app/(app)/page.tsx` 的 `onboardingJustCompleted` 从来没有被设为 `true`,所以 `lib/home-profile.ts` 的 `completedOnboardingMessage` 永远不会显示。
- `components/home-onboarding-shell.tsx` 保留着一个走不到的 `"rectification"` 分支。
- `frontend/DESIGN.md` §9「首页只揭幕一次」那一段仍写着首页有「生时校正卡」(BUG-1247 已经删掉)。
7. **`profiles` 上有三列废列。** `onboarding_payload / onboarding_version / onboarding_generated_at` 在前后端都已没有引用。
## 根因
- 首登流程只做到“排好自己的盘”,没有设计“第一次拿到价值”这一步;计费又默认 0 点,两者叠加,激活在第一问就断了。
- 建档默认用户来看自己,但很多人第一次来是想看身边的人。
- 各功能逐个上线,入口只放进侧栏,一直没有统一的“新用户怎么认识它们”的机制。
## 决策记录(产品负责人 2026-10-22 拍板)
| # | 问题 | 决定 |
| --- | --- | --- |
| D1 | 新账户第一个问题要不要免费 | **免费**,每个账户一次,由服务端判定 |
| D2 | 清单里放不放收费项(合盘 19 点、个人报告) | **放**,价格写在清单那一行 |
| D3 | 老用户看不看得到清单 | **能看到**。勾从真实数据推出;进站时七项已满的账户整张清单不出现 |
| D4 | 先帮别人看的用户,自己的资料要不要必填 | **不必填**。先排 TA 的盘、问 TA 的事;清单多一项「加上你自己」,加完才解锁今日星语和合盘 |
| D5 | 关系分类做不做 | **做**:伴侣、孩子、父母、朋友、其他,外加本人。老档案的关系为空,显示「其他」,用户可改 |
| D6 | 动效 | 产品要求加强(“动画太少了”)。按原型的动效规范做,四处超出现行规范,**只限启程流程**:① 转场、飞勾 650–700 ms,合盘靠拢约 1.2 s;② 提示和勾用回弹曲线;③ 七项全满时迸一次星点;④ 侧栏和页签上的新小点闪两次 |
**这份任务书推翻以下既有约束。执行方必须照改,不得以“与 VOICE / DESIGN / 既有 Bug 历史冲突”为由拒改:**
- `frontend/docs/VOICE.md` 规定「首页开场语只有一行,不带追问句」「入口按钮下面平时不写字」。**仅在起步三问出现时放宽**:问候下面可以加一行引导、一枚「这一次不扣点」、三个起步问题,帮别人看时再加一枚「正在看:{名字} · {关系}」。起步三问消失后,首页恢复原规则。在 VOICE.md 写明这条例外和日期。
- `frontend/DESIGN.md` §6 规定「微交互 120 ms、弹窗 180 ms、只用一条缓动、除加载外不循环」。启程流程按 D6 放宽,DESIGN.md 新增「新用户启程」一节,写明例外和适用范围;产品其他地方不变。
- 建档“必须先有本人资料才进首页”(`missingProfileStep` 的现行语义):改为“本人资料完整,**或**档案里至少有一位资料完整的人”,见 T3。
- `DESIGN.md` 首次建盘过场一节「过场正常结束或被点掉后直接露出下一步,不弹任何提示」:**不推翻**。起步三问是首页内容,不是提示;过场结束那一刻仍然不弹任何浮层。
- 兑换窗「只在第一次因点数不足发不出消息时弹」:**不变**。首问免费后,这一刻自然后移到第二问。
## 硬红线
1. 首问免费只能由**服务端**判定和记账,前端只读一个布尔值。前端改余额、跳过检查、本地记标记,都不算完成。
2. 免费额度**原子**消耗:
- 同一账户并发两条请求,最多一条免费。
- 回答失败、被撤回、被停止、退了点,额度都**还在**。
- 寒暄不消耗额度(寒暄本来就不扣点)。
- 给谁问(本人或档案里的人)都算同一份额度。
3. 免费只覆盖 `chat.standard` 的标准档价格。用户在首问前换了高档模型,按正常价扣或只扣差价,二选一,在进度记录里写明选了哪种、为什么。
4. 揭幕后不得出现新的等待动画、骨架或转圈(AGENTS.md §6、DESIGN.md §9)。D6 的动效全部是对用户操作的回应,不能用来填补等待。
5. 起步问题在揭幕那一帧就能渲染,**不得为生成它多调一次模型、多等一个接口**。
6. 一次性提示不得和以下任何一样同时出现:天空过场、兑换窗、任何弹窗或抽屉、回答流式生成中。同一时刻最多一个提示。
7. 遵守冻结规则:`Home()` 的 `useState` / `useRef` 数不得增长(`frontend/tests/home-shell-growth-contract.test.ts`);`scripts/jyotish_api_server.py` 的类方法数不得增长。新逻辑放进 `hooks/`、`lib/` 和组件。
8. 不引第三方引导库或动画库。动效用 CSS 和 Web Animations API,复用 `TASK-site-motion-20261022.md` M1 的 `lib/motion/` 底座;提示用现有的 `@base-ui/react` Popover。
9. 系统开了 `prefers-reduced-motion` 时,D6 的全部动效退成淡入或直接出现,星点和闪烁去掉。
10. 清单的“做没做”**一律从真实数据推导**。唯一例外是“进过星盘页”(没有数据可推),见 T2。
11. 生时校正仍只支持本人。帮别人看时,任何地方都不出现校正提议。
12. 迁移只加不删(AGENTS.md §7.6):三列废列本单不删,关系列可为空。必须真跑 `npm run test:db`;本机没有 Docker 时,按已记录的无 Docker PG17 做法跑;仍做不了就写进 `BLOCKED.md`,不得写成通过。
13. 测试、fixture、文档里不得出现任何真实用户资料。
## 任务分解
### T1 首问免费(服务端,动表)
- 新增迁移,在账户上记录首问免费是否已用(例如 `profiles.first_consult_free_used_at timestamptz`),列名由执行方定。
- `reserve_consultation_usage`(或新 RPC)在同一事务里判断并占用免费额度:
- 可用:本次 `credit_cost = 0`,并标记占用。
- 撤销、退点路径(`cancel_consultation_credit` 等):把额度还回去。
- 回答完成后才算真正用掉。
- 账户接口新增 `firstConsultFree: boolean`。
- 前端发送前检查改为 `credits <= 0 && !activeSubscription && !firstConsultFree` 才拦;输入框占位的 `creditsEmpty` 同样排除 `firstConsultFree`。
- 计费台账照记一条 0 点记录,标明来源是首问免费,后台用量页能看到。
**验收**
- 新账户 0 点:第一问正常发出,余额仍为 0;第二问弹兑换窗,文案沿用原来的。
- 并发两条首问,只有一条是 0 点(DB 测试)。
- 首问中途停止或失败后,`firstConsultFree` 仍为 `true`(DB 测试)。
- 首问是寒暄时,`firstConsultFree` 仍为 `true`。
- 首问选了高档模型:按红线 3 选定的规则扣点,有测试。
- 迁移前已有完成咨询的老账户,迁移后 `firstConsultFree = false`。
### T2 引导状态与关系字段(动表)
- `profiles` 新增 `onboarding_state jsonb`:
- 加对象类型 check;写权限只给服务端路由,照 `20260715040000_agent_onboarding_cache.sql` 的授权写法。
- 内容只有三样:`checklistDismissedAt`、`hintsSeen: string[]`、`chartVisitedAt`。
- 新增一条已登录接口读写 `onboarding_state`:
- 写入只接受上面三个键。
- `hintsSeen` 只接受 T6 表里列出的 id。
- `chart_profiles` 新增 `relation text null`,check 取值为 `partner | child | parent | friend | other`。本人不写这一列,仍由 `profiles` 表示。
- `lib/subject-birth.ts` 的人物类型加上 `relation`,读写接口同步。
- 星盘档案的新增和编辑表单都加关系选择,文案是「TA 是你的」,选项为伴侣、孩子、父母、朋友、其他。
- 人物卡在名字后显示关系小标签;本人显示「本人」;`relation` 为空时显示「其他」。
**验收**
- DB 测试覆盖:`onboarding_state` 读、写、白名单拒绝、未登录 401、不能写别人的。
- `relation` 非法值被拒;为空的老行正常读出。
- 档案页新增一个人(选「孩子」),刷新后关系仍在。
- 编辑一个老档案,可以补上关系。
### T3 建档第二步「这次先看谁的盘」,本人资料可选(D4)
- 建档从三步变成四步:称呼 → **看谁的盘** → 出生日期与时间 → 出生地。顶部进度条四格。
- 第二步的助手消息:「好的,{称呼}。这次想先看谁的盘?以后随时可以再加别人。」消息下面是两个大选项:
- 「我自己」,副文案「看看自己的性格、事业和这几年的运势」。
- 「帮别人看」,副文案「伴侣、孩子、父母或朋友」。选中后平滑展开两样:「TA 叫什么」输入框,以及「TA 是你的」关系选择(五个选项);下面写「你自己的资料可以以后再补。」
- 选「我自己」:后续流程和现在一致,第三步文案改为「好,先看你自己的。……」。
- 选「帮别人看」:
- 出生两步的资料写进 `chart_profiles` 的新行,带名字和关系;本人 `profiles` 只存称呼。
- 第三步文案:「好,先看{名字}的。请告诉我 TA 的出生日期,以及你对 TA 出生时间知道到什么程度。家人朋友的出生时间常常记不准,选个大概时段就行。」
- 第四步:「最后一个问题,{名字}出生在哪里?」
- 天空过场照常,用 TA 的数据;副句写「那一刻,{名字}的上升在……」。
- 建完后,当前人物设为 TA,首页和对话都以 TA 为当前人物。
- `missingProfileStep()` 改为:本人资料完整,**或**档案里至少有一位资料完整的人,就算建完档。中途退出的,下次从缺的那一步接着问。
- 逐个核对事故实证 4 列出的调用点,按“当前人物”而不是“本人”判断:
- 今日星语:本人没有出生资料时,首页不显示「今日星语」按钮。
- 生时校正:当前人物不是本人时,不出现校正提议(红线 11)。
- 「和我合盘」:本人没有出生资料时,按钮位置改为文字「加上你自己后可合盘」。
- 顺手清理事故实证 6 的死代码,并修正 DESIGN.md §9 那一段。
**验收**
- 选「我自己」走完四步,进首页,行为与基线一致(截图或测试断言)。
- 选「帮别人看」走完四步:
- `chart_profiles` 新增一行,名字和关系正确。
- 本人没有出生资料,照样进首页。
- 首页、聊天顶栏的当前人物都是 TA。
- 首页不显示今日星语;对话里不出现校正提议。
- `missingProfileStep` 的单测覆盖:本人完整 / 只有他人完整 / 都不完整 / 中途退出。
- 帮别人看的账户之后补上本人:今日星语出现,「和我合盘」可点。
### T4 起步三问(首页)
- 显示条件:已建完档、`firstConsultFree === true`、当前是空白首页。用掉首问后不再出现。
- 布局:
- 问候下面一行:本人路径写「盘排好了。先问一个问题吧。」;帮别人看写「{名字}的盘排好了。先问一个关于 TA 的问题吧。」,并加一枚「正在看:{名字} · {关系}」。
- 然后一枚「这一次不扣点」,再是三个起步问题,排在输入框上方。
- 三个问题由 `lib/` 里的新函数按当前人物的盘拼出来,数据取自 `lib/daily-starlanguage.ts` 已有的 `ascendantSign` / `moonSign`,不得新增请求:
| # | 本人 | 帮别人看 |
| --- | --- | --- |
| 1 | 「上升{上}、月亮{月},我是个什么样的人」;没有上升时「月亮在{月},我是个什么样的人」;两样都没有时「从我的盘看,我是个什么样的人」 | 同样三档,「我」换成「{名字}」 |
| 2 | 「接下来这两年,事业上该注意什么」 | 「{名字}接下来这两年,事业上该注意什么」 |
| 3 | 「我想问一件具体的事」 | 「我想问一件关于 TA 的事」 |
- 点 1、2 直接发送,和用户自己打字发送走同一条路径。
- 点 3 不发送,只让输入框亮一下并获得焦点。
**验收**
- 两条路径 × 三种盘数据(全有 / 无上升 / 都无),各一条单测,文案逐字匹配上表。
- 点 3 后不发送,输入框获得焦点。
- 首问回答完成后回到首页,起步三问消失,首页回到原样。
### T5 启程清单
- 七项和“做过”的判定,全部从服务端或现有数据推导:
| id | 本人路径文案 | 帮别人看文案 | 做过 = |
| --- | --- | --- | --- |
| `self` | 排好自己的盘 | 排好{名字}的盘 | 已建完档(T3 口径) |
| `ask` | 问第一个问题 | 同左 | 至少一条完整的非寒暄回答 |
| `daily` | 打开今日星语 | 同左(本人没资料时「去做」跳到档案页) | 至少一次今日星语咨询 |
| `chart` | 看看自己的星盘 | 看看星盘 | `onboarding_state.chartVisitedAt` 有值 |
| `person` | 加一位家人或朋友 | **加上你自己** | 本人路径:档案 ≥ 2 人;帮别人看:本人资料完整 |
| `syn` | 和 TA 合一次盘 · {价} 点 | 和{名字}合一次盘 · {价} 点 | 至少一份合盘报告 |
| `report` | 生成个人报告 | 同左 | 至少一份个人报告 |
- 帮别人看的账户,清单顺序是 `self, ask, person, daily, chart, syn, report`。
- 本人路径 / 帮别人看,按“本人资料是否在档案里第一个建档”判断,不另存标记。如果本人和他人都已完整,用本人路径的文案。
- 合盘价格从计价表读(现为 19),不写死;报告行有价格就显示。
- 功能开关关闭时对应行不显示:
- `product.compatibility` / `account.compatibility` 关闭 → 没有 `syn`。
- 个人报告开关关闭(`product.report_center` 与 `PERSONAL_REPORT_ENABLED`,见 `lib/personal-report-entitlement.ts`)→ 没有 `report`。
- 入口:首页和聊天顶栏右侧一枚胶囊「启程 n/N」,带进度环。
- 点开后,手机上是底部抽屉,桌面上是弹出层。
- 每行未完成的有「去做」,直接跳到真功能:
| 项 | 「去做」跳到哪里 |
| --- | --- |
| `ask` | 首页,聚焦输入框 |
| `daily` | 触发今日星语;本人没资料时跳到档案页 |
| `chart` | `/chart` |
| `person` | `/people` 并打开新增表单(帮别人看时打开「加上你自己」) |
| `syn` | `/people` |
| `report` | `/reports` |
- 抽屉底部「先不看了」→ 写 `checklistDismissedAt`,胶囊此后不再出现。
- 七项全满时,抽屉最后展开一次:标题「都走过一遍了」,正文「以后有新东西,我会在用到的时候再告诉你。」。关掉后写 `checklistDismissedAt`。
- D3:老用户照常显示,勾从数据推导;进站时已经全满的账户,胶囊和「都走过一遍了」都不出现。
**验收**
- 七项判定在两条路径下各一条单测,含边界:寒暄不算 `ask`;本人一人不算 `person`;帮别人看时加了第二个他人也不算 `person`。
- 功能开关关闭时行数变化有测试。
- 新账户进首页时 `self` 已勾,显示「启程 1/7」。
- 老账户 fixture 只差 `chart`、`daily` 时显示「5/7」;全满时不显示。
- 点「先不看了」后刷新、在另一会话打开,都不再出现。
- 手机 375 px 宽无横向滚动,胶囊不挤占输入框。
### T6 一次性提示
一律用一个通用的 `useOneTimeHint(id, condition)`(或同等实现)。id 固定,即 T2 的白名单:
| id | 何时出现 | 指向 | 文案 |
| --- | --- | --- | --- |
| `follow-up` | 第一条完整回答流式结束后 | 追问胶囊 | 想接着聊,点下面任一个就直接问。 |
| `sidebar-chart` | `follow-up` 已看过、`chartVisitedAt` 为空,第一次打开侧栏时 | 侧栏「星盘」 | 你的盘、大运和分盘都在这里。 |
| `tab-chart` | 第一次看到星盘页的「星盘」页签 | 页签内容上方 | 这是你的本命盘,北印度画法。最上面的菱形是第一宫,上升就在这里,往左逆时针依次是二宫到十二宫。 |
| `tab-info` | 第一次打开「基础信息」 | 同上 | 排盘用的原始资料和每颗星的位置都在这里。出生时间不准时,这一页最上面会写明误差。 |
| `tab-dasha` | 第一次打开「大运」 | 同上 | 一段大运管几年到十几年,主色那段是你现在走的。点开任意一段,能看到里面的小运。 |
| `tab-west` | 第一次打开「西洋盘」 | 同上 | 同一时刻的天空,换成西洋占星的算法。两种黄道相差约 24 度,所以星座可能和印度盘不一样。 |
| `syn-ready` | 档案里第一次有了两个人(含本人),且还没有合盘 | 「和我合盘」按钮 | 有了你们两个人,就能看你们俩的关系画像。价格写在按钮上。 |
| `ephemeris` | 第三次打开今日星语之后,下一次打开侧栏时 | 侧栏「星历」 | 每天的天象和行运在这里,可以按日期翻。 |
- 当前人物不是本人时,`tab-chart` 文案里的「你的」改成「{名字}的」。
- 侧栏「星盘」在 `ask` 完成、`chart` 未完成时带一个小圆点;手机上的汉堡按钮也带。`chart` 完成后圆点消失。
- 星盘页四个页签,没看过的右上角带小点,看过就消失(跟随 `hintsSeen` 中 `tab-*` 是否存在)。
- 每条提示点「知道了」、点页面任何地方或按 Esc 即消失,写入 `hintsSeen`,之后不再出现。
- 被红线 6 挡住的提示排队,条件解除后再出,不丢。
**验收**
- 每条提示的出现条件和互斥(流式中、弹窗开着、过场中都不出)各有测试。
- 看过后刷新不再出现;四个页签各自记。
- 提示出现时不抢焦点;Esc 可关闭。
### T7 动效(D6)
照原型「动效规范」表实现,令牌写进 DESIGN.md「新用户启程」一节:
| 类别 | 用在哪 | 时长 | 曲线 |
| --- | --- | --- | --- |
| 按下反馈 | 按钮、问题卡、选项、关系标签被按下时轻微缩小 | 120 ms | `--ease-out` |
| 进场 | 建档消息与卡片、起步问题、追问胶囊、档案卡、目录行;依次上浮,间隔 50–80 ms | 260 ms | `--ease-out` |
| 弹出 | 一次性提示、「这一次不扣点」、勾 | 380 ms | 回弹 `cubic-bezier(.34,1.56,.64,1)` |
| 跟手 | 称呼从输入框飞成气泡;起步问题飞成气泡 | 380 ms | `--ease-out` |
| 勾飞进进度环 | 清单每完成一项:从操作位置沿弧线飞向胶囊,进度环走一格,胶囊轻跳一下 | 700 ms | 弧线 |
| 画出来 | 建档进度条;星盘格线、西洋盘圆与分隔线、大运条 | 520–900 ms | `--ease-out` |
| 场景转换 | 出生地→天空:从点击处圆形展开;天空→首页:淡出放大,首页同时上浮 | 650–700 ms | `--ease-out` |
| 合盘 | 两个头像从两侧靠拢,泛起一圈光,再展开关系画像 | 约 1.2 s | `--ease-out` |
| 庆祝 | 七项全满时,进度环处迸一次星点 | 1000 ms,只一次 | 减速 |
- 每段动效只在元素第一次出现时播放,重绘和来回切换不重播。
- 除回答流式中的呼吸点外,不得有任何无限循环。
- 星点、闪烁在 `prefers-reduced-motion` 下去掉,其余退成淡入(红线 9)。
- 天空过场本身的加强(星星先亮起再汇聚、星体逐个点亮并从上升连线、两行字依次浮出)在 `components/birth-sky/` 内实现,沿用现有的“3 秒内素材没到齐就静默跳过”。
- 动效只在 `opacity`、`transform`、颜色、`clip-path` 上做,不触发布局。
**验收**
- `prefers-reduced-motion` 下没有位移动画(测试或录屏)。
- 本机 Chrome 性能面板录一次完整新用户流程,没有超过 50 ms 的长任务由动效引起;做不了就写环境缺口。
- 真机清单里逐条列出动效,由产品负责人核对。
### T8 文档
- `CONTEXT.md` glossary 加:启程清单、起步问题、一次性提示、关系(人物)。
- `frontend/DESIGN.md`:
- 新增「新用户启程」一节,写胶囊、抽屉、提示的尺寸,T7 的动效令牌,D6 例外的范围,以及互斥规则。
- 修正 §9 那一段。
- `frontend/docs/VOICE.md`:写明本单的例外,收录全部新文案。
- `CHANGELOG.md`:写一条用户可感知的变更。
- `docs/BUG_HISTORY.md`,状态、现象、根因、修复、验证、防复发写全:
- BUG-1420:新账户首问必撞付费,激活断点。
- BUG-1421:首登过场后无下一步,加上死代码。
- BUG-1422:建档只能先看本人。
- `docs/testing/onboarding-journey-20261022.md` 真机清单:
- 新邮箱注册,两条路径各走一遍全程。
- 老账户看清单。
- 手机和桌面各走一遍。
- 开启减少动态效果再走一遍。
- 逐条动效核对。
## 让步顺序
做不完时按这个顺序砍,越靠前越先砍:
1. `ephemeris` 提示
2. 合盘靠拢动画(直接展开关系画像)
3. 七项全满的星点
4. 桌面版清单的弹出层(先统一用抽屉)
5. 老用户清单(D3)可以晚一个版本上,但不得改成其他形态
**不可让:** T1、T2、T3、T4,以及 T5 的新用户路径。
## 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/onboarding-journey-20261022 .worktrees/onboarding-journey-20261022 origin/staging
cd .worktrees/onboarding-journey-20261022
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 && npm test 2>&1 | tail -8 # 记下基线测试数
npm run build # 记下 / 是否 Static、首屏 gzip
```
## 循环交接
- **执行方**:Grok(Windows + Docker)。每个 issue 一个分支,名字见下表;只推自己的分支。
- **Grok 自跑**:
- `tsc --noEmit`、`npm run lint`、相关测试文件、`npm run build`。
- 动了数据库访问层的 issue(一、二、三)必须跑 `npm run test:db`,结果与 `tools/agent-loop/windows-known-failures.txt` 逐条比对,新增的失败一条都不许有。
- **Claude 验收时跑**:
- 前端全量 `npm test`,逐条对比基线。
- 改了 `scripts/**` 或 `tests/**` 时,跑 Python 快速门。
- 复核首屏 gzip 和冻结合同。
- 每个 issue 在同一变更里写好自己那份 `docs/BUG_HISTORY.md`、`CHANGELOG.md`、`CONTEXT.md`、`DESIGN.md`、`VOICE.md` 改动(T8 按 issue 拆开做)。
- 真机清单 `docs/testing/onboarding-journey-20261022.md` 由 issue 一创建,后面的 issue 往里追加。
- 迁移只加不删。staging 部署前会自动应用迁移;上生产前,产品负责人需要先跑迁移。
| issue | 内容 | 分支 | 前置 |
| --- | --- | --- | --- |
| 新用户启程(一)数据层 | T1 首问免费 + T2 引导状态与关系字段(含档案页关系选择) | `codex/onboarding-1-data-20261022` | 无 |
| 新用户启程(二)建档与首页 | T3 先看谁的盘 + T4 起步三问 | `codex/onboarding-2-intake-20261022` | (一)合入 |
| 新用户启程(三)清单与提示 | T5 启程清单 + T6 一次性提示 | `codex/onboarding-3-checklist-20261022` | (二)合入 |
| 新用户启程(四)启程动效 | T7 + DESIGN.md「新用户启程」一节 + 真机清单收尾 | `codex/onboarding-4-motion-20261022` | (三)和「全站动效(一)」都合入;复用 `lib/motion/` 底座 |
## 交付验收口径(Claude 验收时逐条核)
- `tsc --noEmit` 0 错;`npm run lint` 0 error。
- `npm test` fail=0,且总数不低于基线;改了任何既有断言,写明「原值 / 新值 / 原因」。
- `npm run test:db` 真跑通过,或按红线 12 写明环境缺口。
- 改动含 `scripts/**` 或 `tests/**` 时跑 Python 快速门。
- `next build` 后 `/` 仍是 `○ Static`;首屏 gzip 变化在 ±2% 内,超出要写原因(动效代码应按需加载,不进首屏)。
- 冻结合同测试全过。
- 部署后 `https://staging.jyotisha.chat/api/health` 的 `deployment.gitCommit` 等于交付 SHA。上生产前必须先跑本单的迁移。
- 真机清单交给产品负责人,不得写成“已通过”。
@@ -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 清理时)。
+181
View File
@@ -0,0 +1,181 @@
# 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 。原型和本任务书不一致时,以本任务书为准。
## 基线
- 写本单时的 `origin/staging` 是 `8743f6151`。每个 issue 开工时,以当时的 `origin/staging` 为准。
- 本单不动表、不改 workflow,所以交给 agent 循环执行,分三个 issue;拆分见文末「循环交接」。
- 本单**排在** `TASK-onboarding-journey-20261022.md` 的 issue 一至三之后,**排在**它的 issue 四(启程动效)之前。启程动效直接复用本单 M1 的动效底座。
- BUG 编号:开工时用 `grep -oE "BUG-[0-9]+" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1` 核对当前最大号,从下一个号开始。
## 现状实证
1. `frontend/DESIGN.md` §6「Motion & Interaction」现行规定:
- 只有一条缓动曲线 `--ease-out`。
- 时长分档:Micro 120 ms、Standard 180 ms、Sheet 280 ms。
- 「Spatial: Instant」:侧栏状态变化和手机侧栏都是瞬间切换,没有过渡。
2. 各处切换现在都是瞬间完成:
- 侧栏导航(`components/app-sidebar.tsx`)换页、当前项高亮。
- 星盘页签与盘型(`components/chart-page/chart-page-view.tsx`、`chart-type-picker.tsx`):盘型一换,整盘重画。
- 星历换日期(`components/ephemeris/ephemeris-page.tsx`)。
- 档案增删(`components/people/people-page.tsx`)。
- 会话收藏、删除(`components/sidebar-session-row.tsx`)。
- 报告中英切换(`components/personal-report/report-actions.tsx`)。
3. 设置弹窗(`components/account-dialog-overlay.tsx`)只有淡入;导出抽屉(`components/personal-report/report-export-drawer.tsx`)和顶部提示(`components/ui/sonner.tsx`)用的是各自的默认过渡,没有统一。
4. 依赖里没有动画库。`gsap` 只在 `components/chat-message-row.tsx` 里用。
## 决策记录(产品负责人 2026-10-22 拍板)
| # | 决定 |
| --- | --- |
| D1 | **全站动效规范按下表重写**,推翻 DESIGN.md §6 的 180 ms 上限和「Spatial: Instant」。新用户启程里那几处更长的动效(飞勾 700 ms、合盘靠拢、七项全满的星点)仍只在启程流程里用 |
| D2 | **回弹曲线只用于「成功」和「提示」**:顶部提示、勾、一次性提示。换页、换盘、弹窗、抽屉一律用 `--ease-out` |
| D3 | **不引入动画库**。页面切换用浏览器原生的页面过渡能力(View Transitions),其余用 CSS 和 Web Animations API;不支持的浏览器直接切换,不报错。先做半天验证(M0)再铺开 |
**新规范(写进 DESIGN.md §6,替换原表):**
| 动效 | 用在哪 | 时长 | 曲线 |
| --- | --- | --- | --- |
| 按下反馈 | 按钮、行、卡片 | 120 ms | ease-out |
| 页面切换 | 侧栏换页。旧页淡出,新页按侧栏顺序从下方或上方滑入 12–18 px;标题同步换 | 进 280 ms / 出 150 ms | `--ease-out` |
| 选中块跟随 | 侧栏当前项、分段控件 | 260 ms | `--ease-out` |
| 页签切换 | 星盘四页签、设置三分区。下划线滑动,内容按左右方向滑入 | 260 ms | `--ease-out` |
| 盘型切换 | 星体从旧宫位移动到新宫位,上升星座淡入淡出 | 440 ms | `--ease-out` |
| 换人 | 头像翻面,盘格线重画,星体就位 | 300 ms + 800 ms | `--ease-out` |
| 翻日期 | 日期上下滚动,内容左右翻页,方向跟随前后 | 260 ms | `--ease-out` |
| 发送消息 | 用户气泡从输入框升起 | 260 ms | `--ease-out` |
| 切换会话 | 对话内容交叉淡入,不滑动 | 200 ms | `--ease-out` |
| 列表增删与重排 | 新项展开出现,删除项收起,其余平滑补位 | 260–280 ms | `--ease-out` |
| 弹窗 | 从触发它的按钮处放大出现,关闭时缩回去 | 220 / 160 ms | `--ease-out` |
| 抽屉 | 导出、手机侧栏从边缘滑入,遮罩同时淡入 | 280 / 200 ms | `--ease-out` |
| 顶部提示 | 从顶部落下 | 320 ms | 回弹 `cubic-bezier(.34,1.56,.64,1)` |
| 数字变化 | 点数余额上下滚动:扣点往下,充值往上 | 360 ms | `--ease-out` |
| 中英切换 | 段落依次淡出、换文字、再淡入,间隔 40 ms | 约 320 ms | `--ease-out` |
| 展开收起 | 大运小运、思考步骤(沿用现状) | 240 ms | `--ease-out` |
## 硬红线
1. 不新增任何动画或过渡依赖。`gsap` 不扩散到 `chat-message-row.tsx` 以外的地方。
2. `next build` 后 `/` 仍为 `○ Static`。首屏 gzip 变化在 ±2% 以内;动效代码按需加载,不进首屏。
3. 遵守 `prefers-reduced-motion`:位移和缩放全部去掉,只保留 ≤150 ms 的淡入淡出;数字直接变化。
4. 动效不得挡住操作:
- 进行中可以继续点击,新动作直接打断旧动效。
- 不得为了等动效播完而禁用按钮或延迟路由。
5. 动效只回应用户操作,不得用来填补等待。「一次等待一次揭幕」不变:揭幕后没有骨架、转圈、「正在加载」(星盘页盘位骨架仍按 BUG-1016 例外处理)。页面切换时目标页的数据没到,照现有规则留白,不得播一段动效来拖时间。
6. 只对 `transform`、`opacity`、颜色、`clip-path` 做动效,不触发布局;列表补位用 FLIP(先记录旧位置,改完再从旧位置滑过去)。
7. 遵守冻结规则:`Home()` 的 `useState` / `useRef` 数不增长。动效逻辑放在 `lib/motion/` 和各组件里。
8. 流式回答的逐字释放、思考步骤展开等现有行为不改。
9. 改 UI 的同一提交里同步更新 `frontend/DESIGN.md`。
## 任务分解
### M0 页面过渡验证(半天,issue 一的第一步)
分别试两种接法,在进度记录里写清结论:
- **A**:Next 16 的 `experimental.viewTransition` 加 React `<ViewTransition>`。
- **B**:在侧栏导航处自己用 `document.startViewTransition` 包住路由跳转,等新页提交后再结束过渡。
每种接法在下列环境里各走一遍「对话 → 星盘 → 星历 → 档案 → 报告」:
- 桌面 Chrome
- 桌面 Safari
- iOS Safari(Grok 侧做不了就写成环境缺口,留给真机清单)
- Android Chrome
每次记录:能否用、首屏 gzip 变化、有无闪烁。
选型规则:选能用、改动小的那种。两种都不稳定时,退回方案 C:只在主内容区做交叉淡入,不用 View Transitions。三种结果都要写进进度记录。
**验收**:进度记录里有一张「方案 × 浏览器」表,并写明最终选了哪种。
### M1 动效底座(issue 一)
- `globals.css` 里加动效令牌:上表各档时长,以及 `--ease-out` 和 `--ease-spring`。
- `lib/motion/` 里放以下工具,都要能被打断,并且尊重减少动态效果:
- `prefersReducedMotion()`
- `animate()`:对 Web Animations API 的薄封装
- `flipList()`:列表补位
- `rollNumber()`:数字滚动
- `swapContent()`:内容交叉换场
- DESIGN.md §6 按上表重写,注明 D1 / D2 / D3 和启程流程的例外。
**验收**
- `lib/motion` 有单测:减少动态效果时不产生位移;连续调用两次时,第二次会打断第一次。
- 首屏 gzip ±2%。
### M2 第一批:对话与页面框架(issue 一)
- 页面切换:按 M0 选定的方案实现。方向按侧栏顺序:点到下面的页,新页从下方进;点到上面的页,从上方进。标题同步换。
- 侧栏当前项的底色块滑到新位置。
- 手机侧栏从左滑入、遮罩淡入(推翻「Spatial: Instant」)。
- 发送消息时,用户气泡从输入框位置升起。点追问胶囊时同理,从胶囊位置升起。
- 切换会话时,对话内容交叉淡入。
- 点数余额在侧栏和聊天顶栏同步滚动。
- 设置弹窗、确认框从触发按钮处放大出现,关闭时缩回去。
- 顶部提示从顶部落下,用回弹曲线。
**验收**
- 每项动效至少有一个组件测试或交互测试:确认动效被调用,且减少动态效果时不被调用。
- 快速连点侧栏 5 次:最终停在最后点的那一页,且没有残留的旧页。
- `tsc` / `lint` / `npm test` / `build` 都过。
### M3 第二批:星盘页(issue 二)
- 四个页签:下划线滑动,内容按左右方向滑入。
- 盘型切换:同一颗星在新旧盘型里都存在时,从旧位置移动到新位置;新盘里才有的星淡入,消失的星淡出。上升标签交叉淡入。北印度盘和西洋轮盘都要做。
- 换人:头像翻面,盘格线重画(stroke 描线),星体就位。
- 大运展开小运沿用现有的展开收起,时长统一为 240 ms。
**验收**
- 盘型切换后,每颗星的最终位置与切换前一致的直接渲染逐像素相同(快照测试或坐标断言)。动效不得改变数据。
- 连续快速切换 D1 → D9 → D10,最终停在 D10 的正确位置。
### M4 第三批:其余页面(issue 三)
- 星历前一天 / 后一天:日期上下滚动,内容按方向左右翻页;点「今天」时按相对方向翻。
- 档案加人、删人,会话收藏置顶、删除:都用 FLIP 补位。删除动效放完之后才从列表里移除,但数据请求不等动效。
- 报告中英切换:段落依次交叉淡入。继续保持 BUG-1126 的 key 修复,切换后不得残留旧语言的节点。
- 导出抽屉(`report-export-drawer.tsx`)从边缘滑入;手机上从底部滑入。
**验收**
- BUG-1126 的回归测试仍然通过。
- 删除会话、删除人物的接口失败时,行会回到原位,并给出现有的错误提示。
## 让步顺序
做不完时按以下顺序先砍:
1. 中英切换的逐段错开(改成整段一起淡入淡出)
2. 换人时的格线重画
3. 星历日期的上下滚动(只保留内容翻页)
4. 数字滚动
页面切换、侧栏选中块、弹窗、抽屉、顶部提示、盘型切换不让。
## 循环交接
- **执行方**:Grok(Windows + Docker)。每个 issue 一个分支,名字写在下表;只推自己的分支。
- **Grok 自跑**:`tsc --noEmit`、`npm run lint`、相关测试文件、`npm run build`(记录 `/` 是否 Static 和首屏 gzip)。
- **Claude 验收时跑**:前端全量 `npm test`,逐条对比基线;首屏 gzip 用同一种量法复核;DESIGN.md 是否同步更新。
- **真机清单**:`docs/testing/site-motion-20261022.md`,由 issue 一创建,后两个 issue 往里追加。清单逐条写「点哪里、应该看到什么」,交给产品负责人走。
- 每个 issue 在同一变更里更新 `docs/BUG_HISTORY.md`(体验改进类记 `enhancement`)、`CHANGELOG.md` 和 `frontend/DESIGN.md`。
| issue | 内容 | 分支 | 前置 |
| --- | --- | --- | --- |
| 全站动效(一) | M0 + M1 + M2 | `codex/site-motion-1-20261022` | 新用户启程(三)合入 |
| 全站动效(二) | M3 | `codex/site-motion-2-20261022` | 全站动效(一)合入 |
| 全站动效(三) | M4 | `codex/site-motion-3-20261022` | 全站动效(二)合入 |
## 开工前置命令
```bash
git fetch origin --prune
git worktree add -b <上表分支> .worktrees/<上表分支去掉 codex/> origin/staging
cd .worktrees/<…> && 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 && npm run build # 记下基线 gzip
```
@@ -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,不再单独执行。