- 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
16 KiB
任务书 · 采用流程修复单:采集期出口兜底、「改选」可点、开场题不占 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(题库耗尽)时:
persistNextInterviewIfIdle走到if (!followup) return { persisted:false },不建题;ensureNonTerminalTurnExit因decision.canAdopt判satisfied = true,不调用persistExhaustionCollect兜底;- 公开
can_adopt现在为 false → 前端不出采用卡;showCollectStop依赖活跃采集题 → 「先这样」也不出; - 用户只剩空 composer,随便发一句话再跑一轮回到同一状态。
35e5781e 之前这个状态会漏出采用卡(就是 BUG-497 的现象),所以没人碰到过死角;现在门关了,出口必须补上。
修法:
inspectNonTerminalTurnExit的satisfied把decision.canAdopt改成publicCanAdopt(decision)(从core/rectification-decision导入)。语义:只有用户在界面上真的能采用时,"没有下一问"才算满足;否则走persistExhaustionCollect(它建的题自带「先这样」,answerChoice的 stop 路径会把 outcome 推到provisional_range_user_stopped,公开can_adopt随之打开)。persistNextInterviewIfIdle里shouldSkipFollowupPersist({ canAdopt: decision.canAdopt, ... })同步改成publicCanAdopt(decision)。现有 nextAction 白名单已保证采集/区分/holdout 不会跳过持久化,这一改只是让"跳过持久化、改出 adopt 旁白"与公开门一致,不得出现"旁白说可以采用、界面却没卡"的分叉。- 确认
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,写入的 focusintent = collect_method_evidence且projectCurrentQuestion(focus).kind === "collect_spoken",并有rectification_nonterminal_exit_repairedwarn。用 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 调用。
修法:
- 改成
<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的色板)。 showSelectionCards为 false(出卡消息还没重建,或readonly)时不渲染「改选」。globals.css.rectification-adopt-status__link补 button reset(去边框/背景、光标 pointer、焦点环沿用.rectification-candidate的样式)。- 测试:源码锁
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):
OPENING_COLLECT_DOMAIN改为"other";collectQuestionDomain(null | "unknown" | "active_focus")返回"other"。stable id 变为collect:other:collect_method_evidence,persistableFocusDomain("other")本来就合法。declinedDomains与domainCollectFocusAsked忽略target_domain === "other"且questionId以collect:other:开头的行(occupation 走的是collect:occupation:前缀,不受影响;occupationCollectFocusClosed已按 questionId 前缀判断,确认不回归)。tests/rectification-adopt-flow-20260902.test.ts里 "opening collect domain is education rather than unknown" 改为断言"other"——三栏说明:旧education→ 新other→ 保留语义"不得为 unknown、stable id 不含 unknown"。- 新测试:closedCollectFocuses 里只有开场题(
collect:other:…,statusskipped)时,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 年毕业")就取错。
修法:
- 数据源头补齐:
server-focus.tsexpectedAnswerSchemaFor/persistServerOwnedFocus与rectification-v9-tools.tsset-focus 建 reverse_verify / out_of_sample_check focus 时,schema 必写probe_year(取followup.probe_year,来自匹配的 verify 探针;没有就不写,不得从题干抠)。 keepAcceptedFocus的keepProbeYear = matchingVerify?.year || schemaYear,删除promptYear兜底。年份仍缺时不给probe_year,validateSpokenPrompt的year_missing只在probe_year非空时触发(现有语义),不会误伤。- 测试:
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)。
修法:
- 删按钮、删
onStartConsultationprop 链(两个组件 + page.tsx)、删startConsultationAfterRectification及"请用刚才采用的代表性出生时间看盘。"字面量;page.tsx若因此不再引用什么,一并清理,不留 dead export。 - 状态条文案改为:
已采用 04:53 · 范围 04:50–05:15 · 改选,末尾追加一段说明性文字(非按钮):之后新建对话即按此时间排盘。——让用户知道下一步是点侧栏「新对话」,不需要别的入口。 - 校正面顶部那句"先陪你核对出生时间范围,之后会回到你原来的问题:“…”"(
pendingConsultationQuestion,~1351)承诺的是按钮会带回原问题;按钮删掉后改成不食言的写法:先陪你核对出生时间范围;结束后新建对话,按采用的时间再问:“…”。pendingConsultationQuestion的传递链不动。 - 校正结束时的
readonly终态区("该校正已结束,只能查看历史 / 再次校正")不变。 onSaved={() => void refreshAccount()}保留:它让客户端profile.birthTimeStatus及时变成accepted,新对话的resolveBirthTimeConsultationRoute才会走verified_chart。不改resolveBirthTimeConsultationRoute与服务端mode_changed语义。
测试:
- 改既有断言(三栏说明):
rectification-adopt-flow-20260902.test.ts:402assert.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. 硬红线
- 公开
can_adopt只能收紧;collect_evidence/discriminate_candidates/validate_holdout不得放行。确认门恒 fail-closed。 - 选项、answer_class、计分服务端所有,Agent 只写
spokenPrompt;不恢复问题槽、不改asked_turn_id契约。 - 不得用正文/题干字符串反推状态或数据(本单 §4 就是在删一处)。
- 不改 DB check 约束、不加迁移;Python 引擎不动;不 bump Skill 10.0.14。
./node_modules/.bin/tsc --noEmit通过(不用npx tsc);npm run lint --prefix frontend0 错误;rectification-*/consultation-*/consult-*/chat-*测试 fail=0;改动的既有断言逐条三栏说明(旧 → 新 → 保留语义)。- 无凭据不得声称已真实环境验证。
6. 开工前置
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. 验收标准
collect_evidence+ 内部canAdopt = true+ 题库耗尽 →ensureNonTerminalTurnExit建出collect_spoken兜底题(含 warn);adopt_representative同条件不建题。- 「改选」是
<button>,点击滚到出卡消息;showSelectionCards为 false 时不渲染。 - 开场题
target_domain = other、idcollect:other:collect_method_evidence;跳过开场题后 education 不进 declined、教育采集题无collect_retry。 - reverse_verify focus schema 带
probe_year;method-followup.ts无年份正则。 - 「用这个时间看盘」按钮及
onStartConsultation/startConsultationAfterRectification全部删除;状态条为「已采用 … · 范围 … · 改选」+ "之后新建对话即按此时间排盘";待回问题横幅改为新文案;两处既有断言改doesNotMatch并三栏说明。 - tsc + lint + 四组测试 fail=0;BUG_HISTORY 追加条目(含 BUG-502)。
- 真实环境人工清单(部署后,沿用上一单 §9.7):采集阶段把题答到没题 → 出现"先这样"入口而不是空 composer;采用后点「改选」能跳回卡片;采用后状态条无「用这个时间看盘」;点侧栏「新对话」随便问一句 → 回答按采用的时间排盘(回复不出现"使用未校正填报时间"提示)。