# 任务书 · 采用流程修复单:采集期出口兜底、「改选」可点、开场题不占 education、核对题年份持久化、删「用这个时间看盘」按钮(2026-09-03,09-03 增补 §4b) 基线:`origin/staging` `35e5781e`(`TASK-rectification-adopt-flow-20260902.md` 的实现,BUG-497~500)。本任务书是对该实现 review 后的修复单,**不改 `35e5781e` 已定的形态**(公开 `can_adopt` 按 session_outcome 收紧、accept 409、核对题沿用持久化 questionId、卡归出卡消息、状态条承接),只修 1 个 P1 和 4 个 P2(§4b 为 09-03 用户实测后增补)。 ## 0. Review 结论摘要 `35e5781e` 门禁全绿(tsc / lint 0 错 / 1109 测试 fail=0),两个 P0 的修法与任务书一致。但收紧公开 `can_adopt` 之后,**非终止轮出口检查还在看内部 `canAdopt`**,会把一种此前靠"漏出的采用卡"兜住的状态变成真正的死角;另外三处是形态没做完或副作用。 **09-03 增补来源**:用户在真实环境走完一轮(采用 04:53,不可分区间约 29 分钟)后反馈"下面那个 `rectification-consult-handoff`「用这个时间看盘」按钮没用吧"。核对结论:按钮不是死的,但只做了"开一个空会话 + 往输入框塞一句服务端根本不读的话",与用户自己点「新对话」无异;用户随后拍板"我不需要这个按钮",本单 §4b 改为**删除**。用户同一份记录里其余现象**已被 `35e5781e` 覆盖、本单不重复处理**,实现者不要再改: | 用户看到的 | 归属 | 状态 | | --- | --- | --- | | 题干出现两遍(正文一遍、选项卡上方一遍) | 旧构建底部问题槽 + 消息内题干并存 | `35e5781e` 已删底部槽,嵌入卡 legend 为 `sr-only`,题干只剩消息内一处;待部署 | | 核对题卡片底部仍有「先这样,先看当前范围」 | reverse_verify 卡应为「这题跳过」 | `35e5781e` §3.4 已做;待部署 | | 收口报告之后又问一道 2024 年 5 月感情核对题 | 采用后最多核两件前事,是 Skill 既定流程 | 不是缺陷,不改 | | 「用这个时间看盘」是列表末尾裸按钮 | `rectification-consult-handoff` | `35e5781e` 已移进状态条并删裸按钮;本单 §4b 把状态条里的这个按钮也删掉 | ## 1. P1 · 采集期"无题、无卡、无出口" **位置**:`frontend/src/lib/rectification-agentic/v9/answer-choice.ts` `inspectNonTerminalTurnExit`(行号线索 ~992–1013,按符号名定位)。 **现象(静态推演)**:`satisfied` 的最后一项是 `decision.canAdopt`(内部能力位)。当 `session_outcome = collect_evidence`、内部 `canAdopt = true`、`buildMethodFollowupPlan` 的 `next_followup` / `deferred_followup` 都为 null(题库耗尽)时: 1. `persistNextInterviewIfIdle` 走到 `if (!followup) return { persisted:false }`,不建题; 2. `ensureNonTerminalTurnExit` 因 `decision.canAdopt` 判 `satisfied = true`,**不调用 `persistExhaustionCollect` 兜底**; 3. 公开 `can_adopt` 现在为 false → 前端不出采用卡;`showCollectStop` 依赖活跃采集题 → 「先这样」也不出; 4. 用户只剩空 composer,随便发一句话再跑一轮回到同一状态。 `35e5781e` 之前这个状态会漏出采用卡(就是 BUG-497 的现象),所以没人碰到过死角;现在门关了,出口必须补上。 **修法**: 1. `inspectNonTerminalTurnExit` 的 `satisfied` 把 `decision.canAdopt` 改成 `publicCanAdopt(decision)`(从 `core/rectification-decision` 导入)。语义:只有**用户在界面上真的能采用**时,"没有下一问"才算满足;否则走 `persistExhaustionCollect`(它建的题自带「先这样」,`answerChoice` 的 stop 路径会把 outcome 推到 `provisional_range_user_stopped`,公开 `can_adopt` 随之打开)。 2. `persistNextInterviewIfIdle` 里 `shouldSkipFollowupPersist({ canAdopt: decision.canAdopt, ... })` 同步改成 `publicCanAdopt(decision)`。现有 nextAction 白名单已保证采集/区分/holdout 不会跳过持久化,这一改只是让"跳过持久化、改出 adopt 旁白"与公开门一致,不得出现"旁白说可以采用、界面却没卡"的分叉。 3. 确认 `persistExhaustionCollect` 在 `collect_evidence` + 内部 `canAdopt = true` 下能返回一道题(`exhaustionSpokenCollectFollowup` 若也为 null,则至少给 `occupation` 兜底题——现有逻辑已有,验证即可)。 **测试**(`tests/rectification-adopt-flow-fix-20260903.test.ts` 新建): - 构造 dossier:`sessionOutcome = collect_evidence`、内部 `canAdopt = true`、无 active focus、题库耗尽(evidence 覆盖所有 collect 域或 declined 全部)。断言 `ensureNonTerminalTurnExit` 返回 `persisted = true`,写入的 focus `intent = collect_method_evidence` 且 `projectCurrentQuestion(focus).kind === "collect_spoken"`,并有 `rectification_nonterminal_exit_repaired` warn。用 fake accounting,不需要真实环境。 - 对照:`sessionOutcome = adopt_representative`(`publicCanAdopt = true`)同样条件下 `satisfied = true`、不建题(保留既有语义)。 ## 2. P2-a · 状态条「改选」是死的 **位置**:`rectification-agentic-chat.tsx` 状态条 `改选`(~1501);`offerSectionRef`(~439、~1438)赋了 ref 但没有任何 `scrollIntoView` 调用。 **修法**: 1. 改成 ``,onClick:`offerSectionRef.current?.scrollIntoView({ block: "center", behavior: "smooth" })`,并给该消息一次 `rectification-message-flash`(globals.css 加一个 600ms 的背景闪烁 keyframe,复用现有 `.rectification-message-entry` 的色板)。 2. `showSelectionCards` 为 false(出卡消息还没重建,或 `readonly`)时不渲染「改选」。 3. `globals.css` `.rectification-adopt-status__link` 补 button reset(去边框/背景、光标 pointer、焦点环沿用 `.rectification-candidate` 的样式)。 4. 测试:源码锁 `rectification-adopt-status__link` 出现在 ``(~1503)及 `onStartConsultation` prop(~207/~405);`conversational-birth-time-rectification.tsx` 同名 prop(~36);`page.tsx` `onStartConsultation={…}`(~1952);`use-session-management.ts` `startConsultationAfterRectification`(~315)及其在返回对象里的导出(~440)。 **修法**: 1. 删按钮、删 `onStartConsultation` prop 链(两个组件 + page.tsx)、删 `startConsultationAfterRectification` 及"请用刚才采用的代表性出生时间看盘。"字面量;`page.tsx` 若因此不再引用什么,一并清理,不留 dead export。 2. 状态条文案改为:`已采用 04:53 · 范围 04:50–05:15 · 改选`,末尾追加一段说明性文字(非按钮):`之后新建对话即按此时间排盘。`——让用户知道下一步是点侧栏「新对话」,不需要别的入口。 3. 校正面顶部那句"先陪你核对出生时间范围,之后会回到你原来的问题:“…”"(`pendingConsultationQuestion`,~1351)承诺的是按钮会带回原问题;按钮删掉后改成不食言的写法:`先陪你核对出生时间范围;结束后新建对话,按采用的时间再问:“…”`。`pendingConsultationQuestion` 的传递链不动。 4. 校正结束时的 `readonly` 终态区("该校正已结束,只能查看历史 / 再次校正")不变。 5. `onSaved={() => void refreshAccount()}` 保留:它让客户端 `profile.birthTimeStatus` 及时变成 `accepted`,新对话的 `resolveBirthTimeConsultationRoute` 才会走 `verified_chart`。**不改** `resolveBirthTimeConsultationRoute` 与服务端 `mode_changed` 语义。 **测试**: - 改既有断言(三栏说明):`rectification-adopt-flow-20260902.test.ts:402` `assert.match(chat, /用这个时间看盘/)` → `assert.doesNotMatch(chat, /用这个时间看盘/)`(旧:状态条含按钮 → 新:不含 → 保留语义:状态条 `rectification-adopt-status` 仍存在且含「改选」);`rectification-agentic-entry.test.ts:686` 同样改为 `doesNotMatch`;`rectification-activity-receipt.test.ts:134` 只锁 `rectification-adopt-status`,不动。 - 源码锁(并入 `tests/rectification-adopt-flow-fix-20260903.test.ts`):`frontend/src` 下不再出现 `startConsultationAfterRectification`、`onStartConsultation`、`请用刚才采用的代表性出生时间看盘`、`rectification-consult-handoff`;`rectification-agentic-chat.tsx` 含 `之后新建对话即按此时间排盘`。 ## 5. 硬红线 1. 公开 `can_adopt` 只能收紧;`collect_evidence` / `discriminate_candidates` / `validate_holdout` 不得放行。确认门恒 fail-closed。 2. 选项、answer_class、计分服务端所有,Agent 只写 `spokenPrompt`;不恢复问题槽、不改 `asked_turn_id` 契约。 3. 不得用正文/题干字符串反推状态或数据(本单 §4 就是在删一处)。 4. 不改 DB check 约束、不加迁移;Python 引擎不动;不 bump Skill 10.0.14。 5. `./node_modules/.bin/tsc --noEmit` 通过(不用 `npx tsc`);`npm run lint --prefix frontend` 0 错误;`rectification-*` / `consultation-*` / `consult-*` / `chat-*` 测试 fail=0;改动的既有断言逐条三栏说明(旧 → 新 → 保留语义)。 6. 无凭据不得声称已真实环境验证。 ## 6. 开工前置 ```bash git fetch origin --prune git worktree add -b codex/rectification-adopt-flow-fix-20260903 \ ../.worktrees/rectification-adopt-flow-fix-20260903 origin/staging ``` 读 `TASK-rectification-adopt-flow-20260902.md`、`docs/BUG_HISTORY.md`(BUG-497~500,以及 BUG-313/323 链)。BUG 编号从 **BUG-501** 起(先 grep 确认最大号):§1 记 BUG-501(复发自:无;相关 BUG-497),§4b 记 **BUG-502**(复发自:无;相关 BUG-499——`35e5781e` 把裸按钮移进状态条,用户 09-03 实测后拍板删除);§2/§3/§4 各一条或并入 BUG-499/498 的"最近更新",由实现者判断,但 §1 与 §4b 必须独立成条。**行号是线索,按符号名定位。** ## 7. 验收标准 1. `collect_evidence` + 内部 `canAdopt = true` + 题库耗尽 → `ensureNonTerminalTurnExit` 建出 `collect_spoken` 兜底题(含 warn);`adopt_representative` 同条件不建题。 2. 「改选」是 `