Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
75 lines
9.3 KiB
Markdown
75 lines
9.3 KiB
Markdown
# TASK · 带年月题问完不等于收敛:交付前先按剩余候选再出题、再定向补事(2026-09-11)
|
||
|
||
- 基线:`origin/staging` @ `9941c34d`(已部署;代码 `f870d3d7`,含 BUG-651/652)
|
||
- 分支:`codex/rectification-narrow-before-deliver-20260911`,基于 `origin/staging`
|
||
- 执行方:coding agent;验收:Claude
|
||
- BUG 编号起点:**BUG-653**(开工时 `grep -o "^## BUG-[0-9]*" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1` 核对)
|
||
- 不改确认门、不改 `SCORE_DELTA`、不改 `MIN_ACCEPTANCE_*`;Skill 若改用户可见流程升 10.0.24
|
||
|
||
## 1. 事故实证(2026-09-11 staging 真机,代码 f870d3d7)
|
||
|
||
同一序列(两件学业 + 实习入职/离职 + 六道带年月选择题 C/C/C/B/A/C)跑完后,系统按 BUG-651 直接出了三列区间卡:范围 04:48–05:07(20 分钟,5 个簇),头名 04:53 只有 29%,第二名 05:06 22%,第三名 04:50 18%。产品负责人原话:「没到出卡的时候就开始出卡了」「范围没缩短多少,这咋能算结束」。
|
||
|
||
回执事实:
|
||
|
||
| 字段 | 值 | 含义 |
|
||
| --- | --- | --- |
|
||
| `latest_result.createdAt` | 08:10:54(记完实习那一轮) | 之后六道题都是 TS 侧推理,**引擎没有再跑** |
|
||
| `inference_state.probes` | 7 道带年月(事业 3、感情 2、迁居 1、学业质量 1)+ 7 道无年月 + 月宿 | 引擎一次性生成,`MAX_PROBES=8`、每域 ≤3、每域每年 1 道 |
|
||
| 家人 / 财务 / 健康三条线 | 0 道带年月题;`oos_blind_prompts` 列出感情、家人、财务「校时还没用过」 | `("family","existence")` 先验 yes=0.85 > `DOMINANT_ANSWER_PRIOR=0.8`,家人存在题全部被 `dominant_answer_prior` 丢弃;财务无有效换运边界 |
|
||
| 剩余 5 簇的结构差异 | 04:52 D9 换升;05:00 D10 / D4 / D12 / D24 / D2 同时换升;05:06 D24;05:07 D11 | 能切开 04:48–04:59 与 05:00–05:07 的是事业 / 迁居 / 家人 / 学业 / 财务 这些线上的**带年月**事件,不是性格题 |
|
||
| 收敛门 | lead = 16 vs 15,top share 29% | 远未收敛;`rounds` 六轮全 informative,无平台期 |
|
||
|
||
## 2. 根因
|
||
|
||
1. **探针池是静态的**:`event_probes.py` 的区分探针只在引擎跑(账本变化 → `rescoreStaleMinuteSnapshotIfNeeded` → `rescoreMinuteAfterWindowChange`)时按**初始 9 簇**生成一次;选择题答完后 TS 侧只更新候选分数,不会拿**剩余候选**去引擎再生成一批。六道题一问完池就空,BUG-651 于是交付。
|
||
2. **生成上限与先验把可用题掐掉了**:`MAX_PROBES=8`、`MAX_PROBES_PER_DOMAIN=3`、`_best_probe_per_year` 每域每年只留一道;家人存在题被 0.85 的先验整体丢弃(没有任何家人题曾出现过)。
|
||
3. **交付前没有定向补事**:重设计单 S1 的锚定追问在训练门开后就停了;S2 池空直接 S3。可是回执里 `oos_blind_prompts` 已经写明感情 / 家人 / 财务三条线没用过,而这三条线正是 05:00 换升边界所需要的。
|
||
4. **卡片文案把「当前范围」写成「结束」**:「这次给出的范围 04:48–05:07 · 对照了 4 件经历」+ 三个「更像这个」按钮,没有一句「还能再收窄:…」。BUG-651 单 T5 的「再补什么」句在真机卡片上没有出现(待 T0 核实是没落还是被裁)。
|
||
|
||
## 3. 决策记录(产品负责人 2026-09-11)
|
||
|
||
1. **带年月题池空 ≠ 结束**。池空后先做两步再交付:(a) 拿**剩余活跃候选**去引擎刷新一批带年月探针(保持候选集与已答题,`keepAnswers`);(b) 刷新后仍无题,则按剩余候选的换升层做**定向补事**(只问能切开剩余候选的那几条线,带具体例子,不带推算年份)。用户给了新事 → 重算 → 回到 S2;用户说「没有了 / 就这些」→ S3 交付。
|
||
2. **交付条件改为**:收敛门开,或(刷新后带年月池空 **且** 定向补事被拒/池空),或用户主动停。BUG-651 的「池空即交付」收窄为「刷新与定向补事都用尽后交付」。「永远给结果」不变:任何时候用户说停都出卡。
|
||
3. **家人存在题的先验丢弃只对无年月 / 宽窗题生效**:带具体年月的家人换运边界题(`dasha_boundary`,月级)不再因 0.85 先验被整体丢弃;先验只用于排序。
|
||
4. **卡片文案**:标题「目前范围 04:48–05:07(对照了 4 件经历)」;卡下必有一句「还能再收窄:如果记得 …(来自定向补事池的首条)」;「更像这个」按钮保留。禁用「这次给出」「结束」「最终」。
|
||
5. 不承诺分钟级:4 件月精度经历本身分辨力有限,本单目标是把「引擎已生成的 8 道题问完」变成「剩余候选能问的都问完、能补的都补完」,不是保证 5 分钟。
|
||
|
||
## 4. 硬红线
|
||
|
||
- 探针刷新不得改变 `candidate_set_id` / 已答题;重算必须走现有 `rescoreMinuteAfterWindowChange(keepAnswers: true)` 的回放路径(BUG-587 / BUG-594 防复发)。
|
||
- 刷新时 `asked_probe_keys` 必须带上全部已答键,同域同年去重(BUG-559)照旧。
|
||
- 定向补事题干不得带出生年推算年份(重设计单 §4);例子只从「能切开剩余候选的层」对应的领域取。
|
||
- 性格题仍按 BUG-651 决策 2:只作卡后可选,不进本单的刷新池。
|
||
- 不得静默返回空载体(BUG-652 不变量)。
|
||
- 既有测试总数不降;改断言写原值 / 新值 / 原因。
|
||
|
||
## 5. 任务分解
|
||
|
||
- **T0 核实**:回放 §1 序列,打印交付时 `range_delivery` 与 host 正文,确认 BUG-651 T5 的「再补什么」句是否落下;未落则并入 T4 修。
|
||
- **T1 探针刷新(BUG-653)**:新增 `refreshDiscriminatorProbes(caseId)`:带年月池空且未收敛时,用 `inference_state.candidates[status=active]` 作 `candidate_times`、全部 `answered_probes` 作 `asked_probe_keys` 调引擎的探针生成(`build_discriminating_event_probes` 走 `_remaining_contexts` 分支),把新探针并入 `inference_state.probes`(不动 candidates / rounds / answered)。触发点:`persistNextInterviewIfIdle` 与 `persistNextInterviewAfterChoice` 在 `discriminatorProbe=null` 时先刷新一次再决策;每个 `candidate_set_id` 最多刷新 2 次(写进 `inference_state.refresh_count`)。验收:§1 序列第六题答完后先出现新的带年月题(预期落在家人 / 财务 / 迁居 2015–2026 的换运边界),`rounds` 继续为 informative;刷新不改 `candidate_set_id`。
|
||
- **T2 引擎侧放开(BUG-653)**:`event_probes.py` (a) 剩余候选 ≤5 时 `MAX_PROBES_PER_DOMAIN` 提到 4、`MAX_PROBES` 提到 12(只在 `candidate_times` 给定的刷新调用生效,首轮不变);(b) `_dominant_existence_prior` 对 `source=dasha_boundary` 且带月份的家人题不丢弃,只降序。验收:`tests/test_candidate_discriminator_contract.py` 新增剩余 5 簇夹具,家人月级边界题出现在 public 里。
|
||
- **T3 定向补事(BUG-654)**:刷新后仍无带年月题 → `collection-question-pool.ts` 新增 `targetedCollectPool(remainingLayers, evidence, declined)`:按 `LAYER_DOMAIN` 把剩余候选的换升层映射到领域,排除已拒答 / 已覆盖,生成一条带 ≥2 个具体例子的口述题(例如「还有两条线能把 04:50 和 05:06 分开:家里哪年添丁或长辈住院、哪年收入明显变过。记得哪件说哪件,年月大概就行。」),焦点 `collect:targeted:<domain>`;答「没有了」→ 移除并进 S3。验收:§1 序列在刷新池空后先出这条题;答一件家人事 → 引擎重算、回到 S2;答「没有了」→ 出卡。
|
||
- **T4 交付条件与卡片文案(BUG-654)**:`decideRectification` 的 `!separation.sufficient && !probe` 分支在 `refreshExhausted && targetedCollectExhausted` 之前不得走 `completeWithRange / finish`;卡片标题改「目前范围」,卡下「还能再收窄」句取 `targetedCollectPool` 首条;`agent-voice-copy-contract` 加禁词「这次给出」「最终」。
|
||
- **T5 记录**:BUG-653/654 入历史(关联 BUG-651、BUG-629、BUG-559、BUG-587);CHANGELOG;PROGRESS;`docs/testing/rectification-scenarios-20260907.md` 加「六道题问完必须先刷新再补事再出卡」。
|
||
|
||
## 6. 让步顺序
|
||
|
||
T2(a) 的上限放宽可先只做刷新(T1)看真机效果;T2(b)、T3、T4 不可省。
|
||
|
||
## 7. 开工前置命令
|
||
|
||
```bash
|
||
git fetch origin --prune
|
||
git worktree add -b codex/rectification-narrow-before-deliver-20260911 .worktrees/rectification-narrow-before-deliver-20260911 origin/staging
|
||
grep -o "^## BUG-[0-9]*" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1 # 应为 652
|
||
cd frontend && npm test -- tests/rectification-probe-pool-exhausted-20260911.test.ts tests/rectification-replay-20260911.test.ts tests/rectification-probe-replay-loss-20260908.test.ts tests/rectification-collection-question-pool.test.ts
|
||
cd .. && .venv/bin/python -m pytest tests/test_candidate_discriminator_contract.py -q
|
||
```
|
||
|
||
## 8. 验收口径
|
||
|
||
- 前端 `tsc --noEmit` 0 错、`npm run lint` 0 error、相关套件 fail=0、测试总数 ≥ 基线;`next build` 后 `/` 仍 Static、首屏 gzip ±2%。
|
||
- Python:`tests/test_candidate_discriminator_contract.py`、`tests/test_event_probes*.py`;`run_quality_gate.py --profile quick`。
|
||
- 部署后真机:按 §1 序列走,第六题答完不得直接出卡;先出新的带年月题或定向补事题;说「没有了」后出卡,卡片标题为「目前范围」且带「还能再收窄」句。
|