d4627a5019
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>
15 KiB
15 KiB
TASK · 采用后核对线:不重复已问题、「这题跳过」只跳本题、核对结束有收尾、采集题去年份前缀(2026-09-04)
0. 基线
- 基线:
origin/staging@e27d5dc5(含0be51e65补采集顺序修复、a793edb1BUG-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;交付前
origin/staging已占用该号(报告 chart-cap61194690),本单实落 BUG-536~539。
1. 事故实证(staging,真实案例,资料已抽象)
同一案例在采用代表分钟之后,四个现象连着出现。以下只写形状,不写年份、事件与候选分钟。
| # | 用户看到的 | 期望 | 定位(符号) |
|---|---|---|---|
| A | 采用后第一道"核对题",是采集阶段已经问过并回答过的带年份职业题,题干几乎逐字重复 | 采用后核对只问还没问过的可计分前事 | v9/method-followup.ts remainingReverseVerifyProbes(L548–571)过滤了拒答领域、probeYearAlreadyCovered、成年下限,没有用 askedKeys(L554 起同函数的 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_ACTION;v9/answer-choice.ts L265–289 optionId==="stop" → focusStatus:"skipped" + userStopped:true;core/rectification-decision.ts L276 userStopped → completeWithRange → publicCanAdopt 仍 true → answer-choice.ts L1035–1061 走 adoptionNarration(带 adoptCue),不看 case.acceptedTime;无下一焦点 → lib/rectification-surface-state.ts L250 rectificationQuestionGapState 走 preparing → unavailable |
| C | 采用卡旁白承诺"采用后会拿 X 年的教育题以及家庭、财务、健康压力三类域外题来核对",采用后一道都没问 | 旁白只承诺服务端真的会问的题 | v9/adopt-narration.ts L161–173 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 L999–1001 把 year_label/probe_year 拼成"YYYY 年前后,"前缀到 USER_COLLECT_QUESTION.family |
2. 根因
- A:采用前的探针去重(
askedProbeKeysFromReceipt→askedKeys)只接到了冲突探针与轮转,反向核对分支是 BUG-350 单独写的,漏接。引擎的eventProbes里同一 (domain, year) 的题在采集阶段已按semantic_key问过,采用后又被当"未核对"取出。 - B:两层叠加。(1) 核对卡的「这题跳过」是纯标签替换,语义仍是"整案停止";(2) 结构化答案的旁白选择只看
nextAction.can_adopt,已采用后publicCanAdopt依旧 true,于是又念一遍"选一个先用着"。服务端没有"核对结束"这个状态,前端也没收到next_user_action(仅 Agent 工具投影mastra/rectification-v9-tools.tsL265/L316),只能把"无焦点 + 可续"当成"问题还没准备好"。 - C:采用旁白的事实包按我方任务书 §4 用 holdout + OOS 生成,但 BUG-350 已定"采用后不得把不计分盘外核对当默认下一步",采用后真正会问的是
remainingReverseVerifyProbes的结果。两个规格互相矛盾,旁白说的是不会发生的事。 - D(前半,年份前缀):
68f0759e为了满足 BUG-414/418"带年份的口语问、不出无年份 D24 卡",把 dated 探针的年份塞进家庭采集题;USER_COLLECT_QUESTION.family本身以"记得大概哪年就行"结尾,两者拼在一起就自相矛盾。 - D(后半,答"没有"即收口):不是缺陷。该案例已答满 5 轮探针预算,
relatives是BLOCKING_COVERAGE_IDS里最后一条未覆盖线;用户答"没有"记为该领域已回应,覆盖闭合、预算耗尽、剩余探针分不开候选,decideRectification走completeWithRange→ 采用卡。这道题本来就是"开放报事"而不是选择题:用户若报出一件带年份的事,会作为新证据进入账本重算并可能再出区分题;答"没有"就等于这条线没有可用证据。本任务书不改这个收口逻辑,只把这个解释写进docs/testing/清单,让真人验收知道预期。
3. 决策记录(产品负责人 2026-09-04 授权)
- 保留 BUG-350 的采用后设计:采用后默认只问最多
MAX_REVERSE_VERIFY道计分核对题,不把 holdout / OOS 盘外核对当默认下一步。本任务书不推翻它。 - 反向核对必须做已问去重:
remainingReverseVerifyProbes增加askedKeys过滤(semantic_key/candidate_split_hash/probe_id三种键,与remainingConflictProbesL612、L765–767 同口径)。已问过的题不得再作为核对题出现,无论它在采集阶段是被回答还是被跳过。 - 推翻
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 只保留在决策收据里,不再进旁白。 - 核对卡的「这题跳过」只跳本题:新增
SKIP_PROBE_ACTION(或等价的optionId:"skip_probe"),语义 = 该焦点skipped+ 该探针进answered_probes(记 declined-for-verify,不计分)+ 不置userStopped;随后按第 2 条重新取下一道核对题。采用前的区分题/采集题的停止按钮语义不变(仍是STOP_ACTION)。 - 核对结束是显式状态:服务端在 GET 案例投影与结构化答题返回里都暴露
next_user_action(现在只有 Agent 工具投影有);已采用且没有下一道核对题时next_user_action.id === "start_consultation"。前端收到该状态:不进rectificationQuestionGapState的 preparing/unavailable,不出重载按钮,只显示一行收尾文案(见第 6 条),状态栏保持 BUG-502 的"已采用 … · 改选 / 之后新建对话即按此时间排盘。",不加任何新按钮、不出rectification-consult-handoff(BUG-499/502 红线)。 - 已采用后的旁白不得再念采用提示:
answer-choice.ts的旁白选择必须先看case.acceptedTime;已采用时禁止adoptionNarration/adoptCue/completedRangeNarration,改用一条收尾文案(放进RECTIFICATION_USER_COPY,新键postAdoptVerifyDone,措辞对照frontend/docs/VOICE.md,建议:"前事核对到这里。之后新建对话即按已采用时间排盘;对不上随时改选。")。核对题被跳过但还有下一题时,旁白就是下一题,不加收尾。 - 采集题去掉年份前缀(产品决定,可否决):
collectionYearFields不再喂给采集题的口语期间;spokenFollowupForUser对collect_method_evidence不拼"YYYY 年前后,"。BUG-418 的另一半(不出无年份 D24 选择卡)保留:dated 探针的年份仍写进probe_year供计分与去重,只是不进用户题干。若产品负责人否决此条,执行方把本条标记 skipped 并保留现状,其余任务不受影响。 - 答"没有"即收口的逻辑不改(§2 D 后半);只补真人验收清单的说明。
4. 硬红线
- BUG-350(采用后不默认 OOS/holdout)、BUG-463(holdout 只在
not_started && !canAdopt)、BUG-499/502(无 handoff 组件、无「用这个时间看盘」按钮、状态栏文案不变)、BUG-518、BUG-520(declinedDomains只认 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:true、eventProbes两条(domain X/year Y1,domain Z/year Y2),askedProbeKeys含第一条的semantic_key→next_followup.domain === Z; - 两条都在
askedProbeKeys→next_followup === null且buildNextUserAction(...).id === "start_consultation"; askedProbeKeys只含candidate_split_hash也能去重。tests/rectification-eight-method.test.tsL1750/L1790 既有断言不变。
- 给
5.2 旁白事实包与采用后计划同源(决策 3)
adoptDeliveryFacts的post_adopt_verification改由remainingReverseVerifyProbes(同askedKeys、declined)生成;类型改为{kind:"reverse_verify"; domain; year_label};模板兜底与 Agent prompt 相应改口。tests/rectification-adopt-narration-20260904.test.tsL684–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_LABEL;rectification-agentic-chat.tsxL326 同步;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_action;rectificationQuestionGapState增加输入nextUserActionId:为start_consultation时返回新态"verified_idle"(不 preparing、不 unavailable、不重试);组件在该态渲染一行postAdoptVerifyDone,不渲染重载按钮。frontend/DESIGN.md记录该态;VOICE.md对照文案。 - 验收:
tests/rectification-surface-state*.test.ts:已采用 + 无焦点 +start_consultation→verified_idle;已采用 + 无焦点 + 无next_user_action(旧数据)→ 仍走原路径(不回归);- 组件测试:
verified_idle下 DOM 无rectification-question-gap__copy、无重载按钮、无rectification-consult-handoff;状态栏文案与 BUG-502 断言一致。
5.5 采集题去年份前缀(决策 7,可否决)
spokenFollowupForUser对collect_method_evidence不再拼期间前缀;家庭轮转分支保留probe_year但不填year_label。- 验收:
tests/rectification-collect-direction-20260904.test.ts中断言家庭题以"YYYY 年前后,"开头的用例(若有)三栏改写;新增:家庭采集题文本等于USER_COLLECT_QUESTION.family,同时probe_year > 0;BUG-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.md、docs/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-536~539(交付前 origin/staging 已占用 BUG-535;基线任务书最大号曾是 BUG-534)。