Files
Jyotisha/docs/tasks/TASK-people-archive-p1-20260924.md
T

130 lines
14 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.
# 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**,开工时核对。