# TASK · 职业题答出的年月不得被抹掉 + 采集重设计(dd8f35f7)用真机序列回放验收(2026-09-11) - 基线:`origin/staging` @ `e36bda53`(代码 `dd8f35f7`,Skill 10.0.23,含 BUG-646~648;**尚未部署**,staging `/api/health` 仍是 `fff6da68`) - 分支:`codex/rectification-occupation-dated-answer-20260911`,基于 `origin/staging` - 执行方:coding agent;验收:Claude - BUG 编号起点:**BUG-649**(646~648 已被 dd8f35f7 占用;开工时 `grep -o "^## BUG-[0-9]*" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1` 核对) - 不改引擎计分、不改确认门、不升 Skill(10.0.23 文案已写「有日期按 D10 + 本命 10 宫辅助评分」,本单是让代码兑现这句话) ## 1. 事故实证(2026-09-11 staging 真机,Skill 10.0.22 / 代码 52db714b) 流程实录(脱敏):开场 → 用户给两件学业(2016-09 入学、2020-06 毕业)→ 五道带反推年份的题全答「没有」→ 健康题答出 2024-10 事故 → 职业题被 Agent 写成「大概哪一年开始干这一行」,用户答「2024 年 4 月」→ 助手只回「记下了」,`rectification-set-focus` 失败 `no_pending_question`,无问题、无卡、无缺口句,Case 停在 `collecting_evidence`,区间 04:45–05:15 不动。 回执证据: | 字段 | 值 | 含义 | | --- | --- | --- | | `gates.event_quality` | `scoreable_event_count=2 / minimum=3` | 训练门关 | | `gates.domain_diversity` | `domains=["education"]` | 只剩学业 | | `inference_state.events[2024 health]` | `usage=holdout` | 唯一非学业事件被留作 holdout(dd8f35f7 T4 已改成 ≥4 件才留) | | 账本职业行 | `eventKind=occupation_note`、`datePrecision=unknown`、`occurredFrom=2024-04-01` | 用户给的年月被抹成不计分,且精度与日期自相矛盾 | | 最后一轮 `tool_activities` | `record-evidence-batch` methods=[]、`set-focus` failed `no_pending_question` | 第四件事没进引擎,工具轮无出口 | ## 2. 根因 1. **职业焦点下带年月的回答被归一成无日期备注**:`frontend/src/lib/rectification-agentic/v9/evidence-model.ts` `applyOccupationCollectLedgerNorm`(BUG-442 P0)对 occupation 采集焦点下任何 `career/occupation` 条目一律改成 `domain=occupation`、`eventKind=occupation_note`、`datePrecision=unknown`、日期置空。`contracts.py` `AUXILIARY_EVENT_KINDS` 与 `isPrimaryScoreableEvidence` 都不计 `occupation_note`,所以这件事永远进不了训练门。 2. **`??` 回落**:`frontend/src/mastra/rectification-v9-tools.ts` 两处(batch 约 L1464、propose 约 L1618)写 `occurredFrom: normalized?.occurredFrom ?? item.occurredFrom`,归一后的 `null` 回落成原日期,账本出现 unknown 精度却带日期的行。 3. **题干与落库口径不一致**:题干模板 `USER_COLLECT_QUESTION.occupation` 是「你平时主要做什么工作?」,Agent 在真机里改问「哪一年开始干这一行」,用户按要求给了年份,系统丢掉。 4. 工具轮无出口、反推年份、holdout 吃掉第三件:已由 dd8f35f7(BUG-646~648)处理,本单只做回放验收(§5 T3),不重复实现。 ## 3. 决策记录(产品负责人 2026-09-11) 1. **收窄 BUG-442 P0**:occupation 采集焦点下,**无年月**的职业描述仍归一成 `occupation_note`(覆盖判定不依赖模型选对 domain,红线不变);**带年月**的回答在照常写 note 关闭覆盖的同时,**另记一条主评分事件** `domain=career`、`eventKind=career_entry`(模型给出 `career_change / promotion / business_start` 等 career kind 则沿用)、精度按用户所说(年 / 月 / 日)。 2. 「不得把 `occupation_note` 算进 3 件」(BUG-356 / BUG-389 / BUG-442 防复发)不变:计入训练门的是 `career_entry`,note 仍不计分。 3. 职业题不问年份:generic 层只问「你平时主要做什么工作?」;开始年份的追问走 dd8f35f7 的锚定模板(例如「做程序员是哪年开始的?」),焦点 domain 必须是 `career`,不是 `occupation`。 4. 用户的产品口径不变:先等用户说、不用生日推年份、不说「领域」、永远给结果。 ## 4. 硬红线 - 不得改 `MIN_ACCEPTANCE_EVENTS / MIN_ACCEPTANCE_DOMAINS`、不得开 `confirmation_allowed`、不得改已哈希 Skill 10.0.23。 - 归一后的 `null` 就是 `null`:不得再用 `??` 让日期回落。 - 覆盖判定仍不得读正文、不得用关键词、不得依赖模型 domain(BUG-442 防复发)。 - 既有测试总数不降;改动断言逐条写「原值 / 新值 / 原因」。 - Bug 历史、测试、进度记录不得含真实用户资料,例子只用脱敏的领域 + 年月。 ## 5. 任务分解 - **T1 带年月的职业回答另记 career_entry(BUG-649)** - `applyOccupationCollectLedgerNorm`:输入带 `occurredFrom` 或 `occurredTo` 且精度非 unknown 时,返回两条:note(unknown、日期 null)+ 原条目改为 `domain=career`、`eventKind` 为模型给的 career kind(非 career kind 时用 `career_entry`)。无日期时行为同今天。 - `rectification-v9-tools.ts` batch 与 propose 两处改成按归一结果逐条写入,删掉 `?? item.occurredFrom / ?? occurredFrom` 回落。 - 验收:焦点 `collect:occupation:*` + 「2024 年 4 月开始做程序员」→ 账本一条 `occupation_note`(unknown、无日期)+ 一条 `career_entry`(month、2024-04-01);`record-evidence-batch` 回执 `methods` 非空(进引擎);回执 `event_quality.scoreable_event_count` +1、`domains` 含 `career`。只答「程序员」→ 只有 note。新增 `frontend/tests/rectification-occupation-dated-answer-20260911.test.ts`;`rectification-occupation-coverage-exit.test.ts` 若改断言写三栏。 - **T2 职业题干与锚定焦点 domain** - 检查 `collection-question-pool.ts`:generic 职业题不含「哪年 / 哪一年 / 年份」;「毕业后第一份工作哪年开始」类锚定题的焦点 `targetDomain=career`,不落 `collect:occupation:`,`isOccupationCollectFocus` 对它返回 false。 - Agent 系统提示(`agent-run.ts` / `agentic-rectification.ts`)加一句:职业题不得自行追加年份问句;要问年份走锚定题。 - 验收:合同测试锁定上述两点。 - **T3 用本案序列回放验收 dd8f35f7**(不新增 BUG,除非失败) - 写端到端合同测试 `frontend/tests/rectification-replay-20260911.test.ts`:开场 → 两件学业 → 「没有了」→ 健康 2024-10 → 职业「2024 年 4 月开始做程序员」。断言:不出现带出生年反推的「N 年前后」题干;四件带月事件中留 1 件 holdout、训练集 3 件 2 类、训练门开、`next_action` 为区分选择题(`ask_candidate_discriminator`)且有可渲染 `choice_card`。 - 变体:去掉职业年月(只答「程序员」)→ 训练门关 → **工具轮结束时**(`persistNextInterviewIfIdle` 路径,不是点选路径)正文含精确缺口句、例子 ≥2 且来自事业 / 感情 / 家人,无「领域 / 做不了 / 还差 N 件」,客户端状态 `collect_waiting`。若该路径没落缺口句,编 **BUG-650** 修 `agent-run.ts` 工具轮出口并记录。 - **T4 记录**:BUG-649(及可能的 650)写入 `docs/BUG_HISTORY.md`;`CHANGELOG.md` 一行;`docs/tasks/PROGRESS-rectification-occupation-dated-answer-20260911.md`;`docs/testing/rectification-scenarios-20260907.md` 加「职业题答年月」一条真人走查。 ## 6. 让步顺序 T2 的系统提示句可放进度记录待下一单;T1 + T3 不可省。 ## 7. 开工前置命令 ```bash git fetch origin --prune git worktree add -b codex/rectification-occupation-dated-answer-20260911 .worktrees/rectification-occupation-dated-answer-20260911 origin/staging grep -o "^## BUG-[0-9]*" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1 # 应为 648 cd frontend && npm test -- tests/rectification-occupation-coverage-exit.test.ts tests/rectification-collection-question-pool.test.ts tests/rectification-spoken-collect.test.ts tests/rectification-exhaustion-exit-20260906.test.ts ``` ## 8. 验收口径 - 前端 `tsc --noEmit` 0 错、`npm run lint` 0 error、相关套件 fail=0、测试总数 ≥ 基线;`next build` 后 `/` 仍 Static、首屏 gzip ±2%(worktree 里 Turbopack 拒外部软链时在主检出跑)。 - Python:`.venv/bin/python -m pytest tests/test_candidate_discriminator_contract.py`;`run_quality_gate.py --profile quick`。 - 部署后:先打开一个绑定 10.0.22 的历史校正(BUG-621),再按本案序列真机走一遍,第四件事说完必须出选择题。