Files
Jyotisha/TASK-cloud-truth-convergence-20260901.md
T
Jesse_Chen 31c7b48261
Independent Staging Quality Gate / validate (push) Successful in 33m12s
Independent Staging Quality Gate / publish (push) Has been cancelled
docs(chat): add cloud-truth convergence task brief
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVapmh2oGNyr6ECHKjPJY8
2026-09-01 12:55:33 +00:00

94 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 任务书 · 本地/云端双份真相收敛(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` 请求字段(另一轮)。