Files
Jyotisha/docs/tasks/TASK-rectification-targeted-collect-persist-fix-20260911.md
T

59 lines
7.2 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 · 修复单: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` 至多一条)。