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

20 KiB
Raw Blame History

PROGRESS · 状态下沉第二批:synastry / profile / session 三簇 + 稳住外壳 setter2026-09-16

工作树:.worktrees/home-state-lowering-batch2-20260916 分支:codex/home-state-lowering-batch2-20260916 任务书基线写 4f643aa0;开工时 origin/staging = 37e6c519docs(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 整个搬进 hookHomeconst [synastry 出现次数 = 0。六个子树字段打成一个 synastryPanelJSX 里 {...synastryPanel} 展开。

profile 簇(6 个,全部搬下去)

状态 类别 谁在读 处置
profile 外壳也要读 HomeprofileCompletebirthTimeRoutedailyStarlanguage*rectificationDeclaredTime、侧栏头像首字母、ProfilePanel / ChartLibraryPaneluseSessionManagementuseRectificationSurfaceuseSynastryuseConsultationRun 的入参 状态搬进 useProfileOnboardingHome 从它的 return 读回;hook 调用上移到 useBirthTimeGuidedJourney 之前,保证第一个读者之前已定义
profileDraft 外壳也要读 Home:引导三步的题面与提交按钮、BirthTimeIntakeFields / BirthLocationFieldsChartLibraryPanel 同上
profileNotice 外壳也要读(只透传) ChartLibraryPanelsetProfileNotice 还要交给 useRectificationSurfaceuseConsultationRun 状态在 hookHome 只读回值与 setter
avatarNotice 只服务子树 只有 ProfilePanel 搬进 hook,只读回值
avatarSaving 只服务子树 只有 ProfilePanel;写只在 hook 的 persistAvatar 搬进 hook,只读回值
profileSaving 外壳也要读 Home:引导三步按钮 disabled / 「保存中」、重新评估按钮、ChatComposerinputDisabled / submitBlockedChartLibraryPanel 搬进 hook,只读回值

Home 里散装 profile* state = 0(低于任务书 ≤1)。

session 簇(10 个,搬走 6 个,留 4 个)

状态 类别 谁在读 处置
sessionsCursor 外壳也要读 Home 侧栏 hasMore;写在 hook 的 loadMoreSessions 外壳的 bootstrap effect 搬进 useSessionManagement,值与 setter 都读回
showArchivedSessions 外壳也要读 HomevisibleSessions 过滤、侧栏 showingArchived;写在 hook 的 toggleArchivedView 搬进 hookvisibleSessions 一并搬进 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 里没有读者;写在 useSessionManagementuseConsultationRun 状态搬进 hook,只把 setter 返回给 useConsultationRun
sessions 外壳也要读 HomeactiveSession、bootstrap effect 首屏落库、侧栏统计与三处 sessions.findsessionsRef 初值;useSessionManagement / useRectificationSurface / useConsultationRun 的入参与 setSessions 留在外壳:它是三个 hook 的共同输入,且 bootstrap effect 在任何 hook 之外写它
activeSessionId 外壳也要读 HomeactiveSessionvisibleSessions 的「当前会话不过滤」分支、activeSessionIdRef 同步 effect、ensureSessionMessages effect、birthTimeRouteuseConsultationRun 入参、侧栏 留在外壳:两个 effect 的依赖项,且 useRectificationSurface 也写它
creatingSession 外壳也要读 HomeproductEntrypointsDisabled、自动打开校正面的守卫、新对话按钮 disabled、StarterHome、底部按钮;useSessionManagementuseRectificationSurface 双方入参 留在外壳:两个 hook 都要读,谁持有都得回传给另一个
pendingSessionId 外壳也要读 HomeisLoadingproductEntrypointsDisabledensureSessionMessages 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.ts141 行)。use-profile-onboarding.tsuse-session-management.ts 现在各自 useState

5.1:为什么 warning 只能靠 useMemo,不能靠塞 deps

createRectificationShellSetters(setRectification) 改前在 render 体里裸调,每帧返回四个新函数身份,入口摘要 effect 因此多一条 exhaustive-deps119 → 120)。

改法:const {...} = useMemo(() => createRectificationShellSetters(setRectification), [setRectification]);,再把 setRectificationEntrySummary / setRectificationEntrySummarySettled 补进该 effect 的 deps。setRectification 是 React 的 useState setter,身份恒定,所以四个包装函数跨帧稳定。

