Files
Jyotisha/docs/tasks/TASK-rectification-spoken-orphan-and-engine-representative-20260914.md
T
Jesse_ChenandClaude Fable 5 a5acd535d5 docs(tasks): brief for spoken-question orphans and engine representative time
BUG-675 的挂回规则只覆盖选择题,定向补事的年份追问仍掉进 persisted_question
纯文本块,无头像裸行(BUG-678)。同屏两个范围口径:采集文案用活跃候选首尾、
时间轴用 credible_range。BUG-676 依产品提供的 candidates 收敛:前端推理自洽,
05:14 来自引擎自身排序,界面未显示,但它被 case-receipt-projection 算进活跃
时间,已淘汰分钟的宫位表因此进了模型上下文。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-09-14 03:47:30 +00:00

154 lines
12 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 · 口述题孤儿裸行 + 同屏两个范围口径 + 引擎代表时间进模型上下文 — 2026-09-14
- 基线:`origin/staging` @ `d021bca5`staging 已部署该 SHA,含 `69ffa07d` 的 BUG-674/675 修复与 `8221c612` 的 BUG-673 修复)。
- 分支:`codex/rectification-spoken-orphan-20260914`worktree `.worktrees/rectification-spoken-orphan-20260914`
- 关联:BUG-675(本单是它的补漏:同一块兜底,只修了选择题)、BUG-673、BUG-671、BUG-669、BUG-662、BUG-676(本单把它从 `investigating` 收敛)。
- 串行:改 `turn-question.ts``case-receipt-projection.ts`、文案一处。与 BUG-674/675 实现分支无重叠文件冲突;开工前 `git log --oneline origin/staging -20` 复核。
## 1. 事故实证(产品负责人 staging 真机,2026-09-14,部署 SHA `d021bca5`
### P0 · 口述题仍是没有头像的孤儿裸行(BUG-678)
同一屏(截图)自上而下:
1. 定向补事卡「结过婚或订过婚吗?」四选一 —— 正常(BUG-673 已生效)
2. 选 B 之后:`记下了,这方面先跳过。现在还剩 04:50–05:06 里 5 个候选,能把它们分开的是这几条线:哪年…` + 下一张定向卡「收入明显变过或有过大笔进出吗?」 —— 正常(BUG-674 已生效,不再重复念判别题题干)
3. 选 A「有过这件事」之后:助手气泡只有 `记下了。`,随后时间轴 `目前范围 04:48–05:07,再说一件带年月的事就能继续`**再下面一行没有头像的裸题 `大概哪年几月?`**
根因在 `69ffa07d` 的挂回规则只覆盖选择题:
```ts
// frontend/src/lib/rectification-agentic/v9/turn-question.ts:104 起
for (const focus of focuses) {
if (!focus.askedTurnId) {
if (focus.status === "active" && turnQuestionKind(focus) === "choice") {
hanging.push(focus); // ← 只有选择题会挂回最后一条助手消息
}
continue;
}
byTurn.set(focus.askedTurnId, focus);
}
```
定向补事答 A 之后的年份追问题号是 `collect:targeted:<domain>:year`,按设计就是口述题(`targetedCollectYearFollowup``method-followup.ts:1420`),`turnQuestionKind` 不是 `choice` → 不挂回 → 落进 `persisted_question` 兜底块(`rectification-agentic-chat.tsx:1869`),只画一个 `<p>`:无头像、位置在时间轴下方、与其他所有问题长得都不一样。
`persistNextInterviewAfterChoice``open.kind === "collect_spoken"` 分支(`answer-choice.ts:1070`)返回 `hostNarration = open.prompt``persisted: true`,而 `applyStructuredChoice` 只在「accepted / skipThisProbe / skippedNextInterview / 定向卡就绪」四种情况下才把它并进旁白 —— 所以旁白只剩 `记下了。`,题目既不在消息里也没挂上消息。这是 BUG-673 当初那条链的最后一段,本单收口。
### P1 · 同一屏两个范围口径
同屏旁白写「现在还剩 **04:5005:06** 里 5 个候选」,时间轴写「目前范围 **04:4805:07**」。两者都不是 bug 计算,但口径不同:
| 文案 | 来源 | 含义 |
| --- | --- | --- |
| `现在还剩 04:5005:06 里 5 个候选` | `remainingCandidatesLine``collection-question-pool.ts:754`+ `remainingSplitTimes`(同文件 :561,取活跃候选**代表分钟**的首尾) | 5 个活跃候选分钟的最小/最大值 |
| `目前范围 04:4805:07` | `rectificationReadonlyRangeCopy``rectification-surface-state.ts:380`)读 `credible_range` | 活跃候选**簇范围**的并集 |
同一份 receipt 实证:活跃候选 04:50 / 04:53 / 04:59 / 05:03 / 05:06,其 `cluster_range` 并集是 04:48–05:07。用户在同一屏看到两个不同的"还剩多少",会以为范围忽大忽小。
### P1 · 引擎代表时间把已淘汰分钟塞进模型上下文(BUG-676 收敛)
同一份 receipt(同一轮)实证:
```
inference_state.candidates: 04:53 active probability 0.289 posterior 16 ← 前端推理第一
05:06 active probability 0.225 posterior 15
05:14 eliminated probability 0 posterior 7
inference_state.credible_range: ["04:48","05:07"]
receipt.representative_time: "05:14"
```
前端推理自洽;`05:14` 来自 Python 引擎自己的排序:
```python
# scripts/rectification/decision_policy.py:597
representative = candidate_decisions[0] if candidate_decisions else None
# :688 "representative_time": representative["time"] if representative else None
```
引擎按自身事件拟合排整张网格,不吃前端累积的问答。界面侧 `overlayPublicDecision``decision-from-dossier.ts:1047`)在 `projection.fromInference` 时用推理值覆盖,所以**用户界面不会显示 05:14**——上一份任务书里「代表时间取错」的说法就此更正,不是取错,是两套代表时间并存未对账。
但有一条真实副作用:
```ts
// frontend/src/lib/rectification-agentic/v9/case-receipt-projection.ts:16
for (const value of [inference?.representative_time, dossier.latestResult?.representativeTime, dossier.latestResult?.selectedTime]) {
if (typeof value === "string" && value.trim()) activeTimes.add(value.slice(0, 5));
}
```
`latestResult.representativeTime` 就是引擎写进结果行的 05:14,于是**已被淘汰分钟的 `house_tables_by_time["05:14"]` 仍留在送给模型的 receipt 里**,模型有机会照着一个淘汰候选解读。
## 2. 根因
- BUG-678`69ffa07d` 修 BUG-675 时把挂回规则限定成 `turnQuestionKind(focus) === "choice"`。口述题没有卡可画,但同样需要头像与消息位置;`persisted_question` 这个纯文本块从此只该服务"确实没有任何消息可挂"的极端情况。
- 两个范围口径:采集文案用活跃候选分钟首尾,时间轴用 credible_range,两处各写各的。
- BUG-676:引擎代表时间与前端推理代表时间是两套,界面已覆盖,但 `case-receipt-projection` 的活跃时间集合把引擎值也算了进去。
## 3. 决策记录
| 决策 | 内容 |
| --- | --- |
| D1 | **挂回规则对口述题一视同仁。** `attachQuestionsToTurns``hanging` 不再限定 `choice`:任何 `status === "active"`、没有 `askedTurnId` 的焦点都可挂到最后一条尚无题的助手消息上;选择题画卡,口述题画题干,都带头像、都在消息内。`persisted_question` 纯文本块只保留给"一条助手消息都没有"的兜底。 |
| D2 | **同屏只允许一个范围口径。** 采集文案(`remainingCandidatesLine`)改用与时间轴同一个 `credible_range`,候选个数照旧;不得再出现两个不同的起止分钟。若 `credible_range` 缺失才退回活跃候选首尾。此项推翻 `remainingSplitTimes` 作为文案口径的既有做法,但**不改** `remainingSplitTimes` 在排序/去重逻辑里的用途。 |
| D3 | **引擎代表时间不得进活跃时间集合。** `case-receipt-projection` 只用前端推理的活跃候选与 `inference.representative_time``latestResult.representativeTime`(引擎值)与 `selectedTime` 中,只有 `selectedTime`(用户已采用的时间)可以保留。BUG-676 由此从 `investigating` 收敛为 `resolved`,根因写「引擎与前端两套代表时间并存,未对账」,不改任何排序逻辑。 |
| D4 | 不动数据库结构与迁移、不动 `deploy/**` 与 workflow、不动 `page.tsx`、不新增依赖。BUG-661/662/669/670/671/673/674/675 的红线全部维持。 |
## 4. 硬红线
1. `tsc --noEmit` 0 错;`npm run lint` 0 error`npm test` 失败清单与基线逐条一致(本机基线:`8221c612` 上 27 条,全是无 Docker / DB / rsync 缺口;工作树缺 `.venv` 软链会多一条 workflow YAML 假红,补软链即绿);`next build` 通过且 `/``○ Static`Turbopack 拒软链 `node_modules`,须在真实 `node_modules` 的工作树里跑);首屏 gzip ±2%。
2. 新增断言必须是行为断言,**不得用 `readFileSync` + 正则匹配源码**充当验收。
3. 不得新增第二套问题渲染路径;卡片一律走既有 `RectificationChoiceCard`
4. 不得把引擎的 `representative_time` 显示给用户,也不得反过来用它覆盖前端推理值。
5. 年份追问仍是口述题,不得为了"统一"把它做成点选卡(BUG-673 决策维持)。
## 5. 任务分解
### 任务 1 · 口述题也挂回消息(BUG-678,P0)
- `frontend/src/lib/rectification-agentic/v9/turn-question.ts`:去掉 `turnQuestionKind(focus) === "choice"` 限制;保留「只挂一条」「最后一条助手消息且它还没有题」两个约束。
- 复核 `rectification-agentic-chat.tsx``liveQuestionOnMessages` / `questionGap` 在口述题挂回后走 `idle`(题在消息里),不再进 `persisted_question`
- 验收标准:
- 行为单测:一个 `kind = collect_spoken``askedTurnId = null``status = active` 的焦点 + 一条已结算助手消息 → `attachQuestionsToTurns` 把题挂在该消息上;`rectificationQuestionGapState` 返回 `idle`,不是 `persisted_question`
- 行为单测:题号 `collect:targeted:finance:year`、题干「大概哪年几月?」这条真实形状必须覆盖。
- 行为单测:最后一条助手消息**已经**带题时,不得覆盖它(保持现有 `!byTurn.has(lastAssistant.id)` 语义)。
### 任务 2 · 同屏一个范围口径(P1)
- `remainingCandidatesLine` 改为接收 credible range;调用点(`method-followup.ts:1503` 一带、`targetedCollectPool``rangeNarrowHint`)同步传入。
- 文案仍是「现在还剩 X–Y 里 N 个候选,能把它们分开的是这几条线:…」,其中 X–Y 与时间轴 `目前范围` 完全一致。
- 验收标准:单测——给定活跃候选 04:50/04:53/04:59/05:03/05:06、`credible_range = ["04:48","05:07"]`,文案里的起止必须是 04:48–05:07,候选数仍是 5`credible_range` 缺失时才退回 04:5005:06。
### 任务 3 · 引擎代表时间不进模型上下文(BUG-676 收敛,P1)
- `frontend/src/lib/rectification-agentic/v9/case-receipt-projection.ts`:活跃时间集合去掉 `dossier.latestResult?.representativeTime`
- 保留 `69ffa07d` 已加的 `warnRepresentativeTimeInconsistency``core/representative-time-guard.ts`)。
- 验收标准:行为单测——`latestResult.representativeTime = "05:14"`、推理里 05:14 为 `eliminated` 时,投影后的 receipt 的 `house_tables_by_time` **不含** `05:14``selectedTime` 指向的时间仍保留。
### 任务 4 · 记录
- `docs/BUG_HISTORY.md`:新增 BUG-678`resolved`);把 BUG-676 从 `investigating` 更新为 `resolved`,根因改写为「引擎与前端两套代表时间并存未对账,引擎值被算进活跃时间集合」,并写明**界面从未显示过 05:14**(原记录里若有"代表时间取错"的措辞,按事实更正,不得留下未经证实的结论)。
- `CHANGELOG.md` 加一行;`PROGRESS-rectification-spoken-orphan-20260914.md`;真机清单进 `docs/testing/`(要覆盖:定向补事答 A 之后的年份追问带头像出现在消息里、同屏两处范围数字一致)。
## 6. 让步顺序
1. 任务 1 必做(用户可见的"UI 不一样"就剩这一处)。
2. 任务 3 必做(模型上下文正确性)。
3. 任务 2 若牵动的调用点过多,可只改定向补事那一条文案路径,其余写进进度记录;但不得留下同屏两个数字。
## 7. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-spoken-orphan-20260914 \
.worktrees/rectification-spoken-orphan-20260914 origin/staging
cd .worktrees/rectification-spoken-orphan-20260914
ln -s /workspace/Jyotisha/.venv .venv # 否则 workflow YAML 合同测试因缺 PyYAML 假红
cd frontend && npm ci
```
`AGENTS.md` §5,开工前用「裸题」「persisted_question」「askedTurnId」「代表时间」「representative_time」检索 `docs/BUG_HISTORY.md`,至少读完 BUG-675、BUG-674、BUG-673、BUG-671、BUG-676 五条。
## 8. BUG 编号起点
- 起点 **BUG-678**(当前最大号 677,开工时以 `docs/BUG_HISTORY.md` 实际最大号为准)。