# 任务书 · 感情判别题被盖上学业分盘探针(错分 + 无卡,BUG-375 复发)、答完直接出交付卡、卡头把一对相反性格列成共同点(2026-09-16) ## 0. 基线 - 基线 commit:`6aabbe38`(`origin/staging` head,staging 已部署同一 SHA;它是 `TASK-rectification-year-focus-overlay-20260916.md` 的实现,**尚未验收**)。 - 分支:`codex/rectification-dead-d9-choice-20260916`,基于 `origin/staging`。 - 串行:在 year-focus-overlay 验收之后;若验收出修复单,本单排在修复单之后。与 `TASK-console-noise-20260916.md` 无交集。 - BUG 段:**BUG-912 起**(基线最大 BUG-911,开工时再核)。 - 范围:TS 计划层 / 投影层 + Python `refinement_packet.py` 一处;不动 Skill。 ## 1. 事故实证(产品负责人 2026-09-16 staging 真机,模型 deepseek-v4-flash) 删卡后的流程本身已通:健康题选 A →「大概哪年几月?」→ 打字给年月 →「记下了:YYYY-MM 做过手术。」没有追问主体。接下来出的问题在这里: 1. 同一条助手消息正文接着写了判别题:「2020 年前后,你感情上有没有开始过一段认真的关系?」——**正文里有题,下面没有 A/B/C/D**。消息之后是只读范围行「目前范围 04:48–05:07,再说一件带年月的事就能继续」,再往下是「没有拿到下一个问题。」和「接着问」按钮。 2. 当时快照(用户从接口抓的):`interview.session_outcome=collect_evidence`、`precision_stage=collect_events`、`stop_reason=null`、`precision_gate_met=false`、`collection_progress={scoreable:5,minimum:3,missing:0}`、`current_question.question_id="d9_relationship:relationship_style:score"`。即:**焦点落库了**,前端拿不到卡。 3. 用户在输入框打字答「没有」。助手:「已记录,范围收到 04:48–04:59。……现在还剩 04:48–04:59 里 3 个候选。这几个候选按现有信息分不开。」随后直接出三列交付卡(04:59 44% / 04:53 35% / 04:51 21%,均「6 件经历里 6 件对得上」)。 4. 交付卡头部三行「共同点」:「三个时间的事业盘都在巨蟹座:做事以照顾人为主,在意团队里的感受」「希望两边都能说得过去」「必要时会直接选边」——后两行是**同一对相反描述的两极**,被当成三个候选共有的性格写在一起。 ## 2. 代码定位与根因 ### F1 感情判别题被盖上学业分盘的探针:答案计错分 + GET 出不了卡(BUG-912,**复发自 BUG-375 (1)**,P0) 产品负责人补抓的快照(已脱敏): ```json "current_question": { "question_id": "d9_relationship:relationship_style:score", "probe_id": "contrast:varga.d24.白羊座|金牛座|双子座|巨蟹座|狮子座|处女座", "prompt": "2020 年前后,你感情上有没有开始过一段认真的关系?", "kind": "choice", "intent": "distinguish_candidates", "domain": "relationship" }, "choice_card": null, "next_user_action": { "id": "ask_method_followup", "user_meaning": "关系盘仍会换升。按探针年份问感情这条线的前事…本题绑定 D9…" } ``` - 题是 D9 感情题,盖的探针却是 **D24(学业分盘)六星座对照探针**。链路:`method-followup.ts:2929-2945` 由 `varga_observation`(`d9.candidates_differ`)生成 D9 followup,**没有自己的 `semantic_key` / 探针**;`server-focus.ts:133-141` `expectedAnswerSchemaFor` 把它交给 `stampChoiceSchemaWithProbe`,`inference-adapter.ts:553` 在没有 preferred key 时退到 `selectHighestGainProbe(state.probes…)`——拿引擎里增益最高的任意探针盖上去,这次是 D24 对照探针。`:149` 又规定计分题没有探针就不落库,所以这道题**只能**借探针才存得下来。 - BUG-375 (1) 原话:「`stampChoiceSchemaWithProbe` 用引擎最高增益探针盖 schema,把对比探针换成已答教育题」。当时修的是对比探针改用自己的 `semantic_key`,**没有堵住「无探针的分盘风格题借最高增益探针」这条**,本次是同一根因换了入口。 - 后果一(计分错):用户打字答「没有」,`applyChoiceWithoutEvidence` 按 schema 上的探针计分,即按 **D24 六星座的 `expected_outcomes`** 更新后验;正文「已记录,范围收到 04:48–04:59」就是这次错分的产物。**该 Case 现在的区间与三列卡不可信**,任务书 §8 让产品知悉。 - 后果二(无卡):GET `projectRectificationChoiceCard`(`method-followup.ts:3308-3333`)重跑计划得到的 D9 followup 仍无 `semantic_key`/`split_hash`,走到 `focus.questionId !== frame.question_id` 这一条(落库 id 带 `:score`,frame 的 id 由 `stableFollowupQuestionId` 生成)→ `null` → 前端 `deadChoice` →「没有拿到下一个问题」。执行方用回放确认是这一条还是 `mergeChoiceCard` 内部。 - 同族:BUG-375(原发)、BUG-578、BUG-582、BUG-674。 ### F2 打字答「没有」直接出交付卡(BUG-913,先 `investigating`) - 「没有」被当作 D9 判别题的否定答案入账(正文「已记录,范围收到 04:48–04:59」),范围收窄、3 候选平局 → `tied_first` → 交付卡。平局出卡本身是 BUG-689 的设计出口。要核的是两件事: - BUG-751「线没问完不出卡」:此时是否还有未问的线(引导窗口题、跳过线重问、未覆盖领域)。快照 `missing:0`,但 `precision_gate_met=false`。 - BUG-685~688「平局先问参考题再出卡」:三候选 D9 两蝎一秤、D10 同巨蟹,风格题是否可渲染;若可渲染而没问,是 hold 逻辑没触发。 - 用回放定性:设计如此就写成 `investigating → closed_by_design` 并在进度记录说明;是缺陷就修。 ### F3 交付卡把一对相反描述都列成共同点(BUG-914) - `scripts/rectification/refinement_packet.py:26-55` `NAKSHATRA_TRAITS` 每行是一对**相反**的日常描述(如「希望两边都能说得过去」/「必要时会直接选边」);`:534-548` 把 `NAKSHATRA_TRAITS[earlier_index]` **整对**塞进 `options[0].traits`,`[later_index]` 整对塞进 `options[1].traits`。于是一个候选列的性格行里同时出现一对矛盾句。 - `frontend/src/lib/rectification-agentic/v9/divergence-panel.ts:231-237` `nakshatraTraitsForTime`:代表时间与早于代表的候选拿 `earlier` 那对,晚于代表的拿 `later`。事发卡代表分钟是排第一的 04:59,三列都 ≤ 代表 → 三列同一对 → `splitSharedTraits` 把两极都判成「共同点」抬到卡头。 - 同一张表还被 `inference-adapter.ts:134-182` 用来出风格参考题的 A/B 选项标签(`optionLabel("A", optionA.traits)`),A 选项标签也是「两极合一」。 - 修法(产品口径:一列只写一句、不得自相矛盾):`refinement_packet.py` 每个 option 只取该 nakshatra 的**第一极**作 `traits`(`[earlier[0]]` / `[later[0]]`),`user_meaning` 同步改;前端不改结构。golden 若含 `traits` 两句需按 BUG-733 口径只追加/替换这一处并贴 diff。 ## 3. 决策记录 - 产品负责人 2026-09-16:正文里念出来的题必须有可点的卡;候选列与卡头的性格描述不得自相矛盾。 - F1 修法定为两条:**风格题不得借别的分盘的探针**(宁可不落库也不错分);判别题以落库副本投影,过期不静默而是 `superseded` + 落下一问。不推翻 BUG-559 的 split-hash 过期判定,只改过期后的去向。 - 事发 Case 的 04:48–04:59 收窄是错分产物;本单不做数据回滚(用户可用「更像这个」之外的路径继续补经历重算),但 CHANGELOG 要写明「2026-09-16 前分盘风格题答案可能计错分」。 - F2 结论由回放决定,不预设。 - 不改 Skill、不改出卡门槛、不动 BUG-751 的闸。 ## 4. 硬红线 1. BUG-674「判别题落不了库不得写进正文」、BUG-582「无框区分题不得退化成采集句」、BUG-559/578 的过期判定原样保留。 2. 前端 `persisted_question` 块不得为绕过投影而自己拼卡(BUG-673 防复发)。 3. `page.tsx` 不增长;Python 改动跑 `tests/test_rectification_*.py` 全量与快速门。 4. 既有断言只因形状变化更新,写「原值 / 新值 / 原因」。 ## 5. 任务分解 ### T1 分盘风格题不得借探针;从落库副本投影(BUG-912) - **T1a 堵借探针**:`stampChoiceSchemaWithProbe` 在 `preferred` 无 `semantic_key` 时**不得**退到 `selectHighestGainProbe`;无探针即返回未盖戳 schema。`expectedAnswerSchemaFor:149` 的「计分题无探针则 null」保留,于是 D9/D10 风格题要么自带探针要么不落库(落不了库走 BUG-674 的 `persistExhaustionCollect`,不进正文)。 - **T1b 给风格题自己的探针**:`varga_observation` / `precision_stage` 生成 `d9_relationship` / `d10_career` 等 `distinguish_candidates` followup 时,按 BUG-375 对比探针的做法建自己的探针:`semantic_key = :`,`supports/conflicts` 是**剩余候选分钟**按该分盘上升星座分组,`expected_outcomes` 与 `choice_frame` 的 A/B/C/D 一一对应;`candidate_split_hash` 按现有规则算。这样答案按 D9 计分,GET 投影也有 key 可匹配。 - **T1c 从落库副本投影**:`projectRectificationChoiceCard` 在 `persistedTargetedCard` 后加 `persistedDistinguishCard`:焦点 `intent=distinguish_candidates`、`parseAgentChoiceCopy(schema)` 非空、`probe_id` 存在且该探针仍在当前 `inference_state`(未过期)→ 直接投影,`question_id` 用焦点的。过期(split-hash 不同)时由服务端把焦点 `superseded` 并按 `persistNextInterviewIfIdle` 落下一问,不得留下 `kind=choice` 无卡的快照。 - 验收: - 回放事发形状:D9 `candidates_differ`、inference_state 里只有 D24 对照探针 → 修前 schema `probe_id` 以 `contrast:varga.d24` 开头;修后 `probe_id`/`semantic_key` 为 D9 自有,`supports` 为候选分钟;答 B/「没有」后后验只按 D9 分组变化,D24 探针的 `answered_probes` 不含它。 - GET 快照 `choice_card.question_id === current_question.question_id`,且 `interviewChoiceCardUnavailable` 为 false。 - 过期路径:改账本重算使探针失效 → 焦点 `superseded` + 新 `current_question` 有卡。 - BUG-375 的 `rectification-server-focus` / `rectification-choice-card` / `rectification-inference-machine` 原断言绿;BUG-375 条目补「2026-09-16 复发(BUG-912):无探针的分盘风格题借最高增益探针」。 ### T2 「没有」后直接出卡的回放定性(BUG-913) - 用事发快照形状(5 可评分事件、3 候选、D9 两蝎一秤、D10 同巨蟹、`precision_gate_met=false`)回放:答 D9 否定后 `decideFromDossier` 的 `nextAction` / `stopReason`;列出此时 `buildMethodFollowupPlan` 是否还有未问的线;`shouldHoldForTieBreak` 是否为真及为何没 hold。 - 结论三选一写进 BUG-913:设计如此(关闭)/ BUG-751 复发(修)/ BUG-688 复发(修)。复发则关联原号。 ### T3 性格描述一列一句(BUG-914) - `refinement_packet.py` 每个 option 只取第一极;`user_meaning` 改为「A:{earlier[0]}。B:{later[0]}。」;`tests/test_rectification_refinement*.py` 补断言:`options[*].traits` 长度为 1 且两 option 不同句。 - 前端 `splitSharedTraits` 不改;补一条断言:同一对里的两极不得同时出现在 `shared_traits`。 - 验收:Python 定向 + 快速门;前端相关测试绿。 ### T4 记录 - BUG-912~914;CHANGELOG(正文念出的题必有卡;性格描述不再自相矛盾);`PROGRESS-rectification-dead-d9-choice-20260916.md`;真机清单 `docs/testing/rectification-dead-d9-choice-20260916.md`。 ## 6. 让步顺序 1. T1 的「过期即 superseded + 落下一问」若与 `persistNextInterviewIfIdle` 的幂等早退打架,先只做「从副本投影」,过期仍显示旧卡但可答(答案按当前账本计分),进度记录写明。 2. T3 若 golden 牵连过多,先只改 `user_meaning` 与前端去重,Python `traits` 拆分作第二提交。 3. T2、T4 不可让步。 ## 7. 开工前置命令 ```bash git fetch origin --prune git worktree add -b codex/rectification-dead-d9-choice-20260916 .worktrees/rectification-dead-d9-choice-20260916 origin/staging cd .worktrees/rectification-dead-d9-choice-20260916 .venv/bin/python -m pytest tests/test_rectification_refinement_packet.py -q 2>&1 | tail -3 # 若文件名不同用 ls tests | grep refinement cd frontend && ./node_modules/.bin/tsc --noEmit && npm run lint && npm test 2>&1 | tail -20 grep -o "BUG-[0-9]\+" ../docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1 ``` ## 8. 环境缺口 - 事发快照的 `current_question` / `choice_card` / `next_user_action` 已由产品负责人补抓并写进 §2 F1;`method_followup_plan` 不在公开快照里,不需要再抓。 - **产品知悉**:该 Case 当前显示的 04:48–04:59 与三列卡建立在错分的答案上,不要采用;修复部署后补一件带年月的事触发重算,或新开 Case。 - 真机:正文里出现「有没有…」判别题时下面必有 A/B/C/D;交付卡每列性格只有一句、卡头共同点不自相矛盾。