Files
Jyotisha/docs/tasks/TASK-rectification-tied-first-premature-delivery-fix-20260914.md
T
Jesse_ChenandClaude Fable 5 e42a62dfac docs(tasks): regression fix brief for premature tied-first delivery
d5a8db0a 把 tied_first 早退放到 coverageBlocks 之前且没有 probe 守卫,新案子
答两题、区间仍是整个开局窗口就判 adopt_representative/can_adopt:true/代表
分钟(BUG-683)。配套测试传 discriminatorProbe: null,与实现语义不一致,所以
没拦住。另立 BUG-684(investigating):交付态下仍有画不出的死卡,delivered
终态因要求无题而不触发,界面仍印「没有拿到下一个问题」。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-09-14 08:33:17 +00:00

10 KiB
Raw Blame History

TASK · 修复单:并列早退把第二题就判成"可采用",交付态下又出现死卡 — 2026-09-14

  • 基线:origin/staging @ d5a8db0a(staging 已部署该 SHA)。
  • 分支:codex/rectification-tied-first-fix-20260914,worktree .worktrees/rectification-tied-first-fix-20260914。
  • 关联:本单是 TASK-rectification-delivery-vs-collect-split-20260914.md(BUG-680/681/682,实现 d5a8db0a)的回归修复单。BUG-680 的 D2 被实现得比任务书宽,造成线上更严重的问题。另关联 BUG-674(不可盖章探针 → 死卡)、BUG-671。
  • 串行:改 core/rectification-decision.ts 与其测试;与其他在途分支无重叠。

1. 事故实证(产品负责人 staging 真机,2026-09-14,部署 SHA d5a8db0a)

新建校正,只回答了两轮采集(2016-09 上大学 / 2020-06 毕业;2020-04 实习 / 2020-10 离职),助手随即问第三题「2023 年 5 月前后,你有没有入职、换工作,或者当时手上的职责明显变重?」,界面同时弹出 「没有拿到下一个问题。」+「接着问」按钮。

同一会话 GET 快照(已脱敏):

"interview": {
  "type": "complete_with_range",       "session_outcome": "adopt_representative",
  "can_offer_range": true,             "can_adopt": true,
  "selection_allowed": true,           "propose_allowed": true,
  "precision_stage": "ready_to_adopt", "stop_reason": "tied_first",
  "representative_time": "05:06",      "credible_range": ["04:45","05:15"],
  "collection_progress": { "scoreable": 3, "minimum": 3, "missing": 0 }
}

候选(9 个,前三名并列):05:06 = 13、05:08 = 13、05:14 = 13、05:03 = 12、04:59 = 11、04:46 = 10、04:53 = 10、05:15 = 10、04:50 = 8。

credible_range 就是开局窗口 04:45–05:15(30 分钟,完全没有收窄),系统却宣布 can_adopt: true、代表分钟 05:06、precision_stage: ready_to_adopt。

P0-1 · 并列早退缺少"还有题可问"的守卫(BUG-683)

d5a8db0a 在 decideRectification 里加的早退:

// core/rectification-decision.ts,插在 `if (coverageBlocks) {` 之前
if (
  stopClass?.kind === "exhausted"
  && stopClass.reason === "tied_first"
  && input.trainingGateOpen !== false
  && separation.ranked.length > 0
) {
  return completeWithRange(separation, holdout, range, "exhausted", capability, stopClass.reason);
}

问题有两层:

  1. 位置比任务书写的高一层。 TASK-…-delivery-vs-collect-split-20260914.md 的 D2 原文是「decideRectification 的 !separation.sufficient 分支里,把 tied_first 的判断提到 stillNeedNarrowing 之前」。该分支的第一行是 if (probe) return discriminateOrExhaust(...),所以按任务书实现时"还有带年月探针可问"会先接管。实现把它提到了 coverageBlocks 之前、整个 !separation.sufficient 分支之外,于是连"还有探针"和"方法覆盖未达标"都绕过了。
  2. 缺 probe 与定向补事守卫。 校正刚开始时,9 个候选分数都很接近,前两名并列是常态(本例第 2 轮就三方并列 13 分),此时 tiedForFirst 为真 → 直接交付。结果是任何新案子答两题就被宣布"可采用代表分钟",而区间等于整个开局窗口。

配套测试 frontend/tests/rectification-delivery-vs-collect-20260914.test.ts:213 传的是 discriminatorProbe: null,也就是说测试锁的是"没有探针可问时才交付",实现却没有这个条件——测试与实现的语义不一致,所以回归没被拦住。

P0-2 · 交付态下仍出现死卡,delivered 终态因此不触发(BUG-684,investigating)

d5a8db0a 新增的 interviewDeliveredGap 要求 questionMissing === true。本例屏幕上有一道问题(「2023 年 5 月前后…」),说明 current_question 不为空 → delivered 不触发;而这道题又没有可点选项 → interviewChoiceCardUnavailable 判死卡 → questionLoadFailed → 落到 unavailable → 印「没有拿到下一个问题」+「接着问」。

即:决策层已经终局(complete_with_range),对话层还在问题态,而这道题是一张画不出来的死卡。 这与 BUG-674 同源(焦点写得进、卡画不出,或反之)。

证据缺口:需要同一会话 GET 的 current_question、choice_card、question_source,以及最后一条助手 turn 的 offer_result_id。缺这四项之前,本条按 investigating 立号,不得臆断是哪条写入路径。

2. 根因

  • BUG-683:并列(tied_first)被当成"无论如何都问不下去"的终局信号,但它在采集早期只是"分数还没拉开"。终局的真正条件是没有带年月探针可问、且定向补事也问完。
  • BUG-684:待取证(见上)。

