Files
Jyotisha/docs/tasks/TASK-rectification-collect-direction-20260904.md
T

126 lines
15 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.
# 任务书 · 采集阶段第三件事没有方向:盘外核对抢跑到已拒答领域,问题不指向任何领域(2026-09-04)
基线:`origin/staging` 代码 `45bdb63e`(文档头 `45e00f46`)。本单只改 `frontend/src/lib/rectification-agentic/v9/method-followup.ts``spoken-prompt.ts``rectification-v9-tools.ts`(读盘投影 + set-focus 校验)与 `agentic-rectification.ts` 提示词,以及对应测试。不改 Python 引擎、不改 `decideRectification` 的采用/确认门、不动 Skill 版本。
`TASK-report-sensitivity-float-crash-20260904.md` 无文件交集,可并行。
## 0. 用户可感知的现象
staging`45bdb63e`)真实会话,出生窗 30 分钟,账本里 2 条带日期事件(事业 1、感情 1)加 1 条只有年份没有日期的感情事:
| 轮 | 用户 | 助手 | 问题 |
| --- | --- | --- | --- |
| 1 | 家人某年采集题答「没有」 | 「你平时主要做什么工作?」 | 可评分事件 2 < 3,缺的是**带年份的第三件事**;职业备注没有日期,答完也不推进 |
| 2 | 说了职业 | 「当前范围还在继续收窄中,接下来我们继续。」+「除了工作这条线,还有哪件事你能记起大概的年份?」 | 第一句是空话(没有任何数值收窄);第二句**没有指向任何领域**,用户不知道该往哪想 |
第 2 轮服务端落下的焦点是 `oos_blind:holdout``intent=out_of_sample_check``domain=family`——**用户上一轮刚拒答的领域**。Agent 拿到的写作提示是「校时还没用过家人这条线……」,它知道家人刚被否,于是把问题改写成不指向任何领域的泛问。
## 1. 事故实证
用该 Case 的 receipt 形状(9 个候选、2 条可评分事件、`event_quality.passed=false, minimum=3, scoreable_event_count=2``oos_blind_prompts=[family, education, finance]`、7 条 varga 对照探针全部 `year=0` 被丢成 `yearless_ungrounded_contrast`)本地重放,隐私数据不入库:
**1a. 工具路径(Agent 轮)选中已拒答领域的盘外核对。** `buildMethodFollowupPlan` 传入 `holdoutValidation: "not_started"`(与 `rectification-v9-tools.ts` `followupPlanForParsed` / 读盘投影一致):
```
declined=[family] → next = { method: oos_blind, intent: out_of_sample_check, domain: family, frame: false }
```
无论账本里有没有职业备注、家人有没有拒答,三种形状结果相同。
**1b. 决定路径(答「没有」那轮)问的是不带日期的职业。** `persistNextInterviewAfterChoice` 不传 `holdoutValidation`,计划走方法覆盖轮转 relationship → career → family(拒答视作 covered)→ **occupation**。学业、财务、搬家这些带年份的领域根本不在这条轮转里。
**1c. 决定路径再走一轮也是泛问。** 职业答完后若仍走决定路径,计划落到 `!meetsAcceptanceEventQuality` 分支:`domain: null``spokenFollowupForUser``GENERIC_COLLECT_QUESTION`(「从你最容易想起来的一件事开始就好……」)。也就是说,两条路径都不会把用户引到一个具体领域。
代码定位(`method-followup.ts` @45bdb63e,按符号):
- `holdoutValidationStatus` / `canAskHoldout``decision-from-dossier.ts`):只要引擎给了 `oos_blind_prompts` 就判 `not_started`,不看 `event_quality`
- `buildMethodFollowupPlan``if (!next && input.holdoutValidation === "not_started")` 分支:直接取 `input.oosBlindPrompts?.[0]`,不过 `declined`,不看 `meetsAcceptanceEventQuality``sessionOutcome === "validate_holdout"` 早退分支同样不过 `declined`
- 方法覆盖轮转(`!relationshipCovered``!careerCovered``!familyCovered``!occupationCovered`)之后才是 `!meetsAcceptanceEventQuality` 的泛问分支,且该分支 `domain: null`
- `exhaustionSpokenCollectFollowup` 已经有正确的顺序(family → education → finance → occupation → health/relocation/career/relationship → other)和按领域的 `datedCollectFollowup`,但只在拒答/穷尽路径被调用,计划主流程不用它。
- `validateSpokenPrompt`:对采集题只查长度、选项字面、内部 token、`domain_mismatch`(仅当 Agent 显式传了不同 `targetDomain`),**不要求题干提到领域**。Agent 把家人题改写成泛问,服务端照收。
## 2. 根因
1. **盘外核对没有前置门。** BUG-396 已钉死「2 条继续收集,训练门只计 training 事件」,但计划层的 OOS 分支只看 `holdoutValidation === "not_started"`,而这个状态只要引擎输出了提示就成立。于是训练门都没开就开始「盘外核对」,并且用的是被 `declinedDomains`BUG-520)明确排除的领域。
2. **计划里没有「补第三件带年份的事」这一步。** 方法覆盖轮转的四个领域里有三个已覆盖或拒答,剩下的职业不带日期;带年份的学业/财务/搬家/健康只存在于穷尽路径。
3. **Agent 的写作提示与用户上下文矛盾,校验器又不要求方向。** 提示说问家人,用户刚否掉家人,Agent 只能写泛问;服务端没有任何一条规则要求采集题写出领域。
## 3. 决策记录(产品负责人 2026-09-04
1. **可评分事件不足 3 条时,不得进入盘外核对(OOS / holdout 提问)。** 计划的两处 OOS 分支都加 `meetsAcceptanceEventQuality` 前置;不满足就当作没有 OOS 提示。这不推翻 BUG-463(holdout 不绑采用门)——本单只管「什么时候问」,不碰 `canAdopt` / `canConfirmExactMinute`
2. **OOS 提示必须跳过已拒答领域**`declinedDomains`,沿用 BUG-520 的 intent 口径)。全部被拒则退到带年份的 holdout 事件,再没有就 `null`
3. **缺第三件事时,先补带年份的领域,再问职业。** 顺序:家人 → 学业 → 财务 → 搬家 → 健康/压力 → 事业 → 感情,跳过已拒答与已有确认证据的领域;全部走完才轮到职业,职业也关了才用泛问。计划主流程与 `exhaustionSpokenCollectFollowup` 共用同一份顺序,不得再复制一份。这条**只在 `!meetsAcceptanceEventQuality` 时生效**,不改 BUG-442/472 关于职业覆盖不挡采用的结论。
4. **采集题必须指向领域,方向由服务端定、话由 Agent 说。** 这是 Agent 产品,题干继续由 Agent 用自己的话写,但服务端校验题干必须包含目标领域的至少一个关键词;不合格返回 `invalid_spoken_prompt: domain_missing`,两次不合格后服务端用 `USER_COLLECT_QUESTION[domain]` 落焦点(现有 `spokenPromptFailures` 机制)。领域为 `other` 的泛问只允许在 §3.3 全部走完之后出现。
5. **进度句必须有数字,没数字就不说。** 「当前范围还在继续收窄中」这种没有数据支撑的句子不得出现。服务端在读盘投影里给出 `collection_progress = { scoreable, minimum, missing }`(来自 receipt `event_quality`),提示词要求:下一问是采集题时,先用一句话说明还差几件带时间的事,数字只来自这个字段;没有这个字段就不说进度。
## 4. 硬红线
1. 不改 `holdoutValidationStatus` 的返回语义、不改 `decideRectification` / `deliveryCapability` 的采用与确认门(BUG-463 防复发条款)。修法在计划层的分支条件,不在决策层。
2. 不改 `declinedDomains` 的 intent 口径(BUG-520)。
3. 顺序只能有一处定义;`exhaustionSpokenCollectFollowup` 与计划主流程必须调用同一个函数。
4. `validateSpokenPrompt` 的领域关键词表放在 `user-copy.ts``agent-voice-lexicon.ts` 旁边,每个领域至少 3 个日常词(例:education → 上学/大学/毕业/考试/学业;finance → 收入/买房/贷款/欠债/钱;relocation → 搬家/住/外地;health_pressure → 生病/受伤/住院/压力;family → 家里/父母/家人/添丁;career → 工作/入职/换工作;relationship → 交往/分手/结婚/感情)。关键词命中是 `includes`,不做分词。不得把这个校验套到区分题(`choice_frame` 非空)或 OOS 题上——它们已有年份校验。
5. 既有断言若要改,写「原值 / 新值 / 原因」三栏。预计要动的:`rectification-server-focus` 中「exhaustion `oos_blind` 仍走 `USER_COLLECT_QUESTION.other`」相关用例(若受顺序合并影响);`rectification-yearless-ungrounded``validate_holdout` 用例需要补足 4 条带日期证据才能维持 `oos_blind` 结论(它现在的 evidence 是 4 条 year 精度 + 1 条职业,`meetsAcceptanceEventQuality` 是否满足由执行方实测后写明)。
6. 任务书、进度记录、Bug 历史、测试 fixture 不得出现该 Case 的 ID、日期、职业描述;fixture 用虚构年份。
7. 进度记录必须贴 `npm test` 摘要六行(tests/pass/fail/cancelled/skipped/todo)与退出码,以及无 Docker 基线失败清单比对(BUG-523 教训)。
## 5. 任务分解
### 5.1 P0 · 盘外核对加前置门并跳过拒答领域(BUG-524)
1. 新增 `holdoutFollowupFor(input, declined)``meetsAcceptanceEventQuality(input.evidence)` 不满足 → `null`;否则取 `oosBlindPrompts` 中第一个 `!declined.has(domain)` 的提示 → `holdoutAskFields`;没有则退到带年份的 holdout 事件;再没有 → `null`
2. `sessionOutcome === "validate_holdout"` 早退与 `holdoutValidation === "not_started"` 分支都改用它。
验收(新文件 `frontend/tests/rectification-collect-direction-20260904.test.ts`,fixture 用 §1 的形状但年份虚构):
- 2 条可评分事件 + `oosBlindPrompts=[family, education, finance]` + family 拒答 + `holdoutValidation: "not_started"``next_followup.intent !== "out_of_sample_check"`
- 4 条可评分事件(3 个领域)+ family 拒答 + `holdoutValidation: "not_started"` 且无可渲染区分探针 → `next_followup``oos_blind``domain === "education"`
- 同上但三个领域全部拒答、无带年份 holdout 事件 → 不出 OOS`sessionOutcome: "validate_holdout"``next_followup === null`
### 5.2 P0 · 补第三件带年份的事进入计划主流程(BUG-525)
1.`exhaustionSpokenCollectFollowup` 里的领域顺序抽成 `datedCollectOrder`(§3.3 顺序,含跳过规则),穷尽路径与计划主流程共用。
2. `buildMethodFollowupPlan`:在方法覆盖轮转之前、OOS 分支之后加一步:`!next && !meetsAcceptanceEventQuality(input.evidence)` → 走 `datedCollectOrder`,取到带领域的 `datedCollectFollowup`;没有再进原轮转(职业)与原泛问分支。
3. `spokenFollowupForUser` 对这些 followup 必须返回 `USER_COLLECT_QUESTION[domain]`(不是 `GENERIC_COLLECT_QUESTION`),这是决定路径的题干。
验收:
- §1 形状、family 拒答、`holdoutValidation: "not_started"``next_followup = { intent: collect_method_evidence, domain: education }``spokenFollowupForUser` 含「上学」或「升学」。
- 再把 education 也拒答 → `domain: finance`family/education/finance/relocation/health_pressure 全拒答且 career/relationship 已有确认证据 → `domain: occupation`;职业也关闭 → 才出 `domain: other`
- `persistNextInterviewAfterChoice`(答「没有」路径,形状同 `rectification-adopt-narration-20260904.test.ts` 的 denial 用例)在 2 条可评分事件下落下的焦点 `target_domain` 为 education 且 prompt 为 `USER_COLLECT_QUESTION.education`;零次 `decideFromDossier`(沿用 BUG-521 源码断言)。
- 4 条可评分事件时本分支不触发(`rectification-collect-stall``rectification-eight-method` 全绿;BUG-442/472 的采用出口用例不变)。
### 5.3 P1 · 采集题干必须指向领域(BUG-526)
1. `validateSpokenPrompt`:当 `followup.intent === "collect_method_evidence"``followup.domain` 在关键词表内且 `!followup.choice_frame` 时,题干必须命中该领域至少一个关键词,否则 `{ ok: false, reason: "domain_missing" }`
2. `rectification-set-focus` 描述加一句:「采集题必须写出服务端给你的领域(学业/家里/钱/搬家……),不要写成『随便哪件事』」。
3. 第二次仍不合格时的服务端兜底沿用现有 `spokenPromptFailures`;确认兜底题干是 `USER_COLLECT_QUESTION[domain]` 而不是泛问。
验收:`rectification-spoken-prompt`(或新文件):education 题写「除了工作,还有哪件事记得年份?」→ `domain_missing`;写「上学那会儿,哪年升学或大考还记得吗?」→ ok;区分题与 OOS 题不受影响;`rectification-set-focus` 集成用例两次泛问后焦点 prompt 为 `USER_COLLECT_QUESTION.education`
### 5.4 P2 · 进度句有数字才说(BUG-527)
1. 读盘投影(`rectification-read-case` 与 route 的 interview 投影)加 `collection_progress: { scoreable, minimum, missing }`,来自 `decisionReceipt.gates.event_quality`;缺字段时为 `null`
2. `agentic-rectification.ts` 提示词:「下一问是采集题时,先用一句说明还差几件带时间的事,数字只用 `collection_progress`;没有该字段不说进度。不得写『范围在收窄』『继续收窄』这类没有数字的进度句。」把「继续收窄」「范围还在」加进 `MACHINE_VOICE_LEXICON` 只对旁白校验生效(若旁白无校验,则只改提示词并在进度记录写明)。
验收:投影测试锁 `collection_progress` 取值(2/3/1)与缺 gate 时 `null`;提示词测试断言含「collection_progress」与「不得」句。
## 6. 让步顺序
1. 5.1 与 5.2 必做,缺一条用户仍然拿到没方向的问题。
2. 5.3 可以退到只加提示词 + 兜底题干为领域题,但校验器改动是防复发的钉子,不做要在进度记录写明理由。
3. 5.4 可推迟。
## 7. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/rectification-collect-direction-20260904 .worktrees/rectification-collect-direction-20260904 origin/staging
cd .worktrees/rectification-collect-direction-20260904/frontend
./node_modules/.bin/tsc --noEmit
npx tsx --test tests/rectification-eight-method.test.ts tests/rectification-yearless-ungrounded.test.ts tests/rectification-server-focus.test.ts tests/rectification-collect-stall.test.ts tests/rectification-adopt-narration-20260904.test.ts tests/rectification-answer-choice.test.ts
```
记下开工时这批套件的 tests/pass/fail 数与全量 `npm test` 的失败清单(无 Docker 基线 25 条),交付时逐条比对。
## 8. BUG 编号起点
截至本单:BUG-523。本单从 **BUG-524** 起(5.1 → 5245.2 → 5255.3 → 5265.4 → 527);开工时再核对 `docs/BUG_HISTORY.md` 最大号。BUG-524 关联 BUG-396(训练门未开不得进入 holdout)与 BUG-520(拒答口径);BUG-525 关联 BUG-442/472(职业覆盖)与 BUG-426(计划回落到带年份的采集题)。