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