fix(rectification): dedupe post-adopt verify and skip this probe only
Independent Staging Quality Gate / validate (push) Successful in 14m31s
Independent Staging Quality Gate / publish (push) Successful in 1m59s

Adopted reverse-verify repeated already-asked collect stems, treated 「这题跳过」 as stopping the case, and promised holdout/OOS checks that never ran. Collect spoken stems no longer prefix a year.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-04 21:49:53 +08:00
parent 6b038e6d0d
commit d4627a5019
36 changed files with 1065 additions and 91 deletions
+64
View File
@@ -8276,3 +8276,67 @@
- 相关记录:BUG-526、BUG-534;引入半拉子改动的提交 `c2f23131`
- 复发自:无
- 修复版本:`61194690`
## BUG-536 | 采用后第一道核对题重复已问过的采集题
- 状态:resolved
- 首次发现:2026-09-04
- 最近更新:2026-09-04
- 影响面:`remainingReverseVerifyProbes`、采用后 `buildMethodFollowupPlan``accepted` 分支
- 用户现象:采用代表分钟后,第一道核对题与采集阶段已经问过并回答过的带年份题几乎逐字相同。
- 触发条件:采集阶段已按 `semantic_key` 问过该 (domain, year);采用后反向核对从 `eventProbes` 再取第 0 条。
- 根因:冲突探针去重用了 `askedKeys`;BUG-350 单独写下的反向核对漏接。过滤了拒答领域、年份已覆盖、成年下限,没有用 `semantic_key` / `candidate_split_hash` / `domain.year`
- 修复:`remainingReverseVerifyProbes` 增加 `askedKeys`,与 `remainingConflictProbes` 同口径。已问或已跳过的探针不再作为核对题。
- 验证:`rectification-post-adopt-verify-20260904``askedProbeKeys` 含第一条 `semantic_key` 时下一问是另一条;两条都在则 `next_followup === null``next_user_action.id === "start_consultation"`;只含 `candidate_split_hash` 也能去重。`rectification-eight-method` 采用后有剩余探针 / 无剩余探针两条既有断言仍过。
- 防复发:采用后核对必须走 `askedKeys`。不得只靠 `probeYearAlreadyCovered` 当已问去重。
- 相关记录:BUG-350
- 复发自:无
- 修复版本:待发布
## BUG-537 | 核对卡「这题跳过」等于整案停止,已采用仍念采用提示,前端报没有下一问
- 状态:resolved
- 首次发现:2026-09-04
- 最近更新:2026-09-04
- 影响面:`choice-card` 核对卡 stop 槽、`SKIP_PROBE_ACTION``applyRectificationChoice``rectificationQuestionGapState`、校正聊天面
- 用户现象:点「这题跳过」后助手又说可以从下面选一个先用着;随后出现「没有拿到下一个问题。」和重新加载。
- 触发条件:已采用,当前焦点是反向核对卡;用户点跳过。
- 根因:三层叠加。(1) 核对卡只把停止按钮改了标签,动作仍是整案 `STOP_ACTION``userStopped:true`。(2) 结构化旁白只看 `can_adopt`,已采用后 `publicCanAdopt` 仍为 true,再念一遍 `adoptCue`。(3) 服务端没有核对结束状态,前端把「无焦点 + 可续」当成问题还没准备好。
- 修复:核对卡绑定 `skip_probe`:焦点 `skipped`、探针进 `answered_probes`(不计分)、不置 `userStopped`,再取下一道核对题。已采用禁止 `adoptionNarration` / `adoptCue`。GET 与结构化答题暴露 `next_user_action`;已采用且无下一问时为 `start_consultation`。前端该态为 `verified_idle`:一行 `postAdoptVerifyDone`,无重载、无 handoff。关闭核对结束时不得把已跳过的焦点重新挂到收尾消息上。
- 验证:两道核对题跳第一道 → 下一焦点是第二道,`sessionOutcome` 不是 `user_stopped`,旁白不含 `adoptCue`。跳最后一道 → 无新焦点,`next_user_action.id === "start_consultation"`,旁白等于 `postAdoptVerifyDone``verified_idle` 无 gap copy、无重载、无 consult handoff。采用前区分题停止按钮仍是 `STOP_ACTION`
- 防复发:核对卡「这题跳过」不得走 `STOP_ACTION`。已采用旁白不得再念采用提示。`start_consultation` 不得进 preparing/unavailable。
- 相关记录:BUG-350、BUG-499、BUG-502、BUG-518、BUG-520
- 复发自:无
- 修复版本:待发布
## BUG-538 | 采用旁白承诺的核对与采用后计划不同源
- 状态:resolved
- 首次发现:2026-09-04
- 最近更新:2026-09-04
- 影响面:`adoptDeliveryFacts.post_adopt_verification`、采用旁白模板与 Agent 校验器
- 用户现象:采用卡旁白说会拿若干年前事和几类域外题来核对;采用后一道都没问。
- 触发条件:旁白事实包按 holdout 事件与 `oos_blind_prompts` 生成;采用后实际计划走 `remainingReverseVerifyProbes`
- 根因:`TASK-rectification-adopt-narration-20260904.md` §4 把 `post_adopt_verification` 写成 holdout + OOS。BUG-350 已定采用后不得把不计分盘外核对当默认下一步。两套规格矛盾,旁白说的是不会发生的事。这是任务书错误,不是执行方偏离 BUG-350。
- 修复:`post_adopt_verification` 改为与采用后计划同一过滤(同一函数、同一 `askedKeys`、同一 `declined`),项为 `{kind:"reverse_verify"; domain; year_label}`。空列表时模板说没有还能核对的前事;校验器把「会拿……核对」判为 `promise` 失败。holdout / OOS 只留在决策收据,不进旁白。
- 验证:`rectification-adopt-narration-20260904` 原 holdout/oos 三项改写为 reverse_verify 且与 `buildMethodFollowupPlan({accepted:true})` 首题一致。空列表 +「会拿……核对」→ `promise`。模板含「没有还能核对的前事」。
- 防复发:旁白承诺必须与 `remainingReverseVerifyProbes` 同源。空列表不得承诺会核对。不得把 holdout/OOS 写回旁白事实包。
- 相关记录:BUG-350、BUG-519、BUG-520、BUG-521、BUG-522、BUG-523
- 复发自:无
- 修复版本:待发布
## BUG-539 | 家庭采集题带年份前缀,又让用户报年份
- 状态:resolved
- 首次发现:2026-09-04
- 最近更新:2026-09-04
- 影响面:`collectionYearFields``spokenFollowupForUser``collect_method_evidence`
- 用户现象:家庭采集题写成「某年前后,家里如果有结婚、添丁或住院这类事,记得大概哪年就行。」既指定了年份又让用户报年份。
- 触发条件:家庭轮转把 dated 探针的 `year_label` 拼进 `USER_COLLECT_QUESTION.family`
- 根因:`68f0759e` 为 BUG-414/418「带年份的口语问、不出无年份分盘选择卡」把 dated 探针年份塞进采集口语。`USER_COLLECT_QUESTION.family` 本身以「记得大概哪年就行」结尾,拼在一起自相矛盾。答「没有」后出采用卡本身不是缺陷(预算已满、家庭是最后一条必答覆盖线),本单不改该收口。
- 修复:采集口语不再拼「某年前后,」。`probe_year` / `semantic_key` 仍留给计分与去重。不出无年份分盘选择卡的半边保留。
- 验证:家庭采集口语等于 `USER_COLLECT_QUESTION.family`,同时 `probe_year > 0`。答完最后一道区分题后仍落家庭采集焦点,旁白不含探针年份。BUG-418 无年份分盘卡测试仍过。
- 防复发:`collect_method_evidence` 口语不得加年份前缀。不得为去前缀而丢掉 `probe_year` 或放回无年份分盘选择卡。
- 相关记录:BUG-414、BUG-418
- 复发自:无
- 修复版本:待发布
@@ -0,0 +1,103 @@
# PROGRESS · 采用后核对线(2026-09-04
工作树:`.worktrees/rectification-post-adopt-verify-20260904`
分支:`codex/rectification-post-adopt-verify-20260904`
基线:任务书写 `origin/staging` `e27d5dc5`(含 `0be51e65``a793edb1`);任务书提交 `ea28521a`。交付前 `origin/staging` 已到 `6b038e6d`(占用 BUG-535),本单 rebase 上去以便快进推 staging。
任务书:`docs/tasks/TASK-rectification-post-adopt-verify-20260904.md`
未改 Python、未 bump Skill(仍 10.0.14)、未改 `.gitea/workflows/**`、未动迁移、未升级依赖、未改 `page.tsx`2041 行,与 `e27d5dc5` 相同)。
| 任务 | 状态 | BUG |
| --- | --- | --- |
| 5.1 反向核对 `askedKeys` 去重 | 完成 | BUG-536 |
| 5.2 旁白事实包与采用后计划同源 | 完成 | BUG-538 |
| 5.3 「这题跳过」只跳本题 | 完成 | BUG-537 |
| 5.4 `verified_idle` + `next_user_action` + `postAdoptVerifyDone` | 完成 | BUG-537 |
| 5.5 采集题去年份前缀 | 完成(未否决) | BUG-539 |
| 5.6 记录 | 完成 | — |
## 编号
开工时 `docs/BUG_HISTORY.md` 最大号 BUG-534。本单 **BUG-536539**。交付前 `origin/staging` 已占用 BUG-535(报告分盘张数上限),故后移。
## 实现要点
- `remainingReverseVerifyProbes` 增加 `askedKeys``semantic_key` / `candidate_split_hash` / `domain.year`)。`if (input.accepted)` 与采用旁白共用 `reverseVerifyRemainingForAdopt`
- `post_adopt_verification` 项改为 `{kind:"reverse_verify"; domain; year_label}`。空列表模板:「采用后没有还能核对的前事…」。校验器空列表 +「会拿…核对」→ `promise`。holdout / OOS 不进旁白。
- 核对卡 `skip_this_probe``SKIP_PROBE_ACTION` / `optionId:"skip_probe"`。焦点 `skipped`,探针 `answered_probes``unsure`/`declined`**不**置 `userStopped`。采用前停止按钮仍是 `STOP_ACTION`
- GET 案例与结构化答题返回 `next_user_action`。已采用且无下一问 → `start_consultation`。前端 `verified_idle`:一行 `postAdoptVerifyDone`,无 gap copy、无重载、无 handoff。状态栏仍走 BUG-502。
- 已采用禁止 `adoptionNarration` / `adoptCue`。跳过但还有下一问时,旁白就是下一题干。
- 采集口语不再拼「YYYY 年前后,」。`probe_year` 保留。答「没有」出采用卡不改(预算已满、家庭最后一条必答线)。
- 跳过最后一道核对后,收尾消息**不**把已关闭焦点重新 `set-focus` 挂回去(否则测试夹具会把跳过的核对卡又钉到收尾句上)。
## 偏离
- 任务书 5.3 写「随后跑 `persistNextInterviewIfIdle`」。实际走 `persistApplied` 里已有的 `persistNextInterviewAfterChoice`skip / accepted 强制续问)。效果相同:下一道核对或收尾,不另开 idle 入口。
- 任务书验收「无新焦点」在假会计里还要断言不再 `set_agentic_rectification_conversation_focus`。夹具的 GET 仍返回已跳过的 active focus;若收尾后再 fallback `linkFocusAskedTurn`,会把旧卡挂到收尾句。已按收尾不挂焦点修。
- 5.5 按决策 7 做了,产品未否决。
- 交付前 `origin/staging` 已占用 BUG-535,本单实落 BUG-536539,并 rebase 到 `6b038e6d` 以便快进推送。
## 测试
`frontend/src/app/page.tsx` 相对 `e27d5dc5`2041 → 2041,未增长。
`npx tsc --noEmit`
```
exit=0
```
聚焦 `tsx --test`answer-choice / post-adopt-verify / adopt-narration / surface-state / surface-contract / collect-direction / eight-method):
```
# tests 138
# pass 138
# fail 0
# cancelled 0
# skipped 0
# todo 0
exit=0
```
`rectification-post-adopt-verify-20260904` 新文件 6 条:去重、空承诺、`verified_idle`、家庭口语等于 `USER_COLLECT_QUESTION.family`
契约改写后补跑 decision-authority / agentic-entry / application-billing-contract / collect-stall / server-focus102/102。
eslint 本单文件:0 error。`rectification-agentic-chat.tsx` 1 条既有 `react-hooks/exhaustive-deps` 警告(`3659519bd`),未扩依赖数组。
全量 `npx tsx --test tests/*.test.ts`(并行,本机有 Docker):
```
# tests 2674
# pass 2661
# fail 13
# cancelled 0
# skipped 0
# todo 0
exit=1
```
13 条里 5 条是本单契约未改完(enum 加 `skip_probe`、家庭采集不再含年份前缀),已改断言后聚焦 102/102。其余 8 条为 Docker 迁移/连接争用:`admin-database``database-admin-owner-recovery``database-billing-adjustments``database-local-business``database-report-candidate-range``rectification-ingest-p0-database``rectification-v9-database`migration + evidence lifecycle)。`--test-concurrency=1` 隔离重跑上述 7 个文件:
```
# tests 13
# pass 13
# fail 0
# cancelled 0
# skipped 0
# todo 0
exit=0
```
有效失败清单为空。总数 2674 不低于近期 staging 全量(collect-direction-fix 记 2656)。
未改 Python;预检系统 Python / swisseph 噪音不当作本单回归。MEVG 豁免:本单是产品/代码,不是运势解读。
## 改动文件
- 计划/旁白:`method-followup.ts``adopt-narration.ts``adopt-narration-agent.ts``user-copy.ts`
- 跳过/收尾:`choice-action.ts``choice-card.ts``answer-choice.ts``interview-state.ts``turn-exit.ts``message-origin.ts`
- 投影:`cases/[caseId]/route.ts``agent/route.ts``rectification-surface-state.ts``rectification-agentic-chat.tsx`
- 文案:`frontend/DESIGN.md``frontend/docs/VOICE.md`
- 测试:`rectification-post-adopt-verify-20260904.test.ts`(新)、answer-choice、adopt-narration、collect-direction、eight-method、surface-state、surface-contract、collect-stall、server-focus、decision-authority、agentic-entry、application-billing-contract
- 类型:`turn-question.ts``rectification-choice-card.tsx` 接受 `skip_probe`
- 文档:`docs/BUG_HISTORY.md``docs/testing/rectification-post-adopt-verify-20260904.md``docs/tasks/README.md``CHANGELOG.md`、本文件
+1 -1
View File
@@ -49,7 +49,7 @@
| `TASK-rectification-adopt-narration-fix2-20260904.md` | `PROGRESS-rectification-adopt-narration-fix2-20260904.md` | 采用旁白超时用了 unref 的 `AbortSignal.timeout`,测试挂死取消同文件后三条用例,门禁 `npm test` 退出码 1 | 已验收(门禁通过,staging 已部署) | `45bdb63e`BUG-523 |
| `TASK-rectification-collect-direction-20260904.md` | `PROGRESS-rectification-collect-direction-20260904.md` | 可评分事件 2 条时盘外核对抢跑到刚拒答的家人领域,Agent 只能改写成不指向任何领域的泛问;缺第三件带年份的事却先问职业 | 已验收:5.1/5.3/5.4 通过;5.2 顺序错位(任务书责任)见修复单 | `codex/rectification-collect-direction-20260904`BUG-527530 |
| `TASK-rectification-collect-direction-fix-20260904.md` | `PROGRESS-rectification-collect-direction-fix-20260904.md` | dated 补采集分支插在方法轮转之前,新案例第二问被家人抢占感情/事业;`7ee7f825` CSS 与合同测试脱节挡住门禁 | 已验收通过(0be51e65),待 staging 部署核对 | `codex/rectification-collect-direction-fix-20260904`BUG-531532 |
| `TASK-rectification-post-adopt-verify-20260904.md` | `PROGRESS-rectification-post-adopt-verify-20260904.md` | 采用后核对题重复已问题;核对卡「这题跳过」等于整案停止、已采用仍念"选一个先用着"、前端"没有拿到下一个问题";采用旁白承诺的 holdout/OOS 核对与实际计划不同源(原任务书 §4 错误);家庭采集题带年份前缀自相矛盾 | 待执行 | |
| `TASK-rectification-post-adopt-verify-20260904.md` | `PROGRESS-rectification-post-adopt-verify-20260904.md` | 采用后核对题重复已问题;核对卡「这题跳过」等于整案停止、已采用仍念"选一个先用着"、前端"没有拿到下一个问题";采用旁白承诺的 holdout/OOS 核对与实际计划不同源(原任务书 §4 错误);家庭采集题带年份前缀自相矛盾 | 待验收 | `codex/rectification-post-adopt-verify-20260904`BUG-536539 |
| `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505509 |
### 聊天主链路与首页
@@ -5,7 +5,7 @@
- 基线:`origin/staging` @ `e27d5dc5`(含 `0be51e65` 补采集顺序修复、`a793edb1` BUG-533)。
- 开工分支:`codex/rectification-post-adopt-verify-20260904`worktree `.worktrees/rectification-post-adopt-verify-20260904`
- 串行关系:本任务书改 `method-followup.ts``answer-choice.ts``rectification-agentic-chat.tsx`,与当天其他校正任务书无并行;`TASK-rectification-collect-direction-fix-20260904.md` 已合入基线,不再冲突。
- BUG 编号起点:**BUG-535**(开工时再核对 `docs/BUG_HISTORY.md` 最大号)
- BUG 编号起点:任务书写 **BUG-535**;交付前 `origin/staging` 已占用该号(报告 chart-cap `61194690`),本单实落 **BUG-536539**
## 1. 事故实证(staging,真实案例,资料已抽象)
@@ -85,7 +85,7 @@
### 5.6 记录
- `docs/BUG_HISTORY.md` 从 BUG-535 起:A(重复核对题)、B(跳过=停止 + 已采用仍念采用提示 + 前端 unavailable)、C(旁白承诺与计划不同源,关联 BUG-519~523 并写明原任务书 §4 是根因)、D 前半(年份前缀,关联 BUG-414/418)。每条含现象、触发、根因、修复、验证、防复发。
- `docs/BUG_HISTORY.md` 从 BUG-536 起:A(重复核对题)、B(跳过=停止 + 已采用仍念采用提示 + 前端 unavailable)、C(旁白承诺与计划不同源,关联 BUG-519~523 并写明原任务书 §4 是根因)、D 前半(年份前缀,关联 BUG-414/418)。每条含现象、触发、根因、修复、验证、防复发。
- `docs/testing/rectification-post-adopt-verify-20260904.md`:真人清单——采用后核对题不重复;「这题跳过」出下一题;最后一题跳过后一行收尾且无重载按钮;家庭题无年份前缀;**答"没有"后直接出采用卡属预期**(写明原因:预算已满、家庭是最后一条必答线)。
- `CHANGELOG.md``docs/tasks/PROGRESS-rectification-post-adopt-verify-20260904.md`(含 tsc / lint / 测试六行汇总 + 退出码、无 Docker 时失败清单与基线逐条比对)。
@@ -107,4 +107,4 @@ npx tsx --test tests/*.test.ts 2>&1 | tail -8 # 记基线失败清单
## 8. BUG 编号起点
BUG-535(基线最大号 BUG-534)。
BUG-536539(交付前 `origin/staging` 已占用 BUG-535;基线任务书最大号曾是 BUG-534)。
@@ -0,0 +1,57 @@
# Staging 人肉复核 · 采用后核对线(2026-09-04
给产品负责人。实现者无法在真实校正会话里点选。测之前先做第 0 条。
本单对应 BUG-536~539。不要对照真实案例里的年份、事件或候选分钟写进任何记录。
## 0. 确认测的是新版本
浏览器打开 `https://staging.jyotisha.chat/api/health`,看 `deployment.gitCommit` 前 8 位是否等于本单合入 staging 后的提交。不一致 = 先别测。
状态栏在已采用之后仍应是「已采用 {时间} · 改选 / 之后新建对话即按此时间排盘。」不要找「用这个时间看盘」按钮,也不该出现咨询交接条。
## 1. 采用后核对题不重复已问过的采集题(P0 · BUG-536)
1. 新开生时校正,按平常走完采集与区分,直到出现采用卡。
2. 采用一个代表分钟。
- ✅ 预期:若还有核对题,题干**不是**采集阶段已经问过并回答过的那道带年份题(领域+年份同一条不得再出)。
- ❌ 改造前:采用后第一道核对几乎逐字重复采集阶段的职业题。
## 2. 「这题跳过」只跳本题(P0 · BUG-537)
若采用后出现反向核对选择卡:
1. 点「这题跳过」(不要点采用前区分题上的「先这样,先看当前范围」)。
- ✅ 预期:案例不停;不出现「可以从下面选一个先用着」;若还有下一道核对题,正文就是下一题,下方仍是选择卡。
- ❌ 改造前:整案当成停止,再念一遍采用提示,随后「没有拿到下一个问题。」和重新加载。
## 3. 最后一道核对跳过后安静收尾(P0 · BUG-537)
1. 把采用后剩余核对题都跳过,或跳过当时仅剩的最后一道。
- ✅ 预期:一行收尾「前事核对到这里。之后新建对话即按已采用时间排盘;对不上随时改选。」没有「没有拿到下一个问题。」、没有重新加载、没有转圈、没有咨询交接。状态栏仍是已采用那句。
- ❌ 改造前:收尾走问题缺口,强迫刷新。
## 4. 采用旁白不承诺不会问的核对(P1 · BUG-538)
1. 在探针池耗尽、采用卡刚出来时读旁白(点选最后一道区分题,或下面第 5 条的「没有」路径)。
- ✅ 预期:若采用后不会再问核对题,旁白说没有还能核对的前事,并提到之后新建对话即按此时间排盘。不得写「会拿某年某题 / 域外题来核对」。
- ✅ 预期:若旁白点了具体核对领域和年份,采用后第一道核对应是同一类题,而不是 holdout / 盘外核对。
## 5. 家庭采集题无年份前缀;答「没有」出采用卡是预期(P1 · BUG-539)
若走到家人结婚/添丁/住院这类口述采集:
1. 看题干。
- ✅ 预期:等于「家里如果有结婚、添丁或住院这类事,记得大概哪年就行。」**不要**以「某年前后,」开头。
- ❌ 改造前:既指定年份又让用户报年份。
2. 在已经答满区分预算、家人是最后一条必答覆盖线时,回答「没有」。
- ✅ 预期:同一轮出现采用卡。这是设计如此,不是漏问:这条线没有可用证据,覆盖闭合后剩余探针分不开候选。
- ❌ 不要把这条报成本单回归。本单不改这个收口。
## 6. 采用前区分题的停止按钮未改(回归)
若还走得到采用前的区分卡:
1. 停止槽位仍应是整案停下来看当前范围,而不是「这题跳过」。
- ✅ 预期:点了之后进入采用/区间,而不是跳到下一道核对题。