Files
Jyotisha/docs/tasks/TASK-rectification-post-adopt-verify-20260904.md
T
Jesse_Chen d4627a5019
Independent Staging Quality Gate / validate (push) Successful in 14m31s
Independent Staging Quality Gate / publish (push) Successful in 1m59s
fix(rectification): dedupe post-adopt verify and skip this probe only
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>
2026-09-04 21:51:59 +08:00

15 KiB
Raw Blame History

TASK · 采用后核对线:不重复已问题、「这题跳过」只跳本题、核对结束有收尾、采集题去年份前缀(2026-09-04)

0. 基线

  • 基线:origin/staging @ e27d5dc5(含 0be51e65 补采集顺序修复、a793edb1 BUG-533)。
  • 开工分支:codex/rectification-post-adopt-verify-20260904worktree .worktrees/rectification-post-adopt-verify-20260904
  • 串行关系:本任务书改 method-followup.tsanswer-choice.tsrectification-agentic-chat.tsx,与当天其他校正任务书无并行;TASK-rectification-collect-direction-fix-20260904.md 已合入基线,不再冲突。
  • BUG 编号起点:任务书写 BUG-535;交付前 origin/staging 已占用该号(报告 chart-cap 61194690),本单实落 BUG-536539

1. 事故实证(staging,真实案例,资料已抽象)

同一案例在采用代表分钟之后,四个现象连着出现。以下只写形状,不写年份、事件与候选分钟。

