Files
Jyotisha/docs/tasks/TASK-rectification-tiebreak-card-loss-20260915.md
T
Jesse_ChenandClaude Fable 5 3e58e85f8f docs(tasks): P0 — tie-break entry destroys the delivery card; session list lies about rectification
两组缺陷,分两单。

会话列表单追加 BUG-700/701:全仓只有 append_consultation_question 这个
咨询 RPC 写 chat_sessions.updated_at,校正的 turn 走自己的表从不推进,于是
校正会话的 updated_at 冻结在创建时刻——连着几小时答题也不动位置。叠加
BUG-699 的改名,列表变成「名字是今天、排序是旧日期」。而 resolveBootstrap-
SessionSelection 的 ?c= 只比对当前已加载那一页(SESSION_PAGE_SIZE = 40),
翻不到就判 missing、打「该对话不存在或已被删除」并清掉 URL。查过了没有任何
删除路径被触发,会话大概率仍在库里。

新单 BUG-702~704:applyLiveCandidateOffer 遇到任何未答的 choice/collect_spoken
就把 candidateOffer 从每一条消息上剥掉,而 requestTieBreak 必然写入这样一道题
——点卡上的按钮必然丢卡。按钮亮不亮看 tieBreakPersonalityAvailable(只看
followup),题能不能画成选择题看 buildChoiceCard 的 styleOptions.ok /
canRenderYearlessChoice,两个判据不一致,于是按钮亮着、点开是裸题,卡又没了,
两条路都断。交付旁白还写了 VOICE 明令禁止的「相对支持度」,并在自称不再问
分盘题的同一屏提供分盘题入口。

产品重申 BUG-686 的原意:卡上不要参考题按钮,出卡前收集完,以卡结算。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-09-15 07:44:40 +00:00

16 KiB
Raw Blame History

TASK · P0:点卡上「再答两道参考题」后交付卡消失、题目变裸题、无路可退(BUG-702~704)

  • 日期:2026-09-15
  • 基线 commitorigin/staging @ 224244c0
  • 执行分支:codex/rectification-tiebreak-card-loss-20260915
  • 工作树:.worktrees/rectification-tiebreak-card-loss-20260915
  • 严重度:P0。用户已经走到交付,一次点击把交付卡弄没了,且没有任何恢复入口。

1. 用户现象(原话 + 截图)

上一个问题已经出卡了,然后我点击了再问两个问题,然后选择时间的卡片就删了。而且下面这个问题是不是应该是选择题,而不是让用户说的。再说咱们之前不是定了卡里不要写「再问两个问题」的按钮,直接在跳卡前收集,以卡结算就行了?

截图(2026-09-15stagingdeepseek-v4-flash)显示:

点击前 —— 交付卡完整:「目前范围 04:48–05:07(对照了 5 件经历)」、三列候选 04:53 / 05:00 / 05:06(相对可能性 26% / 26% / 20%,各带「更像这个」)、「还有 2 个分钟可能性更低,已并入范围」、「这只是代表性候选,不是已确认的唯一出生分钟」、「查看验证报告」,以及卡内按钮 「再答两道参考题微调排序」

点击后 —— 卡整条消失;只剩两行文字「只微调排序,不改目前范围。」「亲密关系里,你更接近哪一种相处方式?」,没有任何选项;再下面还留着上一轮的「再问下去也分不开了。范围在上面,对不上可以改选。」——而「上面」已经没有卡了。


2. 事故实证

行号会漂移,定位以符号名为准。核对于 origin/staging@224244c0

2.1 BUG-702|任何一道未回答的题都会把交付卡从整个对话里抹掉

frontend/src/lib/rectification-agentic/v9/turn-question.ts

export function interviewQuestionBlocksAdoptOffer(question, adopted): boolean {
  if (adopted) return false;
  if (!question || questionIsAnswered(question)) return false;
  return question.kind === "collect_spoken" || question.kind === "choice";
}

export function applyLiveCandidateOffer(messages, input) {
  
  const liveInterview = messages.some((m) => interviewQuestionBlocksAdoptOffer(m.question, false));
  if (liveInterview) {
    return messages.map((m) => (m.candidateOffer ? { ...m, candidateOffer: undefined } : m));
  }
  
}

frontend/src/components/rectification-agentic-chat.tsxshowSelectionCards 里有同一条守卫:

&& !messages.some((message) => interviewQuestionBlocksAdoptOffer(message.question, Boolean(candidateResult.selectedTime)))

