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

9.1 KiB
Raw Blame History

任务书 · 本地/云端双份真相收敛(2026-09-01

基线:origin/staging @ 924f4202(开工时以 origin/staging 最新为准)。这是 BUG-464 / BUG-465 之后的第三轮:前两轮把聊天消息和会话选择的真相收归了服务端,本轮收剩下的三块——星盘库、合盘历史、会话置顶/归档。与任何同期改 frontend/src/app/page.tsx 的轮次不得并行


为什么要做(事故实证)

下面所有行号基于 924f4202,只是线索,按符号定位。

  1. 星盘库:云端保存失败会制造"假成功",下次刷新静默丢数据。 saveOtherChartpage.tsx:2690-2721)在云端 save 抛错时走 catch,把带客户端 UUID 的记录写进本地 state + localStorage,提示"已保存到本地星盘库"。但启动合并逻辑(page.tsx:1528-1537)是云端列表整体替换本地cloudLibrary.filter(...) 重建,非并集)——那条云端没有的本地记录在下次刷新时无提示消失。用户以为存上了,其实最多活到下一次刷新。
  2. 星盘库:云端删除失败会复活。 deleteOtherChartpage.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-14851504-1507)没有任何云端对应物:换设备/清浏览器数据,置顶和归档状态全部归零。会话本身早在云端,管理它的状态却在设备上。
  5. 历史旁证:仓里存在 20260718100000_repair_missing_chart_profiles.sql20260718101000_repair_missing_synastry_reports.sql 两个"修数"迁移——这套双份真相已经造过一次事故并被人工修过。

决策记录(产品授权,2026-09-01)

  1. 云端为唯一真相。 星盘库与合盘历史的一切读取以云端返回为准;云端写失败 = 操作失败,明确报错、保留用户输入以便重试,不再有"已保存到本地"的降级假成功。现有的 best-effort 文案与行为被本决策推翻。
  2. 置顶/归档升级为账户数据(授权本轮加表列,这是三轮里第一次动表结构):chat_sessionspinned boolean not null default falsearchived_at timestamptz null
  3. 一次性导入:首次启动时把本地 pinned/archived 键的存量尽力导入云端(失败静默、不阻塞启动),成功后停写并删除本地键。星盘库/合盘历史的 localStorage 键直接停写停读、启动时清除旧键。
  4. 两个合法的本地存留不动:每日星语缓存(day+fingerprint 的当日缓存,服务端每天重新生成,语义就是缓存);activeChartId(设备级 UI 偏好)。

硬红线

  1. 迁移只加列,不改不删既有列;照仓内既有 alter table ... add column if not exists 风格,走用户 RLSchat_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

让步顺序:数据不损坏 > 功能与测试不回归 > 可验证的改进 > 代码整洁。

开工前置

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)、saveOtherChart2680-2721)、deleteOtherChart2733-2755)、合盘保存(3195-3215)。
  • pinned/archived:读写(1483-1507)、消费(1394/1399/2402/4111-4112/4357)。
  • frontend/src/app/api/sessions/route.tsSESSION_LIST_COLUMNSfrontend/src/lib/chat-session-write-contract.tschatSessionMetadataPatchSchema(任务 3 的扩展点)。
  • /api/chart-profiles/api/synastry-reports 路由(云端 CRUD 已齐,本轮不需要新端点)。

任务分解

任务 1P0)· 星盘库云端权威化

  • 启动:只从 fetchCloudChartLibrary 取数(self 记录仍由 profile 重建);本地 jyotisha_chart_library:* 键停读停写,启动时 removeItem 清理。云端拉取失败 → 显示可见错误态与重试入口,不再静默用本地副本。
  • 保存/更新:云端成功才更新 state 与 UI;失败保留表单内容、报"保存失败,请重试",不产生任何本地记录。删除 cloudSaved 双轨文案。
  • 删除:云端成功才从 state 移除;失败报错、记录留在列表。
  • 验收:断网(或 mock 500)保存一条 → 明确失败、表单未清空、刷新后列表与云端一致无孤儿。

任务 2P0)· 合盘历史云端权威化

  • 启动:云端列表整体替换(不再与本地并集);本地 jyotisha_synastry_history:* 键停读停写并清理。
  • 保存:saveCloudSynastryReport 失败 → 报告仍在当前页面展示(本次会话内存里),但明确提示"未能存入历史",不写任何本地副本。
  • 验收:在云端删一条记录 → 刷新后不复活;mock 保存失败 → 提示出现且刷新后历史里没有幽灵记录。

任务 3P1)· 置顶/归档上云

  • 迁移:chat_sessionspinnedarchived_at 两列(决策记录 2),补进 SESSION_LIST_COLUMNS 与列表响应解析。
  • 写路径:扩展 chatSessionMetadataPatchSchema 接受 pinned: booleanarchived_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 通过;测试满足红线 6npm run test:db 真跑记录输出。
  2. next build/○ Static
  3. 行为实测逐条附进 PROGRESS(无登录态环境则用合同测试覆盖并如实标注):假成功消失(任务 1)、幽灵复活消失(任务 2)、换设备视角的置顶/归档一致性(任务 3,可用两个浏览器 profile 模拟)、本地旧键被清理且导入只发生一次。
  4. localStorage 审计:验收时在浏览器里列出该域名下全部键,除每日星语、activeChartIdjyotisha.session-url-return 及第三方库键外,不得再有本轮三个废弃键。

明确不做(不要顺手做)

  • 不拆 page.tsxP2b 另立任务书,等本轮落地)。
  • 不动每日星语缓存与 activeChartId(决策记录 4)。
  • 不做星盘库/合盘的离线队列或冲突合并——云端失败就是失败,这是本轮的语义简化,不要把复杂度加回来。
  • 不删 BUG-464 的 PATCH 兼容层、不删 consult 的 history 请求字段(另一轮)。