# 用户看到的 期望 定位(符号)
A 采用后第一道"核对题",是采集阶段已经问过并回答过的带年份职业题,题干几乎逐字重复 采用后核对只问还没问过的可计分前事 v9/method-followup.ts remainingReverseVerifyProbesL548571)过滤了拒答领域、probeYearAlreadyCovered、成年下限,没有askedKeysL554 起同函数的 remainingConflictProbes 用了 L612)。if (input.accepted) 分支(L1722)直接取第 0 条
B 点「这题跳过」后,机器人又说"剩下的题分不开当前候选……可以从下面选一个先用着。",随后界面出现"没有拿到下一个问题。"和重新加载按钮 跳过这一题 → 出下一道核对题;没有了 → 一句收尾,界面安静 v9/choice-card.ts L402/L549 与 components/rectification-agentic-chat.tsx L326:核对卡的 stop 按钮只是改了标签CHOICE_SKIP_QUESTION_LABEL,动作仍是 STOP_ACTIONv9/answer-choice.ts L265289 optionId==="stop"focusStatus:"skipped" + userStopped:truecore/rectification-decision.ts L276 userStoppedcompleteWithRangepublicCanAdopt 仍 true → answer-choice.ts L10351061 走 adoptionNarration(带 adoptCue),不看 case.acceptedTime;无下一焦点 → lib/rectification-surface-state.ts L250 rectificationQuestionGapState 走 preparing → unavailable
C 采用卡旁白承诺"采用后会拿 X 年的教育题以及家庭、财务、健康压力三类域外题来核对",采用后一道都没问 旁白只承诺服务端真的会问的题 v9/adopt-narration.ts L161173 post_adopt_verification = holdout 事件 + oos_blind_prompts;而采用后的计划(A 行)用的是 remainingReverseVerifyProbes,两者是两套数据源。这一段是 TASK-rectification-adopt-narration-20260904.md §4 我方规格错误,见 §3 决策 3
D 家庭采集题写成"YYYY 年前后,家里如果有结婚、添丁或住院这类事,记得大概哪年就行。"——既指定了年份又让用户报年份;答"没有"后直接收口 采集题不带年份前缀;"没有"后收口本身是设计如此(见 §2 D method-followup.ts L1941–1954 家庭轮转分支塞入 collectionYearFields(familyCollect)spokenFollowupForUser L9991001 把 year_label/probe_year 拼成"YYYY 年前后,"前缀到 USER_COLLECT_QUESTION.family

2. 根因

  • A:采用前的探针去重(askedProbeKeysFromReceiptaskedKeys)只接到了冲突探针与轮转,反向核对分支是 BUG-350 单独写的,漏接。引擎的 eventProbes 里同一 (domain, year) 的题在采集阶段已按 semantic_key 问过,采用后又被当"未核对"取出。
  • B:两层叠加。(1) 核对卡的「这题跳过」是纯标签替换,语义仍是"整案停止";(2) 结构化答案的旁白选择只看 nextAction.can_adopt,已采用后 publicCanAdopt 依旧 true,于是又念一遍"选一个先用着"。服务端没有"核对结束"这个状态,前端也没收到 next_user_action(仅 Agent 工具投影 mastra/rectification-v9-tools.ts L265/L316),只能把"无焦点 + 可续"当成"问题还没准备好"。
  • C:采用旁白的事实包按我方任务书 §4 用 holdout + OOS 生成,但 BUG-350 已定"采用后不得把不计分盘外核对当默认下一步",采用后真正会问的是 remainingReverseVerifyProbes 的结果。两个规格互相矛盾,旁白说的是不会发生的事。
  • D(前半,年份前缀)68f0759e 为了满足 BUG-414/418"带年份的口语问、不出无年份 D24 卡",把 dated 探针的年份塞进家庭采集题;USER_COLLECT_QUESTION.family 本身以"记得大概哪年就行"结尾,两者拼在一起就自相矛盾。
  • D(后半,答"没有"即收口)不是缺陷。该案例已答满 5 轮探针预算,relativesBLOCKING_COVERAGE_IDS 里最后一条未覆盖线;用户答"没有"记为该领域已回应,覆盖闭合、预算耗尽、剩余探针分不开候选,decideRectificationcompleteWithRange → 采用卡。这道题本来就是"开放报事"而不是选择题:用户若报出一件带年份的事,会作为新证据进入账本重算并可能再出区分题;答"没有"就等于这条线没有可用证据。本任务书不改这个收口逻辑,只把这个解释写进 docs/testing/ 清单,让真人验收知道预期。

3. 决策记录(产品负责人 2026-09-04 授权)

  1. 保留 BUG-350 的采用后设计:采用后默认只问最多 MAX_REVERSE_VERIFY计分核对题,不把 holdout / OOS 盘外核对当默认下一步。本任务书不推翻它。
  2. 反向核对必须做已问去重remainingReverseVerifyProbes 增加 askedKeys 过滤(semantic_key / candidate_split_hash / probe_id 三种键,与 remainingConflictProbes L612、L765–767 同口径)。已问过的题不得再作为核对题出现,无论它在采集阶段是被回答还是被跳过。
  3. 推翻 TASK-rectification-adopt-narration-20260904.md §4 的 post_adopt_verification 规格(该规格是我方错误):post_adopt_verification 改为由与采用后计划完全相同的过滤结果生成(同一函数、同一 askedKeys、同一 declined),输出 {domain, year_label};当结果为空,事实包必须给出 post_adopt_verification: [],旁白改说"采用后没有还能核对的前事,之后新建对话即按此时间排盘,对不上可改选。"(模板兜底与 Agent 版都要遵守;校验器把"会拿……核对"类承诺在空列表时判为 promise 失败)。holdout / OOS 只保留在决策收据里,不再进旁白。
  4. 核对卡的「这题跳过」只跳本题:新增 SKIP_PROBE_ACTION(或等价的 optionId:"skip_probe"),语义 = 该焦点 skipped + 该探针进 answered_probes(记 declined-for-verify,不计分)+ 不置 userStopped;随后按第 2 条重新取下一道核对题。采用前的区分题/采集题的停止按钮语义不变(仍是 STOP_ACTION)。
  5. 核对结束是显式状态:服务端在 GET 案例投影与结构化答题返回里都暴露 next_user_action(现在只有 Agent 工具投影有);已采用且没有下一道核对题时 next_user_action.id === "start_consultation"。前端收到该状态:不进 rectificationQuestionGapState 的 preparing/unavailable,不出重载按钮,只显示一行收尾文案(见第 6 条),状态栏保持 BUG-502 的"已采用 … · 改选 / 之后新建对话即按此时间排盘。",不加任何新按钮、不出 rectification-consult-handoffBUG-499/502 红线)。
  6. 已采用后的旁白不得再念采用提示answer-choice.ts 的旁白选择必须先看 case.acceptedTime;已采用时禁止 adoptionNarration / adoptCue / completedRangeNarration,改用一条收尾文案(放进 RECTIFICATION_USER_COPY,新键 postAdoptVerifyDone,措辞对照 frontend/docs/VOICE.md,建议:"前事核对到这里。之后新建对话即按已采用时间排盘;对不上随时改选。")。核对题被跳过但还有下一题时,旁白就是下一题,不加收尾。
  7. 采集题去掉年份前缀(产品决定,可否决)collectionYearFields 不再喂给采集题的口语期间;spokenFollowupForUsercollect_method_evidence 不拼"YYYY 年前后,"。BUG-418 的另一半(不出无年份 D24 选择卡)保留dated 探针的年份仍写进 probe_year 供计分与去重,只是不进用户题干。若产品负责人否决此条,执行方把本条标记 skipped 并保留现状,其余任务不受影响。
  8. 答"没有"即收口的逻辑不改(§2 D 后半);只补真人验收清单的说明。

