Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
59 lines
7.2 KiB
Markdown
59 lines
7.2 KiB
Markdown
# TASK · 修复单:BUG-655(512be9b7)验收后仍未闭合的三处——采集焦点 kind 违反表约束、引擎刷新用整网格、无新题时 GET 永远「等待收窄」(2026-09-12)
|
||
|
||
- 基线:`origin/staging` @ `e59dd47c`(已部署;代码 `512be9b7`)
|
||
- 分支:`codex/rectification-targeted-collect-persist-fix-20260911`,基于 `origin/staging`
|
||
- 执行方:coding agent;验收:Claude
|
||
- BUG 编号起点:**BUG-656**(开工时核对 `docs/BUG_HISTORY.md` 最大号,应为 655)
|
||
- 本单是 `TASK-rectification-targeted-collect-persist-20260911.md` 的修复单。执行方当时只拿到该单 T0/T3 的片段(见其 PROGRESS「任务单不在本仓」),原单 §2.1 / §2.2 / §2.3 三条根因一条都没改;原单本次已随本单一并入库。
|
||
|
||
## 1. 验收结论(Claude,2026-09-12,基于 e59dd47c)
|
||
|
||
| 项 | 结论 | 依据 |
|
||
| --- | --- | --- |
|
||
| 512be9b7 T3「空刷新不写库、候选集变更不写库、并入探针 id 与已答同名」 | **通过** | `refresh-discriminator-probes.ts` `refreshWriteBlockedReason` / `alignedProbeId`;`tsc --noEmit` 0 错;相关测试 fail=0 |
|
||
| 原单 §2.1 定向 / 锚定 / 通用采集焦点写不进库 | **未通过(未做)** | `server-focus.ts` L346 仍 `targetKind: input.followup.kind_hint ?? null`;`targeted:<domain>` 违反 `agentic_rectification_conversation_focuses.target_kind` CHECK(`20260814020000_rectification_v10_runtime.sql` L124–131),无后续迁移放宽 |
|
||
| 原单 §2.2 引擎刷新按剩余候选生成 | **未通过(未做)** | `refinement_packet.py` L707 仍 `grid_times = built.candidate_times`,`column_times` 只喂三列对照;`refresh_probes` 分支永远拿到全网格 |
|
||
| 原单 §2.3 「等待收窄」必须有载体 | **未通过(未做),且 512be9b7 让它变成永久状态** | 见 §2 |
|
||
| 真机 | 未做(无登录态) | 需部署后按场景 0c 走 |
|
||
|
||
## 2. 512be9b7 引入的新死角(BUG-658)
|
||
|
||
刷新没有新题时现在**什么都不写**(`rectification_refresh_persist_skipped reason=no_new_probes`)。于是库里 `refresh_count` 恒为 0:
|
||
|
||
- GET `decideFromDossier` → `narrowingExhaustion`:`refreshed=false` 且剩余换升层非空 → `refreshExhausted=false` → `stillNeedNarrowing` 恒真 → `decideRectification` 走 `collect(..., waitToNarrowCapability)`:无题、无卡、`can_adopt=false`。GET 路由本身不调刷新,这个状态**永远**翻不过来。
|
||
- 答题事务 / 空闲路径每次都会再调一次引擎(`runV9CandidateScore`,真机单次约 20 s),因为 `shouldRefreshDatedPool` 只看库里的 `refresh_count`。
|
||
- 由于 §1 的 §2.2 未改,刷新在本案永远是 `no_new_probes`,所以本案在 e59dd47c 上的表现与 6c9a0896 相同:第六题答完后「没有拿到下一个问题」,且不再出卡。
|
||
|
||
## 3. 决策记录(沿用原单 §3,补一条)
|
||
|
||
5. 「刷新已尝试且无新题」必须是**持久化事实**:写进 `inference_state.refresh_attempts[]`(`{candidate_set_id, answer_count, result:"no_new_probes"|"new_probes", at}`),不新增推理 transition 行也可以(允许走 `persistV9InferenceState` 之外的轻量字段,例如 latest_result.decisionReceipt 的 `refresh_attempts`);`refreshExhausted` 由「同一候选集、同一已答数下已有一次尝试记录」判定,`shouldRefreshDatedPool` 同源。
|
||
|
||
## 4. 硬红线
|
||
|
||
沿用原单 §4;另加:不得让 GET 的决策依赖只存在于内存的刷新结果;同一候选集、同一已答数下引擎刷新最多调用一次。
|
||
|
||
## 5. 任务分解
|
||
|
||
- **T1 采集焦点 kind 映射(BUG-656 = 原单 §2.1 / T1)**:`server-focus.ts` `persistCollectFocus` / `persistSpokenChoiceFallback` 写 `targetKind` 前经 `collectFocusTargetKind(followup)`(按领域映射到 CHECK 枚举:family→`family_event`、finance→`finance_gain`、relocation→`relocation`、relationship→`relationship_change`、career→`career_change`、education→`education_milestone`、health→`self_health_event`,其余 null);`kind_hint` 原值写入 `expected_answer_schema.collect_kind`;读取侧(`answer-choice.ts` / `agent-run.ts` / `collection-question-pool.ts` 的 invite / anchor / generic / targeted 判定)改读 `collect_kind`,找不到时回退旧字段。验收:`rectification-server-focus.test.ts` 用真实 CHECK 列表断言三类 followup 的 `targetKind` 合法、`status=created`;回放测试第六题答完 → 定向题「还有 N 条线能把 04:48 和 05:07 分开:…」成为活动焦点。
|
||
- **T2 引擎按剩余候选刷新(BUG-657 = 原单 §2.2 / T2)**:`refinement_packet.py` 在 `request.refresh_probes` 为真且 `column_times` 非空时,把 `column_times` 作为 `_discriminating_event_probe_lists` 的 `candidate_times`;`event_probes.py` `refresh and remaining` 分支据此命中。验收:`tests/test_candidate_discriminator_contract.py` 加「31 格 + column_times=5」夹具,断言簇按 5 个候选重算且探针 `candidate_ids` 只含这 5 个;若确无边界则 `public=[]` 并在回执写 `refresh_result="no_new_probes"`。
|
||
- **T3 刷新尝试持久化 + 等待收窄以「已问过」判定(BUG-658 = 原单 §2.3 / T3 + 本单 §2)**:按 §3.5 写 `refresh_attempts`;`narrowingExhaustion.refreshExhausted` = 当前候选集与已答数下已有尝试记录 或 剩余换升层为空;`targetedCollectExhausted` = `collect:targeted:*` 焦点已存在且非 active 或池空;定向题落库 `skipped` 时同一事务把 `collect:targeted:<domain>` 记为 `skipped` 话题并按 BUG-651 出卡。客户端 `rectificationQuestionGapState`:`sessionOutcome=collect_evidence && !stopReason && 无焦点无卡` → `collect_waiting`。验收:模拟引擎恒返回旧题 → 第一次刷新后 `refresh_attempts` 有记录、第二次不再调引擎、GET 决策进入定向补事或出卡;模拟定向题落库失败 → 同一事务出「目前范围」卡,不出现「没有拿到下一个问题」。
|
||
- **T4 记录**:BUG-656~658 入历史(656 关联 BUG-648 / 442;657 关联 BUG-653;658 关联 BUG-655 / 651 / 652);CHANGELOG;PROGRESS;场景 0c 补「定向题必须出现、无新题不重复调引擎」。
|
||
|
||
## 6. 让步顺序
|
||
|
||
T1 + T3 不可省(它们决定用户能不能继续);T2 可拆下一单,但拆出去时 T3 必须让「无新题」直接进定向补事。
|
||
|
||
## 7. 开工前置命令
|
||
|
||
```bash
|
||
git fetch origin --prune
|
||
git worktree add -b codex/rectification-targeted-collect-persist-fix-20260911 .worktrees/rectification-targeted-collect-persist-fix-20260911 origin/staging
|
||
grep -o "^## BUG-[0-9]*" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1 # 应为 655
|
||
cd frontend && npm test -- tests/rectification-server-focus.test.ts tests/rectification-probe-pool-exhausted-20260911.test.ts tests/rectification-replay-20260911.test.ts
|
||
cd .. && .venv/bin/python -m pytest tests/test_candidate_discriminator_contract.py tests/test_rectification_event_probes.py -q
|
||
```
|
||
|
||
## 8. 验收口径
|
||
|
||
沿用原单 §8;真机:§1 序列第六题答完必须看到定向补事题;说「没有了」后看到「目前范围」卡;全程不出现「没有拿到下一个问题」;同一候选集下引擎刷新只调用一次(容器日志 `rectification_refresh_persist_skipped` 至多一条)。
|