diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 0f02657f..0dbd4668 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -97,6 +97,7 @@ | `TASK-rectification-evidence-turn-empty-answer-20260910.md` | `PROGRESS-rectification-evidence-turn-empty-answer-20260910.md` | 证据轮模型无正文被判整轮失败:证据、评分、下一问都已落库却只剩『没有拿到下一个问题』(BUG-633);答题旁白只说『范围没变』、时间线写死『还在收窄』(BUG-634) | 待验收 | | `TASK-rectification-unwritten-evidence-claim-20260910.md` | `PROGRESS-rectification-unwritten-evidence-claim-20260910.md` | 证据轮模型只说『记下了』却没调 batch、没设下一问,财务一件静默丢失、流程停在原题且照常扣点(BUG-635);校正流不识别 Mastra schema 拒绝信封,会把被拒的 batch 报成 completed(BUG-636) | 待执行 | `codex/rectification-unwritten-evidence-claim-20260910` | | `TASK-rectification-latent-audit-20260910.md` | `PROGRESS-rectification-latent-audit-20260910.md` | 顺带审计:账本派生同年键误杀 `known_event_quality`(BUG-637,BUG-389 复发);双轨一致性按 31 分钟逐分判冲突并降置信度、与报告两套口径(BUG-638);不可分宽度按簇代表分钟少算(BUG-639);引擎与推断层两套代表分钟、回执宫位表不是卡片那一分钟(BUG-640);另 8 条观察项 | 待执行 | `codex/rectification-latent-audit-20260910` | +| `TASK-rectification-collect-semantics-20260910.md` | `PROGRESS-rectification-collect-semantics-20260910.md` | 产品决策三改:Skill 去掉「财务/健康只有主动说才问」并清空 Python `VOLUNTEER_ONLY`,升 10.0.22(BUG-641);带年份线索的采集题优先、题干带线索不矛盾,推翻 BUG-539 的整句去前缀(BUG-642);删掉「没有/记不清」原字匹配,分类器分 no/unsure 两类(BUG-643) | 待执行 | `codex/rectification-collect-semantics-20260910` | ### 聊天主链路与首页 diff --git a/docs/tasks/TASK-rectification-collect-semantics-20260910.md b/docs/tasks/TASK-rectification-collect-semantics-20260910.md new file mode 100644 index 00000000..73dde37e --- /dev/null +++ b/docs/tasks/TASK-rectification-collect-semantics-20260910.md @@ -0,0 +1,73 @@ +# TASK · 采集语义三改:Skill 去掉「财务只有主动说才问」、带年份线索的采集题优先并改口径、跳过/没有一律由分类器判(2026-09-10) + +- 基线:`origin/staging` @ `84572c07`(代码同 `d96b24c2`,staging 已部署) +- 分支:`codex/rectification-collect-semantics-20260910`,基于 `origin/staging` +- 串行:改 `method-followup.ts` 与 `route.ts`,与 `TASK-rectification-unwritten-evidence-claim-20260910.md`(BUG-635/636,改 `route.ts` collect 分支与 `agent-run.ts`)**有文件交集**:先做那份,本单在其分支之上开工;若那份尚未领取,本单先做,那份的 T1 改为在本单的 `collectFocusCloseStatus` 之后接入。与 `TASK-rectification-latent-audit-20260910.md`(BUG-637~640)无交集。 +- 执行方:coding agent;验收:Claude +- BUG 编号起点:**BUG-641**(635/636、637~640 已由前两份任务书占用;开工时核对 `docs/BUG_HISTORY.md` 最大号) +- **本单升 Skill:10.0.21 → 10.0.22**(改 SKILL.md 正文必须升版本,包目录哈希钉在 `skills/skill-package-registry.json`)。 + +## 0. 产品决策(2026-09-10,产品负责人口头授权) + +1. 去掉「财务与健康只有用户主动说才问」。财务、健康与其他领域同权:服务器可以主动问,用户说了就记、就计分。 +2. 带年份的题优先。区分候选的带年月点选题永远先问(10.0.21 已定);采集题里,带年份线索的先于没有线索的,同领域的无年份性格卡不得抢在采集题前面。 +3. 「没有」「记不清」这类业务判断不得靠正则或原字匹配,一律由意图分类器判;分类器要能分出「这方面没有」和「记不清、以后再说」两种。 + +## 1. 现状实证(同一份 Case JSON,Skill 10.0.21) + +| 现象 | 代码事实 | +| --- | --- | +| Skill 说「财务与健康只有用户主动说才问,仍可计分」(`skills/jyotish-birth-time-rectification/SKILL.md` L84),服务器却主动问了财务 | TS 采集轮转 `DATED_COLLECT_ORDER`(`method-followup.ts` L1249)含 `finance`,自 BUG-462 起主动问;Python `event_probes.py` L145 `VOLUNTEER_ONLY = {finance, health_pressure}` 让这两域**不进** `evidence_collection_probes`、不进 `missing_collection_domains`。三处口径各自为政,模型同时读 Skill 和回执。 | +| 家人采集题 `question_id=probe:family.2021`,题干却没有年份 | 2021 来自 `evidence_collection_probes`(`source=age_band`,`_age_band_year` L290:出生年 + 该领域典型年龄段中点)。**它不是引擎按候选算出的区分年份,只是帮回忆的线索。** BUG-539 因「某年前后…记得大概哪年就行」自相矛盾而把前缀整句删掉,`probe_year` 只留给计分与去重。用户答「不记得了」后家人线关闭,丢掉的是一个回忆线索,不是区分题——此前审计单观察项 2 把它说成「带年探针被吞」,措辞过重,以本单为准。 | +| 家人分支 `sameDomainYearlessCard("family") ?? makeFollowup(采集)`(L2578) | 同领域**无年份**性格卡优先级高于带年份线索的采集题,与决策 2 相反。 | +| 「没有」「记不清」走原字匹配 | `route.ts` L524:`isCollectDeclineUtterance` / `isCollectSkipUtterance`(`turn-intent-classifier.ts` L37–43)只认去掉句末标点后**完全等于**「没有」/「记不清」的句子。「不记得了」「记不得」「忘了」「这方面没什么」全部落到 LLM 分类器;而采集题分类器提示词只教了 `no`,`shouldDeclineCollectFocus` 只认 `no`,没有「跳过」一类。本例「不记得了」靠 Agent 自己调 `rectification-resolve-focus` 兜住。 | + +## 2. 任务分解 + +### T1 · BUG-641(P2):财务 / 健康同权采集,三处口径合一,Skill 升 10.0.22 + +- SKILL.md L84 删去「财务与健康只有用户主动说才问,仍可计分。」,改为「财务、健康与其他领域同权:服务器按 `method_followup_plan.next_followup` 主动问,用户说了就记、就计分。」其余句子不动。 +- Python:`VOLUNTEER_ONLY` 清空(或删除常量与 L353 分支),使 finance / health_pressure 进 `missing_collection_domains` 与 `evidence_collection_probes`(带 age_band 年份线索)。`DOMAIN_CATALOG` 若缺这两域的 `age_lo/age_hi`,按事业线同段补。`oos_blind_prompts` 语义不变。 +- Skill 升版:复制 `versions/10.0.21` → `versions/10.0.22`,根 `SKILL.md` 与版本目录逐字节相等;`skill-package-registry.json` 新条目(sha256 / sourceCommit / packagePath / status=active);`case-status.ts` `RECTIFICATION_SKILL_VERSION = "10.0.22"`;钉版本的测试(`skill-registry.test.ts`、`agent-voice-copy-contract`、`rectification-collect-stall` 等)按「原值 / 新值 / 原因」更新;`CHANGELOG.md` 写明 Skill bump。 +- 验收:`grep -n "主动说才问" skills/` 为空;Python 定向测试:`evidence_collection_probes` 对只有学业/感情事件的账本返回含 finance 与 health_pressure 的行;**BUG-621 回归**:绑定 10.0.21 的历史 Case 仍能打开(`open-request.ts` 不要求绑定版本等于当前版本),写进真人清单。 + +### T2 · BUG-642(P2):带年份线索的采集题优先,题干带线索但不矛盾 + +- 口径(推翻 BUG-539 的「整句去前缀」,但保留它指出的矛盾不得复现):有 `probe_year` 的采集题题干写成「家里在 2021 年前后有没有结婚、添丁或住院?有就说个大概年月,别的年份也行。」——年份是线索,不是限定;不再同时出现「记得大概哪年就行」。`USER_COLLECT_QUESTION.` 拆成 `withYearCue(year)` / `withoutCue` 两个模板,文案对照 `frontend/docs/VOICE.md`。 +- 优先级:方法覆盖链里,每个领域先出**带年份线索的采集题**,`sameDomainYearlessCard` 只在该领域已覆盖(账本有事件 / `answeredYes`)或已拒答后才可出;`nextDatedCollectFollowup` 在 `DATED_COLLECT_ORDER` 内先排有 `evidence_collection_probes` 年份线索的领域,再排没有的。带年月的区分点选题(`rankedCatalog` dated)仍在一切采集题之前,不动 10.0.21 规则。 +- 验收:`rectification-spoken-collect.test.ts` / `rectification-method-followup*.test.ts`:家人域有 age_band 2021 且有 D12 无年份对照卡时,`next_followup` 是采集题且 `user_prompt` 含「2021 年前后」、不含「记得大概哪年就行」;家人已拒答后才轮到 D12 卡;无 `probe_year` 的领域用无线索模板;BUG-539 的「不得既指定年份又让报年份」断言改写为新模板断言并注明原值/新值/原因。 + +### T3 · BUG-643(P2):跳过 / 没有一律由分类器判,删掉原字匹配 + +- 删除 `isCollectDeclineUtterance` / `isCollectSkipUtterance` 及其在 `route.ts` L524 的短路;不得用任何正则或词表替代。 +- 采集题分类器提示词(`turn-intent-classifier.ts` 的 collect 分支)加两条明确口径:「没有、没发生过、这方面没什么」→ `answer_current_focus` + `answer_class=no`(该方面没有事,本次不再问);「记不清、不记得、忘了、想不起来、以后再说」→ `answer_current_focus` + `answer_class=unsure`(先放着,以后可补)。带年月的新经历仍按现有规则进 `provide_new_evidence` / `has_new_dated_event`。`weak_yes` / `yes` 对采集题无意义,返回即视为 `unclear` 走 Agent。 +- 路由:`shouldDeclineCollectFocus` 改为 `collectFocusCloseStatus(classified): "declined" | "skipped" | null`,`no`→declined、`unsure`→skipped,两者都走 `applyCollectFocusDenial`(ack 文案沿用 `collectDeclinedAck` / `collectSkippedAck`);`has_new_dated_event` 为 true 时仍 `deferFollowup` 并进 Agent。分类器抛错或返回 null:**不做关键词兜底**,按现状进 Agent(Agent 可调 `resolve-focus`),并在 `RectificationRunDiagnostic` 记 `collectIntent=unclassified`。 +- 多说一句:分类器对每条采集回答都已在跑,本改动不增加模型调用次数,只是删掉两条捷径。 +- 验收:`rectification-turn-intent-classifier.test.ts`:「没有」→no、「不记得了」「记不得」「忘了」→unsure、「2021 年搬过家」→provide_new_evidence、「没有,不过 2019 年换过工作」→no + has_new_dated_event;`rectification-spoken-collect.test.ts` L442「没有 and 记不清 short-circuit without a model run」改为「由分类器判定 no / unsure 后走 denial」,原值/新值/原因写进进度记录;BUG-626(skipped 健康不进 holdout)回归仍过;源码合同测试断言 `route.ts` 与 `turn-intent-classifier.ts` 不再导出/引用这两个原字匹配函数。 + +### T4 · 记录 + +- `docs/BUG_HISTORY.md` BUG-641~643;BUG-539 补「口径被 BUG-642 改为带线索模板」;审计单观察项 2 的措辞在本单 §1 已更正。 +- `CHANGELOG.md`:财务/健康同权采集;采集题带年份线索;「没有 / 记不清」改由分类器判;Skill 10.0.22。 +- `docs/tasks/PROGRESS-rectification-collect-semantics-20260910.md`。 +- `docs/testing/`:真人清单三条——(a)新 Case 走到财务题,回「没有」与回「不记得了」各一次,分别看到「这方面先跳过」与「这题先放着」;(b)家人题题干带年份线索且能用别的年份回答;(c)打开一个绑定 10.0.21 的历史校正。 + +## 3. 让步顺序 + +T2 的「优先级」半边可延后(写 `BLOCKED.md`),T2 的「题干带线索」半边与 T1、T3 不可省。 + +## 4. 开工前置命令 + +```bash +git fetch origin --prune +git worktree add -b codex/rectification-collect-semantics-20260910 .worktrees/rectification-collect-semantics-20260910 origin/staging +grep -o "^## BUG-[0-9]*" docs/BUG_HISTORY.md | tail -1 +.venv/bin/python -m pytest tests -k "event_probes or collection" -q +cd frontend && npm test -- tests/rectification-spoken-collect.test.ts tests/rectification-turn-intent-classifier.test.ts tests/skill-registry.test.ts +``` + +## 5. 验收口径 + +- 前端 `tsc --noEmit` 0 错、`npm run lint` 0 error、相关套件 fail=0、测试总数 ≥ 基线;`next build` 后 `/` 仍 Static,首屏 gzip ±2%。 +- Python 定向测试与 `run_quality_gate.py --profile quick` 通过。 +- 推 staging 后 `/api/health` 的 `deployment.gitCommit` 等于最新含代码改动的提交;**部署后先打开一个旧版本历史校正**(BUG-621 教训),再做其余真人清单。 diff --git a/docs/tasks/TASK-rectification-unwritten-evidence-claim-20260910.md b/docs/tasks/TASK-rectification-unwritten-evidence-claim-20260910.md index 0496125d..a8f06463 100644 --- a/docs/tasks/TASK-rectification-unwritten-evidence-claim-20260910.md +++ b/docs/tasks/TASK-rectification-unwritten-evidence-claim-20260910.md @@ -53,7 +53,7 @@ BUG-278 已实测:`createTool` 的 `inputSchema` 校验失败时**不抛**, 1. **「记下了」只能由写入事实支撑。** 任何正文含「记下了」且本 attempt 没有公开写工具 completed、也没走 `applyHostFallback` 的,该正文不得到达用户;流式已发出的部分用既有 `retractSpoken()` 收回。这是对系统提示第 4 条格式要求的宿主侧兜底,不是改提示词。 2. **允许重试一次,条件与 BUG-633 完全一致**:仅当本 attempt 没有任何公开写工具 completed(无证据重放风险,不触犯 BUG-186)。新增错误码 `evidence_not_written`,与 `empty_stream` 同法——不进 `RETRYABLE_ERROR_CODES` 常量,由收尾分支按 `publicWriteToolCompleted` 决定 `retryable` / `failed`。重试 attempt 的 bootstrap 追加一行:「【重试约束】上一 attempt 没有调用 rectification-record-evidence-batch 就写了『记下了』。本轮必须先把用户原话里的带日期事件提交 batch,再用 rectification-set-focus 写下一问,最后才写正文。」 3. **两次都没写 → fail closed 但不断流。** 主持人正文用新增文案 `RECTIFICATION_USER_COPY.evidenceNotRecorded` = 「这件我还没记上。请再说一次大概年月和发生的事。」(对照 `frontend/docs/VOICE.md`,不写内部错误、不写「系统」「模型」)。turn `status=completed`,`answer_origin=host_fallback`,phases 含 `answer.host_fallback`;**不结算计费**(用户这一轮什么都没得到)。若现有 `finalizeTurn("completed")` 与结算不可分离,允许改为 `failed` + 主持人正文,但客户端必须按 BUG-633 的失败路径 `loadCaseSnapshot` 并渲染问题行;两种实现选哪种写进进度记录。 -4. **守卫的触发信号**:路由在 collect 焦点分支已拿到分类结果,通过 `runV9AgentTurn` 新 option `expectedWrite: "evidence" | null` 传入(`provide_new_evidence`、或 `answer_current_focus` 且 `has_new_dated_event` → `"evidence"`)。分类器为 null / 出错、或消息不在 collect 焦点分支时,退回对用户原话的年份判定 `/(?:19|20)\d{2}\s*年/u`。不得只用正则替代分类器,也不得因分类器失败而放弃守卫。`action` 为 `opening` / `read_only` 时不触发。 +4. **守卫的触发信号**(2026-09-10 产品口径修订:业务判断不得靠正则):路由在 collect 焦点分支已拿到分类结果,通过 `runV9AgentTurn` 新 option `expectedWrite: "evidence" | "none" | "unknown"` 传入(`provide_new_evidence`、或 `answer_current_focus` 且 `has_new_dated_event` → `"evidence"`)。消息不在 collect 焦点分支时,也对用户原话跑一次同一分类器(无焦点时 `current_question` 传空),拿 `has_new_dated_event` / `provide_new_evidence` 判定。分类器为 null / 出错:重试一次,仍失败则 `"unknown"`,守卫 fail-open 并在 `RectificationRunDiagnostic` 记 `expectedWrite=unknown`;**不得用年份正则或关键词兜底**。`action` 为 `opening` / `read_only` 时不触发。 5. **客户端**:完成轮的 assistant 行没有 `question`,而快照 `current_question.question_source === "focus"` 且该焦点不是本轮新设(`askedTurnId` ≠ 本轮 / 本轮回执无 `rectification-set-focus`)→ 在主持人正文下**再渲染一次**问题行(复用 BUG-633 加的主持人问题行,`focus_id` 只能来自 `current_question`)。`liveQuestionOnMessages` 改为只认**最后一条** settled assistant 消息;旧消息上的同 `focus_id` 不再算「仍在显示」。 6. **BUG-636**:`tool-result` 分支按结构识别信封(`error === true` 且 `validationErrors` 为对象),发 `tool.activity` `status=failed`、`code=tool_call_rejected`;`toolTerminalStatus` 记 failed;`batchResultFromToolChunk` 对信封返回 null。不匹配上游英文文案。公开回执不泄漏 `validationErrors` 内容。 7. 不做的事:不改 `turn-intent-classifier.ts` 提示词;不改 Skill 10.0.21;不改引擎计分与淘汰阈值(「范围没变」另议);不给财务/健康域加特殊规则。 @@ -73,7 +73,7 @@ BUG-278 已实测:`createTool` 的 `inputSchema` 校验失败时**不抛**, - `route.ts` collect 焦点分支:分类结果落到 Agent 的两种情形计算 `expectedWrite`;其他路径传 `null`,由运行器按年份正则兜底。 - `V9AgentRunOptions` 新增 `expectedWrite?: "evidence" | null`。 -- 验收:单测覆盖 `provide_new_evidence` → `"evidence"`;`answer_current_focus` + `has_new_dated_event` → `"evidence"`;`answer_current_focus` + `no` 不进 Agent(既有);分类器抛错 → `null`;运行器对 `null` + 带年份原话判定为需要写入,对 `null` + 无年份原话不触发。 +- 验收:单测覆盖 `provide_new_evidence` → `"evidence"`;`answer_current_focus` + `has_new_dated_event` → `"evidence"`;`answer_current_focus` + `no` 不进 Agent(既有);分类器两次都抛错 → `"unknown"` 且守卫不触发、诊断里有记录;源码合同断言 `agent-run.ts` / `route.ts` 不含年份正则。 ### T2 · 收尾守卫:无写入的「记下了」不得交付,可重试一次(BUG-635)