# TASK · 校正候选区分阶段同一道"上大学"题问两次(2026-09-04) - 基线:`origin/staging` @ `a75929c1`(含 `d4627a50` 采用后核对线修复;本任务书不依赖其验收结论) - 分支:`codex/rectification-quality-probe-dedupe-20260904` - 执行方:coding agent;验收:Claude - 涉及文件:`scripts/rectification/event_probes.py`、`frontend/src/lib/rectification-agentic/v9/choice-card.ts`;与 `TASK-rectification-post-adopt-verify-20260904.md`(已实现 `d4627a50`)无文件冲突,可并行。 - BUG 编号起点:**BUG-540**(开工时用 `grep -o "BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | sort -u | tail -1` 复核) ## 1. 事故实证(staging,2026-09-04,候选区分阶段) 用户在同一案例里连续两次看到同一张选择卡:"**YYYY 年 M 月那次上大学,更接近如愿、将就调剂、发挥失常还是说不清**",四个选项完全相同。用户两次都作答,第二次答完范围继续收窄。案例 dump 中可对上的事实(不写具体值): | # | 事实 | 位置 | | --- | --- | --- | | A | 该案例学业域有两条已核实事件:**入学**(`education_start`)与**毕业**(`education_completion`),相隔四年 | 案例 `evidence[]` | | B | 引擎对这两条事件各发了一条 `known_event_quality` 探针;两条的 `expected_outcomes` 的 yes/no 候选分钟集合**完全相同**(同一 D24 星座切分),只有 `semantic_key`(`education.<入学年>.known_event_quality` / `education.<毕业年>.known_event_quality`)、`target_evidence_id`、`candidate_split_hash` 不同 | `scripts/rectification/event_probes.py::_quality_distinguish_probes`(L901–997)、`candidate_contrast.py::candidate_split_hash`(hash 把 `year/month` 掺进去,所以同分组不同年 ≠ 同一 hash) | | C | 第二张卡的 `question_id` / `probe_id` / 评分都指向**毕业**探针,但卡面题干与日期标签是**入学**探针的 | `choice-card.ts::pickProbe`(L233–251):`followup.choice_kind === "event_quality"` 分支取 `pool` 里**第一条**质量探针就返回,`semantic_key` 匹配写在它后面,永远轮不到;`periodFor`(L254)与 `eventQuestionPrompt`(L280)都从 `pickProbe` 拿探针,所以日期与题干一起错 | | D | 即便 C 修好,毕业事件的题干仍会是"YYYY 年 6 月那次**上大学**……"、选项仍是"发挥明显失常 / 只是将就调剂 / 基本如愿录取 / 当时说不清楚" | `event_probes.py::_quality_user_meaning`(L887)只按 `domain == "education"` 套"上大学"模板;`QUALITY_DISTINGUISH_OPTIONS["education"]`(L92)同样不看事件 `kind` | ## 2. 根因 1. **前端选探针的优先级反了**:`pickProbe` 先按"种类"再按"键"。同域只有一条质量探针时看不出来;一旦引擎给了两条,第二次 followup 的卡面永远渲染第一条。用户第二次实际是在**对着入学题、给毕业探针打分**——答案被记到了错的证据上。 2. **引擎对学业域所有事件一律发"高考发挥"题**:质量探针的题干与选项是 BUG-390 时按"高考/录取"设计的,那时只有 `education_start` 一种学业事件会到这里;现在采集线会产生 `education_completion`(毕业)等其它 kind,模板没有跟着分。毕业没有"调剂/发挥失常"这种体验,这道题对毕业事件**没有可回答的语义**。 3. **同分组质量探针没有去重**:两条探针的 yes/no 分钟集合一模一样,问第二道不带来任何新信息(信息增益相同、支持/冲突集合相同),却占了 `MAX_QUALITY_DISTINGUISH_PROBES = 2` 的全部名额和用户一轮回答。`candidate_split_hash` 因为掺了年份而不相等,`askedDiscriminatorKeys` 的 split-hash 去重拦不住。 ## 3. 决策记录 1. **一道题只能绑定一条探针,且必须是 followup 指定的那条**:`pickProbe` 的匹配顺序改为 `probe_id` → `semantic_key` → 种类兜底。找不到 `semantic_key` 对应探针时不得退回"同种类第一条",改为返回 `null`(让上游按无探针路径走,不出卡),并在进度记录里说明这种情况是否在测试中出现过。 2. **质量探针只对有"体验词表"的事件种类发**:学业域仅 `education_start`、`education_change`、`education_interruption` 可发("如愿 / 调剂 / 失常"对这三类都有意义);`education_completion` 与其它 kind **不发**质量探针,而不是新造一套"毕业顺不顺"的模板。理由:产品要求"多余入口宁可删除也不修";毕业是否顺利本身对 D24 切分没有独立证据价值(与入学事件是同一切分)。 3. **同域同分组只发一条**:`_quality_distinguish_probes` 内对 `(domain, frozenset(yes_times), frozenset(no_times))` 去重,保留时间最早(或 `information_gain` 最高、并列取最早)的一条;被去掉的事件不计入 `MAX_QUALITY_DISTINGUISH_PROBES` 名额。 4. **已经答过的错绑答案不追溯修正**。已有案例的历史轮次不回写;只保证新的轮次不再出错。若要为受影响案例做补救,另开任务书。 5. 本任务书不推翻 BUG-389/390/391/392/393 的任何防复发措施;BUG-390 的"质量探针只对学业发出、不得把高考题干套到事业/感情"仍然成立,本次是把它进一步收紧到学业内的特定 kind。 ## 4. 硬红线 - 不得改 `QUALITY_DISTINGUISH_OPTIONS` 四选项的文案与 `answer_class` 映射(BUG-390 / 四选项合同 `tests/test_probe_question_contract.py`、`frontend/tests/rectification-probe-question-contract.test.ts`)。 - 不得新增第二套"毕业体验"模板或选项。 - 不得改 `candidate_split_hash` 的算法(其它模块靠它做 asked-key 去重与回执投影);去重在 `_quality_distinguish_probes` 内部按分组集合做。 - `pickProbe` 改动不得影响 `varga_style` / `existence` 探针的现有选取(`rectification-choice-card.test.ts` 既有断言原样通过;如需改断言写"原值 / 新值 / 原因")。 - 任务书、进度记录、Bug 历史、测试 fixture 一律用抽象年份与虚构事件;不得出现真实 case_id / session_id / 出生候选分钟。 ## 5. 任务分解 ### 5.1 `pickProbe` 按键选探针(前端) - 改 `choice-card.ts::pickProbe`:顺序 `followup.probe_id`(`ChoiceCardFollowup` 需补 `probe_id?: string`,由 `method-followup.ts` 的 followup 透传,见其 L172 已有字段)→ `followup.semantic_key` → 无键时才按 `choice_kind` 兜底;有键但找不到 → `null`。 - 验收:`frontend/tests/rectification-choice-card.test.ts` 新增用例——同域两条 `known_event_quality` 探针(不同 `semantic_key` / `target_evidence_id` / `display_date_label`,相同 expected_outcomes),followup 指向第二条时,`buildChoiceFrame` 的 `question_id`、题干日期标签、`user_meaning` 全部来自第二条;反向再断言指向第一条时不受影响。 ### 5.2 质量探针按事件 kind 发(引擎) - `_quality_distinguish_probes` 只对 `kind ∈ {education_start, education_change, education_interruption}` 的学业事件生成;`education_completion` 及其它 kind 跳过。事件记录里的 kind 字段名以 `contracts.py::EVENT_KINDS` 为准,不要凭 `domain` 猜。 - 验收:`tests/test_rectification_event_probes.py` 新增:同域一条 `education_start` + 一条 `education_completion` → 只产出一条质量探针且 `target_evidence_id` 指向入学;单独一条 `education_completion` → 零条质量探针,且该事件仍参与普通时间探针(不得因此少问它本身)。 ### 5.3 同域同分组去重(引擎) - 在 5.2 之后再做:同域两条可发事件(如 `education_start` + `education_change`)若 yes/no 集合相同,只保留一条。 - 验收:同文件新增用例,两条 `education_start` 类事件、相同分组 → 一条探针;不同分组 → 两条(`MAX_QUALITY_DISTINGUISH_PROBES` 上限不变)。 ### 5.4 记录 - `docs/BUG_HISTORY.md` 新增 BUG-540(前端错绑)、BUG-541(引擎 kind 与去重);关联 BUG-390。 - `docs/tasks/PROGRESS-rectification-quality-probe-dedupe-20260904.md`;`CHANGELOG.md` 一条。 - `docs/testing/rectification-quality-probe-dedupe-20260904.md`:真实环境清单——新建案例录入"入学 + 毕业"两条学业事件后,候选区分阶段最多出现一次"上大学"题,且题干年份 = 入学年份。 ## 6. 让步顺序 5.1 > 5.2 > 5.3 > 5.4。5.3 若与既有 `information_gain` 排序测试冲突,允许先只做 5.1 + 5.2 并把 5.3 写进 `BLOCKED.md`。 ## 7. 开工前置命令 ```bash git fetch origin --prune git worktree add -b codex/rectification-quality-probe-dedupe-20260904 .worktrees/rectification-quality-probe-dedupe-20260904 origin/staging cd .worktrees/rectification-quality-probe-dedupe-20260904 ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules grep -o "BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | sort -u | tail -1 .venv/bin/python -m pytest tests/test_rectification_event_probes.py tests/test_probe_question_contract.py -q cd frontend && npx tsx --test tests/rectification-choice-card.test.ts tests/rectification-probe-question-contract.test.ts ``` 收尾:`tsc --noEmit`、`npm run lint`、`npm test`(与 `origin/staging` 基线失败清单逐条比对)、`.venv/bin/python scripts/run_quality_gate.py --profile quick`。