requestTieBreak 成功后会把新写的 turn 合并进 messages(这是 BUG-685 的修复,本身正确)。那条 turn 带着一道未回答的题 → liveInterview 为真 → candidateOffer 被从每一条消息上剥掉,卡整条消失

这不是偶发,是这条路径的确定行为:点一次按钮,必然丢卡

守卫的本意(有活题时先答题、不要同时诱导采用)本身站得住。问题在于它和「按钮就长在卡上」组合成了陷阱:用户点的是卡上的按钮,代价是卡没了。

2.2 BUG-703|按钮的可用判据和题目的可渲染判据是两个,按钮能亮但选项建不出来

按钮显隐:frontend/src/lib/rectification-agentic/v9/method-followup.ts

export function tieBreakPersonalityAvailable(input): boolean {
  if (input.accepted) return false;
  return tieBreakPersonalityFollowup(input) !== null;     // ← 只看拿不拿得到 followup
}

题目能不能画成选择题:frontend/src/lib/rectification-agentic/v9/choice-card.ts

if (!styleOptions.ok) return null;                        // ← 另一个门

if (kind === "varga_style") {
  if (!canRenderYearlessChoice({ choiceKind: kind, styleOptions: styleOptions.options })) return null;
}

两个判据不一致followup 拿得到、styleOptions 建不出来时,按钮照亮,点下去得到一道没有选项的题。

截图里那句「亲密关系里,你更接近哪一种相处方式?」正是 choice-card.tsvarga_style + domain === "relationship"字面量

return domain === "relationship"
  ? "亲密关系里,你更接近哪一种相处方式?"
  : "平时做事,你更接近下面哪一种?";

它本体就是选择题。渲染成裸题 = 选项没建出来,而不是设计如此。

这同时违反 BUG-686 自己写下的防复发:「分盘上升星座少于两种时该题仍不出现」——这次它出现了。

裸题还带来第二层后果:用户没法点选,而卡已经被 BUG-702 抹掉,两条路都断了

2.3 BUG-704|交付旁白违反 VOICE 红线,并且和卡上的按钮自相矛盾

截图里的交付旁白原文:

已答6轮,再问下去也分不开04:53和05:00,没有年份的分盘题也不再问,所以这一轮不再往下问。可信范围是04:48到05:07,代表分钟取04:5305:00作为次选,两者相对支持度都是16。……

两处问题:

  1. 「相对支持度」是 frontend/docs/VOICE.md 明令禁止的词

    不得对用户写「概率 / 置信度 / 确定 / 相对支持度」。专业数字只留在看盘板。

    ⚠️ 注意区分:卡上三列写的「相对可能性 26%」是 09-08 定稿的交付卡设计(rectification-candidate-compare 那轮),那个是对的,不要动。要去掉的是旁白正文里的「相对支持度都是 16」。

  2. 旁白说「没有年份的分盘题也不再问」,而同一屏的卡上就摆着「再答两道参考题微调排序」——那个按钮点开的正是没有年份的分盘题。正文和控件在同一次渲染里互相打脸。

2.4 与既有记录的关系

  • BUG-686平局直接出卡,参考题留成卡下可选按钮,resolved)产品拍板的是「出卡前只要参考题可用且没用过就先连问最多两道」。实现做了前置,但同时保留了卡上的按钮当第二条路,防复发只写到「用过之后不再显示入口按钮」。本单要求按产品原意把这条路彻底去掉。
  • BUG-685 的修复(把新 turn 合并进对话区)是对的,本单不得回退。
  • BUG-688 的教训(「延迟交付」的守卫必须带出口)同样适用于本单:任何抑制交付卡的守卫都必须有出口。

3. 决策记录(产品已授权)

产品 2026-09-15 重申并确认:

  1. 交付卡上不要「再答两道参考题」这个按钮。 参考题在出卡之前收集完,出卡即结算。BUG-686 已经实现了前置收集,本单把卡上这条冗余的第二路径删掉。按用户既有口径:多余入口宁可删除也不修。
  2. 按钮删掉之后,BUG-702 的「点一下丢卡」随之消失。但 §2.1 的守卫本身仍要带出口(见任务 2),不能只靠"没有入口就触发不到"。
  3. 分盘风格题必须是选择题。 建不出选项就不要问,不得退化成裸题。
  4. 旁白正文不得出现「相对支持度」等内部计分词;交付卡上的「相对可能性 %」保持不变。

