Files
Jyotisha/docs/tasks/TASK-rectification-convergence-exit-fix-20260906.md
T

9.9 KiB
Raw Blame History

TASK · 生时校正穷尽出口修复单:门槛句重复写两遍、穷尽分支吞掉剩余问题、范围小字伪装成按钮(2026-09-06)

  • 基线:origin/staging @ b938c76a(含 BUG-558560 实现 150d7ef1 / 3a9ae736 / 62afa521
  • 分支:codex/rectification-convergence-exit-fix-20260906
  • 执行方:coding agent;验收:Claude
  • 涉及文件:frontend/src/lib/rectification-agentic/v9/answer-choice.tsfrontend/src/app/api/rectification/agent/route.tsfrontend/src/components/rectification-agentic-chat.tsxfrontend/tests/rectification-exhaustion-exit-20260906.test.ts
  • BUG 编号起点:BUG-565(开工时 grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1 复核)
  • 父任务书:TASK-rectification-convergence-exit-20260906.md(验收记录在其末尾)

0. 验收时的门(供对照)

b938c76a 上:tsc 0 错;lint 0 error;非 DB 校正套件 905/0;校准脚本数字与进度记录一致。Python 定向套件结论见父任务书验收段(test_active_rectification_api.py::test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow 执行方报告在基线上也失败,本单不负责,但 §5.4 要求把它记进 BLOCKED.md)。

1. 事故实证

1.1 P1 · 门槛翻译句每回合写两条相同助手消息(BUG-565)

在 A4 用例 2 的形状(方法覆盖完成、无剩余采集、引擎 acceptance_allowed=false)上,对 finalizeSuccessfulTurnExit({ action: "message" }) 计数 append_agentic_rectification_turn2 次,两条 p_assistant_message 完全相同(范围句 + 门槛句)。原因链:

  1. persistExhaustionCollect 的门槛分支自己调用 persistV9DeterministicTurnrequestId: randomUUID(),不幂等)写第 1 条。
  2. finalizeSuccessfulTurnExitpersistNextInterviewIfIdle(触发上面的写入,返回值被丢弃),再 ensureNonTerminalTurnExitinspectNonTerminalTurnExit.satisfied 仍只认「有焦点 / 已采用 / 用户已停 / 可采用」,门槛句不算载体 → 判不满足 → 再次 persistExhaustionCollect → 写第 2 条。
  3. agent-run.tsopening / evidence 动作在 finalize 之前还会先调一次 persistNextInterviewIfIdle → 这两种动作是 3 条
  4. 点选路径:persistAppliedpersistNextInterviewAfterChoice → 门槛分支写 1 条独立消息;persistApplied 又把同一句拼在「已记录你的选择」后面作为点选回合的正文写第 2 条。
  5. route.ts 无焦点分支:persistNextInterviewIfIdle(写 1 条)之后又 persistV9DeterministicTurn 同一段文字(第 2 条)。

用户看到的就是同一段"范围已经收到…还差带月份的经历"连出两三遍。

1.2 P2 · 穷尽分支排在问题持久化之前,吞掉仍可问的题

persistNextInterviewIfIdle 新增的分支 !remainingCollect && !acceptedTime && !decision.canAdopt → persistExhaustionCollect 排在 persistNextInterviewAfterChoice 之前,不看 decision.nextAction。当引擎采用门关闭(例如 low_date_quality)但决策层仍给出 ask_candidate_discriminator(有可渲染区分卡)或 ask_holdout_validation(holdout 题)时,这些题被直接换成门槛句。答题仍能收窄范围,而门槛句只在用户补一件事之后才有意义,两者不冲突,不该二选一。

1.3 P2 · 范围小字变成按钮但文案还是状态句

RectificationReadonlyRange 现在渲染成 <button>,点击即停止访谈,文案仍是「目前范围 …–…,还在收窄」。用户看不出它可点,点了却是"先这样"。同一屏下方已经有明确的「先这样,先看当前范围」按钮,这个入口多余且歧义。产品偏好:多余入口宁可删除。

2. 校准结论(不是本单任务,但要写进记录)

父任务书 C2 的门"覆盖 ≥ 旧方案 − 1 且中位宽度更窄"设计有缺陷:旧方案 proportional 在 20 例公开 holdout 上中位宽度 21 分钟 = 整个候选窗口(半径 10),它"覆盖 17/20"只是因为不收窄。把它当基线,任何真正收窄的方案都过不了。真正的发现是:先验一旦按引擎原始分拉尖(offset),真实分钟落在领先集合里的只有 12/20(中位宽 10 分钟),与随机取 10 分钟(≈48%)相当。引擎原始分在分钟级几乎没有区分力,这是产品级事实:现有八法计分不足以在 20 分钟窗口内可靠收窄;"收敛"只能来自用户答题和更强的引擎证据,不能靠换尺度。本单只要求把这段结论写进 BUG-560(状态保持 blocked,补"根因升级"一段)与 BLOCKED.md,不做算法改动。

