# TASK · page.tsx 状态下沉第二批:session / profile / synastry 三簇 + 稳住外壳 setter - 日期:2026-09-16 - 基线 commit:`origin/staging` @ `4f643aa0`(开工时以最新 `origin/staging` 为准,数字全部重测) - 执行分支:`codex/home-state-lowering-batch2-20260916` - 落点:`frontend/src/app/page.tsx`、`frontend/src/hooks/use-session-management.ts`、`frontend/src/hooks/use-profile-onboarding.ts`、`frontend/src/hooks/use-rectification-surface.ts`、以及为合盘新建的 hook - **串行在 `f8e607c2`(第一批)之后**,已满足 - 与 `TASK-consultation-residual-hotspots-20260916`(纯 Python)、`TASK-api-server-backdoor-close-20260916`(纯 Python)**无文件重叠,三单可并行** - 规模:照第一批的成方再搬三簇。**零行为变化、零文案变化。** --- ## 1. 第一批已经证明这条路走得通 `f8e607c2` 把 15 个 `rectification*` 状态搬走,我验收实测: | 指标 | 第一批前 | 第一批后 | | --- | ---: | ---: | | `Home()` 的 `useState` | 66 | **52** | | `Home()` 的 `useRef` | 41 | **39** | | `page.tsx` 行数 | 1,951 | 1,931 | | 残留散装 `rectification*` state | 15 | **0** | 做法是:先把 15 个逐条分类成「只服务子树」与「外壳也要读」,前者搬进 `useRectificationSurface`,后者合并成**一个**对象 state。全量前端套件两侧完全相同(3329 tests / fail 31,失败清单逐条一致),首屏 gzip 0.00 % 变化,`/` 仍 `○ Static`。增长冻结基线**往紧里收**(66/41/1951 → 52/39/1931)。 本单把同一套做法用在剩下的三簇上。 ## 2. 事故实证:剩下的 52 个里还有 20 个是成簇的 `Home()` 现在 52 个 `useState`,按前缀分群: | 群 | 个数 | 已经有的抽取点 | | --- | ---: | --- | | `session*` | **10** | `use-session-management.ts`(参数式,解构约 40 个参数) | | `profile*` | **6** | `use-profile-onboarding.ts` | | `synastry*` | **4** | 无,散在 `Home()` 里 | | 其余分散 | 32 | — | `useSessionManagement(params)` 的第一件事仍然是解构约 40 个参数——第一批没有碰它。这就是「代码搬走了、状态没搬」的残留部分。 另有第一批留下的一个小尾巴(执行方已在进度记录里主动披露,判断正确): `createRectificationShellSetters(setRectification)` 在 render 体里**无记忆化调用**,每帧返回新的函数身份,于是入口摘要那个 `useEffect` 多了一条 `react-hooks/exhaustive-deps` warning(全仓 119 → **120**)。进度记录写明「不能把它们放进 deps,否则每帧重拉摘要」——这个判断是对的,我核过:那几个 setter 都只是 `setRectification(prev => ...)` 的包装,任何身份行为都一样,**当前是安全的**。但它是个陷阱:后面任何人为了消除 warning 把它们塞进 deps,就会造成每帧重新拉取入口摘要。 ## 3. 决策记录 产品 2026-09-16 授权本单,口径与第一批一致: 1. **继续走「状态下沉」**,不引外部 store、不铺全局 Context Provider(第一批已否决这两条,本单不重开)。 2. **抽出去的 hook 与子组件持有自己的状态**——`AGENTS.md` §6 已于 `3b17c1b2` 改写,「参数式 hook 内部保持 0 个 React hook」那条红线已经推翻。执行方不得以旧 AGENTS 为由拒改。 3. **零行为变化。** 本单不修任何已知交互缺陷,发现了写进进度记录。 4. **那条 warning 要在本单里消掉**,做法是稳住 setter 身份,**不是**把它们加进 deps。 ## 4. 硬红线 1. **先分类再动手。** 三簇共 20 个状态,每一个都要归到「只服务子树 → 搬下去」或「外壳也要读 → 合并成一个对象」,分类表连同「谁在读」写进进度记录,一个不漏。第一批的分类表是格式范本。 2. **零行为变化**是唯一成败判据。逐条对照:会话列表加载/翻页/切换/归档/置顶/重命名/删除、模型切换与同步失败、资料引导各步、头像上传、合盘发起与历史、写满后开新对话。 3. `next build` 后 `/` 仍须 `○ Static`;首屏 gzip 变化在 ±2 % 内(第一批实测 130,872 B,同一种量法)。 4. 测试总数不得低于开工时 `origin/staging` 的实测;改任何既有断言必须写「原值 / 新值 / 原因」三栏(AGENTS §7.3)。第一批那批源码正则合同(`foo={bar}` → `foo: bar`)是可接受的改法范本:**主语不许变**。 5. **`npm run lint` 的 warning 数不得上升**,并且本单要把 120 降回 **119**(消掉 §2 那条)。 6. 不得新写第二个聊天输入框、第二套滚动跟随、第二套加载动画(§6 第三条原样有效)。 7. 不得顺手升级依赖、不得顺手修不在本单里的 warning。 ## 5. 任务分解 ### 5.1 稳住外壳 setter,消掉那条 warning(先做,最小) 把 `createRectificationShellSetters(setRectification)` 的结果记忆化(`useMemo`,依赖只有 `setRectification`,而它是 React 的稳定 setter),使四个 setter 身份跨帧稳定;然后把它们按 eslint 的要求补进那个 `useEffect` 的 deps。 - 验收:`npm run lint` 从 120 warning 回到 **119**,且**没有新增任何其它 warning**(按规则+信息归一化比对,不要只比数字)。 - 验收:入口摘要的 effect 仍然只在 `accountId` / `bootstrapPhase` 变化时触发——新增一条断言或在进度记录里给出证明,**不得靠肉眼**。 ### 5.2 `synastry*` 四个下沉(第二小,边界最清楚) `synastryRelationshipType` / `synastryPendingId` / `synastryReportCard` / `synastryHistory` 目前散在 `Home()` 里,没有对应 hook。新建一个 hook 或直接让合盘子树持有。 - 验收:`Home()` 里 `const [synastry` 的出现次数为 **0**。 - 验收:合盘发起、失败、历史列表三条路径行为不变。 ### 5.3 `profile*` 六个下沉 `use-profile-onboarding.ts` 已存在(445 行,参数式)。改成持有自己的状态。注意资料引导与 `bootstrapPhase`、揭幕门耦合较深,分类时要特别小心哪些是外壳要读的。 - 验收:分类表里每个 `profile*` 都写明谁在读。 - 验收:`Home()` 里散装 `profile*` state ≤ **1**(合并对象),多留必须逐个说明理由。 ### 5.4 `session*` 十个下沉(最大、最后做) `use-session-management.ts` 是三簇里耦合最深的(约 40 个参数,522 行),而且它和校正面有交叉(第一批的进度记录写明:`useSessionManagement` 必须在 surface hook 之前调用,用 `rectificationSessionOpenerRef` 把 opener 递过去)。**这条交叉关系不得破坏**。 - 验收:`useSessionManagement` 的参数个数显著下降,新值写进进度记录(改前约 40)。 - 验收:`Home()` 里散装 `session*` state ≤ **2**。 - 验收:`rectificationSessionOpenerRef` 那条调用顺序约束仍然成立,并有一条断言钉住。 ### 5.5 收口指标 - 验收:`Home()` 的 `useState` 从 **52** 降到 **≤ 36**(三簇共 20 个,允许留 ≤ 4 个在外壳);`useRef` 不得上升(改前 39)。 - 验收:`frontend/tests/home-shell-growth-contract.test.ts` 更新为新基线(**只许往紧里收**),并贴一次反向验证(人为加一个 `useState` 必须变红)。 - 验收:全量前端套件失败清单与开工基线逐条一致。 ### 5.6 真人清单 照 `docs/testing/home-state-lowering-20260915.md` 的格式,为本单三簇补一份可照做的走查条目(本仓无浏览器与登录态)。 ## 6. 让步顺序 1. 5.1 必须做,它是第一批留下的尾巴,最小。 2. 5.2 → 5.3 → 5.4 按由易到难推进。**做不完可以只交前两簇**,砍掉的簇在进度记录里写明,并把 5.5 的目标值按实际达成调整(**只许往紧里收,不许放松**)。 3. 5.5、5.6 不得砍。 ## 7. 开工前置命令 ```bash git fetch origin --prune git worktree add -b codex/home-state-lowering-batch2-20260916 \ .worktrees/home-state-lowering-batch2-20260916 origin/staging cd .worktrees/home-state-lowering-batch2-20260916/frontend git status -sb | head -1 npm ci # 重测基线,不要抄本任务书里的数 grep -cE '\buseState[<(]' src/app/page.tsx grep -cE '\buseRef[<(]' src/app/page.tsx for p in session profile synastry; do printf "%s: %s\n" "$p" "$(grep -cE "const \[$p" src/app/page.tsx)"; done npm run lint 2>&1 | tail -2 ``` 开工前必读:`docs/tasks/PROGRESS-home-state-lowering-20260915.md`(第一批的分类表与收尾实测,是本单的格式范本)。 验收命令: ```bash ./node_modules/.bin/tsc --noEmit npm run lint # 0 error,warning 必须 ≤ 119 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 # 与基线逐条比对失败清单 npm run build # `/` 仍须 ○ Static ``` ## 8. BUG 编号起点 本单不占 BUG 号(结构改造,不是缺陷)。基线 `4f643aa0` 上最大号 **BUG-733**;734/735 已被 `TASK-consultation-residual-hotspots-20260916` 预占。 ## 9. 不在本单范围 - 其余 32 个分散状态(没有成簇,逐个搬性价比低) - Context Provider 或外部 store - 任何交互缺陷(本单零行为变化) - API server 一侧(另一条线)