两组缺陷,分两单。 会话列表单追加 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
16 KiB
TASK · P0:点卡上「再答两道参考题」后交付卡消失、题目变裸题、无路可退(BUG-702~704)
- 日期:2026-09-15
- 基线 commit:
origin/staging@224244c0 - 执行分支:
codex/rectification-tiebreak-card-loss-20260915 - 工作树:
.worktrees/rectification-tiebreak-card-loss-20260915 - 严重度:P0。用户已经走到交付,一次点击把交付卡弄没了,且没有任何恢复入口。
1. 用户现象(原话 + 截图)
上一个问题已经出卡了,然后我点击了再问两个问题,然后选择时间的卡片就删了。而且下面这个问题是不是应该是选择题,而不是让用户说的。再说咱们之前不是定了卡里不要写「再问两个问题」的按钮,直接在跳卡前收集,以卡结算就行了?
截图(2026-09-15,staging,deepseek-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.tsx → showSelectionCards 里有同一条守卫:
&& !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.ts 里 varga_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:53,05:00作为次选,两者相对支持度都是16。……
两处问题:
-
「相对支持度」是
frontend/docs/VOICE.md明令禁止的词:不得对用户写「概率 / 置信度 / 确定 / 相对支持度」。专业数字只留在看盘板。
⚠️ 注意区分:卡上三列写的「相对可能性 26%」是 09-08 定稿的交付卡设计(
rectification-candidate-compare那轮),那个是对的,不要动。要去掉的是旁白正文里的「相对支持度都是 16」。 -
旁白说「没有年份的分盘题也不再问」,而同一屏的卡上就摆着「再答两道参考题微调排序」——那个按钮点开的正是没有年份的分盘题。正文和控件在同一次渲染里互相打脸。
2.4 与既有记录的关系
- BUG-686(
平局直接出卡,参考题留成卡下可选按钮,resolved)产品拍板的是「出卡前只要参考题可用且没用过就先连问最多两道」。实现做了前置,但同时保留了卡上的按钮当第二条路,防复发只写到「用过之后不再显示入口按钮」。本单要求按产品原意把这条路彻底去掉。 - BUG-685 的修复(把新 turn 合并进对话区)是对的,本单不得回退。
- BUG-688 的教训(「延迟交付」的守卫必须带出口)同样适用于本单:任何抑制交付卡的守卫都必须有出口。
3. 决策记录(产品已授权)
产品 2026-09-15 重申并确认:
- 交付卡上不要「再答两道参考题」这个按钮。 参考题在出卡之前收集完,出卡即结算。BUG-686 已经实现了前置收集,本单把卡上这条冗余的第二路径删掉。按用户既有口径:多余入口宁可删除也不修。
- 按钮删掉之后,BUG-702 的「点一下丢卡」随之消失。但 §2.1 的守卫本身仍要带出口(见任务 2),不能只靠"没有入口就触发不到"。
- 分盘风格题必须是选择题。 建不出选项就不要问,不得退化成裸题。
- 旁白正文不得出现「相对支持度」等内部计分词;交付卡上的「相对可能性 %」保持不变。
4. 硬红线
- 不得回退 BUG-685 的修复(新写的 turn 必须合并进对话区)。
- 不得把性格 / 风格题改成淘汰候选(BUG-686 防复发,继续有效)。仍然只 ±1、不淘汰。
- 不得为了凑满两道而放宽「该分盘至少两种上升星座」(BUG-686 防复发,继续有效)。
- 不得删掉交付卡上的「相对可能性 %」三列——那是 09-08 定稿,本单不动。
- 任何抑制交付卡的守卫都必须有出口(BUG-688 的教训)。
tsc --noEmit0 错;npm run lint0 error;测试总数不降;改既有断言写三栏。- 全量
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 requestTieBreak 与 POST /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 / showSelectionCards 的 liveInterview 抑制,改为只抑制采用动作,不抹掉整张卡。具体形态执行方定,两条要求:
- 卡上的范围、三列候选、边界句仍然可见(用户随时知道自己在哪)。
- 「更像这个」在有活题时可以置灰并给一句说明(例如「先答完上面这道,再选时间」),不得整卡消失。
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.3 在 frontend/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 / 688;BUG-703 的「复发自」写 BUG-686(它的防复发「分盘上升星座少于两种时该题仍不出现」没被执行)并关联 BUG-674 / 675 的裸题家族。回到 BUG-686 记录的「防复发」末尾补一句:卡上入口已于本单删除,前置收集是唯一路径。frontend/DESIGN.md:交付卡一节写明卡上没有参考题入口,参考题在出卡前收集,出卡即结算;并写明有活题时卡的表现(按任务 2 的最终形态)。frontend/docs/VOICE.md:任务 4.3。CHANGELOG.md、docs/tasks/PROGRESS-rectification-tiebreak-card-loss-20260915.md。docs/testing/rectification-tiebreak-card-loss-20260915.md:真机清单——走到交付卡,确认卡上没有参考题按钮;确认出卡前确实先问了两道带选项的参考题;确认旁白里没有内部计分词。
6. 让步顺序
- 任务 2 若 2.1(保留卡、只置灰采用)在现有状态机里改不干净,退到 2.2(整卡隐藏但保证出口),并把理由写进 PROGRESS。不得两条都不做。
- 任务 3.2(服务端同门)若牵动面过大,先做 3.1 + 3.3(判据统一 + 前端不画裸题),3.2 记进
BLOCKED.md或下一轮。 - 任务 4.1 若旁白由 Agent 自由书写、模板里改不掉,用 4.4 的禁用词契约 + Agent 指令双管,不得只改其中一边。
- 绝不让步:卡上的参考题按钮必须删;建不出选项的题不得提出也不得渲染成裸题;不得回退 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.md(BUG-699~701)文件不重叠,可并行:那单动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当前最大号。