diff --git a/docs/tasks/README.md b/docs/tasks/README.md index f06dec61..a80f9540 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -149,6 +149,13 @@ | `TASK-secondary-page-entry-20260918.md` | `PROGRESS-secondary-page-entry-20260918.md` | 真机反馈:星盘 / 星历 / 报告进入时抖一下——三页都是「矮的等待文案块 → 高的正文块」一次推挤,且 `use-chart-page` 无缓存所以每次进入都抖(BUG-966)。产品已拍板方案一:统一外壳 + 缓存 + 预取消灭中间态,**不加 spinner**,红线不动。另含 BUG-967:标签页跨过部署后客户端导航静默失效(BUG-965 已复现确认,刷新即恢复),要按 `NEXT_PUBLIC_GIT_COMMIT` 与 `/api/health` 比对自愈。基线 `41902067` | 待验收 | `9ccb65b7` | | `TASK-account-dialog-inert-20260918.md` | `PROGRESS-account-dialog-inert-20260918.md` | 真机:账户弹窗打开后整个弹窗点不动、退出登录做不了,刷新依旧——`e4e73f56` 把 `SidebarInset` 搬进 layout 后 `inert={modalOpen}` 罩住了没有 portal 的 `AccountDialogOverlay`(BUG-968,P0,代码级确认)。另含 BUG-969:校正「换一件事问」后无下文,服务端已出下一题且快照重算完整,客户端停在开场状态,GET 路由与客户端两端静默,本单只做可观测 + 不静默 + 题干进正文。基线 `1061514f` | 待验收 | `824ecff0` | | `TASK-first-paint-dead-screen-fallback-20260917.md` | — | 真机:首页永远停在「正在载入账户」,兜底全在没跑起来的 bundle 里(BUG-936 investigating)。根 layout 加与 bundle 无关的内联兜底 + 去掉本仓正则后行断言 | 待领取 | — | +| `TASK-chart-profile-update-consistency-20260922.md` | `PROGRESS-chart-profile-update-consistency-20260922.md` | 更新出生资料后 `/api/account → /api/chart-view` 真值一致、adopted tuple 完整、错误分类可诊断、旧 chart snapshot 失效;串行核对既有星盘打开链路,不把 `chart_profiles` 当 `/chart` 真值 | 待领取 | — | +| `TASK-consultation-subject-binding-20260922.md` | `PROGRESS-consultation-subject-binding-20260922.md` | 普通聊天按会话绑定解析 self/owned other 的服务端人物资料;资料更新读最新,删除/越权/role 冲突 fail-closed,不 fallback 到本人;**先于人物选择器** | 待领取 | — | +| `TASK-chat-subject-picker-20260922.md` | `PROGRESS-chat-subject-picker-20260922.md` | 开始聊天选择人物;顶部“当前星盘”可访问入口;第一条消息后锁定,换人新建会话;依赖 subject binding 合入 staging | 待领取 | — | +| `TASK-report-public-content-20260922.md` | `PROGRESS-report-public-content-20260922.md` | 普通报告 canonical allowlist/projection:页面、下载、API 统一只展示用户可读正文、结论和必要限制说明;隐藏 workflow/技法/评分/raw tool/任务元数据 | 待领取 | — | +| `TASK-home-bootstrap-reliability-20260922.md` | `PROGRESS-home-bootstrap-reliability-20260922.md` | BUG-936 首屏 bundle 无关 fallback、可读失败状态和用户触发重试;复用既有 first-paint 任务,不重复添加 loading,不提前标记 resolved | 待领取 | — | +| `TASK-starter-entry-soften-20260922.md` | `PROGRESS-starter-entry-soften-20260922.md` | 弱化“今日星语 / 生时校正”入口默认边框与交互状态;保留 native button、44px 命中区、既有动作和 reduced-motion,不恢复旧卡片 | 待领取 | — | +| `TASK-icon-motion-audit-20260922.md` | `PROGRESS-icon-motion-audit-20260922.md` | P2 图标/轻量动效库存与合同审计;优先 Lucide/既有 glyph,不强行换图标、不新增 loading 或循环动画 | 待领取 | — | | `TASK-consultation-answer-start-anchor-20260917.md` | `PROGRESS-consultation-answer-start-anchor-20260917.md` | 主会话回答落在结尾:`useConversationScrollAnchor` 是贴底跟随,流式期间视口钉在最后一个字,回答开头滚出视口;改为发送后问题钉顶、回答向下长、长出视口显示「跳到最新」、末尾动态留白;产品追加拍板:校正面同一语义(推翻 BUG-041/048 贴底),本轮开头 = 用户行或新助手行。BUG 段 930 起 | 已验收(经修复单) | `worktree/green-harbor-5be3` | | `TASK-consultation-answer-start-anchor-fix-20260917.md` | `PROGRESS-consultation-answer-start-anchor-fix-20260917.md` | 验收修复单:F1 头就是留白行时留白按整视口算(BUG-931);F2 留白只在钉住期间存在(BUG-932);前置:先修 e4e73f56 的两处 TS 错否则门禁不过 | 已验收 | `cc1a8980`(Claude 验收:tsc 0 / lint 0 error / npm test 3457 条 39 红与 11c0028d 逐条一致、新增 2 条绿 / `next build --webpack` 通过、`/` Static、首屏 gzip 591,242(较 09-16 基线 582,800 +1.45%,含会话列表单)/ Chrome 真实布局 S1–S6 全部通过,S6 新助手行距顶 16px 且增高不动,S5 不再写留白);真机六条欠 | | — | `PROGRESS-starter-greeting-20260917.md` | 首页开场语改成 claude.ai 式单行问候:`starter-greeting.ts` 的「称呼 + 追问句」五时段十五条收成三个池子(时段 / 星期 / 回访,按 `variantSelection` 确定性取一条),副标题行与 `.starter-salutation` 下线,h1 降到 `clamp(24px, 2.8vw, 30px)`;追问移到输入框占位符「想聊什么都可以」。产品直接拍板,非 Bug,不占 BUG 号 | 待验收 | `codex/starter-greeting-20260917` | diff --git a/docs/tasks/TASK-chart-profile-update-consistency-20260922.md b/docs/tasks/TASK-chart-profile-update-consistency-20260922.md new file mode 100644 index 00000000..16a67384 --- /dev/null +++ b/docs/tasks/TASK-chart-profile-update-consistency-20260922.md @@ -0,0 +1,107 @@ +# 任务书 · 更新出生资料后星盘真值一致性与可诊断错误(2026-09-22) + +## 0. 基线与串行关系 + +- 基线 commit:`1bc6a954c597e2817fe72bfedad93119ab19a527`(当前 `origin/staging`)。执行方开工前必须重新 `git fetch origin --prune`,再以最新 `origin/staging` 为基线。 +- 执行分支:`codex/chart-profile-update-consistency-20260922`;工作树:`.worktrees/chart-profile-update-consistency-20260922`。 +- 本单是星盘现场测试暴露的功能修复,不是整页重设计。与既有 `TASK-chart-page-blocking-open-20260915.md`、`TASK-chart-vedastro-decouple-20260915.md`、`TASK-readonly-pages-fix-20260916.md` 有交集时必须串行:先核对这些分支是否已合入 staging;未合入时不得同时改同一符号,按“既有星盘打开链路 → 本单资料真值一致性”顺序执行。 +- 本单不接入 `chart_profiles` 作为 `/chart` 的主资料来源;`/chart` 继续使用账户权威 `profiles`。人物资料库属于普通聊天的另一条 subject binding 任务。 +- 与既有星盘打开链路任务严格串行:先完成并核对 `TASK-chart-page-blocking-open-20260915.md`、`TASK-chart-vedastro-decouple-20260915.md`、`TASK-readonly-pages-fix-20260916.md` 的 staging 状态,再处理本单真值/缓存;不得并行修改相同 chart-view loader、engine client 或错误映射符号。 + +## 1. 事故实证 + +会议现场反馈:用户已经更新出生时间,但打开星盘仍提示没有可显示内容,或点击后无法打开。按产品设计,填写出生资料后应能生成并查看星盘。 + +调查已确认的代码事实(按符号定位,不依赖脆弱行号): + +- `frontend/src/app/(app)/chart/page.tsx` 只挂载客户端 chart 壳;`frontend/src/hooks/use-chart-page.ts` 通过 `refreshChartPage()` 请求 `/api/chart-view`。 +- `frontend/src/lib/chart-view-client.ts` 对响应做 JSON/Zod 合同解析;解析失败或请求失败会归并为通用 `chart_unavailable`。 +- `frontend/src/lib/secondary-page-data.ts` 的 `chartCache` 是模块级单值缓存,没有账户/profile 版本键;资料 PATCH 成功后没有明确的 chart snapshot 失效边界。 +- `frontend/src/lib/chart-view-service.ts` 从 `profiles` 读取 `ACCOUNT_BIRTH_SELECT`,再经 `server-owned-birth-profile.ts` 与 timezone resolver 组装排盘资料。 +- accepted/confirmed 资料存在 `active_birth_date` 但缺 `active_birth_timezone_offset` 时,`server-owned-birth-profile` / `birth-profile-timezone` 会抛出 `adopted_birth_calculation_incomplete` 或 timezone error;当前错误路径可能最终只显示通用不可用状态。 +- `frontend/src/lib/account-profile-patch.ts` 的普通账户 PATCH 主要处理 active time/status,不会在所有声明变化后原子维护 adopted date/offset/provenance;迁移 `frontend/supabase/migrations/20260920020000_adopted_birth_date.sql` 的 trigger 会在特定字段变化时清理 adopted tuple。这需要实证核对,不能在任务书外擅改出生资料语义。 +- `loadChartView` 对 profile 查询错误的区分不足,数据库读取失败可能被误认为资料不完整。 + +相关历史:BUG-005、BUG-009、BUG-018、BUG-073、BUG-076、BUG-715~717、BUG-936。BUG-936 是首页 JS 死屏,不替代本单的 `/chart` 数据真值问题。 + +## 2. 根因 + +当前已确认的是“错误分类、资料更新一致性和客户端旧快照存在断点”;具体现场账号命中的单一数据库状态尚未证明。因此本单不得把某个假设写成已确认根因。优先验证并修复三层: + +1. `/api/account` PATCH、数据库 trigger/adoption 与 `profiles` 中 date/time/offset/provenance 的原子一致性; +2. `GET /api/chart-view` 对 profile/query/timezone/engine/schema 各类失败的结构化分类; +3. profile 更新、account refresh、chart request 和 `chartCache` 之间的失效与竞态。 + +## 3. 决策记录(产品授权) + +- 产品授权:填写出生资料后必须能够生成并查看星盘;不能用“资料还在,过一会儿再打开”掩盖永久错误。 +- 保持现有安全语义:reported、accepted、confirmed、candidate、active provenance 不得混为一谈;confirmed 的普通资料编辑不得覆盖受保护的 active time。 +- 失败要可理解、可诊断;不得把数据库错误、资料不完整、时区 adopted tuple 错误、引擎忙/超时和响应格式错误静默归成一个空状态。 +- 本单不顺手改星盘算法、Dasha、校正算法、Node/Ayanamsa 业务规则,也不把 `chart_profiles` 作为 `/chart` 真值。 +- 若现有迁移/trigger 与上述边界冲突,执行方必须停在任务书与 BUG 记录中,不能自行放宽或删除保护。 + +## 4. 硬红线 + +1. 不能信任客户端出生日期、时间、地点或 offset 覆盖服务端 profile 真值。 +2. 不得通过清空 `active_birth_*`、放宽 accepted/confirmed 校验或 fallback 到不一致声明值来“修复”页面。 +3. 不得把数据库 query error 当作 `birth_profile_incomplete`;日志不得记录姓名、出生日期/时间、坐标、完整请求体、Cookie、JWT、密钥或模型原文。 +4. 不新增 spinner、骨架屏或第二套滚动/加载机制;继续遵守 `frontend/DESIGN.md` 与 `frontend/docs/VOICE.md`。 +5. 不修改 `scripts/jyotish_api_server.py` 增长红线,不改 `.gitea/workflows/**`,不提升 `main`。 +6. 若动表,必须通过向后兼容迁移并运行 `npm run test:db --prefix frontend`;删除/重命名/收紧约束必须另轮。 +7. 任何既有断言变更都要写“原值 / 新值 / 原因”;测试名不得减少。 + +## 5. 任务分解与验收标准 + +### T1 · 端到端重放并锁定服务端真值 + +- 用 synthetic/golden fixture 覆盖 reported、accepted、confirmed、跨午夜 adopted date;不要写真实用户资料。 +- 逐段核对 `PATCH /api/account → GET /api/account → GET /api/chart-view`,确认 status、reported time、active time、active date、timezone id/offset、provenance 的语义一致。 +- 若发现 PATCH/trigger 清掉 adopted tuple 后仍留下可排盘的 active 状态,修复为原子、可解释的状态转换;confirmed 保护保持不变。 + +验收:同一 fixture 的 account 与 chart-view 返回相同的 server-owned date/time/offset;accepted/confirmed 不出现可排盘但 adopted tuple 不完整的状态;reported 仍按声明字段排盘;跨午夜使用 adopted date;confirmed 普通编辑仍不能覆盖 active time。 + +### T2 · 锁定 chart-view 错误分类 + +- 为 profile query error、profile incomplete、adopted calculation incomplete、timezone resolver failure、engine 429、engine timeout、bad payload、response schema failure 建立稳定内部分类和用户可读映射。 +- 保留 HTTP/API 兼容性,除非测试证明现有状态码无法表达安全边界;不要把 200 的结构化业务状态随意改成异常 500。 +- 日志只记稳定类别、route、耗时和必要的 HTTP status,不记出生资料。 + +验收:每类错误有独立测试;数据库读取失败不再伪装成 profile incomplete;用户页面不再只显示无上下文的“没有可显示内容”;`VOICE.md` 规定的“失败不写过会儿再打开”口径保持一致。 + +### T3 · 修复更新后的缓存与竞态 + +- account PATCH 成功后明确失效/重新验证 chart snapshot,至少按 account + profile version/真值指纹隔离;不得让旧 snapshot 在新资料已成功保存后长期冒充新结果。 +- 检查 `refreshAccount` 与 chart request 的顺序,避免并发请求读到半更新 profile;engine cache key 继续包含 date/time/offset、ayanamsa、node mode 等已有必要维度。 + +验收:资料更新后重新进入 `/chart` 取得新 date/time/offset;失败时旧 snapshot 不覆盖已确认失效的 profile;同账户不同资料不会互相命中;现有 secondary-page cache 合同不被削弱。 + +### T4 · 补完整回归链路 + +至少覆盖:PATCH 后 chart-view 新资料、reported/accepted/confirmed、跨午夜、缺 active offset、query error、引擎 busy/timeout/bad payload、schema parse、旧 snapshot 失效、D1 planets/houses 合同。保留现有 `chart-view-route`、`chart-view-engine`、`server-owned-birth-profile`、`adopted-birth-date`、`account-api`、`secondary-page-entry` 测试名。 + +## 6. 让步顺序 + +1. T1 服务端真值一致性;2. T2 错误分类;3. T3 缓存/竞态;4. T4 全链路回归。若时间不足,不能以只改文案代替 T1;缓存优化可延期,但必须把旧快照风险记入进度和 `BLOCKED.md`。 + +## 7. 开工前置命令 + +```bash +git status -sb +git fetch origin --prune +git worktree add -b codex/chart-profile-update-consistency-20260922 .worktrees/chart-profile-update-consistency-20260922 origin/staging +cd .worktrees/chart-profile-update-consistency-20260922/frontend +./node_modules/.bin/tsc --noEmit +npm run lint +npm test +npm run build +``` + +开工时再次执行 `grep -n '^## BUG-' docs/BUG_HISTORY.md | tail -1`,若最大号变化,以当时最大号为准,不复用本文件预估号。 + +## 8. BUG 编号起点 + +本轮写任务书时 `docs/BUG_HISTORY.md` 最大号为 BUG-995。实现方若确认本现象属于新 Bug,开工前再次核对最大号,按最大号 + 1 连续编号;若只是 BUG-073/076 或 BUG-715~717 的复发,必须关联原记录,不得伪装成无关新 Bug。 + +## 9. 交付记录 + +代码实现同轮必须更新 `docs/BUG_HISTORY.md`、`docs/tasks/PROGRESS-chart-profile-update-consistency-20260922.md`;若有用户可见状态或文案变化,更新 `CHANGELOG.md`。验收未闭环前不得写 `resolved`,不得声称 staging 已部署。 diff --git a/docs/tasks/TASK-chat-subject-picker-20260922.md b/docs/tasks/TASK-chat-subject-picker-20260922.md new file mode 100644 index 00000000..7f441c04 --- /dev/null +++ b/docs/tasks/TASK-chat-subject-picker-20260922.md @@ -0,0 +1,91 @@ +# 任务书 · 开始聊天选择人物与会话对象锁定(2026-09-22) + +## 0. 基线与串行依赖 + +- 基线 commit:`1bc6a954c597e2817fe72bfedad93119ab19a527`;执行方开工前必须 fetch 并以最新 `origin/staging` 重核。 +- 执行分支:`codex/chat-subject-picker-20260922`;工作树:`.worktrees/chat-subject-picker-20260922`。 +- 强依赖:必须在 `TASK-consultation-subject-binding-20260922.md` 的服务端 resolver、`/api/consult` 接入和服务端测试合入 staging 后再开工;不得两个分支并行改会话 binding 或聊天顶栏。 +- 与首页可靠性、starter-entry 和图标动效任务遵守文件级串行:若这些任务尚未合入,不能同时修改 `page.tsx`、`starter-home.tsx`、聊天顶栏或相关共享 CSS;功能绑定先于 UI 选择器,入口视觉和图标审计随后进行。 +- 本轮 other 只覆盖普通聊天。报告、每日星语、生时校正不接入人物选择器。 + +## 1. 会议事实与现状 + +会议明确:切换其他人物后聊天不能继续使用本人资料;“我的星盘”不适合查看他人,建议改为“当前星盘”;业务认可点击聊天顶部人名展开人物列表;开发侧提出开始聊天时选对象,会话进行中不再更换,业务认可方向但未展开所有边界。 + +代码事实: + +- `frontend/src/hooks/use-session-management.ts` 的新会话目前隐式继承账户级 `activeChartId`,没有开始聊天前的显式确认流程。 +- `frontend/src/app/(app)/page.tsx` 的 `.chat-header-chart` 当前是静态 `span`,没有 button/popover/键盘语义。 +- `frontend/src/components/chart-library-panel.tsx` 是账户设置资料管理,不应整块复制成聊天选择器。 +- `frontend/src/lib/home-profile.ts` 已有 `chartSnapshotForSession` 与 `sessionChartLabel`;`ChatSession` 已有 chart profile 三字段,但 UI 选择必须最终写会话 binding,而不是只改 localStorage。 +- `frontend/DESIGN.md` Chat header 目前是一行 quiet chart chip;本轮改动必须同步 DESIGN,并保持 44px 透明命中区、暖色 palette、popover 120ms 与 reduced-motion。 + +## 2. 决策记录(产品已拍板) + +- 第一条消息发送前可以选择人物;本人可作为明确的默认选中项,但必须让用户看得到当前对象。 +- 第一条消息发送后人物锁定;需要换人时新建会话,不改绑旧会话。 +- 历史会话人物绑定固定;打开历史会话不得修改其 binding。 +- 人物资料编辑后,后续普通聊天读取该人物最新资料。 +- 人物删除后旧会话可阅读,但发送阻断,不自动退回本人。 +- 顶部人物名是人物入口;空会话可选择,有消息会话点击其他人物时走“新建会话并使用此人物”路径,不能静默改绑。 + +## 3. 硬红线 + +1. 不得只改 `activeChartId` 或 localStorage;选择必须改变新会话的服务端 binding,或创建新会话。 +2. 有消息的会话不得改人物;不得通过重写 `chart_profile_name` 伪造切换。 +3. 人物列表和错误态不得泄露其他账户资料;删除人物不得回退 self。 +4. 使用现有 `ChatComposer`、`useConversationScrollAnchor`、既有 popover/加载机制;不新增输入框、滚动跟随或 spinner。 +5. `Home()` 的 useState/useRef 和行数增长冻结不得被绕过;新状态放 hook/lib/组件。 +6. 所有可见文案对照 `frontend/docs/VOICE.md`;交互合同同步 `frontend/DESIGN.md`;源码符号/旧文案删除前先 `git grep -n` 覆盖 `tests/ frontend/`。 + +## 4. 任务分解与验收标准 + +### T1 · 会话启动选择器 + +- 从 chart library 读取本人和当前用户拥有的 other 资料,建立独立的 `ChatProfilePicker`/等价组件,不复制账户设置编辑表单。 +- 空会话首次发送前显示当前人物与切换入口;选择状态清晰;资料加载中/失败/不完整有明确非阻塞状态。 +- 新会话创建时写入服务端确认过的 `chart_profile_id`,服务端名称/关系不能被客户端任意覆盖。 + +验收:新建聊天能看到 self + owned other;选择 other 后创建/发送的会话绑定正确;未选择时默认行为可理解且不依赖隐式 localStorage;加载失败不创建错误 binding。 + +### T2 · 顶部当前星盘入口 + +- 将 `page.tsx` 中 `.chat-header-chart` 静态 span 改为可访问 button/等价控件,名称改为“当前星盘”语义,实际显示当前人物名。 +- click、Enter、Space 均能打开/关闭 popover;实现 `aria-expanded`、`aria-controls`、焦点回收、Escape、点击外部关闭。 +- 列表显示本人、owned other、当前选中、资料删除/不可用状态;至少 44px 触控目标,遵循现有 popover motion。 + +验收:键盘和触摸均可用;顶栏仍保持单行/移动端尺寸;不引入第二个顶部入口或第二套列表。 + +### T3 · 已有消息的锁定行为 + +- 空会话选择人物直接更新该会话的待发送 binding。 +- 有消息的会话选择其他人物不改旧会话;给出“新建会话并使用此人物”的单一路径,创建后才发送。 +- 发送中/流式请求期间不允许创建冲突 binding;等待当前请求结束后再执行新会话动作。 + +验收:旧会话历史和服务端 subject 不变;新会话绑定新人物;刷新、深链、跨设备仍一致;删除人物显示阻断而不是改成本人。 + +### T4 · UI/行为回归 + +至少覆盖:picker self/other、默认状态、空会话选择、已有消息锁定、新建会话路径、键盘/焦点/44px、资料加载失败、删除资料、刷新/深链。扩展现有 `chart-library-session`、`session-open-preserves-identity`、chat session write/authority 合同;不删除旧测试名。 + +## 5. 让步顺序 + +T1 服务端绑定接线 > T2 可访问入口 > T3 锁定/新建路径 > T4 细节视觉。若无法安全实现顶部入口,宁可保留静态显示并记录 blocked,不得交付假切换。 + +## 6. 开工前置命令 + +```bash +git status -sb +git fetch origin --prune +# 确认 consultation-subject-binding 已合入 origin/staging 后再创建本分支 +git worktree add -b codex/chat-subject-picker-20260922 .worktrees/chat-subject-picker-20260922 origin/staging +cd .worktrees/chat-subject-picker-20260922/frontend +./node_modules/.bin/tsc --noEmit +npm run lint +npm test +npm run build +``` + +## 7. 记录与验收 + +同轮更新 `frontend/DESIGN.md`、必要的 `frontend/docs/VOICE.md`、`docs/tasks/PROGRESS-chat-subject-picker-20260922.md` 和 `docs/tasks/README.md` 状态板;若确认原始人物错配 Bug 尚未有记录,按开工时 `BUG_HISTORY.md` 最大编号 +1 连续登记并关联相关历史。浏览器级登录验收写入 `docs/testing/`;未具备登录态/Chrome 时写环境缺口,不得写通过。 diff --git a/docs/tasks/TASK-consultation-subject-binding-20260922.md b/docs/tasks/TASK-consultation-subject-binding-20260922.md new file mode 100644 index 00000000..d13d77be --- /dev/null +++ b/docs/tasks/TASK-consultation-subject-binding-20260922.md @@ -0,0 +1,115 @@ +# 任务书 · 普通聊天人物资料服务端真值与会话绑定(2026-09-22) + +## 0. 基线与依赖 + +- 基线 commit:`1bc6a954c597e2817fe72bfedad93119ab19a527`(当前 `origin/staging`);执行方开工前必须 `git fetch origin --prune` 并重新确认。 +- 执行分支:`codex/consultation-subject-binding-20260922`;工作树:`.worktrees/consultation-subject-binding-20260922`。 +- 本单先于人物选择 UI。`TASK-chat-subject-picker-20260922.md` 必须等待本单的服务端 subject resolver 与集成测试完成后再开工;两轮不得同时改 `page.tsx`、会话绑定或咨询 route。 +- 本轮 other 资料只覆盖普通聊天;报告、今日星语、生时校正和其他入口另立任务,不得顺手扩范围。 +- 与本日人物选择 UI 任务严格串行:本单先完成并合入 staging,`TASK-chat-subject-picker-20260922.md` 才能开工;两单不得同时改 `frontend/src/app/api/consult/route.ts`、会话 binding 契约或聊天顶栏接线。 + +## 1. 事故实证 + +会议反馈:切换到其他人物后,聊天仍使用用户本人的资料。 + +调查已确认: + +- `frontend/src/hooks/use-session-management.ts` 的 `startNewChat` 通过账户级 `activeChartId` 调用 `chartSnapshotForSession`,会话已有 `chart_profile_id/name/role` 元数据,但没有服务端 subject 解析。 +- `frontend/src/app/api/consult/route.ts` 读取会话时只取 `chart_profile_role`,不取 `chart_profile_id`;随后通过 `frontend/src/lib/consultation-route-service.ts` 的 `loadProfile(userId)` 无条件读取登录用户的 `profiles`。 +- `frontend/src/hooks/use-consultation-run.ts` 从全局 `profile` 组装客户端出生字段;这些字段不能作为服务端真值,否则会绕过所有权和资料完整性边界。 +- `frontend/src/app/api/chart-profiles/route.ts` 与 `[id]/route.ts` 有基本 `user_id` 所有权约束,但没有接入咨询真值。 +- 删除 other 资料后前端可能显示“资料已删除”,而 `/api/consult` 仍回退到本人,形成 fail-open 和用户误导。 + +相关历史:BUG-001、BUG-010、BUG-018、BUG-073、BUG-076、BUG-011、BUG-188;当前最大号开工前重新核对。 + +## 2. 根因 + +会话绑定字段目前只是客户端可写/可读的展示元数据,不是服务端确认过的 subject binding。咨询服务没有按 `chat_sessions.chart_profile_id` 解析人物,而是固定使用当前登录用户 `profiles`,所以 UI、会话显示与实际排盘对象可以不一致。 + +## 3. 决策记录(产品已拍板) + +| 决策 | 本轮口径 | +|---|---| +| 会话人物 | 第一条消息发送后锁定;如需换人,新建会话,不改绑已有消息的会话 | +| 资料编辑 | 会话保留人物 ID,后续普通聊天使用该人物最新资料 | +| 删除人物 | 旧会话保留阅读;资料不存在/被删除时阻断继续发送,不自动退回本人 | +| other 范围 | 本轮只打通普通聊天;报告、每日星语、生时校正另立任务 | +| self 真值 | `self` 始终映射当前用户权威 `profiles`,不信任 chart library JSON 或客户端出生字段 | +| 名称/关系 | 会话快照用于历史展示;排盘对象由服务端按 ID/role 重新解析 | + +## 4. 硬红线 + +1. 不得只改 `activeChartId`、顶部文案或客户端 request body 就声称修复。 +2. 不得信任客户端 `name/year/month/day/hour/minute/city/lat/lon/tz` 覆盖服务端 resolver。 +3. `other` 必须校验当前用户所有权、存在性、role、资料完整性;越权、伪造 role/name、删除或缺失必须 fail-closed,不得 fallback 到 self。 +4. 有消息的会话不得 PATCH 改绑;若实现层需要字段存在,必须由服务端拒绝并覆盖测试。 +5. 不改数据库结构,除非任务书另列迁移;不改计费语义、模型选择、普通聊天以外入口。 +6. 不把出生资料、姓名、会话原文或凭据写进测试 fixture、日志、BUG 历史或任务记录;fixture 只能使用 synthetic/golden 数据。 +7. 不增加 `page.tsx` 的 Home 状态/引用,不新增第二个输入框、滚动跟随或加载动画。 + +## 5. 任务分解与验收标准 + +### T1 · 服务端 subject resolver + +新增独立服务端 helper(建议放 `frontend/src/lib/`,不要把实现堆进 `route.ts`): + +- 输入当前 `userId` 与会话 binding;读取服务端会话和 chart profile。 +- `self` → 当前用户 `profiles`;`other` → 当前用户拥有的 `chart_profiles.profile`。 +- 服务端重新派生 name/role;会话展示快照不作为排盘真值。 +- 统一处理不存在、删除、越权、role 不一致、资料不完整,并返回稳定内部错误类别。 +- 客户端出生字段只能被忽略或用于非权威一致性检查,绝不能覆盖 resolver。 + +验收:self/other 都由服务端真值生成;other 不会取当前用户 profiles;越权 ID、伪造 name/role、缺资料、删除资料均 fail-closed;错误不泄露内部数据库细节。 + +### T2 · 接入咨询主链 + +- `frontend/src/app/api/consult/route.ts` 读取 `chart_profile_id` 及必要 binding 信息。 +- 将 resolver 注入 `frontend/src/lib/consultation-route-service.ts` 的 prepare 流程。 +- 保留会话 owner 校验、会话类型校验、计费前失败语义;resolver 失败不得扣点、不得进入模型调用。 +- 只修改普通聊天咨询入口,不扩到报告、每日星语、校正。 + +验收:同一问题分别绑定 self/other 时,传入计算与最终咨询上下文的出生资料来自各自服务端 profile;resolver 失败时没有模型调用、没有扣点、公开错误可理解。 + +### T3 · 会话绑定写入与不可变边界 + +- `frontend/src/app/api/sessions/route.ts` 与 `[id]/route.ts` 在写入时校验 chart profile ID/role/name 的一致性,或由服务端根据 ID 填充展示字段。 +- 未发送第一条消息的空会话可按产品规则创建绑定;已有消息的会话不得改绑。 +- 刷新、深链、跨设备恢复保持 binding;打开历史会话不能只靠全局 `activeChartId` 改变实际对象。 + +验收:客户端提交任意伪造 name/role 不能伪造会话主体;服务端保存的绑定可被再次解析;有消息后改绑明确失败;空会话策略与下一轮 picker 一致。 + +### T4 · 服务端回归测试 + +新增或扩展测试,至少覆盖: + +- self 使用权威 profiles;other 使用当前用户拥有的 chart profile; +- 另一用户 ID、随机 ID、role/name 不匹配、删除/缺失/不完整资料; +- 客户端出生字段与服务端资料冲突时仍使用服务端; +- `/api/consult` 集成确实把 other 资料交给 chart/consultation preparation; +- resolver 失败不扣点、不调模型、不 fallback; +- 会话有消息后改绑拒绝;刷新/深链保留 binding; +- 并发删除与发送时结果确定且 fail-closed。 + +保留现有测试名;若改变既有断言,进度记录写原值/新值/原因。 + +## 6. 让步顺序 + +T1 > T2 > T4 > T3 的 UI 接线。若时间不足,优先完成 resolver、咨询接入和服务端集成测试;不得先交付只改前端显示的 picker。 + +## 7. 开工前置命令 + +```bash +git status -sb +git fetch origin --prune +git worktree add -b codex/consultation-subject-binding-20260922 .worktrees/consultation-subject-binding-20260922 origin/staging +cd .worktrees/consultation-subject-binding-20260922/frontend +./node_modules/.bin/tsc --noEmit +npm run lint +npm test +``` + +开工时核对:`grep -n '^## BUG-' docs/BUG_HISTORY.md | tail -1`。当前任务书编写时最大号是 BUG-995;若确认新 Bug,按开工时最大号 +1,若为 BUG-073/076 复发则关联原记录。 + +## 8. 交付与记录 + +实现同轮必须更新 `docs/BUG_HISTORY.md`,说明现象、触发条件、根因、修复、验证和防复发;如尚不能闭环,状态只能是 `investigating` 或 `blocked`。写 `docs/tasks/PROGRESS-consultation-subject-binding-20260922.md`,并在 `docs/tasks/README.md` 状态板登记。不得声称已部署,除非 staging SHA 与 `/api/health.deployment.gitCommit` 核对一致。 diff --git a/docs/tasks/TASK-home-bootstrap-reliability-20260922.md b/docs/tasks/TASK-home-bootstrap-reliability-20260922.md new file mode 100644 index 00000000..629549b5 --- /dev/null +++ b/docs/tasks/TASK-home-bootstrap-reliability-20260922.md @@ -0,0 +1,78 @@ +# 任务书 · 首页加载失败可恢复性与 BUG-936 收口(2026-09-22) + +## 0. 基线与已有任务关系 + +- 基线 commit:`1bc6a954c597e2817fe72bfedad93119ab19a527`;执行方开工前 fetch 并重核最新 `origin/staging`。 +- 执行分支:`codex/home-bootstrap-reliability-20260922`;工作树:`.worktrees/home-bootstrap-reliability-20260922`。 +- 既有 `docs/tasks/TASK-first-paint-dead-screen-fallback-20260917.md` 已定义 BUG-936 的首屏内联 fallback 方案。本单不是另起炉灶:若该任务尚未合入,执行方应直接按其未完成项执行并以本单产品验收为准;若已合入,禁止重复添加第二套 fallback。 +- 本单与 `/chart` 数据真值任务独立;不要用首页 bootstrap 改动掩盖星盘 API 问题。 +- 本单与人物选择器都涉及首页装配时,优先完成并合入本单的 bundle 无关 fallback,再开始 `TASK-chat-subject-picker-20260922.md` 的首页接线;执行方不得同时修改 `page.tsx` 或同一 loading/reveal 符号。 + +## 1. 事故实证 + +BUG-936(`docs/BUG_HISTORY.md`)记录:iPhone Safari 可能永久停在“正在载入账户 / 同步个人资料与对话记录”。已确认 `/api/account`、`/api/sessions`、`/api/models` 等接口返回 200;预渲染 HTML 本身包含加载屏;4 秒揭幕、8 秒 bootstrap timeout 和 401 处理全部在可能未执行的客户端 bundle 中。只要 chunk 未加载、解析失败、旧缓存损坏或 hydrate 失败,CSS 转圈会无限继续。 + +现有任务书已确认:根 layout 需要与 bundle 无关的内联经典脚本;不能自动刷新;本仓自有后行断言需移除,但不能声称因此解决所有 Safari 版本兼容问题。 + +## 2. 根因与边界 + +本单负责的是“首屏失败时用户没有恢复出口和诊断”的确定性缺陷;具体设备 iOS 版本、chunk、缓存或内容拦截器根因仍需 staging/真人分流,不得提前编造。正常接口速度不是本单的优化目标。 + +## 3. 决策记录 + +- 首屏必须在 JS 未执行、关键 chunk 失败或 hydrate 超时时给出可读失败状态和“重新加载”动作。 +- 兜底必须与主 bundle 无关:内联、经典 `