docs(chat): add cloud-truth convergence task brief
Independent Staging Quality Gate / validate (push) Successful in 33m12s
Independent Staging Quality Gate / publish (push) Has been cancelled

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVapmh2oGNyr6ECHKjPJY8
This commit is contained in:
Jesse_Chen
2026-09-01 12:55:33 +00:00
parent 924f420202
commit 31c7b48261
+93
View File
@@ -0,0 +1,93 @@
# 任务书 · 本地/云端双份真相收敛(2026-09-01
基线:`origin/staging` @ `924f4202`(开工时以 `origin/staging` 最新为准)。这是 BUG-464 / BUG-465 之后的第三轮:前两轮把聊天消息和会话选择的真相收归了服务端,本轮收剩下的三块——星盘库、合盘历史、会话置顶/归档。与任何同期改 `frontend/src/app/page.tsx` 的轮次**不得并行**。
---
## 为什么要做(事故实证)
下面所有行号基于 `924f4202`,只是线索,按符号定位。
1. **星盘库:云端保存失败会制造"假成功",下次刷新静默丢数据。** `saveOtherChart``page.tsx:2690-2721`)在云端 save 抛错时走 catch,把带客户端 UUID 的记录写进本地 state + localStorage,提示"已保存到本地星盘库"。但启动合并逻辑(`page.tsx:1528-1537`)是**云端列表整体替换本地**`cloudLibrary.filter(...)` 重建,非并集)——那条云端没有的本地记录在下次刷新时**无提示消失**。用户以为存上了,其实最多活到下一次刷新。
2. **星盘库:云端删除失败会复活。** `deleteOtherChart``page.tsx:2733-2755`)云端删失败时只删本地,提示文案自己承认"稍后云端可能仍显示旧记录"——下次启动云端替换本地,记录原样回来。
3. **合盘历史:并集合并,删不掉也同步不全。** 启动逻辑(`page.tsx:1539-1550`)把本地和云端**按 id 取并集**再截 10 条:服务端删掉的记录会从设备本地复活;`saveCloudSynastryReport` 失败时(`page.tsx:3202-3211`)记录只进本地,在这台设备靠并集永远活着,换台设备永远看不到。
4. **置顶/归档只存本地。** `jyotisha-session-controls:${accountId}:pinned/archived` 两个键(`page.tsx:1483-1485``1504-1507`)没有任何云端对应物:换设备/清浏览器数据,置顶和归档状态全部归零。会话本身早在云端,管理它的状态却在设备上。
5. 历史旁证:仓里存在 `20260718100000_repair_missing_chart_profiles.sql``20260718101000_repair_missing_synastry_reports.sql` 两个"修数"迁移——这套双份真相已经造过一次事故并被人工修过。
## 决策记录(产品授权,2026-09-01)
1. **云端为唯一真相。** 星盘库与合盘历史的一切读取以云端返回为准;**云端写失败 = 操作失败**,明确报错、保留用户输入以便重试,**不再有"已保存到本地"的降级假成功**。现有的 best-effort 文案与行为被本决策推翻。
2. **置顶/归档升级为账户数据**(授权本轮加表列,这是三轮里第一次动表结构):`chat_sessions``pinned boolean not null default false``archived_at timestamptz null`
3. **一次性导入**:首次启动时把本地 pinned/archived 键的存量尽力导入云端(失败静默、不阻塞启动),成功后停写并删除本地键。星盘库/合盘历史的 localStorage 键直接停写停读、启动时清除旧键。
4. **两个合法的本地存留不动**:每日星语缓存(`day`+`fingerprint` 的当日缓存,服务端每天重新生成,语义就是缓存);`activeChartId`(设备级 UI 偏好)。
## 硬红线
1. **迁移只加列,不改不删既有列**;照仓内既有 `alter table ... add column if not exists` 风格,走用户 RLS`chat_sessions_update_own` 已存在,本轮不需要新 RPC、不改 grant)。
2. **不得破坏 BUG-464 的 PATCH 兼容层**`/api/sessions/[id]` PATCH 对含 `messages` 的旧请求仍须接受并忽略;`chatSessionMetadataPatchSchema` 允许扩展 `pinned` / `archived_at` 字段,但既有字段语义不变。
3. **数据库测试必须真跑。** 本轮动了表结构,`npm run test:db` 需要 Docker;没有 Docker 就登记 `BLOCKED.md` 停下,不允许跳过。
4. **不得修改既有测试断言** —— 例外仅限锁住本轮要修缺陷本身的断言(本地降级保存、并集合并、localStorage pinned/archived);须注释原值与原因,并在 PROGRESS 单列。
5. 推 staging 前必须 `./node_modules/.bin/tsc --noEmit` 通过。**不要用 `npx tsc`**(空包 `tsc@2.0.4` 坑)。
6. 测试总数不得低于基线 **2424**,且 fail=0、skipped=0(有 Docker 环境)。无 Docker 既有缺口为 24 条数据库/部署类失败 + 10 skipped(清单与 `924f4202` 一致),逐条比对不得新增。
7. 不得改 `.gitea/workflows/**`。不得在脏工作树切分支。不得自行提升 main。`next build``/` 仍须 `○ Static`
8. 新增/改动的提示文案在浅色深色两套主题下检查。不得手写 `useCallback` / `useMemo`
让步顺序:数据不损坏 > 功能与测试不回归 > 可验证的改进 > 代码整洁。
## 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/cloud-truth-20260901 \
../.worktrees/cloud-truth-20260901 origin/staging
```
基线必须是 `origin/staging`。读 `pre_work_error_ledger.md`,跑 `scripts/pre_work_check.py`,读 `frontend/AGENTS.md`。改前在 `docs/BUG_HISTORY.md` 检索(BUG-464/465 是直接上游,两个 repair 迁移是本问题的前科)。
**先读这几处再动手**
- `page.tsx` 星盘库/合盘历史全链路:storage 键(500-523)、启动合并 effect1514-1560)、云端 helpers615-690)、`saveOtherChart`2680-2721)、`deleteOtherChart`2733-2755)、合盘保存(3195-3215)。
- pinned/archived:读写(1483-1507)、消费(1394/1399/2402/4111-4112/4357)。
- `frontend/src/app/api/sessions/route.ts``SESSION_LIST_COLUMNS``frontend/src/lib/chat-session-write-contract.ts``chatSessionMetadataPatchSchema`(任务 3 的扩展点)。
- `/api/chart-profiles``/api/synastry-reports` 路由(云端 CRUD 已齐,本轮不需要新端点)。
## 任务分解
### 任务 1(P0)· 星盘库云端权威化
- 启动:只从 `fetchCloudChartLibrary` 取数(self 记录仍由 profile 重建);本地 `jyotisha_chart_library:*` 键停读停写,启动时 `removeItem` 清理。云端拉取失败 → 显示可见错误态与重试入口,不再静默用本地副本。
- 保存/更新:云端成功才更新 state 与 UI;失败保留表单内容、报"保存失败,请重试",**不产生任何本地记录**。删除 `cloudSaved` 双轨文案。
- 删除:云端成功才从 state 移除;失败报错、记录留在列表。
- 验收:断网(或 mock 500)保存一条 → 明确失败、表单未清空、刷新后列表与云端一致无孤儿。
### 任务 2(P0)· 合盘历史云端权威化
- 启动:云端列表**整体替换**(不再与本地并集);本地 `jyotisha_synastry_history:*` 键停读停写并清理。
- 保存:`saveCloudSynastryReport` 失败 → 报告仍在当前页面展示(本次会话内存里),但明确提示"未能存入历史",不写任何本地副本。
- 验收:在云端删一条记录 → 刷新后不复活;mock 保存失败 → 提示出现且刷新后历史里没有幽灵记录。
### 任务 3(P1)· 置顶/归档上云
- 迁移:`chat_sessions``pinned``archived_at` 两列(决策记录 2),补进 `SESSION_LIST_COLUMNS` 与列表响应解析。
- 写路径:扩展 `chatSessionMetadataPatchSchema` 接受 `pinned: boolean``archived_at: string|null`,置顶/归档动作走既有 PATCH(乐观更新,失败回滚 + 提示)。
- 一次性导入:启动读到本地 pinned/archived 键且非空 → 对存在的会话逐条 PATCH(尽力而为,失败静默),完成后删除本地键;导入只做一次(以本地键存在与否为标志)。
- UI 消费点(1394/1399/2402/4111-4112/4357)改读服务端字段;`archived_at` 非空即视为归档。
- 验收(db 测试真跑):迁移幂等重放;PATCH 写入两列走 RLS 只能改自己的;旧 bundle 含 `messages` 的 PATCH 仍被接受忽略(BUG-464 合同测试保持绿灯)。
### 任务 4P2)· 清理
- 删除 `readChartLibrary` / `writeSynastryHistory` 等失去用途的本地读写函数与 storage 键常量;`page.tsx` 里不再残留对三个已废弃键的引用(每日星语与 `activeChartId` 除外,见决策记录 4)。
## 总验收
1. `tsc --noEmit` 通过;测试满足红线 6`npm run test:db` 真跑记录输出。
2. `next build``/``○ Static`
3. 行为实测逐条附进 PROGRESS(无登录态环境则用合同测试覆盖并如实标注):假成功消失(任务 1)、幽灵复活消失(任务 2)、换设备视角的置顶/归档一致性(任务 3,可用两个浏览器 profile 模拟)、本地旧键被清理且导入只发生一次。
4. localStorage 审计:验收时在浏览器里列出该域名下全部键,除每日星语、`activeChartId``jyotisha.session-url-return` 及第三方库键外,不得再有本轮三个废弃键。
## 明确不做(不要顺手做)
- 不拆 `page.tsx`P2b 另立任务书,等本轮落地)。
- 不动每日星语缓存与 `activeChartId`(决策记录 4)。
- 不做星盘库/合盘的离线队列或冲突合并——云端失败就是失败,这是本轮的语义简化,不要把复杂度加回来。
- 不删 BUG-464 的 PATCH 兼容层、不删 consult 的 `history` 请求字段(另一轮)。