# PROGRESS · 小刀轮:孤儿契约红修复 + popstate 副作用对齐(2026-09-01) 工作树:`.worktrees/contract-repairs-20260901` 分支:`codex/contract-repairs-20260901`(跟踪 `origin/staging`) 基线:`origin/staging` @ `a6148af8`(任务书提交;产品代码与第一批 `54269fcf` 相同,另加两份任务书) 不要写成 `PROGRESS.md`。小刀轮与第二批在同一工作树落地(第二批任务书写「小刀轮落地后的 origin/staging」;小刀当时尚未推送,故未另开 `home-split-batch2` 工作树)。交付为快进推 `origin/staging`。 开工后 `origin/staging` 又快进了 4 个无关文档提交(`ff97de5e`…`8b7eda9f`),未并入本树。 ## 任务 1 · 归档契约 文件:`tests/test_session_management_entrypoints.py::test_archiving_never_calls_the_delete_endpoint` | 项 | 原值 | 新值 | | --- | --- | --- | | 读取面 | `PAGE.read_text()`(仅 `page.tsx`) | `_home_surface()`:`page.tsx` + 第一批抽出文件 + 若存在则拼接 `hooks/use-session-management.ts`、`hooks/use-consultation-run.ts` | | 切片 | `function toggleArchivedSession` → `async function shareSession` | 同左(两函数第二批一起搬进 session hook,切片标记仍相邻) | | 正断言 | `"setArchivedSessionIds" in archive_action` | `'writeChatSession(sessionId, { archived_at: nextArchivedAt }, "update")' in archive_action` 且 `"archived_at: nextArchivedAt" in archive_action` | | 负断言 | `"/api/sessions/" not in archive_action` | `'method: "DELETE"' not in archive_action` 且 `"DELETE" not in archive_action` | | PATCH | (无) | `chat-session-write-contract.ts` 含 `method: mode === "create" ? "POST" : "PATCH"` | 同文件 `test_chat_history_management_actions_are_exposed` 的读取面从 `PAGE + STYLES + SESSION_ROW` 改为 `_home_surface() + STYLES + SESSION_ROW`,断言 token 未改。`test_chat_session_delete_is_server_controlled_and_granted` 仍读路由/迁移。 `CORE_PYTEST_TARGETS`:加入 `tests/test_session_management_entrypoints.py`,注释「Locks archive as PATCH archived_at, never HTTP DELETE (data-safety).」 ## 任务 2 · 引导旅程契约 文件:`tests/test_birth_time_journey_contract.py::test_web_onboarding_uses_the_deterministic_free_journey` | 项 | 原值 | 新值 | | --- | --- | --- | | page 读取面 | `page.tsx` + `home-profile.ts` | `_home_surface()`(同上,含可选 hook 文件) | | mastra 读取面 | `frontend/src/mastra/index.ts` | `_mastra_consultation_surface()` = `index.ts` + `consultation-workflow.ts`(`index.ts` 只再导出;token 在 workflow) | | 正断言 | `'entry_mode: entryMode' in mastra` | 不变(现码 `consultation-workflow.ts`) | | 负断言 | `'entry_mode: "direct_chart"' not in mastra` | 不变(schema 默认值是 `entryMode: z.enum(...).default("direct_chart")`,不是 `entry_mode: "direct_chart"`;`consultation-tools.ts` 未拼进此读取面) | 白名单决定:**加入** `CORE_PYTEST_TARGETS`。整文件只读 SQL 迁移与 TS 源码,无 subprocess、无网络、无 Docker。注释「Pure source/SQL regex for the birth-time journey; no runtime services.」 ## 任务 3 · popstate 副作用对齐 `applySessionPopStateRef`(现位于 `use-session-management.ts`,小刀改完后原样搬): | 项 | 原值 | 新值 | | --- | --- | --- | | `requestedId` | `query.present ? query.sessionId : fallbackId`(无 `?c=` 时直接等于列表首项,空 id 分支实际上只有「列表为空」才会进) | `query.sessionId`(无 `?c=` 时进空 id 分支) | | 空 id 分支 | `setActiveSessionId(""); return;` | 有 `fallbackId`:`sessionSelectionSource.current = "history"; selectSession(fallbackId);`;否则维持 `setActiveSessionId("")` | 有会话时 back 到 `/` 仍走 `selectSession`(清草稿/提示、切绑定星盘),来源保持 history,不二次 `pushState`。无会话时行为不变。 `frontend/tests/chat-session-url.test.ts`: - 原 P1 合同「popstate reuses selectSession without a second push」未改断言;切片里 `writeSessionUrl(..., "push")` 仍只有 `selectSession` 内那一处。 - 新增「popstate to a missing session query reuses selectSession side effects for the default chat」,锁空 id 分支源码形态,且 popstate 切片内 `writeSessionUrl(..., "push")` 次数为 0。 `frontend/tests/home-surface.ts` 同步加入可选 hook 文件(`existsSync`),避免第二批把 `selectSession` / popstate 搬走后本文件立刻再红。 ## 前瞻(第二批) 两份 Python 合同与 `chat-session-url.test.ts` 都走拼接读取。第二批把 `toggleArchivedSession` / `applySessionPopStateRef` 搬进 `use-session-management.ts` 后,hook 文件进可选清单即可。切片标记保持相邻。 ## 验证 - `./node_modules/.bin/tsc --noEmit`:通过(小刀后与第二批抽取后各跑一次)。 - `python3.12 -m pytest tests/test_session_management_entrypoints.py tests/test_birth_time_journey_contract.py -q`:12 passed(3 + 9)。 - 连同 `tests/test_supabase_user_data_contract.py` `tests/test_daily_and_rectification_entrypoints.py`:24 passed。 - `tsx --test tests/chat-session-url.test.ts`:12 pass / 0 fail / 0 skipped。 - 前端全量 `npm test`:2428 pass / 0 fail / 0 skipped(小刀新增 1 条 popstate 源码合同;相对第一批 2427)。 - `./node_modules/.bin/next build --webpack`:`┌ ○ /` Static。首屏 JS gzip-9 = 501104 B(第一批后 501315 B,−0.04%)。 未改 `.gitea/workflows/**`。未动数据库。未提升 `main`。