Files
Jyotisha/docs/tasks/PROGRESS-rectification-dup-question-20260926.md
T
Jesse_ChenandClaude Opus 5.5 e4c1c7a342 fix(rectification): one stem per turn, one card per focus (BUG-1045/1046)
BUG-1045 (recurrence of BUG-585 via BUG-969): the snapshot merge now drops
the attached stem from the streamed "ack + stem" text with the same
stripQuestionSentences the GET route uses; server text unchanged.

BUG-1046: a failed choice submit (409 / network) or typed send withdraws
the local answered mark, remounts the card, re-reads the Case and shows
"这次没提交上,请再点一次。"; the persisted question hangs on the latest
settled assistant message with other copies of the same focus removed
(standalone block only when nothing can carry it); willContinue and the
send() settle merge unseen assistant turns (same gap as BUG-685).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-26 10:49:25 +08:00

56 lines
6.6 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.
# PROGRESS · 生时校正:同一轮问题两次 + 同一道题两张卡(2026-09-26)
- 任务书:`TASK-rectification-dup-question-20260926.md`
- 执行:Claude 子代理(直接执行模式,产品授权),分支 `codex/rectification-dup-question-20260926`,worktree `.worktrees/rectification-dup-question-20260926`
- 基线:开工 `ac224cef`;BUG-1042 合入后变基到 `origin/staging` `509987b9`(按任务书「以新 head 为准」)。本单未推送。
- BUG:BUG-1045(复发自 BUG-585)、BUG-1046;开工时 staging 最大号 BUG-1044,核对无冲突。状态均 investigating(真机与部署未做)。
## 做了什么
| 项 | 实现 | 位置 |
|---|---|---|
| T1 / D1 实时去重 | `mergeTurnQuestions` 在快照回合带问题时对正文跑 GET 同款 `stripQuestionSentences`,幂等。服务端 BUG-969 兜底文本**不改** | `src/lib/rectification-snapshot-messages.ts` |
| T2 / D2 失败回滚 | `submitStructuredChoice` 的 `!ok` 与网络错误:撤回本次本地标记(`withdrawAnswerMark`,题回 active、无 `answer_option`)、移除占位、`choiceNonce++`(卡片 key 变化 → 重新挂载、清掉卡内本地选中)、重读快照(`applySnapshotTurnsToMessages`)、错误行「这次没提交上,请再点一次。」(`RECTIFICATION_CHOICE_SUBMIT_FAILED_COPY`,VOICE 已加一行)。`send` 打字回答的 `!ok` / 无 body / 网络错误同样撤回 `typed` 标记 | `rectification-agentic-chat.tsx`、`rectification-surface-state.ts`、`docs/VOICE.md` |
| T2 / D3 兜底闸门 | `placePersistedQuestion` + `turnQuestionFromCurrent`:缺口为 `persisted_question` 时,把快照当前题挂到最新一条已结算助手消息上,其它消息上同 `focus_id` 的副本摘掉;独立问题块只在没有助手消息、或最新助手消息已挂另一道题时出现。缺口状态机、`liveQuestionOnMessages` 只看最新消息(BUG-635 合同)都不改 | `rectification-snapshot-messages.ts`、`turn-question.ts`、`rectification-agentic-chat.tsx` |
| T3 / D4 漏收回合 | `willContinue` 分支用 `appendUnseenAssistantTurns(..., { beforeRenderKey })` 把未见回合插在跟进行之前;`send()` 成功结算后改用 `applySnapshotTurnsToMessages` | 同上 |
| T4 记录 | BUG-1045 / 1046、CHANGELOG、DESIGN 状态表两行、VOICE 一行、真机清单、状态板 | `docs/` |
## 偏离与说明
1. **D3 的一个例外**:最新助手消息已经挂着*另一道*题(不同 `focus_id`)时,当前题无处可挂,仍用独立问题块。这时不会重复同一 focus;硬挂到更早的消息会让题出现在已答题上方、打乱顺序。任务书写的是「兜底块只在没有任何助手消息时渲染」,此处放宽一格,写进 DESIGN 状态表。
2. **BUG-635 合同保留**:旧消息上的同焦点仍不算「仍在显示」(`liveQuestionOnMessages` 只读最新消息,源码合同未动);D3 的做法是把题**挪到**最新消息、摘掉旧副本,所以「只画一张」与 BUG-635 同时成立。
3. **最新消息上同焦点但本地已答、服务端仍 active** 时(失败未回滚的残留),D3 用服务端的 active 题替换本地标记。只在缺口为 `persisted_question`(非 busy)时发生,不会在提交途中闪回。
4. 为不改 7 处按 `{messages.map((message) => {` 切片的源码合同,渲染循环保留 `messages.map`,循环内按 `renderKey` 取放置后的消息(`entryMessage`)。
5. `rectification-agentic-chat.tsx` 注释里提到 `requestTieBreak` 会撞两条禁用该标识符的源码合同,已改写措辞(第一次全量跑出 2 条新失败,修正后通过)。
6. 未改 `use-conversation-scroll-anchor.ts`(另一单在改)。H2 若真机确认服务端出了完全相同的题:BUG-540 / 559 / 592 守卫已核对仍在(`pickProbe` 先 `probe_id` → `semantic_key`;`sameYearProbeAsked` / `same_year_asked` 硬排除),本单不改选题。
## 改动的既有断言
没有改任何既有断言。`rectification-answer-choice.test.ts`(BUG-969 改成「正文含题干」的那条)与 `account-dialog-inert-20260918.test.ts` 的服务端断言保持原样——服务端文本没变,按任务书只**追加**客户端合并断言(正文不再含题干、问题块是这道题)。DESIGN.md 状态表改写时保留了 `rectification-unwritten-evidence.test.ts` 锁定的两句。
## 验证
| 项 | 结果 |
|---|---|
| `tsc --noEmit` | 0 错 |
| `npm run lint` | 0 error,126 warnings(与基线 126 相同;改动文件无新增) |
| 新测试 `tests/rectification-dup-question-20260926.test.tsx` | 11/11 通过;挂载真实 `RectificationAgenticChat`,假 `fetch` 走「流式 → 结算 → 快照合并」。前 10 条在 `509987b9` 代码上 9 条失败(「无助手消息兜底」本来就对) |
| 全量 `npm test`(Node 20.19.2,无 Docker) | 3972 条:3884 通过 / 61 失败 / 27 跳过。失败名单与基线 `test11.log`(3960 / 61)**逐条一致**,新增失败 0;测试名消失 0、新增 12(本单 11 条 + BUG-1042 合入的 1 条) |
| `npm run build -- --webpack` | 通过;`┌ ○ /` Static |
| 首屏 gzip | `rootMainFiles` 4 个文件 130933 B → 130933 B(0%);`index.html` 引用的 36 个静态文件 656741 → 657563 B(+0.13%) |
| 浏览器(`next start` + Chrome 151 无头,CDP 回放虚构 `/api/*`) | 见下表 |
浏览器,同一份虚构数据,基线 `509987b9` 构建 vs 本分支构建(1280 宽;计数排除 `legend.sr-only`):
| 场景 | 修复前 | 修复后 | 截图 |
|---|---|---|---|
| A 打字回答命中选择题 → 流 ack+stem → 快照挂卡 | 下一题可见 2 次 | 1 次;4 个可点选项 | `docs/testing/rectification-dup-question-20260926/A-before-1280.png` / `A-after-1280.png` |
| B 点 B → POST 409 `stale_question` → 快照不变 | 题干 2 次、2 张卡、B 仍高亮、兜底块 1 个、错误行为服务端原句 | 1 张卡、0 个已选、4 个可点、错误行「这次没提交上,请再点一次。」 | `B-before-1280.png` / `B-after-1280.png` |
B 修复前截图与产品真机截图同形(已选 B 的卡 → 操作按钮 → 同题空白卡),即 H1 在虚构数据上可复现。截图右下「合盘历史暂时无法读取」是回放脚本没 mock `/api/synastry-reports`,与本单无关。
## 环境缺口
- 无登录态真机、无真实模型:真机清单 `docs/testing/rectification-dup-question-20260926.md`。H1 / H2 以哪条为准需真机当时 `/api/rectification/agent` 的状态码。
- Node 20:`mock.module` 类测试 ERR_MODULE_NOT_FOUND;无 Docker:DB 套件。失败清单与基线逐条一致(61 条)。