docs(tasks): brief for people archive P1 (multi-subject charts)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-09-24 15:11:29 +08:00
co-authored by Claude Opus 5.5
parent b975617603
commit edc9c22c37
2 changed files with 130 additions and 0 deletions
+1
View File
@@ -117,6 +117,7 @@
| 任务书 | 进度 | 主题 | 状态 | 落点 |
| --- | --- | --- | --- | --- |
| `TASK-people-archive-p1-20260924.md` | — | **星盘档案(多人物)P1**:每个档案 = 一个人(含本人),最多 5 人、无关系字段;标题旁人名 = 全局切换器(对话 / 星盘 / 星历 / 报告 / 档案页同一组件),星盘、星历、报告、今日星语、新建对话都跟当前人物走(推翻 BUG-1000 新对话固定本人);历史只显示当前人物;档案管理改侧栏 `/people` 页、设置删星盘资料;删人连带删对话与报告;取消「设为默认」/`activeChartId`(BUG-1030 错人)。他人资料改类型化列 + 与户主同级守卫,统一 `resolveSubjectBirth`;户主暂留 `profiles`。生时校正跟人与任意两人合盘留 P2。**同日四单全部合入后开工**,需 Docker 跑 test:db | 待领取 | — |
| `TASK-home-slow-network-20260924.md` | — | **慢网首页半揭幕 + 账户与点数打开慢(BUG-1021/1022)**:8 秒硬超时包住多跳串行请求、超时不补拉,独立的会话列表 provider 晚到 → 正常首页+报错条、模型「暂不可用」、新建对话灰、重问称呼。产品 09-24 拍板:推翻 8 秒硬超时,等齐再揭幕,20 秒才出错误屏且重试为局部重请求(不整页刷新);账单面板删无用 `/api/account`、预取 chunk、套餐缓存。先于 chart-ephemeris T3 合入 | 待领取 | — |
| `TASK-secondary-new-chat-intent-20260923.md` | `PROGRESS-secondary-new-chat-intent-20260923.md` | **次级页「新建对话」落到最近一次对话**:`/chart` `/ephemeris` `/reports` 的侧栏没有 `controls`,「新建对话」只是 `href="/"` 的回首页链接;首页无参启动的落点是 `nextSessions[0]`(最近更新那条),只有最近一条是校正会话才改落空咨询。BUG-745 修过同一入口的「慢」,没修「回到哪」。产品 09-23 拍板:链接改 `/?new=1`,首页看到 `new` 就本地建空咨询并 `replaceState` 抹掉参数;`new` 胜过 `c` 与登录返回存根;沿用 BUG-989 不落库不写 `?c=`;页脚与品牌行的 `/` 不改;刷新无参 `/` 仍落最近对话属既有设计。红线:`/` 保持 Static、不得用 `useSearchParams`;解析与生成只在 `chat-session-url.ts` 一处。BUG-1015;执行中追加 D7:保留旧任务恢复但不抢新建落点。 | 已验收(Claude 09-24:tsc/lint/95 定向/全量失败名单同基线/Static/gzip 同基线;BUG-1015 resolved) | `8902e484`,含于已部署 `1420471a`;真机清单欠 |
| `TASK-chat-message-authority-20260901.md` | — | 消息服务端权威化 | 已验收 | `b6989c3e`(BUG-464) |
@@ -0,0 +1,129 @@
# TASK · 星盘档案(多人物)P1:人物统一 + 标题人名切换 + 功能跟人走(2026-09-24)
原型(形态参考,**以本文决策为准**,原型里的侧栏顶部切换、关系、「全部 / 只看当前人」筛选、首页小字均已被产品否掉):https://claude.ai/artifact/8EFiEZBbD8JnToHfdwZej2
## 基线
- `origin/staging = b9756176`。分支 `codex/people-archive-p1-20260924`。
- **串行(本单最后做)**:同日待领取单与本单大面积重叠——`TASK-home-slow-network-20260924`(`page.tsx`)、`TASK-chart-ephemeris-fixes-20260924`(`/api/ephemeris`、星盘空状态 `?settings=chart`)、`TASK-report-reader-actions-20260924`(报告列表)、`TASK-chart-dasha-western-redesign-20260924`(星盘页)。**这四单全部合入后再开工**,基线以开工时 `origin/staging` 为准。
- 动表:必须真跑 `npm run test:db --prefix frontend`(需 Docker);数据库迁移部署前要先执行 Migrate Staging Database。
## 事故实证(按 `origin/staging` e0db23f4 调查)
**数据**
1. 户主资料在 `public.profiles`(带 `reported_birth_time` 不可改、`active_birth_*` 只由校正 RPC 写入等触发器守卫)。
2. 其他人在 `public.chart_profiles`(`20260718100000_repair_missing_chart_profiles.sql`):`id, user_id, role('self'|'other'), profile jsonb`。**出生资料全在无校验的 jsonb 里**,没有 reported / active 区分和守卫;`/api/chart-profiles`(GET / POST、`[id]` PUT / DELETE)只校验"是对象 + 属于本人";**无数量上限**。关系 `chartRelationship` 只在 JSON 里。
3. 户主还有一份 `chart_profiles role='self'` 镜像:`use-profile-onboarding.ts` `persistProfile` 写入,但读取时被 `chartLibraryFromCloudOthers` 丢掉、由 `buildSelfChartRecord` 从 `profile` 重建——写而不读。
**各功能对"他人"的支持**
| 功能 | 现状 | 证据 |
| --- | --- | --- |
| 普通对话 | ✅ 真按所选人排盘(BUG-997/1000)| `consultation-subject-resolver.ts` `resolveConsultationSubject`;`/api/consult` `loadOwnedChartProfile`;首问后 `subject_locked` |
| 生时校正 | ❌ 只读 / 只写 `profiles` | `rectification-agentic/v9/case-service.ts` `loadV9RectificationProfile`;RPC `accept_/confirm_agentic_rectification_candidate_for_case_v2` |
| 个人报告 | ❌ `chartProfileId` 收了、校验了、存了,**不用于取资料** | `api/reports/route.ts`、`personal-report-route-core.ts`;客户端 `buildPersonalReportCreateRequest` 不发 |
| 星盘页 | ❌ 只 `profiles` | `chart-view-service.ts` `loadChartView` → `prepareChartViewProfile` → `resolveServerOwnedChartBirth` |
| 星历 | ❌ 只 `profiles` | `api/ephemeris/route.ts` `globalBirthProfileFromAccountRow` |
| 今日星语 | ❌ 服务端丢弃请求体读 `profiles` | `api/daily-starlanguage/route.ts` |
| 合盘 | ⚠️ 只能「我 × 他人」 | `api/synastry/route.ts` |
**遗留「当前星盘 / 设为默认」造成的错位(Bug)**
4. `page.tsx` `activeChartId`(localStorage `jyotisha_active_chart:${accountId}`,`home-cloud-sync.ts` `activeChartStorageKey`)+ 一个 effect **把页面级 `profile` 换成别人的**;`makeDefaultChart` 同样 `setProfile(record.profile)`。服务端从不读它,但它仍喂给:`use-synastry.ts` `buildSynastryQuestion(profile, …)`(合盘问题里"我的资料"写成别人)、首页今日星语卡的可用判断、`use-consultation-run.ts` 的咨询模式 / 完整度门、`use-rectification-surface.ts` `chartSnapshotForSession(activeChartId, …)`(**校正会话标签显示别人名字、实际算户主**)、个人资料昵称。设置里「当前默认」徽章又写死在户主行。
## 根因
- 多人物只在对话一条链上打通(BUG-997/1000 明确"报告、每日星语、生时校正另立任务");其余功能都直接读 `profiles`。
- 他人资料没有与户主同级的服务端真值与守卫,不能直接接到报告 / 星盘这些"结论型"功能上。
- 「设为默认」是旧模型残留,只改客户端全局 `profile`,与服务端真值分叉。
## 决策记录(产品 2026-09-24)
- **D1 每个星盘档案 = 一个人,户主本人也是其中一个**,统一列表、统一操作;户主不可删。
- **D2 最多 5 人(含本人)**:他人最多 4 个;数据库与接口双重限制,第 6 个时添加按钮禁用并提示「最多保存 5 个人的星盘」。
- **D3 不要「关系」字段**:档案只有称呼 + 出生资料(日期、时间与精度、地点 / 时区、岁差)。UI 删关系选择;存量 JSON 里的 `chartRelationship` 忽略、不迁移。
- **D4 切换入口 = 标题旁的人名**:复用现有对话标题旁的人名组件(`ChatProfilePicker`),扩成全局「当前人物」切换器 `SubjectSwitcher`,同一组件放在:对话标题旁、星盘 / 星历 / 我的报告 / 星盘档案页标题旁。**侧栏顶部不放切换器。**切换后全局生效。
- 在已发过消息的对话里点人名切到别人 = 切换当前人物 **并新建一个空对话**(首问锁定规则不变);空对话里点 = 直接改绑。
- **D5 全功能跟随当前人物**:星盘页、星历(本命叠加)、生成个人报告、今日星语 / 今日节奏、新建对话默认人物 = 当前人物(**推翻 BUG-1000「新建对话固定本人」**)。
- **D6 历史只显示当前人物的**:侧栏会话列表、报告列表都只列当前人物的记录;切人,列表跟着换。列表项不标人名,不做"全部"筛选。
- **D7 计费**:给任何人对话 / 报告 / 校正都按正常规则扣点,不区分本人与他人。
- **D8 档案管理 = 侧栏独立页「星盘档案」** `/people`(与星盘 / 星历 / 我的报告并列,`○ Static`),列表 + 详情;详情里有「和 TA 对话 / 看星盘 / 生成报告」快捷入口和「和我合盘」(沿用现有合盘能力)。**设置弹窗删除「星盘资料」面板**;`?settings=chart`(若 chart-ephemeris-fixes T3 已加)改为跳 `/people`。
- **D9 删除一个人 = 连同他的对话与报告一起删除**,删除前显示「将同时删除 N 个对话和 M 份报告,无法恢复。」;服务端事务内删除,删除后服务器不保留其出生资料。当前人物被删 → 回到本人。
- **D10 首页开场不加任何"在看某某"提示**,问候保持原样(今日星语内容随当前人物变)。
- **D11 取消「设为默认」与 `activeChartId`**:删除 localStorage 键与"替换页面 `profile`"的 effect;当前人物只作为请求参数传给服务端,**任何客户端代码不得再用他人资料覆盖页面级 `profile`**。
- **D12 生时校正本单不跟人**(P2):当前人物不是本人时,生时校正入口禁用,文案「生时校正暂时只支持本人」。任意两人合盘也在 P2。
- **D13(架构,Claude 决定)**:P1 **不把户主迁出 `profiles`**——校正 RPC、`resolveServerOwnedChartBirth`、首页引导都依赖它,迁移风险与本单收益不成比例。产品层面"户主也是其中一个人"通过统一的服务端解析实现:新增唯一入口 `resolveSubjectBirth({ userId, subjectId })`:`self` → 现有 `profiles` 真值链;其他 → `chart_profiles` 新增的**类型化列**(见 T1)。户主的 `chart_profiles role='self'` 镜像停止写入并在迁移里删除。
## 硬红线
1. 服务端永不信任客户端出生资料;所有功能的出生资料只来自 `resolveSubjectBirth`,按 `user_id` 校验所有权,失败 fail-closed(不回落本人、不扣点),沿用 `consultation-subject-resolver` 的失败码。
2. 他人类型化列具备与户主同级的守卫:`reported_*` 写入后不可改;`active_*` 只能由(P2 的)校正流程写,P1 客户端与 `/api/chart-profiles` 不得写 `active_*` / `birth_time_status = accepted|confirmed`。
3. 解析函数只一处实现;星盘 / 星历 / 报告 / 今日星语 / 对话都调用它,不得各写一份读取。
4. 缓存键(星盘页 `secondary-page-data` 身份、服务端引擎缓存、今日星语指纹)必须包含 `subjectId`,切人不得闪现上一个人的盘。
5. `page.tsx` 不得净增行、`Home()` 不新增 useState / useRef;`jyotish_api_server.py` 不增长。
6. `/`、`/chart`、`/ephemeris`、`/people` 为 `○ Static`;首屏 gzip ±2%。
7. 隐私:fixture / 测试 / 文档只用虚构人物;日志不记姓名、生日、坐标。
8. 既有断言三栏;测试总数不低于基线;不改 workflow / DNS。
## 任务分解
### T1 · 数据层(迁移 + 服务端)
- 迁移:`chart_profiles` 增类型化列(`name, birth_date, reported_birth_time, birth_time_source, birth_time_period, declared_window_start/end, uncertainty_before/after_minutes, latitude, longitude, timezone_id, timezone_offset, birth_place_label/type/provider/provider_id, ayanamsa, active_birth_time, active_birth_date, active_birth_timezone_offset, birth_time_status`)+ 守卫触发器(仿 `guard_birth_time_journey` / `zz_guard_adopted_birth_date`);从现有 `profile` jsonb **回填**;删 `role='self'` 行;每用户 `role='other'` 行数 ≤ 4 的约束(触发器)。jsonb 列保留只读一版以便回滚,下一轮再删。
- 删除级联:`delete_chart_subject(id)` RPC(事务内删 `chat_sessions where chart_profile_id = id`、`personal_reports where chart_profile_id = id`、该档案),与计数查询 `chart_subject_usage(id)`。
- `frontend/src/lib/subject-birth.ts`(新)`resolveSubjectBirth`;`/api/chart-profiles` 改读写类型化列并做字段校验(日期 / 时间 / 坐标 / 时区合法、长度上限),POST 超限返回 409 `subject_limit_reached`。
验收:`npm run test:db` 新增用例——回填后类型化列与原 JSON 一致;第 5 个他人插入被拒;`reported_birth_time` 更新被拒;客户端写 `active_*` 被拒;删除 RPC 连带删会话与报告且只删本人的;跨用户读写被 RLS 拒绝。
### T2 · 功能跟人走(服务端 + 请求参数)
- `/api/chart-view`、`/api/ephemeris`、`/api/reports`(POST 用 `chartProfileId` 取资料)、`/api/daily-starlanguage`、`/api/consult`(改为经 `resolveSubjectBirth`)都接受 `subjectId`(缺省 `self`),经 `resolveSubjectBirth` 取资料。
- 列表:`GET /api/sessions?subject=`、`GET /api/reports?subject=` 服务端过滤(`self` 匹配 `chart_profile_id is null or = 'self'` 的存量行)。
- 生时校正入口在非本人时禁用(D12)。
验收:路由测试——各接口 `subjectId=他人` 时引擎收到的是该人的出生资料(fixture 用虚构人物);他人不存在 / 不属于本人 → fail-closed 且不扣点;列表按人过滤正确;存量本人会话在 `subject=self` 下可见。
### T3 · 当前人物与切换器(前端)
- `frontend/src/lib/current-subject.ts`(新):当前人物 id(localStorage 按账户存,读到无效 id 回落 `self`),订阅式 store,不进 `Home()` 状态。
- `SubjectSwitcher`(由 `ChatProfilePicker` 抽出):对话标题旁、星盘 / 星历 / 我的报告 / 星盘档案页标题旁;D4 的锁定对话行为;列表最多 5 人。
- 删除 `activeChartId`、`activeChartStorageKey`、`makeDefaultChart`、替换 `profile` 的 effect、「当前默认」徽章;`use-synastry.ts` 改为服务端取"我"的资料;`use-rectification-surface.ts` 会话绑定固定 `self`。
- 新建对话默认人物 = 当前人物(`NEW_CHAT_SUBJECT_ID` 改为读 store)。
验收:切人后星盘页 / 星历 / 报告列表 / 侧栏会话同时换人且不闪旧盘;锁定对话里切人 → 新空对话;`grep -rn "activeChartId\|activeChartStorageKey\|makeDefaultChart" frontend/src` = 0;合盘问题文本中"我"为本人。
### T4 · 星盘档案页 `/people`(前端)
- 侧栏新增入口;列表(本人第一)+ 详情(称呼、出生时间两格:填报时间 / 排盘时间、地点与岁差、快捷入口、和我合盘、删除);添加 / 编辑表单复用 `ChartProfileForm`(删关系字段);第 5 人上限提示;删除确认显示计数(D9)。
- 设置弹窗删除「星盘资料」面板与相关导航;`?settings=chart` → `/people`。
验收:`/people` `○ Static`;添加第 5 个他人(总数第 6)被禁用;删除确认计数来自 `chart_subject_usage`;删除当前人物后回到本人;设置弹窗不再有星盘资料。
### T5 · 记录
- `docs/BUG_HISTORY.md`:**BUG-1030** 「设为默认」/`activeChartId` 覆盖页面 `profile` 致合盘问题与校正会话标签错人(关联 BUG-466、997、1000)。其余为功能,进 CHANGELOG。
- `CONTEXT.md` 术语:新增「人物 / 星盘档案 / 当前人物」,替换「当前星盘」。
- DESIGN(星盘档案页、SubjectSwitcher、设置弹窗面板变更)、VOICE(「最多保存 5 个人的星盘」「生时校正暂时只支持本人」「将同时删除 N 个对话和 M 份报告,无法恢复。」)、CHANGELOG、PROGRESS、`docs/testing/people-archive-p1-20260924.md`(加 4 个虚构人物 → 第 5 个被拦;切人后四个页面同步;锁定对话切人开新对话;删人连带删除;本人不可删;校正入口禁用;旧账号升级后资料不丢)。
## 让步顺序
1. 类型化列回填遇到脏 JSON(缺字段)时,允许该行保持"资料不完整"状态并在档案页提示补全,不得猜值。
2. 若 `/api/consult` 改走 `resolveSubjectBirth` 牵动过大,允许 consult 暂保留现有 resolver,但其读他人资料必须改读类型化列。
3. 列表服务端过滤若与 `SessionListProvider` 分页冲突,允许按人物分开缓存键重新拉取。
4. 不得让步:红线 1、2、3;D11(清除 activeChartId);D9 连带删除。
## 开工前置命令
```bash
git fetch origin --prune
git log --oneline origin/staging | head -20 # 确认四个前置单已合入
git worktree add -b codex/people-archive-p1-20260924 .worktrees/people-archive-p1-20260924 origin/staging
cd .worktrees/people-archive-p1-20260924/frontend
./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -5
npm run test:db # 需 Docker;无 Docker 写 BLOCKED 并停在 T1 之前
```
## BUG 编号
写单时最大 BUG-1020,1021–1028 已预留,1029 留给大运 / 西洋单备用;本单 **BUG-1030**,开工时核对。