4. 硬红线

  • BUG-350(采用后不默认 OOS/holdout)、BUG-463holdout 只在 not_started && !canAdopt)、BUG-499/502(无 handoff 组件、无「用这个时间看盘」按钮、状态栏文案不变)、BUG-518、BUG-520declinedDomains 只认 collect 意图)全部保留,任何一条被改都要先回到本任务书决策记录。
  • frontend/src/app/page.tsx 不增长;无第二套加载动画;核对结束状态不得出现 spinner / 骨架 / "正在加载"。
  • 不得把真实案例的年份、事件、候选分钟、案例 ID 写进测试、Bug 历史、进度记录、CHANGELOG;测试用抽象形状或公开名人数据。
  • 改任何既有断言写"原值 / 新值 / 原因"三栏;测试总数不低于基线实测。
  • 不顺手改 .gitea/workflows/**、不动迁移、不升级依赖。
  • Agent 旁白(adopt narration)的校验器只能收紧不能放宽。

5. 任务分解

5.1 反向核对去重(决策 2

  • remainingReverseVerifyProbes 增加 askedKeys 参数并在 if (input.accepted) 分支传入 L1554 的 askedKeys
  • 验收:新测试 tests/rectification-post-adopt-verify-20260904.test.ts
    • accepted:trueeventProbes 两条(domain X/year Y1domain Z/year Y2),askedProbeKeys 含第一条的 semantic_keynext_followup.domain === Z
    • 两条都在 askedProbeKeysnext_followup === nullbuildNextUserAction(...).id === "start_consultation"
    • askedProbeKeys 只含 candidate_split_hash 也能去重。
    • tests/rectification-eight-method.test.ts L1750/L1790 既有断言不变。

5.2 旁白事实包与采用后计划同源(决策 3)

  • adoptDeliveryFactspost_adopt_verification 改由 remainingReverseVerifyProbes(同 askedKeysdeclined)生成;类型改为 {kind:"reverse_verify"; domain; year_label};模板兜底与 Agent prompt 相应改口。
  • tests/rectification-adopt-narration-20260904.test.ts L684–688 三条断言改写,三栏说明:原值"holdout/oos 出现在 post_adopt_verification";新值"仅出现采用后真会问的 reverse_verify 项,且与 buildMethodFollowupPlan({accepted:true}) 首题一致";原因"决策 3 推翻原任务书 §4"。
  • 验收:新增用例——探针全部已问 → post_adopt_verification[]validateAdoptNarration 对含"会拿……核对"的文本返回 promise 失败;模板兜底输出含"没有还能核对的前事"。

5.3 「这题跳过」只跳本题(决策 4)

  • choice-action.ts 新增动作;choice-card.ts 核对卡(kind==="reverse_verify")的 stop 槽位改绑新动作,标签沿用 CHOICE_SKIP_QUESTION_LABELrectification-agentic-chat.tsx L326 同步;answer-choice.ts 新分支:focusStatus:"skipped"、探针写入 answered_probes(标记 skipped,不计分)、userStopped 不置位、随后跑 persistNextInterviewIfIdle 取下一题。
  • 验收:
    • 采用后有两道核对题 → 跳第一道 → 返回的下一焦点是第二道,sessionOutcome 不变为 user_stopped,旁白不含 adoptCue
    • 跳最后一道 → 无新焦点,next_user_action.id === "start_consultation",旁白等于 RECTIFICATION_USER_COPY.postAdoptVerifyDone
    • 采用前区分题的停止按钮仍触发 STOP_ACTION(既有测试不改)。

5.4 核对结束显式状态 + 前端安静收尾(决策 5、6)

  • GET 案例投影与结构化答题返回增加 next_user_actionrectificationQuestionGapState 增加输入 nextUserActionId:为 start_consultation 时返回新态 "verified_idle"(不 preparing、不 unavailable、不重试);组件在该态渲染一行 postAdoptVerifyDone,不渲染重载按钮。frontend/DESIGN.md 记录该态;VOICE.md 对照文案。
  • 验收:
    • tests/rectification-surface-state*.test.ts:已采用 + 无焦点 + start_consultationverified_idle;已采用 + 无焦点 + 无 next_user_action(旧数据)→ 仍走原路径(不回归);
    • 组件测试:verified_idle 下 DOM 无 rectification-question-gap__copy、无重载按钮、无 rectification-consult-handoff;状态栏文案与 BUG-502 断言一致。

5.5 采集题去年份前缀(决策 7,可否决)

  • spokenFollowupForUsercollect_method_evidence 不再拼期间前缀;家庭轮转分支保留 probe_year 但不填 year_label
  • 验收:tests/rectification-collect-direction-20260904.test.ts 中断言家庭题以"YYYY 年前后,"开头的用例(若有)三栏改写;新增:家庭采集题文本等于 USER_COLLECT_QUESTION.family,同时 probe_year > 0BUG-418 的"不出无年份 D24 卡"测试保持通过。

5.6 记录

  • 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.mddocs/tasks/PROGRESS-rectification-post-adopt-verify-20260904.md(含 tsc / lint / 测试六行汇总 + 退出码、无 Docker 时失败清单与基线逐条比对)。

6. 让步顺序

5.1 → 5.2 → 5.3 → 5.4 → 5.6 必做;5.5 可被产品否决或最后做。5.4 若前端投影改动超预期,允许先只做服务端 next_user_action 暴露 + 旁白收尾(决策 6),前端 verified_idle 拆到修复单,但必须在进度记录里写明,且 B 现象中的"没有拿到下一个问题"在此情况下不得写成已修。

7. 开工前置命令

git status -sb | head -1
git fetch origin --prune
git worktree add -b codex/rectification-post-adopt-verify-20260904 .worktrees/rectification-post-adopt-verify-20260904 origin/staging
cd .worktrees/rectification-post-adopt-verify-20260904/frontend && ln -s /workspace/Jyotisha/frontend/node_modules node_modules
grep -o "BUG-5[0-9][0-9]" ../docs/BUG_HISTORY.md | sort -u | tail -1
grep -n "BUG-350\|BUG-463\|BUG-499\|BUG-502\|BUG-518\|BUG-520" ../docs/BUG_HISTORY.md | head
npx tsx --test tests/*.test.ts 2>&1 | tail -8   # 记基线失败清单

8. BUG 编号起点

BUG-536539(交付前 origin/staging 已占用 BUG-535;基线任务书最大号曾是 BUG-534)。