Files
Jyotisha/docs/tasks/TASK-rectification-adopt-flow-fix-20260903.md
T
Jesse_Chen 8db71aaf81 docs: product-level README, AGENTS.md split into code/reading parts, add CLAUDE.md, move task briefs to docs/tasks
- README.md is now the product/repo front door (architecture, repo map,
  local dev, test tiers, delivery flow, doc map). Engine positioning,
  VedAstro/Codex setup and the oracle/benchmark command reference move
  verbatim to docs/engine/README.md, docs/engine/vedastro-gateway.md and
  docs/benchmark/README.md. Capability badges realigned with the registry
  (91/78/8/0); tests/test_readme_badges.py was red on staging.
- AGENTS.md: Part A (environment truth, delivery, worktrees, record
  placement, bug workflow, growth freeze, frontend red lines, privacy,
  pre-work check, test tiers) and Part B (reading-rigor constraints).
  GitHub issue-tracker/triage boilerplate removed: GitHub is a read-only
  mirror. All strings locked by tests/ are preserved.
- CLAUDE.md added: roles, three working modes, task-brief sections,
  acceptance criteria, session discipline; imports AGENTS.md.
- 50 tracked TASK-*/PROGRESS-* files and 3 never-committed briefs move to
  docs/tasks/ with an index; REPO_LAYOUT.md merged into README.

Docs-only change (no gated path touched).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-03 06:56:06 +00:00

