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

103 lines
9.9 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.
# 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.ts``frontend/src/app/api/rectification/agent/route.ts``frontend/src/components/rectification-agentic-chat.tsx``frontend/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_turn`**2 次**,两条 `p_assistant_message` 完全相同(范围句 + 门槛句)。原因链:
1. `persistExhaustionCollect` 的门槛分支自己调用 `persistV9DeterministicTurn``requestId: randomUUID()`,不幂等)写第 1 条。
2. `finalizeSuccessfulTurnExit``persistNextInterviewIfIdle`(触发上面的写入,返回值被丢弃),再 `ensureNonTerminalTurnExit``inspectNonTerminalTurnExit.satisfied` 仍只认「有焦点 / 已采用 / 用户已停 / 可采用」,门槛句不算载体 → 判不满足 → 再次 `persistExhaustionCollect` → 写第 2 条。
3. `agent-run.ts``opening` / `evidence` 动作在 finalize 之前还会先调一次 `persistNextInterviewIfIdle` → 这两种动作是 **3 条**
4. 点选路径:`persistApplied``persistNextInterviewAfterChoice` → 门槛分支写 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 }`
- `finalizeSuccessfulTurnExit``persistNextInterviewIfIdle` 返回 `terminalNote` 时,由它写**一次** deterministic turn`requestId``${askedTurnId}:gate` 之类可幂等的键),并跳过 `ensureNonTerminalTurnExit``agent-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.satisfied``persistNextInterviewIfIdle` 共用。
- 验收:A4 用例 2 之后再调 `ensureNonTerminalTurnExit``append` 计数不增加。
### 5.3 穷尽分支不吞题
- 分支加 `decision.nextAction` 白名单(§3.3)。
- 验收:新用例——同 A4 用例 2 形状但 `discriminating_event_probes` 留一条可渲染探针、receipt `acceptance_allowed=false` → 持久化的是区分卡焦点,不是门槛句;holdout 同理一例。
### 5.4 范围小字还原 + 记录
- `RectificationReadonlyRange` 回到 `<p>`;删 `onStop` prop`frontend/DESIGN.md` 对应行改回;源扫描断言(`rectification-exhaustion-exit` "collect spoken stop button and range line share CHOICE_STOP_LABEL")改为只要求口述态按钮,写三栏。
- `docs/BUG_HISTORY.md`BUG-565(重复消息)、BUG-566(吞题)、BUG-567(伪按钮);BUG-560 追加 §2 的校准结论。
- `BLOCKED.md``test_long_real_conversation_reaches_vedastro_after_local_range_is_narrow` 在基线失败一事(复现命令、失败断言、首次失败 SHA 若能二分)。
- `CHANGELOG.md``docs/tasks/PROGRESS-rectification-convergence-exit-fix-20260906.md``docs/testing/rectification-convergence-exit-20260906.md` 第 1 条补"门槛句只出现一次"。
## 6. 让步顺序
5.1 + 5.2 不可拆;5.3 必做;5.4 的 UI 还原可后置到同分支第二次提交,记录不可省。
## 7. 开工前置命令
```bash
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 --noEmit``npm run lint`
## 验收(Claude2026-09-07`origin/staging` @ `814c924e`
实现随 `814c924e` 一次提交合入(与 explain-layer / range-reading / unknown-time 同一提交)。独立复跑:tsc 0 错;lint 0 error / 82 warning`rectification-*` + `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 穷尽态视为已交付 | 通过。`isExhaustedGateState``inspectNonTerminalTurnExit` 共用 |
| 5.3 不吞题 | 通过。白名单 `offer_provisional_range / complete_with_range / ask_fact_collection` |
| 5.4 范围小字还原 + 记录 | 通过。BUG-560 根因升级已写;BLK-001 写进了 `docs/BLOCKED.md`——仓库约定是根目录 `BLOCKED.md`,见修复单 P3 |