Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
11 KiB
11 KiB
已被取代:2026-09-10 产品口径变更,本单决策 1/2/3 由
TASK-rectification-collection-redesign-20260910.md取代,请勿单独领取。
TASK · 六条线全答「没有」、只有三件带年月经历时:门槛句之后没有任何载体,页面只剩「没有拿到下一个问题」;第三件被留作验证导致永远没有选择题(2026-09-10)
- 基线:
origin/staging@fff6da68(代码52db714b,已部署;含 BUG-635~645) - 分支:
codex/rectification-exhausted-gate-exit-20260910,基于origin/staging - 串行:改
answer-choice.ts(persistExhaustionCollect)、method-followup.ts、turn-exit.ts、客户端缺口态。BUG-641~645 均已合入,无并行单。 - 执行方:coding agent;验收:Claude
- BUG 编号起点:BUG-646(当前最大 645)
- 优先级:BUG-646 P1(真实用户第三次断流,同一天);BUG-647 P2(三件带年月经历仍没有选择题、范围不动,用户以为坏了)
- 不改 Skill 10.0.22、不改引擎计分、不加迁移。
0. 给产品的即时绕行
页面没坏。输入框还能用:直接再打一件带年月的经历(任何领域,哪怕只记得年份),流程会继续;或者按「先这样」按现有材料要区间。「没有拿到下一个问题」这行字在这个状态下是误报,见 §2。
1. 事故实证(staging 2026-09-10 晚,Skill 10.0.22;只写结构)
顺序:开场 → 学业两件(同一轮)→ 感情「没有」→ 事业「没有」→ 家人「没有」→ 财务「也没有」→ 迁居「没有」→ 健康采集题答一件带年月的外伤 → 职业「程序员」→ 助手:「记下了:你提到自己是程序员。目前还差 1 件带大概年月的经历,想到哪件就说哪件——比如…」→ 下面「没有拿到下一个问题」,顶部「目前范围 04:45–05:15,还在核对」。全程没有一道选择题,范围没动。
按代码复盘这个状态:
| 项 | 值 |
|---|---|
| 账本 | 学业 2 件(带月)、健康 1 件(带月)、职业备注 1 条(不计分) |
| 拒答 | 感情、事业、家人、财务、迁居五个带年份领域全部 declined |
| 训练门 | MIN_ACCEPTANCE_EVENTS=3、MIN_ACCEPTANCE_DOMAINS=2;带月事件 3 件 ≥ MIN_EVENTS_TO_RESERVE_HOLDOUT=2,splitHoldoutEvents 留出 1 件作 holdout → 训练 2 件,门关;collection_progress.missing=1 |
| 区分探针 | 训练门关 → rankedCatalog 为空 → 没有任何带年月点选题,范围不可能动 |
| 采集计划 | nextDatedCollectFollowup 全部被 declined 挡住;职业已覆盖;exhaustionSpokenCollectFollowup 返回 null(BUG-558 决策:不再问 other) |
| 出口 | persistExhaustionCollect:followup=null → 训练门关 → 不 adopt → 写门槛翻译句(acceptanceGateNarration → 「还差 1 件带月份的经历,领域不限」,再经旁白 Agent 改写成用户看到的那段)→ terminalNote=true,不落任何焦点(rectification-exhaustion-exit-20260906.test.ts「closed ceiling translates the gate and does not persist a focus」正是锁这个行为) |
| 客户端 | 快照 current_question=null、question_source="focus";rectificationQuestionGapState 走 preparing → 重试用尽 → unavailable → 「没有拿到下一个问题」+「接着问」(BUG-627)。按「接着问」调 repair-exit,回来的还是同一句门槛,无限循环 |
2. 根因
2.1 BUG-646(P1):「穷尽 + 门关」是合法终态,但既没有载体也没有出口,客户端还把它当故障
- BUG-558 去掉了「也可以再说一件」的无限循环,改成「写门槛句、不落焦点、
terminalNote」;BUG-627 让非终止闸门接受 terminalNote host turn。两条都只保证服务端不抛错,没有给用户任何可点的东西。 - 客户端的缺口态(BUG-633)是为「服务端明明有下一问但没渲染出来」设计的;它分不清「没拿到」和「本来就没有」,于是把设计内的终态渲染成故障提示,按钮「接着问」又把用户送回同一句。
- 「先这样」在口述采集下是否可见,取决于 BUG-558 放在输入框上方的停止控件;本例用户没看到可点的东西(截图只有门槛句与缺口提示)。
- 门槛句「还差 1 件带月份的经历,领域不限」对用户不成立:他刚刚已经把六个领域都答了「没有」。句子没有告诉他「任何领域再补一件都行、哪怕只记得年份」,也没告诉他「说不出就按先这样」。
2.2 BUG-647(P2):第三件带月经历被留作 holdout,用户给了三件仍看不到一道选择题
splitHoldoutEvents 在带月事件 ≥2 时保留 1 件作 holdout;训练门要 3 件。于是用户必须给到 4 件才有选择题,而所有文案(开场「想到几件说几件」、门槛「还差 1 件」)都让人以为 3 件就够。当五个领域都已拒答、再补一件的可能性很低时,把第三件继续留作验证等于放弃筛选。
3. 决策记录(产品负责人 2026-09-10 口头授权处理「走不下去」;以下为 Claude 的产品建议,执行前若产品另有口径以产品为准)
- 终态必须有载体。 穷尽 + 门关时,服务端落一个一次性的开放领域采集焦点
collect:any:collect_method_evidence,题干:「带年月的经历现在只有 N 件,还分不开候选。任何领域再补一件都行,哪怕只记得年份;实在没有,就按「先这样」按现有材料给区间。」它只问一次(同 Case 已问过则不再落),答「没有 / 记不清」→ 进入决策 3。这不是恢复 BUG-558 删掉的other循环:那时是每轮都回同一句且没有出口;这里是一次明确的开放追问 + 明确的出口句。 - 客户端不得把设计内终态当故障。 快照新增
interview.stop_reason="collect_exhausted_gate_closed"(或question_source="terminal");有此标记时缺口态不渲染「没有拿到下一个问题」与「接着问」,改渲染门槛句 + 「先这样」按钮 + 输入框占位「再补一件带年月的经历,任何领域都行」。 - 开放追问也被拒后,释放 holdout 打开筛选。 带月事件 ≥3、训练 <3 且全部采集领域已穷尽时,
splitHoldoutEvents不再留出(回执holdout.status="released_exhausted"),训练门按实际事件数判定,允许出带年月的区分点选题;overall_confidence上限low,确认门、唯一分钟门不动(本来也关着)。若产品不接受释放 holdout,则改为:直接交付区间卡(selection_allowed)并写明「只有 N 件经历,没有做区分」,本单 T3 换成这一版;执行方开工前向产品确认一次,未确认按释放 holdout 做。 - 门槛句改口径。 「还差 N 件带月份的经历,领域不限」→ 「带月份的经历还差 N 件;已经答过的领域也可以再补别的年份的事,哪怕只记得年份。」当 holdout 占了一件时加一句「有一件留作最后核对,不参与筛选」。
- 不改
MIN_ACCEPTANCE_EVENTS/DOMAINS、不改SCORE_DELTA、不改 Skill。
4. 硬红线
- 开放追问焦点每个 Case 最多落一次;不得再造成每轮重复(BUG-558 防复发条款仍有效:
exhaustionSpokenCollectFollowup不返回other,本单用新 domainany与独立判定函数实现,不复用other)。 - 释放 holdout 只在「采集穷尽 + 开放追问已拒」后发生,不得在正常流程里提前用掉 holdout;回执必须留下
released_exhausted痕迹,报告的 Technique Audit 里Real Case Calibration / holdout行写released。 - 确认门与唯一分钟门不得因本单任何改动打开。
- 客户端不得新增 spinner;「没有拿到下一个问题」只在
question_source表明确实缺失时出现。 - 既有测试总数不降;改动「closed ceiling translates the gate and does not persist a focus」等断言写原值 / 新值 / 原因。
5. 任务分解
T1 · 开放追问焦点(BUG-646)
method-followup.ts:新增exhaustedOpenCollectFollowup(evidence, declinedTopics, closedCollectFocuses),条件:dated 领域全部 declined/covered、职业已覆盖、训练门关、且collect:any:*未问过 → 返回domain="any"、intent=collect_method_evidence、题干见决策 1。persistExhaustionCollect在门槛句之前先试它;persistableFocusDomain("any")允许落库(映射到 DB 允许的other或新增枚举,执行方按 DB 约束选,写进进度记录)。- 验收:本单事故形状(学业 2 + 健康 1 + 五领域拒答 + 职业)→
next_followup.domain="any"、焦点落库、hostNarration为题干;同 Case 再次到达 → 不再落,走 T3。
T2 · 客户端终态(BUG-646)
- 快照
interview.stop_reason/question_source="terminal";rectificationQuestionGapState新增terminal_gate态;缺口组件按决策 2 渲染;frontend/DESIGN.md更新缺口态表。 - 验收:
rectification-surface-state.test.ts:terminal 标记下不得出现「没有拿到下一个问题」「接着问」;有「先这样」按钮与新占位。
T3 · 释放 holdout 打开筛选(BUG-647)
split-holdout.ts增加options.releaseHoldout;trainingScoreableGate/decideFromDossier在「开放追问已拒」时传入;回执holdout.status="released_exhausted",overall_confidence上限 low。- 验收:3 件带月(2 学业 + 1 健康)+ 开放追问 declined → 训练门开、
rankedCatalog非空、出带年月点选题;未拒开放追问前仍留 holdout;确认门断言不变;报告 audit 行写released。
T4 · 门槛句(BUG-646)
acceptanceGateNarration按决策 4 改;agent-voice-copy-contract词表同步。- 验收:missing=1 且 holdout 占一件 → 句子含「留作最后核对」;missing=1 无 holdout → 不含。
T5 · 记录
docs/BUG_HISTORY.mdBUG-646、647(关联 BUG-558、627、633);BUG-558 补「本单加一次性开放追问,不是恢复循环」。CHANGELOG.md、PROGRESS-rectification-exhausted-gate-exit-20260910.md、docs/testing/真人清单:六个领域全答「没有」+ 一件健康经历 → 期望看到开放追问;再答「没有」→ 期望出现带年月选择题或区间卡,页面不得出现「没有拿到下一个问题」。
6. 让步顺序
T3 可延后并改为「直接交付区间卡」的备选;T1 + T2 + T4 最低交付。
7. 开工前置命令
git fetch origin --prune
git worktree add -b codex/rectification-exhausted-gate-exit-20260910 .worktrees/rectification-exhausted-gate-exit-20260910 origin/staging
grep -o "^## BUG-[0-9]*" docs/BUG_HISTORY.md | tail -1
cd frontend && npm test -- tests/rectification-exhaustion-exit-20260906.test.ts tests/rectification-surface-state.test.ts tests/rectification-spoken-collect.test.ts
8. 验收口径
tsc --noEmit0 错、npm run lint0 error、相关套件 fail=0、测试总数 ≥ 基线;next build后/仍 Static,首屏 gzip ±2%。- 推 staging 后
/api/health的deployment.gitCommit等于最新含代码改动的提交;真人清单见 T5。