123 lines
16 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.
# 任务书 · 采用流程修复单:采集期出口兜底、「改选」可点、开场题不占 education、核对题年份持久化、删「用这个时间看盘」按钮(2026-09-0309-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` 状态条 `<span className="rectification-adopt-status__link">改选</span>`~1501);`offerSectionRef`~439、~1438)赋了 ref 但没有任何 `scrollIntoView` 调用。
**修法**
1. 改成 `<button type="button" className="rectification-adopt-status__link">改选</button>`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` 出现在 `<button` 上、文件内存在 `offerSectionRef.current?.scrollIntoView`;不做 DOM 测试。
## 3. P2-b · 开场题把 `education` 记成"已问/已拒"
**位置**`method-followup.ts` `OPENING_COLLECT_DOMAIN = "education"`~1068)、`collectQuestionDomain`、开场 followup `domain: OPENING_COLLECT_DOMAIN`~1800);`server-focus.ts` `persistCollectFocus``targetDomain` 兜底;`declinedDomains` / `domainCollectFocusAsked`~351/~366)。
**问题**:开场题是"随便一件带大概时间的经历",不是教育题。现在它以 `target_domain = education``questionId = collect:education:collect_method_evidence` 持久化:
- 用户对开场题点「先这样」或不答 → `declinedSkippedTopics` 里出现 `education``declinedDomains` 含 education → `exhaustionSpokenCollectFollowup` 跳过教育、D5 refine 的 `!declined.has("education")` 分支全部关闭;
- 用户答完开场题(说的是搬家)→ `domainCollectFocusAsked(rows, "education")` 为真 → 之后真正的教育采集题被打上 `collect_retry`,文案变成"再补一件"。
**修法**(不改 DB check 约束,`target_domain` 只能是 education/career/relationship/relocation/finance/health/family/other):
1. `OPENING_COLLECT_DOMAIN` 改为 `"other"``collectQuestionDomain(null | "unknown" | "active_focus")` 返回 `"other"`。stable id 变为 `collect:other:collect_method_evidence``persistableFocusDomain("other")` 本来就合法。
2. `declinedDomains``domainCollectFocusAsked` 忽略 `target_domain === "other"``questionId``collect:other:` 开头的行(occupation 走的是 `collect:occupation:` 前缀,不受影响;`occupationCollectFocusClosed` 已按 questionId 前缀判断,确认不回归)。
3. `tests/rectification-adopt-flow-20260902.test.ts` 里 "opening collect domain is education rather than unknown" 改为断言 `"other"`——三栏说明:旧 `education` → 新 `other` → 保留语义"不得为 unknown、stable id 不含 unknown"。
4. 新测试:closedCollectFocuses 里只有开场题(`collect:other:…`status `skipped`)时,`exhaustionSpokenCollectFollowup` 仍会给出 education 题;`buildMethodFollowupPlan` 的教育采集 followup 不带 `collect_retry`
## 4. P2-c · 核对题年份靠正则从题干文本抠
**位置**`method-followup.ts` `keepAcceptedFocus` 分支 `promptYear`~1592)——`text.match(/19\d{2}|20\d{2}/)`
**问题**`schema.probe_year` 缺失时才走到它,但它是"从已展示文案反推数据",与"不得用字符串反推状态"的红线同一个方向;一旦题干写了两个年份("2016 年上大学、2020 年毕业")就取错。
**修法**
1. 数据源头补齐:`server-focus.ts` `expectedAnswerSchemaFor` / `persistServerOwnedFocus``rectification-v9-tools.ts` set-focus 建 reverse_verify / out_of_sample_check focus 时,schema 必写 `probe_year`(取 `followup.probe_year`,来自匹配的 verify 探针;没有就不写,不得从题干抠)。
2. `keepAcceptedFocus``keepProbeYear = matchingVerify?.year || schemaYear`,删除 `promptYear` 兜底。年份仍缺时不给 `probe_year``validateSpokenPrompt``year_missing` 只在 `probe_year` 非空时触发(现有语义),不会误伤。
3. 测试:`rectification-question-ownership.test.ts` 加一条:采用后 set-focus 建的 reverse_verify focus`expectedAnswerSchema.probe_year` 等于探针年份;`keepAcceptedFocus` 在 schema 无 `probe_year`、无匹配探针时 followup 不带 `probe_year`,且 `method-followup.ts` 源码不再含 `/19\\d{2}|20\\d{2}/`
## 4b. P2-d · 删除「用这个时间看盘」按钮(09-03 增补;09-03 产品拍板改为删除)
**决策记录**`TASK-rectification-adopt-flow-20260902.md` §3.3 规定状态条承载"改选 · 用这个时间看盘",`35e5781e` 已照做。用户 09-03 实测后明确表示"我不需要这个按钮",产品拍板**删除**该按钮,本单据此推翻上一单 §3.3 中"用这个时间看盘"这一项;"改选"保留(§2)。
**为什么删了不损失功能**:采用时 RPC `accept_agentic_rectification_candidate_for_case_v2` 已写 `profiles.active_birth_time` + `birth_time_status='accepted'`,之后任何普通咨询在 `/api/consult``prepareConsultationRoute` 里都按 `verified_chart``active_birth_time` 排盘。按钮现在做的只是 `startNewChat()` + 往输入框塞一句服务端不读的"请用刚才采用的代表性出生时间看盘。",等价于用户自己点「新对话」。
**位置**`rectification-agentic-chat.tsx` 状态条里的 `用这个时间看盘` `<Button>`~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:5005: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-497500,以及 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. 「改选」是 `<button>`,点击滚到出卡消息;`showSelectionCards` 为 false 时不渲染。
3. 开场题 `target_domain = other`、id `collect:other:collect_method_evidence`;跳过开场题后 education 不进 declined、教育采集题无 `collect_retry`
4. reverse_verify focus schema 带 `probe_year``method-followup.ts` 无年份正则。
5. 「用这个时间看盘」按钮及 `onStartConsultation` / `startConsultationAfterRectification` 全部删除;状态条为「已采用 … · 范围 … · 改选」+ "之后新建对话即按此时间排盘";待回问题横幅改为新文案;两处既有断言改 `doesNotMatch` 并三栏说明。
6. tsc + lint + 四组测试 fail=0BUG_HISTORY 追加条目(含 BUG-502)。
7. 真实环境人工清单(部署后,沿用上一单 §9.7):采集阶段把题答到没题 → 出现"先这样"入口而不是空 composer;采用后点「改选」能跳回卡片;采用后状态条无「用这个时间看盘」;点侧栏「新对话」随便问一句 → 回答按采用的时间排盘(回复不出现"使用未校正填报时间"提示)。