4. 硬红线

  1. 不得回退 BUG-685 的修复(新写的 turn 必须合并进对话区)。
  2. 不得把性格 / 风格题改成淘汰候选(BUG-686 防复发,继续有效)。仍然只 ±1、不淘汰。
  3. 不得为了凑满两道而放宽「该分盘至少两种上升星座」BUG-686 防复发,继续有效)。
  4. 不得删掉交付卡上的「相对可能性 %」三列——那是 09-08 定稿,本单不动。
  5. 任何抑制交付卡的守卫都必须有出口(BUG-688 的教训)。
  6. tsc --noEmit 0 错;npm run lint 0 error;测试总数不降;改既有断言写三栏。
  7. 全量 npm test 失败清单与基线逐条一致。

5. 任务分解

任务 1 · 删掉交付卡上的参考题入口(产品决策,BUG-702 的根治)

1.1 frontend/src/components/rectification-range-delivery.tsx:删除

{delivery?.tie_break_available && onTieBreak && !readonly ? (
  <button className="rectification-range-delivery__tie-break" >
    {RECTIFICATION_USER_COPY.rangeDeliveryTieBreakEntry}
  </button>
) : null}

连同 onTieBreak prop、rectification-agentic-chat.tsx 里的 onTieBreak={() => void requestTieBreak()}、以及 globals.css.rectification-range-delivery__tie-break 规则。

1.2 requestTieBreakPOST /api/rectification/cases/[caseId]/tie-break 保留——出卡前的前置收集仍然要用它。只删掉「卡上点击」这一个调用点。若确认前置路径不经过 requestTieBreak,再评估是否连客户端函数一起删,结论写进 PROGRESS,不要凭猜删。

1.3 tie_break_available 字段保留(服务端仍用它决定要不要在出卡前先问),只是不再驱动任何按钮。

1.4 RECTIFICATION_USER_COPY.rangeDeliveryTieBreakEntry 若再无引用则删除;若被前置路径复用则保留。

验收标准:交付卡上不再出现任何「再答两道参考题」类按钮;grep -rn "rangeDeliveryTieBreakEntry\|__tie-break" frontend/src 无渲染态命中;出卡前的两道参考题仍照 BUG-686 正常先问。

任务 2 · BUG-702:守卫要带出口

即使入口没了,守卫本身的行为仍然危险——将来任何写出未答题的路径都会再次抹掉卡。

2.1 applyLiveCandidateOffer / showSelectionCardsliveInterview 抑制,改为只抑制采用动作,不抹掉整张卡。具体形态执行方定,两条要求:

  • 卡上的范围、三列候选、边界句仍然可见(用户随时知道自己在哪)。
  • 「更像这个」在有活题时可以置灰并给一句说明(例如「先答完上面这道,再选时间」),不得整卡消失

2.2 若产品口径确实是「有活题时不该看到卡」,那么必须有出口:至少保证活题一旦被回答或跳过,卡立刻回来;且活题本身必须是可回答的(任务 3 保证)。两者选一,选哪个写进 PROGRESS 并说明理由。 默认取 2.1。

验收标准:构造「交付卡在场 + 新写入一道未答题」的状态,卡不整条消失;答完该题后一切恢复;新增测试覆盖。

任务 3 · BUG-703:建不出选项就不要问

3.1 统一两个判据:tieBreakPersonalityAvailable 必须和 buildChoiceCard同一个可渲染性判断——即 followup 存在 styleOptions.ok canRenderYearlessChoice 通过,才算 available。

实现上优先复用现有函数而不是复制条件,避免又出现第三个判据。

3.2 服务端在写 varga_style 焦点之前也要过同一道门:建不出选项就根本不写这道题,而不是写了让前端画不出来。

3.3 前端兜底:varga_style 类型的题若拿不到 choice_card不得渲染成裸题。按既有做法(BUG-674 / 675 的裸题修复)处理——要么不渲染并请求下一题,要么给出明确的修复入口。具体沿用哪条,看那两条 Bug 的现有实现,不要新发明第三套

验收标准

  • 构造「followup 拿得到但 styleOptions 建不出」的用例:按钮/前置收集都不会提出这道题。
  • 构造「焦点已落库但 choice_card 缺失」的用例:前端不渲染裸题。
  • 新增测试覆盖两条;rectification-tiebreak-before-card-20260914.test.ts 的既有断言不得变弱。

