docs(tasks): brief for targeted-collect question stored as spoken focus
定向补事题以采集型 schema 存进会话焦点,前端 persisted_question 兜底把它 裸画成一行,无卡无修复入口;同轮计划的 D9 题因 nextInterviewPersisted 被 误置而静默丢失。BUG-670 的识别条件只认 targeted_collect 字段,救不回存量 焦点。任务书给出写入侧禁降级、复原侧按题号识别、不得吞题、前端兜底四项。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
This commit is contained in:
co-authored by
Claude Fable 5
parent
007a05fe48
commit
9a943cc2dc
@@ -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-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` |
|
||||
|
||||
## 命名与归档
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
# TASK · 定向补事题被存成口述题,裸题顶掉卡片 — 2026-09-13
|
||||
|
||||
- 基线:`origin/staging` @ `007a05fe`(最近一次含门禁路径改动的提交是 `14d199e6`,staging 已部署该 SHA;其后均为纯文档提交)。
|
||||
- 分支:`codex/rectification-targeted-collect-spoken-focus-20260913`,worktree `.worktrees/rectification-targeted-collect-spoken-focus-20260913`。
|
||||
- 关联:BUG-661(定向补事必须逐条点选)、BUG-669(快照投影拿不到 choice_card)、BUG-670(承接焦点分支让模型改写定向题)、BUG-671(选择题无卡时不得停在采集等待态)。本单是 BUG-670 的复发:用户现象完全一致,但触发路径不经过模型,670 的识别条件拦不住。
|
||||
- 串行:本单改 `method-followup.ts`、`server-focus.ts`、`answer-choice.ts`。开工前 `git log --oneline origin/staging -20` 确认没有同文件在途分支;若 09-10 采集重设计单(BUG-646~648)或 09-11 定向补事单(BUG-661~663 后续)正在改同一批文件,本单排在其后。
|
||||
|
||||
## 1. 事故实证(产品负责人 staging 真机,2026-09-13)
|
||||
|
||||
### 现象
|
||||
|
||||
校正采集阶段,用户答完一道带年月的关系题(选 C「明确没有发生」)后,屏幕依次出现:
|
||||
|
||||
1. 正常助手气泡:`已记录,范围收到 04:48–05:07。`
|
||||
2. 浅色状态行:`目前范围 04:48–05:07,再说一件带年月的事就能继续`(时间轴只读范围行,`rectification-surface-state.ts:380` `rectificationReadonlyRangeCopy`,**设计内,非缺陷**)
|
||||
3. 一行没有头像、没有 A/B/C/D 卡片的粗体裸题:`家里添过丁或长辈住过院吗?`
|
||||
|
||||
用户可见后果:这道题看起来和前面所有题"UI 不一样",无法点选;且本轮服务端原本计划的那道 D9 关系题彻底消失。
|
||||
|
||||
### 实证 1 · 那句题干只可能来自定向补事卡
|
||||
|
||||
`frontend/src/lib/rectification-agentic/v9/collection-question-pool.ts:502`:
|
||||
|
||||
```ts
|
||||
const TARGETED_EXISTENCE_PROMPT: Readonly<Record<CollectKind, string>> = {
|
||||
…
|
||||
family: "家里添过丁或长辈住过院吗?",
|
||||
```
|
||||
|
||||
全仓唯一出处,只被 `targetedCollectPool` → `targetedCollectExistenceFollowup` → `buildTargetedCollectExistenceFrame` 使用。按 BUG-661,它必须以四选一卡出现(A 有过这件事 / B 没有发生过 / C 记不太清楚 / D 这条先跳过,`scoring:false`)。
|
||||
|
||||
### 实证 2 · 本轮服务端根本没打算问这道题
|
||||
|
||||
`choice.applied` 响应(产品负责人提供,已脱敏)关键字段:
|
||||
|
||||
| 字段 | 值 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| `nextChoiceReady` | `false` | 服务端没有准备好可点卡 |
|
||||
| `nextInterviewPersisted` | `true` | 服务端自认"下一题已就绪" |
|
||||
| `next_user_action.id` | `ask_method_followup` | |
|
||||
| `next_user_action.user_meaning` | d9_refine 模板("关系盘仍会换升…本题绑定 D9…点选卡只出 A/B/C/D") | 本轮计划的下一题是 **D9 关系题**,不是家庭题 |
|
||||
| `session_outcome` / `precision_stage` | `collect_evidence` / `collect_events` | |
|
||||
|
||||
`nextInterviewPersisted: true` 会让前端**不再追一轮 Agent**:
|
||||
|
||||
```ts
|
||||
// frontend/src/lib/rectification-agentic/v9/choice-action.ts:132
|
||||
export function shouldContinueAfterStructuredChoice(…) {
|
||||
if (receipt?.nextInterviewPersisted) return false;
|
||||
```
|
||||
|
||||
于是本轮既没有新题进消息,也没有新焦点落库,屏幕上那道家庭题是**上一段遗留的活动焦点**被快照渲染出来的。
|
||||
|
||||
### 实证 3 · 裸题渲染是前端的 `persisted_question` 兜底块
|
||||
|
||||
`frontend/src/lib/rectification-surface-state.ts:309` `rectificationQuestionGapState` 的判定顺序里,`questionPersisted` 一旦为真就返回 `persisted_question`;组件按该状态只画题干:
|
||||
|
||||
```tsx
|
||||
// frontend/src/components/rectification-agentic-chat.tsx:1868
|
||||
{questionGap === "persisted_question" && currentQuestion?.prompt && (
|
||||
<div className="rectification-message-wrap rectification-message-entry" data-testid="persisted-question">
|
||||
<div className="rectification-message-question">
|
||||
<p className="rectification-message-question__prompt">{currentQuestion.prompt}</p>
|
||||
```
|
||||
|
||||
无头像、无卡、无修复入口——与截图逐像素对应。BUG-671 的兜底只覆盖 `kind === "choice"` 且无 `choice_card`(`interviewChoiceCardUnavailable`,`rectification-surface-state.ts:350`);这次快照里的 `current_question.kind` 是 `collect_spoken`(`turn-decision.ts:72` `projectCurrentQuestion` 对采集型 schema 的投影),被当成"合法口述题"放行。
|
||||
|
||||
## 2. 根因
|
||||
|
||||
**一道定向补事题以"采集型 schema"存在于会话焦点里,而系统没有任何一条路径能把它复原成卡片。**
|
||||
|
||||
采集型 schema 形如 `{ prompt, collect: true, collect_kind: "targeted:family" }`(`server-focus.ts:321` `collectFocusSchema`),其 `prompt` 取 `spoken_prompt`;定向题在没有"现在还剩 … 个候选"那句时 `spoken_prompt` 就等于存在题题干本身 —— 正好是截图那句。
|
||||
|
||||
### P0-a 写入侧:三条路径会把定向题降级成采集型 schema
|
||||
|
||||
| 位置 | 降级动作 |
|
||||
| --- | --- |
|
||||
| `server-focus.ts:337` `serverOwnedExpectedAnswerSchema` | 有 `choice_frame` 但 `expectedAnswerSchemaFor` 没产出 `.choice` 时,对采集意图直接 `collectFocusSchema(spokenCollectFallbackFollowup(followup))` |
|
||||
| `frontend/src/mastra/rectification-v9-tools.ts:1303` `rectification-set-focus` | 同样的口述兜底,写进焦点 |
|
||||
| `server-focus.ts:645` `persistSkippedCollectFocus` | 先用采集型 schema 写一条 **active** 焦点(会 supersede 真正的活动焦点),再调 `resolveV10ConversationFocus` 置 `skipped`;第二步失败(异常被外层 `catch` 吞掉)就留下一条活动的口述定向焦点 |
|
||||
|
||||
### P0-b 复原侧:BUG-670 的识别条件覆盖不到采集型 schema
|
||||
|
||||
```ts
|
||||
// frontend/src/lib/rectification-agentic/v9/method-followup.ts:2231
|
||||
const targetedCopy = isTargetedCollectChoiceSchema(focus.expectedAnswerSchema) ? … : null;
|
||||
|
||||
// frontend/src/lib/rectification-agentic/v9/choice-card.ts:546
|
||||
export function isTargetedCollectChoiceSchema(value: unknown): boolean {
|
||||
return (value as { targeted_collect?: unknown }).targeted_collect === true
|
||||
&& parseAgentChoiceCopy(value) !== null;
|
||||
}
|
||||
```
|
||||
|
||||
采集型 schema 既没有 `targeted_collect: true`(那是 `expectedAnswerSchemaFor` 只在写点选 schema 时盖的章),也没有可解析的选项 → 重建 `choice_frame` 的分支永不触发。焦点一旦以口述形态落库,本轮、下一轮、刷新之后都还是裸题。
|
||||
|
||||
### P1 卡片落不了库时本轮的题被静默吞掉
|
||||
|
||||
`answer-choice.ts:1154` `persistNextInterviewAfterChoice` 尾部:
|
||||
|
||||
```ts
|
||||
const spoken = (spokenFollowupForUser(followup) ?? "").trim();
|
||||
…
|
||||
return { hostNarration: spoken, choiceReady: false, followup }; // 没有 persisted 字段
|
||||
```
|
||||
|
||||
`followup` 带 `choice_frame` 时 `spokenFollowupForUser` 返回的是卡片题干字符串。焦点落库失败(`zero_information_gain` / `probe_already_answered` / `duplicate_focus` / `invalid_choice_schema`)时就走到这里。`applyStructuredChoice`(`answer-choice.ts:2036` 起)据此:
|
||||
|
||||
- `skippedNextInterview = nextInterview.persisted === false` → `undefined !== false` → **false**
|
||||
- 只有 `accepted / skipThisProbe / skippedNextInterview / 定向卡就绪` 四种情况才会把 `hostNarration` 并进本轮旁白 → 题干**没有**进消息
|
||||
- 但 `if (nextInterview.hostNarration) nextInterviewPersisted = true` → 前端不再追一轮
|
||||
- `keptNextQuestion`(`answer-choice.ts:2109`)为真 → `next_user_action` 仍然引用这道**既没进消息也没进焦点**的题
|
||||
|
||||
即"焦点没写成功"与"题已经问出去了"这两件事共用了一个 `hostNarration` 非空的判断。
|
||||
|
||||
### P2 前端兜底不对称
|
||||
|
||||
BUG-671 只拦 `kind === "choice"` 无卡;题号以 `collect:targeted:` 开头的题以 `collect_spoken` 形态出现时没有任何校验,直接裸画。
|
||||
|
||||
## 3. 决策记录
|
||||
|
||||
| 决策 | 内容 |
|
||||
| --- | --- |
|
||||
| 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"——那会多烧一次模型、多一次揭幕,违反"一次等待一次揭幕"。 |
|
||||
| D4 | `persistSkippedCollectFocus` 不得留下活动焦点。resolve 失败时必须把这条焦点补置 `skipped`(重试一次)或整体回滚,不允许一条 `status=active` 的口述定向焦点活到下一轮。 |
|
||||
| D5 | 不动数据库结构、不动迁移、不动 `deploy/**` 与 workflow。不放宽 BUG-661(定向题必须逐条点选、四选项固定)、BUG-662(不得写范围两端钟点)、BUG-669、BUG-671 的任何红线。 |
|
||||
|
||||
## 4. 硬红线
|
||||
|
||||
1. `./node_modules/.bin/tsc --noEmit` 0 错;`npm run lint` 0 error;`npm test` 失败清单与开工时 `origin/staging` 的无 Docker 基线逐条一致(新增测试只许让通过数上升);`next build` 后 `/` 仍 `○ Static`;首屏 gzip ±2%。
|
||||
2. 测试总数不得低于开工时实测;改任何既有断言写"原值 / 新值 / 原因"三栏。
|
||||
3. 定向存在题的题干与四个选项仍是服务端自有,模型不得改写(BUG-670 防复发条维持)。
|
||||
4. `frontend/src/app/page.tsx` 不动;不新增依赖;不顺手修不在本单里的 warning。
|
||||
5. 不得用"把 `persisted_question` 块画成卡片"来绕过:数据层是采集型 schema 时前端不得假装有卡。
|
||||
|
||||
## 5. 任务分解
|
||||
|
||||
### 任务 1 · 写入侧:定向题不许降级成口述(P0)
|
||||
|
||||
- `frontend/src/lib/rectification-agentic/v9/server-focus.ts`
|
||||
- 新增 `isTargetedCollectFollowupLike(followup)`(或复用 `method-followup.ts` 已有的 `isTargetedCollectFollowup`)判定。
|
||||
- `serverOwnedExpectedAnswerSchema`(l.337):定向题走 `expectedAnswerSchemaFor` 失败时,用 `buildTargetedCollectExistenceFrame({ questionId, methodId, prompt })` 重建 frame 再取一次点选 schema;仍失败则返回 `null`,**不得** `collectFocusSchema(spokenCollectFallbackFollowup(...))`。
|
||||
- `persistServerOwnedFocusCore`(l.513):定向题落到 `!frame` 分支时同样先重建 frame,不得直落 `persistCollectFocus`。
|
||||
- `frontend/src/mastra/rectification-v9-tools.ts:1303`:同一条件下不降级;模型给的 `spokenPrompt` 只能进 `withSpokenPrompt`,不得替换卡片题干。
|
||||
- 验收标准:
|
||||
- 单测:给一个 `collection_key = "collect:targeted:family"` 且 `expectedAnswerSchemaFor` 返回 null 的 followup,`serverOwnedExpectedAnswerSchema` 返回的 schema 含 `.choice` 四选项与 `targeted_collect: true`,或返回 `null`;断言**不会**返回 `{ collect: true }`。
|
||||
- 单测:`rectification-set-focus` 在活动焦点为定向存在题时写入的 `expected_answer_schema` 始终含 `.choice`。
|
||||
|
||||
### 任务 2 · 复原侧:存量口述定向焦点复原成卡(P0)
|
||||
|
||||
- `frontend/src/lib/rectification-agentic/v9/method-followup.ts:2231` 承接分支:`targetedCopy` 取不到时,按 D2 的条件用 `schema.prompt` 重建 frame,`spoken_prompt` 用同一句,`collection_key` 用焦点 `question_id`,`user_prompt_hint` 用既有的 `TARGETED_COLLECT_KEEP_HINT`。
|
||||
- `frontend/src/lib/rectification-agentic/v9/turn-decision.ts:72` `projectCurrentQuestion`:同条件下把该焦点投影成 `kind: "choice"` 并给出四选项,使 GET 快照与 `choice_card` 一致(对齐 BUG-669)。
|
||||
- 验收标准:
|
||||
- 单测(新文件 `frontend/tests/rectification-targeted-spoken-focus-recovery-20260913.test.ts`):活动焦点 `{ questionId: "collect:targeted:family", intent: "collect_method_evidence", expectedAnswerSchema: { collect: true, prompt: "家里添过丁或长辈住过院吗?", collect_kind: "targeted:family" } }` 时
|
||||
- `buildMethodFollowupPlan(...).next_followup.choice_frame` 非空,`choice_frame.question_id === "collect:targeted:family"`,四个选项与 `buildTargetedCollectExistenceFrame` 一致,`scoring === false`;
|
||||
- `projectCurrentQuestion(focus)` 返回 `kind === "choice"` 且选项可解析。
|
||||
- 单测覆盖题号变体:`collect:targeted:family:next`(采集重试后缀)同样能复原。
|
||||
|
||||
### 任务 3 · 卡片落不了库时不得吞题(P1)
|
||||
|
||||
- `frontend/src/lib/rectification-agentic/v9/answer-choice.ts:1154` 尾部 return 加 `persisted: false`。
|
||||
- `applyStructuredChoice`(l.2036 起)确认:`skippedNextInterview` 为真时题干拼进旁白(现有逻辑已覆盖),且 `keptNextQuestion` 为假时 `buildNextUserAction` 拿到的 `nextFollowup` 为 `null`(现有三元已覆盖,补测试锁住)。
|
||||
- `persistSkippedCollectFocus`(`server-focus.ts:645`)按 D4 补 resolve 失败处理。
|
||||
- 验收标准:
|
||||
- 单测:`persistNextInterviewAfterChoice` 在焦点落库返回 `zero_information_gain` / `duplicate_focus` 时,返回值 `persisted === false` 且 `hostNarration` 为卡片题干。
|
||||
- 单测:同一场景下 `applyStructuredChoice` 的 `narration` 同时包含本轮旁白与题干,`next_user_action` 不再引用未落库的题。
|
||||
- 单测:`persistSkippedCollectFocus` 的 resolve 抛错路径不留 `status === "active"` 的焦点。
|
||||
|
||||
### 任务 4 · 前端兜底对齐 BUG-671(P2)
|
||||
|
||||
- `frontend/src/lib/rectification-surface-state.ts:350` `interviewChoiceCardUnavailable` 增加 `questionId` 入参:`kind === "collect_spoken"` 且 `questionId` 以 `collect:targeted:` 开头时同样判为不可用 → 走 `unavailable` + repair-exit(「接着问」),不得进 `persisted_question`。
|
||||
- 调用点 `frontend/src/components/rectification-agentic-chat.tsx`(`deadChoice` 一带)同步传题号。
|
||||
- 验收标准:`frontend/tests/rectification-surface-state.test.ts` 或表面合同测试新增用例:`collect_spoken` + `collect:targeted:*` → `unavailable`;普通 `collect_spoken`(如 `collect:other:collect_method_evidence`)仍为 `persisted_question`,不得误伤。
|
||||
|
||||
### 任务 5 · 记录(必做)
|
||||
|
||||
- `docs/BUG_HISTORY.md` 新增 **BUG-673**(开工时核对当前最大号,现为 672):状态 `resolved`,现象/触发条件/根因/修复/验证/防复发按上文写实;`相关记录` 写 BUG-670、BUG-669、BUG-671、BUG-661;`复发自` 写 BUG-670;修复版本填实际 staging SHA。
|
||||
- 防复发条必须写明:**定向补事题的焦点 schema 只允许点选形态;识别定向题以题号前缀为准,不得只认 `targeted_collect` 字段。**
|
||||
- `CHANGELOG.md` 加 2026-09-13 一行;`frontend/DESIGN.md` 本轮无视觉改动可不动(若动了修复入口样式则同提交更新)。
|
||||
- `docs/tasks/PROGRESS-rectification-targeted-collect-spoken-focus-20260913.md` 写执行记录与失败清单比对。
|
||||
|
||||
## 6. 让步顺序
|
||||
|
||||
1. 任务 2(复原侧)+ 任务 4(前端兜底)**必做**:前者救存量 case,后者保证再出现同类死角时用户有出口。
|
||||
2. 任务 1(写入侧)必做;若 `rectification-v9-tools.ts` 那条路径改动风险大,可先只改 `server-focus.ts` 两处,工具侧在进度记录里写成待办。
|
||||
3. 任务 3 若 `nextInterviewPersisted` 语义牵连过广,最小改动只做"尾部 return 加 `persisted: false`",不重构该标志的语义;重构写进 `BLOCKED.md`。
|
||||
4. 任何一项做不完,写进度记录说明剩什么,不得标"通过"。
|
||||
|
||||
## 7. 开工前置命令
|
||||
|
||||
```bash
|
||||
git fetch origin --prune
|
||||
git worktree add -b codex/rectification-targeted-collect-spoken-focus-20260913 \
|
||||
.worktrees/rectification-targeted-collect-spoken-focus-20260913 origin/staging
|
||||
cd .worktrees/rectification-targeted-collect-spoken-focus-20260913/frontend
|
||||
npm ci
|
||||
grep -n "BUG-6[6-7][0-9]" ../docs/BUG_HISTORY.md | tail -20 # 核对最大号
|
||||
```
|
||||
|
||||
按 `AGENTS.md` §5,动手前用「定向补事」「口述题」「choice_card」「collect_spoken」「targeted_collect」检索 `docs/BUG_HISTORY.md`,读完 BUG-661 / 669 / 670 / 671 四条命中记录,逐条确认其防复发措施是否仍在代码里。
|
||||
|
||||
## 8. 验收(Claude 侧独立再做)
|
||||
|
||||
- `./node_modules/.bin/tsc --noEmit`、`npm run lint`、`npm test`、`npm run build` 四项口径按 §4。
|
||||
- 对照任务 1~4 的验收标准逐条跑测试。
|
||||
- 环境缺口如实写:无 staging 登录态与数据库/日志权限,**"线上那条存量家庭焦点已被复原成卡片"这一条只能由产品负责人在真机复验**,写成 `docs/testing/` 清单条目:进入那个校正会话 → 刷新 → 确认「家里添过丁或长辈住过院吗?」以四选一卡出现(或给出「接着问」修复入口),不再是裸题。
|
||||
|
||||
## 9. BUG 编号起点
|
||||
|
||||
- 起点 **BUG-673**(当前最大号 672,开工时以 `docs/BUG_HISTORY.md` 实际最大号为准)。
|
||||
Reference in New Issue
Block a user