Files
Jyotisha/docs/tasks/TASK-rectification-collect-vs-offer-consistency-20260905.md
T

86 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 · 采集没问完就出采用卡、出卡后又被采集题挤掉(BUG-546 的另一半)(2026-09-05
- 基线:`origin/staging` @ `1052a027`(含 BUG-546/547/549
- 分支:`codex/rectification-collect-vs-offer-consistency-20260905`
- 执行方:coding agent;验收:Claude
- 涉及文件:`frontend/src/lib/rectification-agentic/core/rectification-decision.ts``frontend/src/lib/rectification-agentic/v9/decision-from-dossier.ts`;测试见 §5。不改客户端隐藏规则(BUG-544)、不改 Skill 正文、不改引擎。
- BUG 编号起点:**BUG-550**(开工时 `grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1` 复核;BUG-542 仍为 `TASK-api-not-configured-mislabel-20260904.md` 预留)
- 串行:与 `TASK-rectification-probe-answer-covers-domain-20260905.md`(已合入 `988d97ae`)无冲突;若 `TASK-api-not-configured-mislabel` 同日开工,两者不改同一文件。
## 1. 事故实证(staging2026-09-05,部署 `afd14948`
同一个 Case,用户连续经历:
| 步 | 用户看到 | 期望 |
| --- | --- | --- |
| 1 | 家人题答「没有」→「记下了,这方面先跳过。」→ 财务口述采集题「钱的方面…」 | 只问财务题 |
| 2 | 财务题还没答,**候选时间卡(9 个候选 + 相对支持)已经挂在下面** | 采集没完不出卡 |
| 3 | 用户答完财务题 → Agent 调 `rectification-offer-candidates`,正文写出整份八法「生时校正验证报告」,含「唯一分钟确认 · 本会话以代表性时间收口」「当前可以采用 HH:MM」 | 出牌轮才写报告 |
| 4 | 报告末尾紧跟搬家采集题「有没有哪年搬家,或开始长期住在外地?」,**时间卡消失** | 出了牌就不再追问;或者压根不出牌 |
## 2. 根因
一句话:**决策层已经判成「可采用」,计划层却还有没问完的带年份采集,两层各说各的,四个消费者听了不同的一层。**
1. `core/rectification-decision.ts::decideRectification`:候选分离度够(`separation.sufficient`)时直接 `finish("adopt_representative")``datedMethodCollectOpen` 只在 `!separation.sufficient` 分支被看(约 L307),而且 `v9/method-followup.ts::datedMethodCollectOpen`L377)只数 `dasha_events / d9_relationship / d10_career / relatives` 四个阻塞方法,**不数** `DATED_COLLECT_ORDER` 里的学业、财务、搬家、健康与职业口述。事业 + 家人一过,四个方法都 covered,决策就成了 `adopt_representative`
2. `v9/method-followup.ts::buildMethodFollowupPlan` 尾部(约 L23602371):BUG-546 让 `isRemainingEvidenceCollect(next)` 的采集**豁免** `deferAdoption`,于是 `next_followup = 搬家采集``session_outcome = adopt_representative` 同时成立。
3. 四个消费者:
- `buildNextUserAction`(约 L1434):只看 `sessionOutcome`,输出 `adopt_representative`「请用户从下方时间卡片选择」→ 模型照 Skill 10.0.14 L119「出牌/采用轮正文按八法写出验证报告」写了整份报告,并调 `rectification-offer-candidates`
- `mastra/rectification-v9-tools.ts` offer 工具(约 L1961):门只看 `offerSessionKinds()``adopt_representative` → 放行,Case 转 `candidate_ready`
- `answer-choice.ts::shouldSkipFollowupPersist`L135):`isRemainingEvidenceCollect` → 不跳过 → `persistNextInterviewIfIdle` 在同一轮把搬家采集写成焦点、挂到本轮 turn。
- 客户端 `rectification-agentic-chat.tsx`(约 L13741383):`canShowRectificationSelectionCards``sessionOutcome` 为真 → 卡挂在最后一条已结算助手消息;唯一的挡板是「某条消息挂着未答问题」(BUG-544)。步 2 卡出现时那条财务题没有把卡挡住(焦点绑 turn 是 best-effort`linkFocusAskedTurn` 失败只 warn),步 4 搬家题绑上了 turn 就把卡挡掉——同一条规则,一次没挡住、一次挡住了,看起来就是「卡先来、再丢」。
所以步 2 与步 4 是一个病:状态本身自相矛盾。步 3 的长报告不是模型自作主张,是 `next_user_action` 告诉它「这是出牌轮」。
## 3. 决策记录
1. **采集优先,与 BUG-546 的既定意图一致**:只要采集计划(`decideFromDossier` 里的 `collecting` 计划)的 `next_followup` 仍是 `isRemainingEvidenceCollect`(学业 / 财务 / 搬家 / 健康 / 职业口述,未覆盖且未拒答),且 Case 未 accepted、用户未停止、未 exhausted,决策就保持 `collect_evidence / ask_fact_collection`**不得**给出 `adopt_representative`。带年份域问完(覆盖、拒答或 BUG-549 的 yes 探针)之后,才进 `adopt_representative`,此时出牌 + 报告 + 卡片一次到位,且不再追问。
2. 由决策层一处改,四个消费者自然一致:`next_user_action``ask_fact_collection`offer 工具因 kind 不在 `offerSessionKinds` 而拒绝(`offer_not_allowed`);客户端 `canShowRectificationSelectionCards` 为假,改走 `canShowRectificationReadonlyRange`(只读区间,`collect_evidence` 已允许);`shouldSkipFollowupPersist` 行为不变。**不改** BUG-544 的隐藏规则,不改 offer 工具描述,不改 Skill 正文。
3. 产品可否决项:若产品更想「卡先给、问题照问」(用户可随时选,也可再补一件事),改为放宽客户端 BUG-544 对 `collect_spoken` 的隐藏而**不动**决策层——两条路只能选一条,执行方按本单默认走 1;产品改主意时在进度记录写明。
4. 报告里「本会话以代表性时间收口」这句来自 Skill 10.0.14 `SKILL.md` L41「本会话以此收口」与 L112 的转述,BUG 历史 L6151 删掉的是宿主机械文案,没删 Skill。本单不动 Skill;产品若要模型也不说这句,另开 Skill 10.0.15 单(词表禁用 + Skill 措辞)。
5. 步 3 之前用户看到一段原始流事件 JSON(`{"type":"skill.bound"}`…)短暂出现又消失:本次代码通读没有找到把流事件当正文渲染的路径(客户端只把 `answer.delta.text` 进正文,服务端 `answer.delta` 只来自模型 `text-delta`,事件不持久化、模型看不到)。**不在本单**,见 §7。
## 4. 硬红线
- BUG-546(无区分卡时继续补采集)、BUG-547(有卡先问卡)、BUG-549yes 探针视为覆盖)、BUG-544(未答问题在场时隐藏采用卡)的用例原样通过。
- `accepted` 早退路径(采用后只走 `remainingReverseVerifyProbes`)与 BUG-536539 不受影响:`input.accepted` 为真时本单新增判断不得生效。
- 用户停止(`userStopped` / `paused`)与 `exhausted` 的现有出口不变:用户说「没有更多了」仍按现有规则给区间或代表时间,不得因本单变成继续追问。
- 不放宽 `meetsAcceptanceEventQuality``buildConfirmationGate`;不写账本;不改 `DATED_COLLECT_ORDER`
- 任务书 / 进度 / Bug 历史只用抽象年份与抽象分钟。
## 5. 任务分解
### 5.1 决策层:带年份采集未完不判「可采用」
- `decision-from-dossier.ts::decideFromDossier``decideAfterInferenceChange`:把传给 `decideRectification``datedMethodCollectOpen` 改为 `datedMethodCollectOpen(collecting.methods) || isRemainingEvidenceCollect(collecting.next_followup)`(两处同改;`collecting` 计划已用 `sessionOutcome: "collect_evidence"` 构建,`next_followup` 现成)。
- `core/rectification-decision.ts::decideRectification``separation.sufficient` 分支在 `finish("adopt_representative")` 之前(放在 `input.accepted` 判断之后、`confirmationAllowed` 之前),若 `input.datedMethodCollectOpen === true && !input.userStopped && stopClass?.kind !== "exhausted"``collect(separation, holdout, range, null, capability, stopReason)``accepted``awaiting_confirmation``confirmationAllowed`)路径不受影响。
- 验收:`tests/rectification-decide-next-action.test.ts` 新增——分离度足够、`datedMethodCollectOpen: true`、未 accepted、未 stopped → `sessionOutcome === "collect_evidence"``nextAction === "ask_fact_collection"``can_adopt === false`;同输入 `userStopped: true` → 现有停止出口不变;同输入 `accepted: true``adopt_representative`
### 5.2 端到端:出卡与追问不再同轮
- `tests/rectification-collect-direction-20260904.test.ts``rectification-candidate-offer-anchor.test.ts` 新增用例(用现有 dossier fixture 的组装方式):学业 / 感情 / 事业有带年份证据、家人已拒答、财务未问、候选分离度足够 → `decideFromDossier``collect_evidence``buildNextUserAction``ask_fact_collection``persistNextInterviewIfIdle` 的计划 `next_followup.domain === "finance"`;把财务、搬家、健康都补齐(或拒答)后同一 dossier → `adopt_representative``next_followup === null`
- offer 工具:在 `tests/rectification-v9-tools*.test.ts`(或现有 offer 工具测试)加一条——上面「财务未问」的 dossier 调 `rectification-offer-candidates``offer_not_allowed`
- 验收:`npx tsx --test tests/rectification-*.test.ts tests/agent-voice-copy-contract.test.ts` fail=0,总数不低于开工时。
### 5.3 记录
- `docs/BUG_HISTORY.md`:BUG-550(决策层「可采用」与计划层「还要采集」并存;关联 BUG-544、546、547、549,说明 BUG-546 为何只修了一半)。
- `CHANGELOG.md` 一条;`docs/tasks/PROGRESS-rectification-collect-vs-offer-consistency-20260905.md``docs/testing/rectification-collect-vs-offer-consistency-20260905.md`(真实环境:家人「没有」之后到财务 / 搬家 / 健康问完之前,页面只有只读区间、没有可点的时间卡、Agent 不写验证报告;带年份域问完后一轮内同时出现报告 + 卡片,且下面不再跟采集题)。
## 6. 让步顺序
5.1 与 5.2 一起做,不可拆;5.3 不可省。产品若选决策记录 3 的另一条路,5.1 整段作废、改客户端,须先在进度记录写明并通知验收方。
## 7. 不在本单(待定位)
- 原始流事件 JSON 短暂出现在对话区:需要产品下次复现时截图(是出现在助手气泡里,还是「正在…」工作行里)并记下当时选的模型;执行方顺手查该轮服务端日志是否有 `attempt.reset` / 重试。定位前不得加「防御性」过滤代码。
## 8. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-collect-vs-offer-consistency-20260905 .worktrees/rectification-collect-vs-offer-consistency-20260905 origin/staging
cd .worktrees/rectification-collect-vs-offer-consistency-20260905
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
cd frontend && npx tsx --test tests/rectification-*.test.ts tests/agent-voice-copy-contract.test.ts 2>&1 | grep -E "^# (tests|pass|fail)|^not ok"
```
收尾跑同一条命令 fail=0,再 `tsc --noEmit``npm run lint`