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
This commit is contained in:
Jesse_Chen
2026-09-15 07:44:40 +00:00
co-authored by Claude Fable 5
parent 4cacea8eb1
commit 3e58e85f8f
3 changed files with 347 additions and 4 deletions
+2 -1
View File
@@ -120,7 +120,8 @@
| `TASK-chat-reading-load-20260915.md` | `PROGRESS-chat-reading-load-20260915.md` | 首页主题卡下线 + 回答两层可见性 + 折叠层宽表可横滑 + 输入框 16px。内容一字不删。⚠️ BUG-630 走查路径失效 | 已验收(2 条契约测试未通过,见修复单) | `8144fca2` |
| `TASK-mobile-touch-and-breakpoints-20260915.md` | `PROGRESS-mobile-touch-and-breakpoints-20260915.md` | 消息操作按钮触屏命中区 27×34、相邻仅隔 1px(BUG-695);CSS 平板上限 900px 与 `sidebarViewportForWidth` 的 1024 不一致,9011023 是混合态(BUG-696);报告域 720/760/860 三个断点互不对齐,761–860 目录已塌、正文还是桌面(BUG-697)。含断点白名单契约测试。**串行在 chat-reading-load 之后** | 已验收 | `6c748d86`BUG-695697 |
| `TASK-chat-reading-load-fix-20260915.md` | `PROGRESS-chat-reading-load-fix-20260915.md` | **验收修复单**`chat-bundle-splitting-contract``message-markdown` 包裹数 1→2、`plainParagraphs(spoken)` 字面量已不存在)与 `chat-navigation-a11y-contract``window.location.assign("/login")` 4→3,建议问题入口删除带走一处)两条既有断言被本轮打红且未更新;Linux 全量 27→29。只改测试不改产品代码 | 待验收 | `codex/chat-reading-load-fix-20260915` |
| `TASK-rectification-open-retitles-session-20260915.md` | `PROGRESS-rectification-open-retitles-session-20260915.md` | **P0 复发单**:点侧栏历史校正会话,`openRectificationCase` 无条件用墙钟重算 `title` 并把 `updatedAt` 盖成此刻(同一处 `pinned`/`archivedAt` 却从 `existing` 继承了),`persistSession` 把改名写回服务端 → 12 号的会话被改成「9月15日 · 生时校正」并顶到列表最前。标题**永久损坏**、排序刷新可恢复。复发自 BUG-553(其防复发只是一句话,无测试执行,且没覆盖校正 open 路径)。含历史标题按 `created_at` 修回(BUG-699 | 待验收 | `codex/rectification-open-retitles-session-20260915` |
| `TASK-rectification-open-retitles-session-20260915.md` | `PROGRESS-rectification-open-retitles-session-20260915.md` | **P0 复发单**:点侧栏历史校正会话,`openRectificationCase` 无条件用墙钟重算 `title` 并把 `updatedAt` 盖成此刻(同一处 `pinned`/`archivedAt` 却从 `existing` 继承了),`persistSession` 把改名写回服务端 → 12 号的会话被改成「9月15日 · 生时校正」并顶到列表最前。标题**永久损坏**、排序刷新可恢复。复发自 BUG-553(其防复发只是一句话,无测试执行,且没覆盖校正 open 路径)。含历史标题按 `created_at` 修回(BUG-699;另两条:校正答题从不推进 `chat_sessions.updated_at`,会话边用边下沉、名字是今天而位置是旧日期(BUG-700);`?c=` 只比对已加载那一页,翻不到就报「该对话不存在或已被删除」并清掉 URL(BUG-701) | 待验收 | `codex/rectification-open-retitles-session-20260915` |
| `TASK-rectification-tiebreak-card-loss-20260915.md` | — | **P0**:点交付卡上「再答两道参考题」,新写的未答题让 `applyLiveCandidateOffer``candidateOffer` 从每条消息剥掉 → 卡整条消失(BUG-702);按钮可用判据(`tieBreakPersonalityAvailable`)与题目可渲染判据(`buildChoiceCard``styleOptions.ok`)不一致 → 题目渲染成没有选项的裸题,两条路都断(BUG-703);交付旁白写「相对支持度都是 16」违反 VOICE 禁用词,且自称不再问分盘题却同时提供分盘题入口(BUG-704)。产品重申:**卡上不要这个按钮,出卡前收集、以卡结算** | 待领取 | `codex/rectification-tiebreak-card-loss-20260915` |
| `TASK-settings-dialog-size-and-nav-20260915.md` | — | **复发单**:设置弹窗四个分区尺寸仍随内容跳变(BUG-698,复发自 BUG-554——旧防复发只查「有没有写 height」,查不到「写了没生效」);首要嫌疑是 `.settings-modal``dvh` 没有 `vh` 回退,不支持时整条 `height` 作废退化成内容高度,需先复现确认。另按产品要求去掉分区菜单左侧强调条,并拆开与悬停共用的选中态 | 待领取 | `codex/settings-dialog-size-and-nav-20260915` |
### 个人报告
@@ -1,4 +1,4 @@
# TASK · P0打开历史校正会话会把它改名成今天、并顶到列表最前(BUG-699,复发自 BUG-553
# TASK · P0校正会话在列表里「名字是今天、位置是旧日期、刷新后报已删除」(BUG-699/700701
- 日期:2026-09-15
- 基线 commit`origin/staging` @ `ff5023a2`= staging 当前部署)
@@ -103,11 +103,57 @@ BUG-553 也是「命名+排序」一起坏,用户这次的描述与当时几
---
### 2.6 追加实证(2026-09-15 第二次回报):另外两条独立缺陷
用户随后回报:「刷新之后也恢复不到这个 session,返回『该对话不存在或已删除』,左边 session 列表也没有。」URL 是 `/?c=6cf7653b-c33d-4f1d-b4c4-bb190eed03ff`,会话标题是「9月15日 · 生时校正 15:19」(`uniquifySessionTitle` 追加了时分,说明当天已有同名标题——即 §2.1 的改名已经发生过不止一次)。
查证后**没有找到任何删除路径**`grep -rn 'method: "DELETE"' frontend/src` 里只有一处作用于会话,是 `use-session-management.ts` 的显式删除动作;服务端 `DELETE /api/sessions/[id]` 也只被它调用。会话大概率仍在库里。真正让它「消失」的是下面两条。
#### BUG-700|校正活动从不推进 `chat_sessions.updated_at`,会话边用边下沉
全仓只有一处写 `chat_sessions.updated_at`
`frontend/supabase/migrations/20260901010000_append_consultation_question.sql`
```sql
updated_at = clock_timestamp()
```
这是**普通咨询**的 RPC。校正的 turn 走 `/api/rectification/cases/[caseId]/turns` 与自己的表,**不碰 `chat_sessions.updated_at`**`grep -rn "chat_sessions" src/lib/rectification-agentic/v9/` 无命中)。
于是一条校正会话的 `updated_at` **冻结在创建时刻**。用户连着几小时在里面答题,它在侧栏的位置纹丝不动。
和 §2.1 的改名叠加,结果是列表**自相矛盾**:
- **显示的名字**:今天(被 open 改的)
- **实际的排序与分组**:创建那天(`updated_at` 从未推进)
「一大堆 15 号的记录但实际上都不是 15 号的」正是这个——名字全变成今天,位置还在各自的旧日期。
BUG-553 的防复发「`updatedAt` 只由对话活动推进」在这里被反向违反:**校正答题是不折不扣的对话活动,却不推进。**
#### BUG-701`?c=` 找不到就报「已被删除」,从不问服务端
`frontend/src/lib/chat-session-url.ts``resolveBootstrapSessionSelection`
```ts
if (query.sessionId && input.listedIds.includes(query.sessionId)) { }
return { sessionId: input.defaultSessionId, urlAction: "replace-clear", missing: true, };
```
`listedIds` 只是**当前已加载那一页**的 id。`missing: true` 会让 `page.tsx` 打出 `SESSION_MISSING_NOTICE``"该对话不存在或已被删除"`),并且把 URL 里的 `?c=` 清掉。
而列表是游标分页的:`frontend/src/lib/session-cursor.ts``SESSION_PAGE_SIZE = 40``GET /api/sessions``updated_at desc, id desc` 取 40 条(置顶另算)。被 BUG-700 压在旧日期上的校正会话,只要前面攒了 40 条更新的,首屏就加载不到它。
**「不在已加载的这一页」被当成了「已被删除」**,还顺手把用户唯一能回去的 URL 抹了。这是本次「找不到 session」最直接的原因,也是三条里最容易修的。
## 3. 决策记录(产品已授权)
1. **打开已有会话不得改标题、不得改 `updatedAt`。** 只有新建才算首次命名。
2. 已经被改坏的历史标题**要修回**,见任务 3。修不回真实日期的,宁可退回中性标题,也不要留一个错的日期。
3. 本单只改校正 open 路径与随之而来的守卫,不重构会话列表。
4. **校正答题必须推进 `updated_at`**(BUG-700)。它和普通咨询一样是对话活动,BUG-553 的规则本来就该覆盖它。
5. **`?c=` 指向的会话不在已加载页时,必须先问服务端再下结论**(BUG-701)。确认服务端也没有,才允许说「已被删除」;在此之前不得清掉 URL 里的 `?c=`
---
@@ -179,6 +225,41 @@ BUG-553 的防复发只写在 Bug 记录里,没有任何东西执行它,所
**验收标准**staging 上跑完,3.1 的核对脚本返回 0 行;随机抽 3 条核对标题日期等于 `created_at` 本地日期。
### 任务 5 · BUG-700:校正活动推进 `updated_at`
**5.1** 校正写入 turn(以及采用、停止这类推进会话状态的动作)时,同步把该会话的 `chat_sessions.updated_at` 推到当前时间。
实现位置由执行方按既有架构定——优先复用 `append_consultation_question` 那种**服务端 RPC 内部顺带更新**的做法,**不要**让客户端 `persistSession` 开始写 `updated_at`(§4 红线 2)。
**5.2** 只有真正的对话活动才推进:写 turn、提交选择、采用、停止。**打开、刷新、读取快照、改名、收藏、归档一律不推进**(BUG-553 的规则,继续有效)。
**5.3** 已经冻结的历史数据:校正会话的 `updated_at` 若明显早于其最后一条 turn 的时间,按**最后一条 turn 的时间**回填。与任务 3 同一次数据修补里做,同样要求幂等、只动匹配行、先在 staging 报影响行数。
**验收标准**
- 在一条旧校正会话里答一道题,侧栏里它移动到「今天」最上面。
- 打开同一条会话但不答题,位置不变。
- 回填后,抽查 3 条校正会话的 `updated_at` 等于其最后一条 turn 的时间。
### 任务 6 · BUG-701`?c=` 找不到时先问服务端
**6.1** `resolveBootstrapSessionSelection``listedIds` 未命中时,**不得直接判定 missing**。改为返回一个「需要向服务端确认」的状态,由 `page.tsx``GET /api/sessions/{id}` 单条查询:
- 服务端有 → 把这条会话并进本地列表并选中它,URL 保持 `?c=` 不变。
- 服务端 404 → 这时才是真的没有,打 `SESSION_MISSING_NOTICE`、清 `?c=`
- 请求失败(网络 / 5xx)→ **既不选中也不宣告删除**,保留 `?c=` 并给可重试的提示。不得把一次网络抖动说成「已被删除」。
**6.2** 文案复核:`SESSION_MISSING_NOTICE` 现在是「该对话不存在或已被删除」。只有在服务端确实 404 时才允许出现这句。
**6.3** 顺带确认侧栏:会话确实存在但在后面的分页里时,侧栏的无限滚动(`app-sidebar.tsx``session-list-sentinel` + `loadMoreSessions`)能把它翻出来。若选中了一条不在已加载页的会话,侧栏应当能正确高亮它——查一下这条是否成立,结论写进 PROGRESS;不成立就单独记一条,**不要在本单顺手改**。
**验收标准**
- 构造一条排在第 40 条之后的会话,用它的 `?c=` 直接打开:能正常进入,URL 不被清,不出现「已被删除」。
- 服务端确实 404 的 id:仍然给出「该对话不存在或已被删除」并清 `?c=`
- 单条查询失败时:不宣告删除,`?c=` 保留。
- 新增测试覆盖上面三条分支。
### 任务 4 · 测试与文档
**4.1** 基线:
@@ -191,7 +272,7 @@ npm test 2>&1 | tail -20
**4.2** 文档:
- `docs/BUG_HISTORY.md` 新增 **BUG-699**,字段齐全。**「复发自」写 `BUG-553`**,并在根因里写明 BUG-553 的防复发为何没拦住——它只是一句话,没有任何测试执行它,且当时只覆盖了元数据 PATCH 与五条客户端路径,漏了校正 open。回到 BUG-553 记录的「防复发」末尾补一句指向 BUG-699 与新测试。
- `docs/BUG_HISTORY.md` 新增 **BUG-699 / BUG-700 / BUG-701**,字段齐全。BUG-700 的「复发自」同样写 `BUG-553`——它的规则说 `updatedAt` 只由对话活动推进,而校正答题就是对话活动却没推进,是同一条规则的另一半没落实。BUG-701 写清「不在已加载页」被当成「已删除」的判定错误。**「复发自」写 `BUG-553`**,并在根因里写明 BUG-553 的防复发为何没拦住——它只是一句话,没有任何测试执行它,且当时只覆盖了元数据 PATCH 与五条客户端路径,漏了校正 open。回到 BUG-553 记录的「防复发」末尾补一句指向 BUG-699 与新测试。
- `frontend/DESIGN.md`:在会话列表/Copy 一节补一句——**会话标题在创建时定名,打开已有会话不得改名;`updatedAt` 只由对话活动推进**,并指向新测试。
- `CHANGELOG.md`:一条日期 + 一句话标题 + 要点(含"历史校正标题已修回")。
- `docs/testing/rectification-open-identity-20260915.md`:真机/浏览器清单——打开 3 条不同日期的历史校正,确认标题、分组、位置都不变;新建一条确认仍按今天命名。
@@ -233,4 +314,4 @@ npm run lint
| --- | --- |
| 生产是否受影响 | 生产停在 `7b620c7a`2026-08-16)。该提交里 `use-rectification-surface` 是否已有这段 `merged` 构造,需执行方 `git show 7b620c7a:frontend/src/hooks/use-rectification-surface.ts` 核对后写进 PROGRESS。若是,任务 3 的修补要不要在生产跑由产品定。 |
| 用户丢失的那条会话 | 服务端 `updated_at` 没坏,刷新后排序会回到真实位置;只是标题写着今天的日期。可以按**位置**(在「最近 7 天」里 12 号那一段)找回。 |
| BUG-698 | 已`TASK-settings-dialog-size-and-nav-20260915.md` 占用本单 **BUG-699**。 |
| BUG 编号 | BUG-698 由 `TASK-settings-dialog-size-and-nav-20260915.md` 占用本单 **BUG-699 / 700 / 701**`TASK-rectification-tiebreak-card-loss-20260915.md`**BUG-702704**。 |
@@ -0,0 +1,261 @@
# 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` 当前最大号。