# 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`: ```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` 里有同一条守卫: ```ts && !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` ```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` ```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"` 的**字面量**: ```ts 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。…… 两处问题: 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`:删除 ```tsx {delivery?.tie_break_available && onTieBreak && !readonly ? ( ) : 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. 让步顺序 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. 开工前置命令 ```bash 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` 当前最大号。