Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
103 lines
9.9 KiB
Markdown
103 lines
9.9 KiB
Markdown
# TASK · 生时校正穷尽出口修复单:门槛句重复写两遍、穷尽分支吞掉剩余问题、范围小字伪装成按钮(2026-09-06)
|
||
|
||
- 基线:`origin/staging` @ `b938c76a`(含 BUG-558~560 实现 `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`。
|
||
|
||
## 验收(Claude,2026-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 |
|