Files
Jyotisha/docs/tasks/TASK-rectification-other-collect-fallback-20260908.md
T

102 lines
11 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-08)
- 基线:`origin/staging` @ `3973b6c3`(代码头 `a00b069d`staging 已部署 `a00b069d`;事故转录来自 2026-09-07 晚间实测,跑在 `84a29754` 还是 `a00b069d` 上待用户确认,下文两版都对过)
- 分支:`codex/rectification-other-collect-fallback-20260908`,基于 `origin/staging`
- 执行方:coding agent;验收:Claude
- 涉及文件:`frontend/src/lib/rectification-agentic/v9/server-focus.ts``persistableFocusDomain``stableFollowupQuestionId`)、`v9/method-followup.ts`active-focus 承接 followup、`collectQuestionDomain``otherCollectFollowup` 死代码)、`frontend/src/mastra/rectification-v9-tools.ts`set-focus 的 `invalid_spoken_prompt` 兜底 L10851100)、`frontend/src/lib/rectification-agentic/user-copy.ts``USER_COLLECT_QUESTION.other` / `_RETRY.other`
- BUG 编号起点:**BUG-586**584/585 已由重复旁白修复单占用;开工时 `grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1` 复核)
- 优先级:**P1**(采集走完后无出口,第四次实测复现;BUG-558 同现象、不同来路)
## 1. 事故实证(只写结构)
七个带年月领域全部问过或拒过(学业、感情、事业、家人拒答、财务、迁居、健康),中间答了 5 张点选卡,范围从 30 分钟收到 8 分钟。最后一轮:
1. 助手气泡两段旁白(BUG-584);
2. 「本轮对照了 …」方法句;
3. 问题块:**「也可以再说一件你记得大概时间的事。」**——这是 `USER_COLLECT_QUESTION.other` 逐字;
4. 没有区间交付卡、没有职业题「你平时主要做什么工作?」、没有停止按钮以外的出口。receipt 显示该轮 6 步(此前每轮 3 步)。
按当前设计,这一步应当是:职业口述题 → (若有未问领域则 holdout)→ `adopt_representative` 自动出区间交付卡。职业题一次都没出现,而「other」这句取代了它。
## 2. 已由代码确认的事实
### 2.1 这句固定文案在 `a00b069d` 仍有三条活路
`spokenFollowupForUser``domain="other"` 的采集 followup:只有「`source=method_coverage``method_id=dasha_events` 且非 retry 且账本无带年月事件」才给开场句 GENERIC;其余一律查表落到 `USER_COLLECT_QUESTION.other`。BUG-558 只堵了 `exhaustionSpokenCollectFollowup` 这一条(`otherCollectFollowup` 现已无人调用,是死代码),但 `domain="other"` 还能从下面三处产生:
| # | 来路 | 位置 |
| --- | --- | --- |
| A | **职业焦点落库时领域被压成 other**DB 约束 `target_domain in (education, career, relationship, relocation, finance, health, family, other)` 没有 `occupation``persistableFocusDomain("occupation")` 返回 `"other"`(测试 `rectification-server-focus.test.ts` L716/L775 锁定)。随后 `buildMethodFollowupPlan` 有 active focus 时用 `domain: focus.targetDomain` 重建「承接 followup」(`method-followup.ts` L20162025`source="active_focus"`),职业焦点被重建成 `domain=other, intent=collect``spokenFollowupForUser` 给出「也可以再说一件」。`stableFollowupQuestionId` 对它算出 `collect:other:collect_method_evidence`,与原职业焦点 `collect:occupation:collect_method_evidence` 不同,因此不会被判 `already_open`,会 supersede 掉职业题 | `server-focus.ts` L201206、L5253`method-followup.ts` L2016 |
| B | **set-focus 工具的兜底**:模型 `spokenPrompt` 两次 `invalid_spoken_prompt` 后,`USER_COLLECT_QUESTION[nextFollowup.domain]` 直接落库;`nextFollowup.domain="other"` 时就是这句 | `rectification-v9-tools.ts` L10851100 |
| C | `spokenCollectFallbackFollowup` / `collectQuestionDomain(null | "unknown" | "active_focus")` 把无领域的采集 followup 归为 other | `method-followup.ts` L1343、L1371 |
### 2.2 尚未确认的触发链(`investigating`
最符合转录的链:该轮模型先用 set-focus 写出职业题(焦点 `target_domain=other`),同一轮再次调用 set-focus(6 步);第二次的 `next_followup` 是承接 followup`domain=other`),模型的 `spokenPrompt``domain_mismatch`(模型填 `targetDomain="career"``persistableFocusDomain` 后 ≠ `"other"`)或词表问题两次被拒,走 2.1-B 兜底,把职业题覆盖成「也可以再说一件」。**这一段需要事故 Case 的 receipt 证实**(预期能看到两条 `rectification-set-focus / intent.classified / failed / invalid_spoken_prompt` 和一条 completed,以及 `collect:occupation:*` 焦点被 `collect:other:*` supersede)。执行方拿不到 staging 数据时,把这一节保留为 `investigating`,不得编造。
无论触发链是哪条,2.1 的三条活路都是确定性缺陷,修掉后这句话在代码里不再存在。
### 2.3 为什么没有自动出卡
出卡的服务端路径是 `persistNextInterviewIfIdle → persistExhaustionCollect``exhaustionSpokenCollectFollowup` 为空且 `acceptanceAllowed && trainingGate.open && canAdopt` 时写 `adopt_representative` 旁白 + `terminalNote`,客户端据此渲染区间交付卡。它只在**没有 active focus** 时运行(L1158 提前返回)。一个 `collect:other:*` 焦点挂着,出卡永远轮不到;用户答了这句「再说一件」之后,若职业仍未覆盖,还会再来一遍。
## 3. 决策记录
1. **删掉 `USER_COLLECT_QUESTION.other` 与 `USER_COLLECT_QUESTION_RETRY.other`**(产品口径:多余入口宁可删除也不修)。`spokenFollowupForUser``domain=other` 只剩开场 GENERIC 一条路;非开场的 other 返回 `null`,上游按「无可问」处理。`otherCollectFollowup` 一并删除。
2. **职业焦点不再丢领域。** 不改表(本轮不动迁移):`target_domain` 仍写 `other`,但 `questionId` 已是 `collect:occupation:collect_method_evidence`;承接 followup 的 `domain` 改为「先从 `questionId` 解析 `collect:<domain>:` 段,再退回 `focus.targetDomain`」。新增 `parseCollectFocusQuestionId``persistableFocusDomain("occupation") === "other"` 的两条测试保留(DB 约束未变)。
3. **set-focus 兜底只允许表内真实领域。** L10851100`fallbackDomain` 必须是 `USER_COLLECT_QUESTION` 的键且 ≠ `other`(删表后自然满足),否则返回 `invalid_spoken_prompt` 不落地;且兜底题干必须与当前 `next_followup` 同领域。
4. **同一轮第二次 set-focus 必须承接、不得覆盖。** 若 active focus 的 `askedTurnId === turnId`(本轮刚写的),set-focus 返回该焦点投影并标 `idempotent: true`,不重建、不 supersede。
5. **有 `collect:other:*` 非开场焦点的存量 Case**`persistNextInterviewIfIdle` 开头把这种焦点判为 stale 并 `skipped`(复用 `isStalePreAdoptFocus` 旁边加一条 `isOrphanOtherCollectFocus``questionId``collect:other:` 开头且账本已有带年月事件),让出卡路径能跑起来。
6. 不动采用门、确认门、`MIN_SEPARATION_LEAD``_relative_support``minute_step=1` 指纹;不动 `page.tsx`Skill 不 bump。
## 4. 任务分解
### 4.1 BUG-586:三条活路 + 出卡
- 决策 15 全部落地。
- 验收(形状用例,`rectification-collect-stall.test.ts` / `rectification-occupation-coverage-exit.test.ts` 扩):
- (a) 7 个带年月领域已覆盖或拒答、职业未覆盖、active focus 为 `collect:occupation:*``target_domain=other`)→ `buildMethodFollowupPlan` 的承接 followup `domain === "occupation"``spokenFollowupForUser` 给「你平时主要做什么工作?」,`stableFollowupQuestionId` 等于原焦点 id`already_open`,不 supersede)。
- (b) 同 (a),模型 `spokenPrompt` 两次无效 → set-focus 返回 `invalid_spoken_prompt`**不**落库任何 `collect:other:*`
- (c) 同一轮 set-focus 两次(第二次 `next_followup` 为承接 followup)→ 第二次返回 `idempotent: true`,焦点 id 不变。
- (d) 职业已答、无剩余采集、`acceptanceAllowed && trainingGate.open && canAdopt``persistNextInterviewIfIdle` 返回 `terminalNote: true` 的采用旁白,`agent-run` 写 exhaustion gate turn,客户端 `canOfferCards` 为真(区间交付卡)。
- (e) 存量 `collect:other:*` 焦点 + 账本有带年月事件 → 进入 `persistNextInterviewIfIdle` 时被 `skipped`,随后走 (d)。
- (f) `agent-voice-copy-contract`:源码与 `user-copy.ts` 不再含「也可以再说一件」;`rectification-server-focus.test.ts` L517/L727、`rectification-adopt-flow-*.test.ts` 里以 `collect:other:collect_method_evidence` 为期望值的断言逐条写「原值 / 新值 / 原因」。
### 4.2 记录
- `docs/BUG_HISTORY.md` BUG-586:状态先写 `investigating`2.2 未证实)或 `resolved`(若执行方能从 staging receipt 证实触发链);复发自 BUG-558(同现象),关联 BUG-580、BUG-582`CHANGELOG.md``PROGRESS-rectification-other-collect-fallback-20260908.md``docs/testing/rectification-scenarios-20260907.md` 加「七领域问完后下一问必须是职业题或直接出区间卡,不得出现『再说一件』」。
## 5. 让步顺序
决策 1、2、3 必做且同一提交;决策 4 若 set-focus 工具改动牵扯太广可后置为 P2 但要写进度记录;决策 5 必做(否则存量 Case 卡死);4.2 不可省。
## 6. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-other-collect-fallback-20260908 .worktrees/rectification-other-collect-fallback-20260908 origin/staging
cd .worktrees/rectification-other-collect-fallback-20260908
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
cd frontend && ./node_modules/.bin/tsc --noEmit; npm run lint 2>&1 | tail -1
ls tests/rectification-*.test.ts tests/agent-voice-copy-contract.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)"
grep -rn "也可以再说一件" src tests # 收工时必须为空
```
`codex/rectification-duplicate-narration-20260907`BUG-584/585,改 `step-answer.ts` / `agent-run.ts` / `collect-prompt.ts`)无文件重叠;谁后合谁 rebase。
## 验收(Claude2026-09-08`origin/staging` @ `df182c16`staging 已部署同 SHA
| 门 | 结果 |
| --- | --- |
| tsc | 0 错 |
| lint | 0 error / 102 warning |
| 前端 rectification + consultation + voice + skill-registry(非 DB | 1253 / 0 |
| Python v5_services + growth contract | 全绿(执行方记的 15s 壁钟用例本机未复现超时) |
| Docker DB 套件、浏览器走查 | 环境缺口,同执行方记录 |
| 项 | 结论 |
| --- | --- |
| 决策 15 | 通过。`grep 也可以再说一件` 为空;`parseCollectFocusQuestionId` 让职业承接不再压成 other;同轮二次 set-focus 幂等;`isOrphanOtherCollectFocus` 放行出卡 |
| 2.2 触发链 | 仍 investigating(执行方按事实写 mitigated,接受) |
| 真实 Case2026-09-08 | 职业题正常出现、其后不再出 other 句;但暴露下一道题(BUG-590,见 `TASK-rectification-covered-domain-recollect-20260908.md` |
结论:通过。