docs(tasks): brief for unstampable probes, naked cards and stale representative time

探针池空后判别题盖不上 probe,焦点写不进库,上一单 D3 把题干念进正文导致
同一道题反复问且回答不算数(BUG-674,本单 D1 推翻该决策,改走定向补事卡)。
快照已有 choice_card 时 persisted_question 仍只画一行裸题(BUG-675)。同一轮
receipt 的 representative_time 落在 eliminated_ids 内,先取证加守卫(BUG-676)。

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-13 18:34:23 +00:00
co-authored by Claude Fable 5
parent 8221c6121b
commit 9375012f17
4 changed files with 164 additions and 1 deletions
+2
View File
@@ -184,6 +184,8 @@
| `TASK-rectification-targeted-card-dead-20260913.md` | `PROGRESS-rectification-targeted-card-dead-20260913.md` | **P0**:定向补事卡在快照投影里拿不到 `choice_card`(承接焦点分支不重建 `choice_frame`),卡片看得见点不动、流程停在采集等待态;模型还会把定向题改写成口述题(BUG-669~671)。先于 tie-break 修复单执行 | 待验收 | `codex/rectification-targeted-card-dead-20260913` |
| `TASK-rectification-unstampable-probe-and-naked-card-20260914.md` | `PROGRESS-rectification-unstampable-probe-20260914.md` | **P0**:探针池空后判别题盖不上 probe → 焦点写不进 → 题干被念进正文、同一题反复问且答了不算数(BUG-674,推翻前一单 D3);快照已有 `choice_card` 但 `persisted_question` 仍只画一行裸题(BUG-675);同一轮 receipt 里 `representative_time` 取到 `eliminated_ids` 里的候选(BUG-676investigating | 待执行 | `codex/rectification-unstampable-probe-20260914` |
| `TASK-rectification-targeted-collect-spoken-focus-20260913.md` | `PROGRESS-rectification-targeted-collect-spoken-focus-20260913.md` | **P0**:定向补事题以采集型 schema 存进焦点,前端只能裸画一行题干(无头像无卡无修复入口),本轮计划的 D9 题被静默吞掉;BUG-670 的识别条件只认 `targeted_collect` 章,复原不了(BUG-673,复发自 BUG-670 | 待验收 | `codex/rectification-targeted-collect-spoken-focus-20260913` |
| `TASK-rectification-house-lord-gochara-research-20260913.md` | `PROGRESS-rectification-house-lord-gochara-research-20260913.md` | 研究单:宫主触发与木星/土星过运(合冲本命宫主、罗睺紧密合、年精度、用于 block 选上升)四种放宽,20 例公开 AA 离线量 block 层与 minute 层两组指标;引擎里已有宫主/功能吉凶/受控过运,只量缺的四条 | 待验收(无收益,关闭;不立实现单) | `codex/rectification-house-lord-gochara-research-20260913` |
@@ -123,7 +123,7 @@ BUG-671 只拦 `kind === "choice"` 无卡;题号以 `collect:targeted:` 开头
| --- | --- |
| D1 | **定向补事题只有点选一种形态。** 写入侧禁止降级:`collection_key` / `question_id``collect:targeted:` 开头(或 `kind_hint``targeted:` 开头)时,任何口述兜底都必须先用 `buildTargetedCollectExistenceFrame` 重建 frame 再写点选 schema;重建不出来就**不写焦点**(返回 skipped),不得写采集型 schema。此处推翻"采集意图一律可降级为口述"的既有兜底惯例,执行方不得以"兜底总比没有好"为由保留。 |
| D2 | **复原侧按题号识别,不再只认 `targeted_collect` 章。** `method-followup.ts:2231` 的承接分支增加一条:焦点 `intent === "collect_method_evidence"` 且(`question_id``collect:targeted:` 开头 或 `collect_kind``targeted:` 开头)且 schema 是采集型(`isCollectFocusSchema`)时,用 `schema.prompt` 当题干重建 frame。这条是**存量 case 的唯一救法**(线上已经存在这样的焦点行,只修写入侧救不回来)。 |
| D3 | **题没落库就必须进旁白。** `persistNextInterviewAfterChoice` 尾部改为显式 `persisted: false`,让 `applyStructuredChoice` 把题干拼进本轮旁白。不改成"让前端再追一轮 Agent"——那会多烧一次模型、多一次揭幕,违反"一次等待一次揭幕"。 |
| D3 | **(已被 `TASK-rectification-unstampable-probe-and-naked-card-20260914.md` 的 D1 推翻:自 2026-09-14 起只适用于采集题,判别题落不了库一律转定向补事卡)** **题没落库就必须进旁白。** `persistNextInterviewAfterChoice` 尾部改为显式 `persisted: false`,让 `applyStructuredChoice` 把题干拼进本轮旁白。不改成"让前端再追一轮 Agent"——那会多烧一次模型、多一次揭幕,违反"一次等待一次揭幕"。 |
| D4 | `persistSkippedCollectFocus` 不得留下活动焦点。resolve 失败时必须把这条焦点补置 `skipped`(重试一次)或整体回滚,不允许一条 `status=active` 的口述定向焦点活到下一轮。 |
| D5 | 不动数据库结构、不动迁移、不动 `deploy/**` 与 workflow。不放宽 BUG-661(定向题必须逐条点选、四选项固定)、BUG-662(不得写范围两端钟点)、BUG-669、BUG-671 的任何红线。 |
@@ -0,0 +1,160 @@
# TASK · 探针耗尽后题干进正文反复问 + 有卡仍画裸题 + 代表时间取到被淘汰候选 — 2026-09-14
- 基线:`origin/staging` @ `8221c612`staging 已部署该 SHA,含 BUG-673 修复)。
- 分支:`codex/rectification-unstampable-probe-20260914`worktree `.worktrees/rectification-unstampable-probe-20260914`
- 关联:BUG-673(本单是它修复后暴露出的下一层)、BUG-670、BUG-669、BUG-671、BUG-661、BUG-559。
- 串行:改 `answer-choice.ts``rectification-agentic-chat.tsx``candidate-separation.ts` / `build-state.ts` 一线。开工前 `git log --oneline origin/staging -20` 确认无同文件在途分支。
## 1. 事故实证(产品负责人 staging 真机,2026-09-14,部署 SHA `8221c612`
### P0-1 · 同一道判别题反复问,回答不算数(BUG-674)
真机记录(产品负责人转述,已脱敏):
```
已记录,范围收到 04:4805:07。
2023 年 5 月前后,有没有开始一段认真关系、分手或结婚? ← 正文,无 A/B/C/D
(用户打字回答「没有」)
记下了,这方面先跳过。
2023 年 5 月前后,有没有开始一段认真关系、分手或结婚? ← 同一句又出现一次,仍无卡
```
同一次会话的 decision receipt(同一轮)关键字段:
| 字段 | 值 |
| --- | --- |
| `prospective_probes` | `[]` |
| `precision_stage.current` | `d9_refine` |
| `oos_blind_prompts` | relationship / family / finance 三条都标「校时还没用过」 |
| `last_inference_round.probe_id` | `probe:relationship.2024.04.dasha_boundary:…`(已答) |
链路:探针池空 → `stampChoiceSchemaWithProbe``inference-adapter.ts:517`)既无 `preferredKey`(精度阶段 followup 不带 `semantic_key`)又无可选探针,返回未盖章的 schema → `expectedAnswerSchemaFor` 末行
```ts
return state && stamped.scoring !== false && !schemaProbeId(stamped) ? null : stamped;
```
返回 `null` → 焦点状态 `invalid_choice_schema``open` 为空 → `persistNextInterviewAfterChoice` 尾部(`answer-choice.ts:1154`)把 `spokenFollowupForUser(followup)` —— 对带 `choice_frame` 的判别题就是卡片题干 —— 当正文返回。
`8221c612` 给这个 return 加了 `persisted: false`(上一单 T3),于是题干被拼进本轮旁白。**但焦点仍然不存在**:用户打字回答不会解析任何焦点,计划层下一轮照旧生成同一道 d9_refine 关系题,题干再被念一遍。用户看到的是「同一道题反复问、答了也不算数」。
### P0-2 · 快照里已经有卡,界面仍画裸题(BUG-675)
同一屏底部:`目前范围 04:48–05:07,再说一件带年月的事就能继续` 下面一行裸题 `结过婚或订过婚吗?`(前一次是 `家里添过丁或长辈住过院吗?`),无头像、无选项、**也没有「接着问」修复入口**。
没有修复入口 ⇒ `interviewChoiceCardUnavailable` 判定为 false ⇒ BUG-673 的复原已经生效、`current_question.kind === "choice"``choice_card` 存在。问题在渲染:
```ts
// frontend/src/components/rectification-agentic-chat.tsx:1579
questionPersisted: Boolean(currentQuestion?.prompt && questionSource === "focus") && !deadChoice,
// :1868 起
{questionGap === "persisted_question" && currentQuestion?.prompt && (
<p className="rectification-message-question__prompt">{currentQuestion.prompt}</p>
```
`persisted_question` 块只画 `prompt` 一行,**不看 `choiceCard`**;而卡片只在消息内渲染(要求 `latestSettledAssistant.question.focus_id === currentQuestion.focus_id`)。焦点没有 `asked_turn_id`、没挂到任何消息上时,卡片没有渲染位置 → 数据修好了,界面照旧裸题。这是 BUG-669/671 那条线最后一个死角。
### P1 · 代表时间取到被淘汰候选(BUG-676investigating
同一份 receipt、同一轮:
```
last_inference_round.winner_id = "04:53"
last_inference_round.scores_after = { "04:53": 16, "05:06": 15, "04:50": 14, "05:03": 14,
"04:59": 13, "05:15": 9, "05:08": 7, "05:14": 7, "04:46": 5 }
last_inference_round.eliminated_ids = ["05:14", "05:08", "04:46", "05:15"]
representative_time = "05:14" ← 7 分,且在 eliminated_ids 里
```
界面同屏显示的范围是 `04:4805:07`,也不含 05:14。
两条取值路径:`representative_time``evaluateCandidateSeparation``candidate-separation.ts:81`)取 `ranked[0].time`(按 `score` 降序);`evaluateConvergence``convergence-evaluator.ts:34`)则用 `rankActive`,先滤掉 `status === "eliminated"`、再按 `probability` / `posterior_score` 排序。一个被本轮淘汰、分数倒数的候选成为代表时间,只可能是下面之一:
- 本轮 `eliminated_ids` 没有写回 `candidates[].status`
- `candidates[].probability` / `posterior_score` 没跟着 `scores_after` 重算(停留在旧轮);
- 送进 `evaluateCandidateSeparation` 的候选行分数与 `scores_after` 不是同一份。
**证据缺口**:需要同一份 receipt 的 `inference_state.candidates`(每条的 `id` / `time` / `status` / `probability` / `posterior_score`)与 `inference_state.credible_range`。没有这段之前,本条只能立 `investigating`,不得臆断根因、不得直接改排序。
## 2. 根因
- BUG-674:焦点写不进库这件事,被上一单的 D3 当成「题目还在,只是没进消息」处理。实际上探针耗尽时这道题**根本不该再问**,正确出口是既有的 `persistExhaustionCollect` → 定向补事卡。把题干念出来既不可点、也不可答、还会每轮重复。
- BUG-675`persisted_question` 是纯文本兜底,从设计起就只服务口述题;BUG-673 把定向题的数据形态修成点选后,这个块成了新的裸题来源。
- BUG-676:待定(见证据缺口)。
## 3. 决策记录
| 决策 | 内容 |
| --- | --- |
| D1 | **推翻 `TASK-rectification-targeted-collect-spoken-focus-20260913.md` 的 D3。** 原文「题没落库就必须把题干拼进本轮旁白」自本单起只适用于 `intent === "collect_method_evidence"` 的采集题。`intent === "distinguish_candidates"`(含精度阶段 d9/d10/d4 等)落不了库时**一律不得把题干写进正文**,改走 `persistExhaustionCollect`。执行方不得以「上一份任务书这么写」为由保留旧行为。 |
| D2 | 探针耗尽是一个**可见的状态流转**,不是静默降级:转出定向补事卡时按现有文案说清「现在还剩 … 个候选,能把它们分开的是这几条线」,不再让用户对着一句念出来的判别题打字。 |
| D3 | 正文里同一句题干**不得连续两轮出现**。即便将来某条路径仍要念题干,也要带去重守卫。 |
| D4 | `persisted_question` 只负责口述题。`current_question.kind === "choice"` 且有 `choice_card` 时必须渲染可点卡;有卡却画成纯文本视为缺陷。实现优先选「把该题挂到最后一条助手消息上再走既有卡片渲染」,避免第二套卡片渲染路径(§6 红线)。 |
| D5 | BUG-676 本轮**只做取证与守卫**,不动 `evaluateCandidateSeparation` / `rankActive` 的排序逻辑:补一条不变量断言 + 一条结构化日志,把「代表时间落在被淘汰候选或落在 `credible_range` 之外」变成可观测事件。根因确认后另立修复单。 |
| D6 | 不动数据库结构与迁移、不动 `deploy/**` 与 workflow、不动 `page.tsx`、不新增依赖。BUG-661/662/669/670/671/673 的红线全部维持。 |
## 4. 硬红线
1. `./node_modules/.bin/tsc --noEmit` 0 错;`npm run lint` 0 error`npm test` 失败清单与基线逐条一致(本机基线:`8221c612` 上 27 条,全部是无 Docker / DB / rsync 的既有缺口;若工作树缺 `.venv` 软链会多出 `changed staging workflows are syntactically valid YAML` 一条,补软链后应转绿)。
2. `next build``/``○ Static`;首屏 gzip ±2%。注意 Turbopack 拒绝软链 `node_modules`,必须在有真实 `node_modules` 的工作树里跑。
3. 不得新增第二套选择题渲染路径(§6 既有红线:一律 `ChatComposer` / `useConversationScrollAnchor` / 单一卡片渲染)。
4. 不得用「把裸题藏起来」来收口:藏了以后必须有卡、有修复入口,或有一句诚实的「为什么现在没有下一题」。
5. 新增断言必须是行为断言。**不得再用 `readFileSync` + 正则匹配源码**来充当验收证据(上一单 T3 的做法,本单不接受)。
## 5. 任务分解
### 任务 1 · 判别题落不了库不得念题干(BUG-674,P0)
- `frontend/src/lib/rectification-agentic/v9/answer-choice.ts` `persistNextInterviewAfterChoice` 尾部(l.1154 起):
- `followup.intent === "distinguish_candidates"`(或 `followup.choice_frame` 存在但 `open` 为空)→ 直接 `return persistExhaustionCollect({...})`,不再返回 `hostNarration: spoken`
-`intent === "collect_method_evidence"` 保留「题干进正文」的旧路径。
- 同文件补去重守卫:本轮要写进正文的题干若与上一条助手消息正文里的题干相同,则丢弃(改为走耗尽分支或不写)。
- 验收标准:
- 行为单测:构造 `expectedAnswerSchemaFor` 返回 `null`(探针池空)的 d9 精度阶段 followup,断言 `persistNextInterviewAfterChoice` 的返回**不含**该题干,且 `choiceReady === true`(定向补事卡)或 `followup` 为定向补事项。
- 行为单测:连续两轮同一 followup,第二轮正文不得再出现同一句题干。
- 行为单测:`applyStructuredChoice` 在该场景下的 `narration` 不含判别题题干,`next_user_action` 不引用未落库的题。
### 任务 2 · 有卡不得画裸题(BUG-675,P0)
- 优先方案:焦点有 `choice_card` 但没有 `asked_turn_id` 时,把该题挂到最后一条已结算助手消息上(复用 `mergeTurnQuestions` / `attachQuestionsToTurns` 的既有通路),让卡片走既有渲染。
- 兜底方案(仅当上面做不稳):`persisted_question` 分支在 `currentQuestion.kind === "choice" && choiceCard` 时渲染既有卡片组件,不得只画 `<p>`
- 验收标准:
- 行为单测:`current_question.kind === "choice"``choice_card` 非空、无消息携带该题 → 界面状态不得是「只有题干」;断言卡片可点(选项 4 个、`focus_id` 匹配)。
- `rectification-surface-state.test.ts` 补:有卡的持久化选择题不得落在 `persisted_question` 纯文本分支。
### 任务 3 · 代表时间一致性取证与守卫(BUG-676P1investigating
- 在组装 decision receipt 的位置加不变量检查:`representative_time` 必须满足「不在本轮 `eliminated_ids` 内」且「落在 `credible_range` 之内」。违反时 `console.warn(JSON.stringify({ event: "rectification_representative_time_inconsistent", … }))`,带 `winner_id``representative_time``credible_range``eliminated_ids``scores_after` 的极值(**不得带出生资料、caseId 之外的身份信息**)。
- 不改排序逻辑。
-`docs/BUG_HISTORY.md``investigating` 立 BUG-676,写明已确认事实与证据缺口(需要 `inference_state.candidates``credible_range`)。
- 验收标准:单测覆盖「代表时间在 eliminated 列表里」与「代表时间在 credible_range 外」两种输入各触发一次告警;正常输入不告警。
### 任务 4 · 记录
- `docs/BUG_HISTORY.md`BUG-674、BUG-675 写 `resolved`(含验证与防复发),BUG-676 写 `investigating`。BUG-674 的防复发必须写明:**判别题落不了库时不得把题干写进正文;探针耗尽必须转定向补事卡。**
- `CHANGELOG.md` 加 2026-09-14 一行。
- `docs/tasks/PROGRESS-rectification-unstampable-probe-20260914.md`;真机清单进 `docs/testing/`
- 上一单 `TASK-rectification-targeted-collect-spoken-focus-20260913.md` 的 D3 处补一行「已被 TASK-rectification-unstampable-probe-and-naked-card-20260914 的 D1 推翻」。
## 6. 让步顺序
1. 任务 1、任务 2 必做,二者缺一用户都还会看到「问题反复出现 / 点不动」。
2. 任务 2 的优先方案(挂到消息上)若在时限内做不稳,退兜底方案(`persisted_question` 里渲染卡片),但不得新增第二套卡片组件。
3. 任务 3 可只做告警与 `investigating` 记录;不得为了「让数字看起来对」而顺手改排序。
## 7. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-unstampable-probe-20260914 \
.worktrees/rectification-unstampable-probe-20260914 origin/staging
cd .worktrees/rectification-unstampable-probe-20260914
ln -s /workspace/Jyotisha/.venv .venv # 否则 workflow YAML 合同测试因缺 PyYAML 假红
cd frontend && npm ci
```
`AGENTS.md` §5,开工前用「探针」「probe」「invalid_choice_schema」「代表时间」「representative_time」「裸题」检索 `docs/BUG_HISTORY.md`,至少读完 BUG-673、BUG-671、BUG-669、BUG-559 四条。
## 8. BUG 编号起点
- 起点 **BUG-674**(当前最大号 673,开工时以 `docs/BUG_HISTORY.md` 实际最大号为准)。