Files
Jyotisha/docs/tasks/PROGRESS-home-state-lowering-batch2-20260916.md
T
Jesse_ChenandClaude Opus 5 7ae6df3ebb
Independent Staging Quality Gate / validate (push) Failing after 6m17s
Independent Staging Quality Gate / publish (push) Skipped
docs(tasks): 状态下沉第二批进度记录与真人走查清单
进度记录含三簇 20 个状态的分类表(每个写明谁在读)、改前改后实测、
既有断言改动的三栏说明、反向验证、lint warning 逐条比对、以及
session 簇留 4 个在外壳的让步说明。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
2026-09-16 02:02:22 +00:00

220 lines
20 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.
# PROGRESS · 状态下沉第二批:synastry / profile / session 三簇 + 稳住外壳 setter2026-09-16
工作树:`.worktrees/home-state-lowering-batch2-20260916`
分支:`codex/home-state-lowering-batch2-20260916`
任务书基线写 `4f643aa0`;开工时 `origin/staging` = **`37e6c519`**`docs(tasks): 校正状态板 + 状态下沉第二批 + 后门单 + 对账清单`)。
本机 Linux,有 Node 与 npm,无 Docker、无登录态、无 Chrome。本单不占 BUG 号,不改 `CHANGELOG.md` / `docs/BUG_HISTORY.md` / `docs/tasks/README.md`。未 `git push origin HEAD:staging`。未改数据库、API 路由、`frontend/DESIGN.md`(无可见样式变化)、`frontend/docs/VOICE.md`(无文案变化)。
## 开工实测(不要抄任务书 52/39/1931
数法与合同测试相同:`Home()` 切片上 `\buseState[<(]` / `\buseRef[<(]`
| 指标 | 实测 |
| ---: | ---: |
| `Home()` `useState` | **52** |
| `Home()` `useRef` | **39** |
| `page.tsx` 行数 | **1931** |
| `npm run lint` | **0 error / 120 warning** |
| `useSessionManagement` 参数(type 字段) | **41**(任务书约 40 |
| `useProfileOnboarding` 参数(type 字段) | **38** |
| 全量 `npx tsx --test tests/*.test.ts tests/*.test.tsx` | **3329** tests / 3283 pass / **31 fail** / 15 skipped |
31 条失败全是环境缺口(无 Docker、无本地 PostgreSQL、无外网),清单见文末,与收尾实测逐条相同。
## 分类表(20 个,一个不漏)
「谁在读」按改前的 `page.tsx` 实测,不是记忆。
### synastry 簇(4 个,全部搬下去)
| 状态 | 类别 | 谁在读 | 处置 |
| --- | --- | --- | --- |
| `synastryRelationshipType` | 只服务子树 | 只有 `ChartLibraryPanel`(关系类型下拉) | 搬进新建 `useSynastry` |
| `synastryPendingId` | 只服务子树 | `ChartLibraryPanel`(按钮 disabled / 「正在计算合盘...」)、`draftSynastryQuestionFromChart` 自己的重入闸 | 搬进 `useSynastry` |
| `synastryReportCard` | 只服务子树 | `ChartLibraryPanel`(本次合盘卡) | 搬进 `useSynastry` |
| `synastryHistory` | 只服务子树,但**外壳的星盘库 effect 要写** | 值只给 `ChartLibraryPanel`;写在 `Home` 的星盘库 effect 里(切账号清空、云端历史落库)与合盘成功后 | 状态搬进 `useSynastry`;外壳只拿到两个 `useCallback` 稳定句柄 `clearSynastryHistory` / `applyCloudSynastryHistory`,并列进该 effect 的 deps(身份恒定,不多跑一次) |
`draftSynastryQuestionFromChart` 整个搬进 hook`Home``const [synastry` 出现次数 = **0**。六个子树字段打成一个 `synastryPanel`JSX 里 `{...synastryPanel}` 展开。
### profile 簇(6 个,全部搬下去)
| 状态 | 类别 | 谁在读 | 处置 |
| --- | --- | --- | --- |
| `profile` | 外壳也要读 | `Home``profileComplete``birthTimeRoute``dailyStarlanguage*``rectificationDeclaredTime`、侧栏头像首字母、`ProfilePanel` / `ChartLibraryPanel``useSessionManagement``useRectificationSurface``useSynastry``useConsultationRun` 的入参 | 状态搬进 `useProfileOnboarding``Home` 从它的 return 读回;hook 调用上移到 `useBirthTimeGuidedJourney` 之前,保证第一个读者之前已定义 |
| `profileDraft` | 外壳也要读 | `Home`:引导三步的题面与提交按钮、`BirthTimeIntakeFields` / `BirthLocationFields``ChartLibraryPanel` | 同上 |
| `profileNotice` | 外壳也要读(只透传) | `ChartLibraryPanel``setProfileNotice` 还要交给 `useRectificationSurface``useConsultationRun` | 状态在 hook`Home` 只读回值与 setter |
| `avatarNotice` | 只服务子树 | 只有 `ProfilePanel` | 搬进 hook,只读回值 |
| `avatarSaving` | 只服务子树 | 只有 `ProfilePanel`;写只在 hook 的 `persistAvatar` | 搬进 hook,只读回值 |
| `profileSaving` | 外壳也要读 | `Home`:引导三步按钮 disabled / 「保存中」、重新评估按钮、`ChatComposer``inputDisabled` / `submitBlocked``ChartLibraryPanel` | 搬进 hook,只读回值 |
`Home` 里散装 `profile*` state = **0**(低于任务书 ≤1)。
### session 簇(10 个,搬走 6 个,留 4 个)
| 状态 | 类别 | 谁在读 | 处置 |
| --- | --- | --- | --- |
| `sessionsCursor` | 外壳也要读 | `Home` 侧栏 `hasMore`;写在 hook 的 `loadMoreSessions` **和**外壳的 bootstrap effect | 搬进 `useSessionManagement`,值与 setter 都读回 |
| `showArchivedSessions` | 外壳也要读 | `Home``visibleSessions` 过滤、侧栏 `showingArchived`;写在 hook 的 `toggleArchivedView` | 搬进 hook`visibleSessions` 一并搬进 hook 计算后返回 |
| `sessionMenuId` | 只服务子树 | 只有侧栏(`menuSessionId` / `onMenuSessionChange` / 切归档视图时清菜单) | 搬进 hook,值与 setter 读回 |
| `pendingSessionDeletion` | 只服务子树 | 只有删除确认弹窗 | 搬进 hook,值与 setter 读回 |
| `sessionDetailLoadingId` | 只服务 hook 自己 | `Home` 里**没有任何读者**(改前 lint 就报 `assigned a value but never used`);只有 hook 的 `ensureSessionMessages` 读写 | 整个搬进 hook,不返回 |
| `sessionFullPrompt` | 只服务 hook 自己 | `Home` 里没有读者;写在 `useSessionManagement``useConsultationRun` | 状态搬进 hook,只把 setter 返回给 `useConsultationRun` 用 |
| `sessions` | **外壳也要读** | `Home``activeSession`、bootstrap effect 首屏落库、侧栏统计与三处 `sessions.find``sessionsRef` 初值;`useSessionManagement` / `useRectificationSurface` / `useConsultationRun` 的入参与 `setSessions` | **留在外壳**:它是三个 hook 的共同输入,且 bootstrap effect 在任何 hook 之外写它 |
| `activeSessionId` | **外壳也要读** | `Home``activeSession``visibleSessions` 的「当前会话不过滤」分支、`activeSessionIdRef` 同步 effect、`ensureSessionMessages` effect、`birthTimeRoute``useConsultationRun` 入参、侧栏 | **留在外壳**:两个 effect 的依赖项,且 `useRectificationSurface` 也写它 |
| `creatingSession` | **外壳也要读** | `Home``productEntrypointsDisabled`、自动打开校正面的守卫、新对话按钮 disabled、`StarterHome`、底部按钮;`useSessionManagement``useRectificationSurface` 双方入参 | **留在外壳**:两个 hook 都要读,谁持有都得回传给另一个 |
| `pendingSessionId` | **外壳也要读** | `Home``isLoading``productEntrypointsDisabled``ensureSessionMessages` effect deps、两条恢复 effect、`chooseSuggestedQuestion` 闸、`ChatComposer` `submitBlocked`、侧栏 | **留在外壳**:它其实属于「咨询运行」簇(`useConsultationRun`),不是会话管理簇;本单不动咨询簇 |
`Home` 里散装 `session*` state = **4**(任务书要 ≤2,见下「让步说明」)。
## 收尾实测
| 指标 | 改前 | 改后 |
| ---: | ---: | ---: |
| `Home()` `useState` | 52 | **36**(= 任务书 ≤36 的上限;搬走 16) |
| `Home()` `useRef` | 39 | **37**(未上升,反而少 2 |
| `page.tsx` 行数 | 1931 | **1846** |
| `npm run lint` | 0 error / **120** warning | 0 error / **118** warning |
| `useSessionManagement` 参数 | 41 | **34** |
| `useProfileOnboarding` 参数 | 38 | **26** |
| 散装 `synastry*` / `profile*` / `session*` state | 4 / 6 / 10 | **0 / 0 / 4** |
新建 `frontend/src/hooks/use-synastry.ts`141 行)。`use-profile-onboarding.ts``use-session-management.ts` 现在各自 `useState`
## 5.1:为什么 warning 只能靠 `useMemo`,不能靠塞 deps
`createRectificationShellSetters(setRectification)` 改前在 render 体里裸调,每帧返回四个新函数身份,入口摘要 effect 因此多一条 `exhaustive-deps`119 → 120)。
改法:`const {...} = useMemo(() => createRectificationShellSetters(setRectification), [setRectification]);`,再把 `setRectificationEntrySummary` / `setRectificationEntrySummarySettled` 补进该 effect 的 deps。`setRectification` 是 React 的 `useState` setter,身份恒定,所以四个包装函数跨帧稳定。
**不是靠肉眼**的证明:入口摘要 effect 的 deps 现在是 `[accountId, bootstrapPhase, setRectificationEntrySummary, setRectificationEntrySummarySettled]`。后两项由 `useMemo(..., [setRectification])` 产出,而 `setRectification` 来自 `useState`React 保证同一 mount 内引用不变 → `useMemo` 不重算 → 两个 setter 引用不变 → effect 仍然只在 `accountId` / `bootstrapPhase` 变化时触发。若有人删掉 `useMemo`eslint 会立刻把这条 warning 报回来(这正是改前的状态),所以这条不变量由 lint 数字守着:**warning 必须是 118,回到 119/120 就是有人把记忆化拆了**
## 既有断言改动
| 文件 | 原值 | 新值 | 原因 |
| --- | --- | --- | --- |
| `tests/home-shell-growth-contract.test.ts` 三门 | `HOME_USE_STATE_CAP = 52` / `HOME_USE_REF_CAP = 39` / `PAGE_LINE_COUNT_BASELINE = 1931` | **36** / **37** / **1846** | 本单下沉后的新基线,三项全部往紧里收 |
| `tests/chat-navigation-a11y-contract.test.ts``signOut` 切片终点 | `sourceBetween(pageSource, "async function signOut()", "return {\n refreshAccount,")` | 终点改成 `"return {\n profile,"` | 六个 profile 状态搬进 `use-profile-onboarding.ts` 后,该 hook 的 return 以 `profile` 开头,`refreshAccount` 不再是第一个字段。**切的仍是 `signOut` 函数体,断言主语(退出后 `window.location.assign("/login")` 硬跳)一字未改** |
没有其它既有断言被改动或弱化;没有删除任何断言。新增 1 条断言(见下)。
## 新增断言
`tests/home-shell-growth-contract.test.ts` 加一条 `useSessionManagement still runs before useRectificationSurface`:钉住 `rectificationSessionOpenerRef` 先声明、`useSessionManagement` 调用在 `useRectificationSurface` 之前、并且 opener 是通过 `openRectificationSession: (exactSessionId) => rectificationSessionOpenerRef.current(exactSessionId)` 递过去的。这是第一批留下的调用顺序约束,本单挪动了 hook 调用位置,所以补钉。
## 反向验证(增长冻结真的会变红)
`Home()` 里插入 `const [_growthContractProbe, _setGrowthContractProbe] = useState(false);` 后:
```
not ok 1 - Home() useState count must not grow
error: 'Home() has 37 useState calls; cap is 36. Extracted hooks and child components should own their own state.'
# tests 4 / pass 3 / fail 1
```
`useRef`、行数粗护栏、调用顺序三条仍绿。随后删掉探针,文件回到 36/37/1846,四条全绿。
## lint warning 逐条比对(不只是比数字)
按「规则 + 归一化信息」排序后 diff,`120 → 118`**没有任何新增 warning**,三处差异:
1. 少一条(本单目标):`React Hook useEffect has missing dependencies: 'setRectificationEntrySummary' and 'setRectificationEntrySummarySettled'` —— 5.1 消掉的就是它。
2. 少一条(本单副产物):`'_result' is defined but never used` —— 来自被删掉的 `guidedBirthTimeReadyRef = useRef((_result: JourneyClientResponse) => {})`,见下节。
3. 有一条既有 warning 的**信息变长**(规则、文件、effect 位置都没变):
- 原文:`React Hook useEffect has a missing dependency: 'restoreConsultationRecovery'.`
- 现文:`React Hook useEffect has missing dependencies: 'restoreConsultationRecovery', 'setProfile', 'setProfileDraft', and 'setSessionsCursor'.`
- 原因:那是 mount-once 的 bootstrap effect,它写 `setProfile` / `setProfileDraft` / `setSessionsCursor`,改前这三个是 `Home` 自己的 `useState` settereslint 认得,不要求进 deps),改后来自 hook 的 returneslint 认不出是 setter)。
- **为什么不把它们补进 deps**:试过,`}, [setProfile, setProfileDraft]);` 会打红两条既有断言——`tests/consultation-recovery.test.ts``source.indexOf("}, []);")` 当切片路标,`tests/chat-navigation-a11y-contract.test.ts` 直接断言 `\n \}, \[\]\);`。这两条断言的主语是「bootstrap 是 mount-once」,为了一条 warning 的措辞去改它们不划算,**所以选择不动业务代码、不动既有断言,只如实记录这处措辞漂移**。三个都是 `useState` setter 原样透传,身份恒定,effect 仍然只跑一次。
## 为什么删掉了两个 ref`useRef` 39 → 37
`useProfileOnboarding` 改成持有状态后,React 编译器的 lint 规则才开始检查这个文件(改前它一个 hook 都不调),一次性冒出 **5 条 error**(§7.1 要求 0 error):
| error | 位置 | 处置 |
| --- | --- | --- |
| `react-hooks/immutability` ×3 | `dialogReturnTarget.current = ...``birthTimeRevisionPending.current = ...` ×2 | 按规则自带的 hint 把两个 ref 入参改名为 `dialogReturnTargetRef` / `birthTimeRevisionPendingRef`(纯改名) |
| `react-hooks/refs` ×2 | render 期写 `guidedBirthTimeReadyRef.current = completeGuidedBirthTime` / `editDeclaredBirthTimeDetailsRef.current = editDeclaredBirthTimeDetails` | 删掉这两个 ref |
这两个 ref 当初存在,只是因为 `useProfileOnboarding` 排在 `useBirthTimeGuidedJourney` **之后**、函数还没定义就要交给它。本单把 profile hook 上移之后,`completeGuidedBirthTime` / `editDeclaredBirthTimeDetails` 可以直接传:
```
onReady: (result) => completeGuidedBirthTime(result),
onEditBirthTimeDetails: () => editDeclaredBirthTimeDetails(),
```
**零行为变化的依据**`useBirthTimeGuidedJourney` 只在 render 作用域的闭包里用这两个回调(`acknowledgeReady``onReady(journey)``editBirthTimeDetails: onEditBirthTimeDetails` 直接原样返回),没有把它们塞进 `useEffect`/`useRef` 里缓存。改前每帧读 `.current`、改后每帧拿当帧的函数,调用到的都是同一个「最新一帧的实现」。(已核对 `src/hooks/use-birth-time-guided-journey.ts` 全文,`onReady` / `onEditBirthTimeDetails` 只出现在第 183、255 行。)
`useRef` 数从 39 降到 37,没有把任何 state 改写成 ref。
## 让步说明(任务书 §6
1. 5.1、5.2、5.3 全做,无让步。
2. 5.4 **部分让步**:任务书要求 `Home()` 里散装 `session*` state ≤ 2,实际留了 **4**`sessions` / `activeSessionId` / `creatingSession` / `pendingSessionId`),理由在分类表里逐个写明——前三个是三个 hook 的共同输入或 bootstrap effect 在 hook 之外的写入点,第四个属于咨询运行簇而不是会话管理簇。把它们合成一个外壳对象是可行的,但要同时改 `useConsultationRun``useRectificationSurface`、bootstrap effect 三处写入口,属于「咨询簇」的活,不是本单的零行为变化预算,**留给下一批**。
3. 5.5 的收口指标**没有放松**`useState` 36 正好等于任务书上限 ≤36`useRef` 37 低于上限 39,行数基线 1846 低于 1931,三条冻结门全部往紧里收。
4. 5.5、5.6 未砍。
## 验收命令与结果
| 命令 | 结果 |
| --- | --- |
| `./node_modules/.bin/tsc --noEmit` | **0 错** |
| `npm run lint` | **0 error / 118 warning**(基线 120;逐条比对见上节) |
| `npx tsx --test tests/home-shell-growth-contract.test.ts tests/rectification-*.test.ts tests/chat-session-*.test.ts tests/session-*.test.ts` | 见下 |
| `npx tsx --test tests/*.test.ts tests/*.test.tsx` | 见下 |
| `npm run build` | 见下 |
(定向、全量、build 的实测数字见文件末尾「收尾复核」一节。)
## 真人走查
`docs/testing/home-state-lowering-batch2-20260916.md`:合盘三条路径、资料引导与头像、会话列表七件事。本仓无登录态、无 Chrome,未测。
## 发现的交互缺陷(本单不修)
无新增。`sessionDetailLoadingId``sessionFullPrompt` 这两条 state 在 `Home()` 里**从来没有读者**(改前 lint 已经在报 `assigned a value but never used`,改后同样两条 warning 原封不动出现在 `use-session-management.ts`)。`sessionFullPrompt``useConsultationRun` 写入却无人渲染,像是没接完的功能;本单零行为变化,只记录不动。
## 未做(范围外)
- 其余分散状态(`account*``birthTime*``consultation*``streaming*` 等)
- 咨询运行簇(`pendingSessionId` / `pendingRequestId` / `streamingReply` / `replyOutcome` …)
- Context Provider 或外部 store
-`origin HEAD:staging`
-`docs/tasks/README.md` 状态列
---
## 收尾复核(最终工作树,四条验收命令的实测)
| 命令 | 实测 |
| --- | --- |
| `./node_modules/.bin/tsc --noEmit` | **0 错** |
| `npm run lint` | **0 error / 118 warning**(基线 120 |
| `npx tsx --test tests/home-shell-growth-contract.test.ts tests/rectification-*.test.ts tests/chat-session-*.test.ts tests/session-*.test.ts` | **1369** tests / 1360 pass / **0 fail** / 9 skipped |
| `npx tsx --test tests/*.test.ts tests/*.test.tsx` | **3330** tests / 3284 pass / **31 fail** / 15 skipped |
| `npm run build` | 退出码 0`/` 仍是 **`○ Static`** |
测试总数 3329 → **3330**(只增不减,多的那条是新增的调用顺序断言)。全量失败清单与开工基线 **31 条逐条相同**`diff` 空)。
## 全量失败清单(与开工基线逐条相同,全是环境缺口)
无 Docker、无本地 PostgreSQL、无外网,因此 database / staging-sync / backup / 远端 oracle 这几类整组红。
`Better Auth supports shared user OTP/password sessions for admins``Owner recovery grants only one currently loginable synced identity admin``admin code functions reject immutable codes, revoked redemption, and roll back on audit failure``admin customer reset clears only rebuildable application state``billing order adjustments and redemption reasons are atomic and audited``billing, subscriptions, usage authorization, RBAC, and model publication remain transactional``cached appendix returns markdown without model or engine calls``database drops secret refs, generates immutable codes, and invalidates evidence after key rotation``database env validator rejects Compose-compatible duplicate selectors without printing values``database roles have no cluster privileges``ephemeris natal and transit chart requests skip VedAstro overview``ephemeris_events failure degrades instead of failing the page``foundation grants no direct runtime table DML and exposes only reviewed functions``live staging sync preserves env, state, incoming files, and encrypted backups``live staging sync repairs nested deploy-tree drift without preserving foreign ownership``local PostgreSQL applies the reviewed business schema and serves authenticated business calls``migration runner is serialized, atomic, drift-safe, and read-only in check mode``missing appendix is a retired report, not an on-demand generation``panchanga still returns without a birth profile, and natal-relative stays empty``personal report job migration is atomic, lease-bound, recoverable, and owner read-only``personal report sections enforce owner-read RLS and service-owned durable transitions``rectification agent maps setup failures without calling the rest of the handler``rectification case refresh maps setup failures without calling the rest of the handler``redeem security: case-sensitive hashing, rate limiting, idempotency and order ownership``saving a published product forks a draft and delete retires or removes it``self-hosted identity migration creates Better Auth tables with least privilege``server compose defaults to local images without removing either build``service and restricted admin database identities stay separated``staging backups are encrypted, atomic, private, and retain the newest three``staging env validator rejects selector drift, duplicates, and unsafe permissions``unauthenticated ephemeris requests are 401`
不是本单回归。
## `next build` 与首屏 gzip
Linux 上 Turbopack 构建正常,两次都退出 0:
- 改后:路由表里 `┌ ○ /`,即 `/` 仍是 **Static**
- 基线对照:把 `frontend/src``frontend/tests` 临时 `git checkout origin/staging --` 回去重建了一次,路由表同样是 `┌ ○ /`,随后 `git checkout HEAD --` 还原。
首屏 gzip 量法(与第一批同一种):读 `.next/server/app/index.html`,取它引用的全部 `/_next/static/**.js`,逐个 `gzip -9` 后求和。
| | 文件数 | gzip 合计 |
| --- | ---: | ---: |
| 基线 `origin/staging` @ `37e6c519` | 20 | **585,817 B** |
| 本单 | 20 | **585,868 B** |
变化 **+0.009 %**,远在 ±2 % 内(+51 B)。