任务 4 · BUG-704:旁白文案

4.1 交付旁白不得出现「相对支持度」。把内部计分词从旁白模板 / Agent 指令里清掉,改用不含数字的说法(例如「这两个时间按现有信息分不开」)。具体文案对照 frontend/docs/VOICE.md

4.2 旁白不得断言「分盘题不再问」而同时仍有分盘题会被提出。任务 1 删掉卡上入口后这条自然缓解,但旁白的措辞仍要复核:只有当服务端确实判定不再问时才允许这么说。

4.3frontend/docs/VOICE.md 的禁用词一节补一行好/坏对照,用本次这句真实文案当坏例。

4.4 新增(或扩展既有的)用户可见文案契约测试,把「相对支持度」加进 MACHINE_VOICE_LEXICON 一类的禁用词表里,让它以后自动被拦。这条是本单最重要的防复发。

验收标准agent-voice-copy-contract.test.ts(或等价套件)断言用户可见文案不含「相对支持度」;VOICE.md 有新对照行。

任务 5 · 测试与文档

5.1 基线:npm ci && npm test 2>&1 | tail -20,三个数字写进 PROGRESS。

5.2 文档:

  • docs/BUG_HISTORY.md 新增 BUG-702 / 703 / 704,字段齐全。BUG-702 的「相关记录」写 BUG-685 / 686 / 688BUG-703 的「复发自」写 BUG-686(它的防复发「分盘上升星座少于两种时该题仍不出现」没被执行)并关联 BUG-674 / 675 的裸题家族。回到 BUG-686 记录的「防复发」末尾补一句:卡上入口已于本单删除,前置收集是唯一路径。
  • frontend/DESIGN.md:交付卡一节写明卡上没有参考题入口,参考题在出卡前收集,出卡即结算;并写明有活题时卡的表现(按任务 2 的最终形态)。
  • frontend/docs/VOICE.md:任务 4.3。
  • CHANGELOG.mddocs/tasks/PROGRESS-rectification-tiebreak-card-loss-20260915.md
  • docs/testing/rectification-tiebreak-card-loss-20260915.md:真机清单——走到交付卡,确认卡上没有参考题按钮;确认出卡前确实先问了两道带选项的参考题;确认旁白里没有内部计分词。

6. 让步顺序

  1. 任务 2 若 2.1(保留卡、只置灰采用)在现有状态机里改不干净,退到 2.2(整卡隐藏但保证出口),并把理由写进 PROGRESS。不得两条都不做。
  2. 任务 3.2(服务端同门)若牵动面过大,先做 3.1 + 3.3(判据统一 + 前端不画裸题),3.2 记进 BLOCKED.md 或下一轮。
  3. 任务 4.1 若旁白由 Agent 自由书写、模板里改不掉,用 4.4 的禁用词契约 + Agent 指令双管,不得只改其中一边。
  4. 绝不让步:卡上的参考题按钮必须删;建不出选项的题不得提出也不得渲染成裸题;不得回退 BUG-685;不得删卡上的「相对可能性 %」三列。

7. 开工前置命令

cd /workspace/Jyotisha
git status -sb | head -1
git fetch origin --prune
git worktree add -b codex/rectification-tiebreak-card-loss-20260915 \
  .worktrees/rectification-tiebreak-card-loss-20260915 origin/staging
cd .worktrees/rectification-tiebreak-card-loss-20260915/frontend
npm ci
npm test 2>&1 | tail -20

开工前必读 docs/BUG_HISTORY.md 的 BUG-685 / 686 / 687 / 688 / 674 / 675 全文——本单三条都落在它们的射程里。

交付:git push origin HEAD:staging,推完核对远端 SHA。


8. 串行与依赖

  • TASK-rectification-open-retitles-session-20260915.mdBUG-699701文件不重叠,可并行:那单动 use-rectification-surface.ts / chat-session-url.ts / 会话列表,本单动 rectification-range-delivery.tsx / turn-question.ts / method-followup.ts / choice-card.ts
  • 但两单都可能改 rectification-agentic-chat.tsx(那单不改,本单删一个 prop)。若同时执行,本单排后。

9. BUG 编号

  • 本单占 BUG-702 / 703 / 704
  • 已占用:BUG-698(设置弹窗单)、BUG-699 / 700 / 701(会话列表单)。
  • 开工时仍需核对 docs/BUG_HISTORY.md 当前最大号。