两组缺陷,分两单。 会话列表单追加 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
262 lines
16 KiB
Markdown
262 lines
16 KiB
Markdown
# 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 ? (
|
||
<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. 让步顺序
|
||
|
||
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` 当前最大号。
|