3. 决策记录

  1. 门槛翻译句是回合正文,不是独立消息。写入方只能有一个:谁负责写本回合的助手消息,谁把门槛句并入正文;persistExhaustionCollect 本身不得写 turn。
  2. inspectNonTerminalTurnExit 的"满足"要认得穷尽态:!remainingCollect && blockingMethodsCovered && !canAdopt(同 persistNextInterviewIfIdle 的判定,抽成一个纯函数共用),满足即视为已交付,不再修复。
  3. 穷尽分支只在 decision.nextAction ∈ {offer_provisional_range, complete_with_range, ask_fact_collection} 且无剩余采集时进入;ask_candidate_discriminator / ask_holdout_validation 照常持久化问题。
  4. 范围小字还原为纯状态句(<p role="status">),删掉 onStop 入口;口述态停止按钮保留。
  5. 不改 Skill、不改引擎、不改任何门槛数值。

4. 硬红线

  • BUG-558 三个形状用例原样通过(改断言写三栏);BUG-503/520/544/546/547/549/550 用例不动。
  • 不得重新引入 USER_COLLECT_QUESTION.other 兜底。
  • tsc 0 错、lint 0 error、测试总数 ≥ 905(非 DB 校正套件)。

5. 任务分解

5.1 单一写入方(BUG-565

  • persistExhaustionCollect 门槛分支:删除内部 persistV9DeterministicTurn,只返回 { hostNarration, terminalNote: true, persisted: false }
  • finalizeSuccessfulTurnExitpersistNextInterviewIfIdle 返回 terminalNote 时,由它写一次 deterministic turnrequestId${askedTurnId}:gate 之类可幂等的键),并跳过 ensureNonTerminalTurnExitagent-run.ts 对 opening/evidence 的那次调用返回 terminalNote 时同样只写一次,finalize 不再写。
  • route.ts 无焦点分支:只保留 route 自己的那次写入。
  • 点选路径:persistApplied 拼进正文即可,不再有独立消息。
  • 验收:在 rectification-exhaustion-exit-20260906.test.ts 加三例——finalizeSuccessfulTurnExit({action:"message"}){action:"evidence"} 经 agent-run 路径、applyRectificationChoice 最后一张卡——各自 append_agentic_rectification_turn 计数为 1,正文含门槛句一次。

5.2 穷尽态视为已交付

  • isExhaustedGateState(decision, remainingCollect, methods) 纯函数;inspectNonTerminalTurnExit.satisfiedpersistNextInterviewIfIdle 共用。
  • 验收:A4 用例 2 之后再调 ensureNonTerminalTurnExitappend 计数不增加。

5.3 穷尽分支不吞题

  • 分支加 decision.nextAction 白名单(§3.3)。
  • 验收:新用例——同 A4 用例 2 形状但 discriminating_event_probes 留一条可渲染探针、receipt acceptance_allowed=false → 持久化的是区分卡焦点,不是门槛句;holdout 同理一例。

5.4 范围小字还原 + 记录

  • RectificationReadonlyRange 回到 <p>;删 onStop propfrontend/DESIGN.md 对应行改回;源扫描断言(rectification-exhaustion-exit "collect spoken stop button and range line share CHOICE_STOP_LABEL")改为只要求口述态按钮,写三栏。
  • docs/BUG_HISTORY.mdBUG-565(重复消息)、BUG-566(吞题)、BUG-567(伪按钮);BUG-560 追加 §2 的校准结论。
  • BLOCKED.mdtest_long_real_conversation_reaches_vedastro_after_local_range_is_narrow 在基线失败一事(复现命令、失败断言、首次失败 SHA 若能二分)。
  • CHANGELOG.mddocs/tasks/PROGRESS-rectification-convergence-exit-fix-20260906.mddocs/testing/rectification-convergence-exit-20260906.md 第 1 条补"门槛句只出现一次"。

6. 让步顺序

5.1 + 5.2 不可拆;5.3 必做;5.4 的 UI 还原可后置到同分支第二次提交,记录不可省。

7. 开工前置命令

git fetch origin --prune
git worktree add -b codex/rectification-convergence-exit-fix-20260906 .worktrees/rectification-convergence-exit-fix-20260906 origin/staging
cd .worktrees/rectification-convergence-exit-fix-20260906
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
cd frontend && ls tests/rectification-*.test.ts tests/agent-voice-copy-contract.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)"

收尾同一命令 fail=0、总数 ≥ 905;tsc --noEmitnpm run lint

验收(Claude2026-09-07origin/staging @ 814c924e

实现随 814c924e 一次提交合入(与 explain-layer / range-reading / unknown-time 同一提交)。独立复跑:tsc 0 错;lint 0 error / 82 warningrectification-* + consultation-* + report-* + personal-report-* + voice 契约 1518 pass / 0 fail。

结论
5.1 单一写入方 通过。persistExhaustionCollect 不再写 turn;流式消息回合经 actionToBudget 映射为 agent-run evidence,由 agent-run 用 ${turnId}:gate 幂等键写一次,finalize 见 askedTurnId 跳过;点选路径只拼进正文。测试对 message / evidence / 最后一张卡各计 1 次
5.2 穷尽态视为已交付 通过。isExhaustedGateStateinspectNonTerminalTurnExit 共用
5.3 不吞题 通过。白名单 offer_provisional_range / complete_with_range / ask_fact_collection
5.4 范围小字还原 + 记录 通过。BUG-560 根因升级已写;BLK-001 写进了 docs/BLOCKED.md——仓库约定是根目录 BLOCKED.md,见修复单 P3