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

12 KiB
Raw Blame History

TASK · 口述题孤儿裸行 + 同屏两个范围口径 + 引擎代表时间进模型上下文 — 2026-09-14

  • 基线:origin/staging @ d021bca5staging 已部署该 SHA,含 69ffa07d 的 BUG-674/675 修复与 8221c612 的 BUG-673 修复)。
  • 分支:codex/rectification-spoken-orphan-20260914worktree .worktrees/rectification-spoken-orphan-20260914
  • 关联:BUG-675(本单是它的补漏:同一块兜底,只修了选择题)、BUG-673、BUG-671、BUG-669、BUG-662、BUG-676(本单把它从 investigating 收敛)。
  • 串行:改 turn-question.tscase-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 的挂回规则只覆盖选择题:

// 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,按设计就是口述题(targetedCollectYearFollowupmethod-followup.ts:1420),turnQuestionKind 不是 choice → 不挂回 → 落进 persisted_question 兜底块(rectification-agentic-chat.tsx:1869),只画一个 <p>:无头像、位置在时间轴下方、与其他所有问题长得都不一样。

persistNextInterviewAfterChoiceopen.kind === "collect_spoken" 分支(answer-choice.ts:1070)返回 hostNarration = open.promptpersisted: true,而 applyStructuredChoice 只在「accepted / skipThisProbe / skippedNextInterview / 定向卡就绪」四种情况下才把它并进旁白 —— 所以旁白只剩 记下了。,题目既不在消息里也没挂上消息。这是 BUG-673 当初那条链的最后一段,本单收口。

P1 · 同一屏两个范围口径

同屏旁白写「现在还剩 04:5005:06 里 5 个候选」,时间轴写「目前范围 04:4805:07」。两者都不是 bug 计算,但口径不同:

文案 来源 含义
现在还剩 04:5005:06 里 5 个候选 remainingCandidatesLinecollection-question-pool.ts:754+ remainingSplitTimes(同文件 :561,取活跃候选代表分钟的首尾) 5 个活跃候选分钟的最小/最大值
目前范围 04:4805:07 rectificationReadonlyRangeCopyrectification-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 引擎自己的排序:

# scripts/rectification/decision_policy.py:597
representative = candidate_decisions[0] if candidate_decisions else None
# :688  "representative_time": representative["time"] if representative else None

引擎按自身事件拟合排整张网格,不吃前端累积的问答。界面侧 overlayPublicDecisiondecision-from-dossier.ts:1047)在 projection.fromInference 时用推理值覆盖,所以用户界面不会显示 05:14——上一份任务书里「代表时间取错」的说法就此更正,不是取错,是两套代表时间并存未对账。

但有一条真实副作用:

// 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-67869ffa07d 修 BUG-675 时把挂回规则限定成 turnQuestionKind(focus) === "choice"。口述题没有卡可画,但同样需要头像与消息位置;persisted_question 这个纯文本块从此只该服务"确实没有任何消息可挂"的极端情况。
  • 两个范围口径:采集文案用活跃候选分钟首尾,时间轴用 credible_range,两处各写各的。
  • BUG-676:引擎代表时间与前端推理代表时间是两套,界面已覆盖,但 case-receipt-projection 的活跃时间集合把引擎值也算了进去。

3. 决策记录

决策 内容
D1 挂回规则对口述题一视同仁。 attachQuestionsToTurnshanging 不再限定 choice:任何 status === "active"、没有 askedTurnId 的焦点都可挂到最后一条尚无题的助手消息上;选择题画卡,口述题画题干,都带头像、都在消息内。persisted_question 纯文本块只保留给"一条助手消息都没有"的兜底。
D2 同屏只允许一个范围口径。 采集文案(remainingCandidatesLine)改用与时间轴同一个 credible_range,候选个数照旧;不得再出现两个不同的起止分钟。若 credible_range 缺失才退回活跃候选首尾。此项推翻 remainingSplitTimes 作为文案口径的既有做法,但不改 remainingSplitTimes 在排序/去重逻辑里的用途。
D3 引擎代表时间不得进活跃时间集合。 case-receipt-projection 只用前端推理的活跃候选与 inference.representative_timelatestResult.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 errornpm test 失败清单与基线逐条一致(本机基线:8221c612 上 27 条,全是无 Docker / DB / rsync 缺口;工作树缺 .venv 软链会多一条 workflow YAML 假红,补软链即绿);next build 通过且 /○ StaticTurbopack 拒软链 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.tsxliveQuestionOnMessages / questionGap 在口述题挂回后走 idle(题在消息里),不再进 persisted_question
  • 验收标准:
    • 行为单测:一个 kind = collect_spokenaskedTurnId = nullstatus = active 的焦点 + 一条已结算助手消息 → attachQuestionsToTurns 把题挂在该消息上;rectificationQuestionGapState 返回 idle,不是 persisted_question
    • 行为单测:题号 collect:targeted:finance:year、题干「大概哪年几月?」这条真实形状必须覆盖。
    • 行为单测:最后一条助手消息已经带题时,不得覆盖它(保持现有 !byTurn.has(lastAssistant.id) 语义)。

任务 2 · 同屏一个范围口径(P1

  • remainingCandidatesLine 改为接收 credible range;调用点(method-followup.ts:1503 一带、targetedCollectPoolrangeNarrowHint)同步传入。
  • 文案仍是「现在还剩 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,候选数仍是 5credible_range 缺失时才退回 04:5005:06。

任务 3 · 引擎代表时间不进模型上下文(BUG-676 收敛,P1)

  • frontend/src/lib/rectification-agentic/v9/case-receipt-projection.ts:活跃时间集合去掉 dossier.latestResult?.representativeTime
  • 保留 69ffa07d 已加的 warnRepresentativeTimeInconsistencycore/representative-time-guard.ts)。
  • 验收标准:行为单测——latestResult.representativeTime = "05:14"、推理里 05:14 为 eliminated 时,投影后的 receipt 的 house_tables_by_time 不含 05:14selectedTime 指向的时间仍保留。

任务 4 · 记录

  • docs/BUG_HISTORY.md:新增 BUG-678resolved);把 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. 开工前置命令

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 实际最大号为准)。