不是靠肉眼的证明:入口摘要 effect 的 deps 现在是 [accountId, bootstrapPhase, setRectificationEntrySummary, setRectificationEntrySummarySettled]。后两项由 useMemo(..., [setRectification]) 产出,而 setRectification 来自 useStateReact 保证同一 mount 内引用不变 → useMemo 不重算 → 两个 setter 引用不变 → effect 仍然只在 accountId / bootstrapPhase 变化时触发。若有人删掉 useMemoeslint 会立刻把这条 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.tssignOut 切片终点 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.tssource.indexOf("}, []);") 当切片路标,tests/chat-navigation-a11y-contract.test.ts 直接断言 \n \}, \[\]\);。这两条断言的主语是「bootstrap 是 mount-once」,为了一条 warning 的措辞去改它们不划算,所以选择不动业务代码、不动既有断言,只如实记录这处措辞漂移。三个都是 useState setter 原样透传,身份恒定,effect 仍然只跑一次。

为什么删掉了两个 refuseRef 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 作用域的闭包里用这两个回调(acknowledgeReadyonReady(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,实际留了 4sessions / activeSessionId / creatingSession / pendingSessionId),理由在分类表里逐个写明——前三个是三个 hook 的共同输入或 bootstrap effect 在 hook 之外的写入点,第四个属于咨询运行簇而不是会话管理簇。把它们合成一个外壳对象是可行的,但要同时改 useConsultationRunuseRectificationSurface、bootstrap effect 三处写入口,属于「咨询簇」的活,不是本单的零行为变化预算,留给下一批
  3. 5.5 的收口指标没有放松useState 36 正好等于任务书上限 ≤36useRef 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,未测。

发现的交互缺陷(本单不修)

无新增。sessionDetailLoadingIdsessionFullPrompt 这两条 state 在 Home()从来没有读者(改前 lint 已经在报 assigned a value but never used,改后同样两条 warning 原封不动出现在 use-session-management.ts)。sessionFullPromptuseConsultationRun 写入却无人渲染,像是没接完的功能;本单零行为变化,只记录不动。

未做(范围外)

  • 其余分散状态(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 adminsOwner recovery grants only one currently loginable synced identity adminadmin code functions reject immutable codes, revoked redemption, and roll back on audit failureadmin customer reset clears only rebuildable application statebilling order adjustments and redemption reasons are atomic and auditedbilling, subscriptions, usage authorization, RBAC, and model publication remain transactionalcached appendix returns markdown without model or engine callsdatabase drops secret refs, generates immutable codes, and invalidates evidence after key rotationdatabase env validator rejects Compose-compatible duplicate selectors without printing valuesdatabase roles have no cluster privilegesephemeris natal and transit chart requests skip VedAstro overviewephemeris_events failure degrades instead of failing the pagefoundation grants no direct runtime table DML and exposes only reviewed functionslive staging sync preserves env, state, incoming files, and encrypted backupslive staging sync repairs nested deploy-tree drift without preserving foreign ownershiplocal PostgreSQL applies the reviewed business schema and serves authenticated business callsmigration runner is serialized, atomic, drift-safe, and read-only in check modemissing appendix is a retired report, not an on-demand generationpanchanga still returns without a birth profile, and natal-relative stays emptypersonal report job migration is atomic, lease-bound, recoverable, and owner read-onlypersonal report sections enforce owner-read RLS and service-owned durable transitionsrectification agent maps setup failures without calling the rest of the handlerrectification case refresh maps setup failures without calling the rest of the handlerredeem security: case-sensitive hashing, rate limiting, idempotency and order ownershipsaving a published product forks a draft and delete retires or removes itself-hosted identity migration creates Better Auth tables with least privilegeserver compose defaults to local images without removing either buildservice and restricted admin database identities stay separatedstaging backups are encrypted, atomic, private, and retain the newest threestaging env validator rejects selector drift, duplicates, and unsafe permissionsunauthenticated ephemeris requests are 401

不是本单回归。

next build 与首屏 gzip

Linux 上 Turbopack 构建正常,两次都退出 0:

  • 改后:路由表里 ┌ ○ /,即 / 仍是 Static
  • 基线对照:把 frontend/srcfrontend/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)。