3. 决策记录

决策 内容
D1 tied_first 早退必须同时满足:没有可问的带年月探针(probe === null)、定向补事不再有题(targetedCollectExhausted !== false)、训练门开、ranked 非空。 refreshExhausted 不参与该判断(它的落库不可靠,BUG-680 已证)。这修正上一单 D2 的表述含糊:不是"提到 stillNeedNarrowing 之前就完事",而是"在已经没有别的问法时才算终局"。
D2 区间没有收窄就不得宣布可采用。 当 credible_range 等于开局候选窗口(首尾与 case.candidateRange 相同)时,即便走到交付,sessionOutcome 也只能是 provisional_range / completed_with_range,can_adopt 必须为 false:可以给并列区间、可以让用户看候选,但不得给"推荐采用的代表分钟"。这是 AGENTS.md Part B B4 的诚实边界,不得因为想让流程收尾而放宽。
D3 测试与实现语义必须一致:tied_first 的交付测试要成对——有探针时不交付、无探针且定向补事问完时交付。上一轮只写了后者。
D4 BUG-684 本轮只做取证:在死卡分支加一条结构化日志(event: "rectification_delivered_state_dead_choice",带 session_outcome、stop_reason、question_id、has_choice_card),并把 delivered 终态的判定从"必须无题"放宽为"无题或当前题是画不出来的死卡"——交付态下不得再出现「没有拿到下一个问题」。根因确认后另立修复单。
D5 不动数据库结构与迁移、不动 deploy/** 与 workflow、不动 page.tsx、不新增依赖。不得放宽任何置信度或确认边界。

4. 硬红线

  1. tsc --noEmit 0 错;npm run lint 0 error;npm test 失败清单与基线逐条一致;next build 通过且 / 仍 ○ Static;首屏 gzip ±2%。
  2. 新增断言必须是行为断言。
  3. 不得回退 BUG-680/681/682 的三项修复(刷新落库失败不算已尝试、交付话术与交付卡同真同假、delivered 终态)——本单只收紧 tied_first 的触发条件。
  4. 不得为了让并列局面"能收尾"而把 can_adopt 或 confirmation_allowed 放宽。

5. 任务分解

任务 1 · 收紧 tied_first 早退(BUG-683,P0)

  • core/rectification-decision.ts:早退条件加 && !probe && input.targetedCollectExhausted !== false;位置维持在 coverageBlocks 之前即可(有了 !probe 守卫就不会抢走判别题)。
  • 验收标准(成对):
    • 行为单测:并列 + discriminatorProbe 非空 → 决策不是 complete_with_range,仍走判别/采集,can_adopt === false。
    • 行为单测:并列 + discriminatorProbe: null + targetedCollectExhausted: true → complete_with_range、stop_reason = tied_first、can_offer_range = true(保留 :213 现有用例,改成 targetedCollectExhausted: true,并在测试里写"原值 / 新值 / 原因"三栏说明)。
    • 行为单测:并列 + discriminatorProbe: null + targetedCollectExhausted: false → 不交付,继续定向补事。
    • 回归用例(真机形状):9 个候选、前三名并列 13 分、datedEventCount: 3、探针非空、credible_range = 开局窗口 → 断言 session_outcome !== "adopt_representative"。

任务 2 · 未收窄不得宣布可采用(BUG-683 的第二半,P0)

  • decideRectification / completeWithRange:当 range 与开局候选窗口相同(需把 case.candidateRange 传进决策输入,followupCaseArgs 已有该字段,确认可直接取用)时,capability.canAdopt 与 selectionAllowed 的"采用"语义降级为 provisional_range。
  • 验收标准:行为单测——credible_range = ["04:45","05:15"] 且开局窗口相同 → session_outcome 属于交付集合但 can_adopt === false;收窄过(如 04:48–05:07)→ 维持现有行为。

任务 3 · 交付态死卡取证与兜底(BUG-684,P1,investigating)

  • interviewDeliveredGap:questionMissing 放宽为 questionMissing || deadChoice(入参增加 deadChoice),调用点同步。
  • 死卡且处于交付态时打上述结构化日志。
  • 验收标准:行为单测——交付 outcome + 死卡(kind = choice、无 choice_card)→ gap 返回 delivered,不是 unavailable;非交付态的死卡仍返回 unavailable(不得误伤 BUG-671 的修复入口)。

任务 4 · 记录

  • docs/BUG_HISTORY.md:BUG-683 resolved(必须写明"复发自 BUG-680 的修复",并说明实现位置比任务书更宽、测试用 discriminatorProbe: null 而实现无此守卫,所以没被拦住);BUG-684 investigating,列出缺的四项证据。
  • CHANGELOG.md 一行;PROGRESS-rectification-tied-first-fix-20260914.md;docs/testing/ 清单加一条:新建校正答两题后,不得出现「可采用 / 代表分钟」,范围应仍在收窄中。

6. 让步顺序

  1. 任务 1 必做且优先——线上每个新案子答两题就被判"可采用",是当前最严重的问题。
  2. 任务 2 必做(诚实边界)。
  3. 任务 3 可只做兜底与日志。

7. 开工前置命令

git fetch origin --prune
git worktree add -b codex/rectification-tied-first-fix-20260914 \
  .worktrees/rectification-tied-first-fix-20260914 origin/staging
cd .worktrees/rectification-tied-first-fix-20260914
ln -s /workspace/Jyotisha/.venv .venv
cd frontend && npm ci

开工前读 docs/BUG_HISTORY.md 的 BUG-680、681、682、674 四条,以及上一份任务书的决策记录。

8. BUG 编号起点

  • 起点 BUG-683(当前最大号 682)。