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

262 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-15staging`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 ? (
<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 / 688BUG-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` 当前最大号。