From d4627a501924084f83e8375b3fa6dbdd6a89c124 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Fri, 4 Sep 2026 21:49:53 +0800 Subject: [PATCH] fix(rectification): dedupe post-adopt verify and skip this probe only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 4 + docs/BUG_HISTORY.md | 64 ++++++ ...ectification-post-adopt-verify-20260904.md | 103 +++++++++ docs/tasks/README.md | 2 +- ...ectification-post-adopt-verify-20260904.md | 6 +- ...ectification-post-adopt-verify-20260904.md | 57 +++++ frontend/DESIGN.md | 3 +- frontend/docs/VOICE.md | 1 + .../src/app/api/rectification/agent/route.ts | 21 +- .../api/rectification/cases/[caseId]/route.ts | 3 +- .../components/rectification-agentic-chat.tsx | 36 ++- .../components/rectification-choice-card.tsx | 6 +- .../lib/rectification-agentic/user-copy.ts | 2 + .../v9/adopt-narration-agent.ts | 5 +- .../v9/adopt-narration.ts | 53 +++-- .../rectification-agentic/v9/answer-choice.ts | 193 ++++++++++++++-- .../rectification-agentic/v9/choice-action.ts | 10 +- .../rectification-agentic/v9/choice-card.ts | 11 +- .../v9/interview-state.ts | 35 ++- .../v9/message-origin.ts | 2 +- .../v9/method-followup.ts | 67 +++++- .../lib/rectification-agentic/v9/turn-exit.ts | 4 +- .../rectification-agentic/v9/turn-question.ts | 7 +- .../src/lib/rectification-surface-state.ts | 6 +- .../application-billing-contract.test.ts | 5 +- ...ification-adopt-narration-20260904.test.ts | 20 +- .../tests/rectification-agentic-entry.test.ts | 5 +- .../tests/rectification-answer-choice.test.ts | 164 +++++++++++++- ...ication-collect-direction-20260904.test.ts | 24 ++ .../tests/rectification-collect-stall.test.ts | 7 +- .../rectification-decision-authority.test.ts | 6 +- .../tests/rectification-eight-method.test.ts | 8 +- ...ication-post-adopt-verify-20260904.test.ts | 207 ++++++++++++++++++ .../tests/rectification-server-focus.test.ts | 7 +- .../rectification-surface-contract.test.ts | 1 + .../tests/rectification-surface-state.test.ts | 1 + 36 files changed, 1065 insertions(+), 91 deletions(-) create mode 100644 docs/tasks/PROGRESS-rectification-post-adopt-verify-20260904.md create mode 100644 docs/testing/rectification-post-adopt-verify-20260904.md create mode 100644 frontend/tests/rectification-post-adopt-verify-20260904.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2256ea1a..db3a5164 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 印度占星 Skill 更新日志 +## 2026-09-04 — 采用后核对不再重复已问题,跳过只跳本题 + +采用代表分钟后,核对题会跳过采集阶段已经问过的前事。核对卡上的「这题跳过」只关掉这一题;没有下一题时收一行「前事核对到这里…」,不再念「选一个先用着」,也不再出现「没有拿到下一个问题」。采用旁白只承诺服务端真会问的核对;采集口语不再加「某年前后,」前缀。Skill 版本仍是 10.0.14。 + ## 2026-09-04 — 五主题完整报告不再因分盘张数上限装不出来 默认五章(含健康)会带上全部正式分盘。以前文档最多只许 6 张图,五章写完后整份报告仍会被判失败。现在张数上限跟着分盘枚举走。Skill 版本未变。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index c82982e5..f5bd157c 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -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 +- 复发自:无 +- 修复版本:待发布 diff --git a/docs/tasks/PROGRESS-rectification-post-adopt-verify-20260904.md b/docs/tasks/PROGRESS-rectification-post-adopt-verify-20260904.md new file mode 100644 index 00000000..dac88285 --- /dev/null +++ b/docs/tasks/PROGRESS-rectification-post-adopt-verify-20260904.md @@ -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-536~539**。交付前 `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-536~539,并 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-focus:102/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`、本文件 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 1cb602be..5013e7cd 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -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-527~530) | | `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-531~532) | -| `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-536~539) | | `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505~509) | ### 聊天主链路与首页 diff --git a/docs/tasks/TASK-rectification-post-adopt-verify-20260904.md b/docs/tasks/TASK-rectification-post-adopt-verify-20260904.md index 0ad3093c..033874fc 100644 --- a/docs/tasks/TASK-rectification-post-adopt-verify-20260904.md +++ b/docs/tasks/TASK-rectification-post-adopt-verify-20260904.md @@ -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-536~539**。 ## 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-536~539(交付前 `origin/staging` 已占用 BUG-535;基线任务书最大号曾是 BUG-534)。 diff --git a/docs/testing/rectification-post-adopt-verify-20260904.md b/docs/testing/rectification-post-adopt-verify-20260904.md new file mode 100644 index 00000000..de9f1773 --- /dev/null +++ b/docs/testing/rectification-post-adopt-verify-20260904.md @@ -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. 停止槽位仍应是整案停下来看当前范围,而不是「这题跳过」。 +- ✅ 预期:点了之后进入采用/区间,而不是跳到下一道核对题。 diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index eb92d2f9..51ab23b6 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -228,6 +228,7 @@ The birth-time rectification session is the consultation transcript plus a house | `question-live` | the asking message carries the embedded card or the spoken stem | enabled, placeholder points at the card | | `question-gap`, retries left | one timeline live row “正在准备下一个问题…”, refetching on a 2s timer up to two retries | enabled | | `question-gap`, retries spent | “没有拿到下一个问题。” and a 44px “重新加载” | enabled | +| `verified_idle` | one closing line `postAdoptVerifyDone` after adopted reverse-verify is exhausted; no spinner, no reload | enabled | | `choice-pending` | the answered card (`data-selected` fill, a top row “正在记录…”) and the same live row from “正在记录本次选择…” through the follow-up turn | disabled, stop visible | | `candidates` | the offer cards under the message that offered them | enabled | | `adopting` | “正在采用 HH:MM…” through the follow-up turn | disabled during the chain | @@ -472,7 +473,7 @@ or user IDs. Agent 的 live 标记只有 `InlineSpinner` 一种。曾经并存的 canvas 小球(`thinking-orbs`)已移除,不得再引入第二种 live 标记。 -校正面的所有等待复用行内等待:进入前的 hydration 在揭幕之前完成,进入后唯一的等待形态是时间线 live 行(含「正在准备下一个问题…」这一条独立 live 行)。 +校正面的所有等待复用行内等待:进入前的 hydration 在揭幕之前完成,进入后唯一的等待形态是时间线 live 行(含「正在准备下一个问题…」这一条独立 live 行)。采用后前事核对结束走 `verified_idle`:一行收尾文案,没有 live 行、没有重载。 首页只揭幕一次。揭幕前的加载屏分两阶段:先取账户、模型目录与会话列表,再并行取当前会话消息、推荐问题、今日星语与校正入口摘要,并预热校正分包;全部就绪或 4 秒预算到期(`BOOTSTRAP_PREPARE_TIMEOUT_MS`)才揭幕。揭幕后不得再出现任何阻塞等待或组件级 spinner:推荐问题未到显示安全默认问题,今日星语未到显示静态文案「今天的星语还没写出来。」(不带 `aria-busy`),校正卡用无摘要文案,内容到达后静默替换。切换到消息尚未缓存的会话时消息区留白并只给 `sr-only` 文案,不转圈;揭幕后按侧栏顺序后台预取最近 5 条会话(`SESSION_PREFETCH_COUNT`)让常见切换零等待。轨道环消失后不得再换一套动效继续等。 diff --git a/frontend/docs/VOICE.md b/frontend/docs/VOICE.md index 407f25fe..1458a7da 100644 --- a/frontend/docs/VOICE.md +++ b/frontend/docs/VOICE.md @@ -31,6 +31,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 | 我没能确定这句话是否在回答当前问题。请点选下面的选项,或换一种说法。 | 我不太确定这句是不是在回答上面的问题——点个选项,或者换个说法都行。 | 承认不确定,不训人。 | | 当前可信区间是 05:00–05:07,代表分钟 05:00。代表分钟只是代表性候选,不是已确认的唯一出生分钟。 | 已经从最初的 30 分钟收到 05:00–05:07 这 7 分钟。代表分钟是 05:00,这只是代表性候选,不是已确认的唯一出生分钟。 | 同一边界语义,带进度,少法务腔。 | | 可以从下面的时间里选一个采用。 | 可以从下面选一个先用着。 | 采用仍是采用,不说成已确认。 | +| 采用后会拿盘外核对来验证。 | 前事核对到这里。之后新建对话即按已采用时间排盘;对不上随时改选。 | 已采用且没有下一道核对题时用 `postAdoptVerifyDone`,不再念 `adoptCue`。 | | 当前问题已更新,请刷新后重新作答。 | 这一问刚换成新的,刷新后再答就行。 | 机器状态句改成下一步。 | | 好的,记下了。 | 2016 年 9 月上大学,记下了——这类有明确月份的节点对校正特别有用。 | 确认收到什么,并说清为什么有用。 | | 界面上继续有下一问。 | 接下来我们继续。 | 正文不得断言界面当前有没有题;题干在同一条消息里正文之后出现,过渡用中性句。 | diff --git a/frontend/src/app/api/rectification/agent/route.ts b/frontend/src/app/api/rectification/agent/route.ts index 3b0b4e47..4dd920d8 100644 --- a/frontend/src/app/api/rectification/agent/route.ts +++ b/frontend/src/app/api/rectification/agent/route.ts @@ -18,7 +18,7 @@ import { } from "@/lib/rectification-agentic/v9/answer-choice"; import { createAdoptNarrationWriter } from "@/lib/rectification-agentic/v9/adopt-narration-agent"; import { mapRectificationRpcError } from "@/lib/rectification-agentic/v9/case-service"; -import { CHOICE_ACTION, STOP_ACTION } from "@/lib/rectification-agentic/v9/choice-action"; +import { CHOICE_ACTION, SKIP_PROBE_ACTION, STOP_ACTION } from "@/lib/rectification-agentic/v9/choice-action"; import { runV9AgentTurn, type V9RunBilling } from "@/lib/rectification-agentic/v9/agent-run"; import { safePublicEvent } from "@/lib/rectification-agentic/v9/stream-mapping"; import { RECTIFICATION_SKILL_NAME, RECTIFICATION_SKILL_VERSION } from "@/lib/rectification-agentic/v9/case-status"; @@ -82,14 +82,14 @@ const agentRequestSchema = z.object({ caseId: z.string().uuid(), sessionId: z.string().uuid(), requestId: z.string().uuid(), - action: z.enum(["opening", "message", "read_only", "answer_choice", "stop_and_review"]), + action: z.enum(["opening", "message", "read_only", "answer_choice", "stop_and_review", "skip_probe"]), message: z.string().trim().min(1).max(4000).optional(), modelId: z.string().trim().min(1).max(64).optional(), actionId: z.string().uuid().optional(), focusId: z.string().uuid().optional(), questionId: z.string().trim().min(1).max(200).optional(), probeId: z.string().trim().min(1).max(200).nullable().optional(), - optionId: z.enum(["A", "B", "C", "D"]).optional(), + optionId: z.enum(["A", "B", "C", "D", "skip_probe"]).optional(), expectedRevision: z.number().int().min(0).max(10_000).optional(), origin: z.enum([ "typed", @@ -300,11 +300,19 @@ export async function POST(request: Request) { caseId, sessionId, actionId, - action: action === "stop_and_review" ? STOP_ACTION : CHOICE_ACTION, + action: action === "stop_and_review" + ? STOP_ACTION + : action === "skip_probe" + ? SKIP_PROBE_ACTION + : CHOICE_ACTION, focusId, questionId: parsed.data.questionId, probeId: parsed.data.probeId ?? null, - optionId: action === "stop_and_review" ? "stop" : parsed.data.optionId!, + optionId: action === "stop_and_review" + ? "stop" + : action === "skip_probe" + ? "skip_probe" + : parsed.data.optionId!, expectedRevision, narrateAdopt, }); @@ -327,6 +335,7 @@ export async function POST(request: Request) { sourceQuote: applied.sourceQuote, derivedContext: applied.derivedContext, nextAction: applied.nextAction, + next_user_action: applied.nextUserAction, nextInterviewPersisted: applied.nextInterviewPersisted, nextChoiceReady: applied.nextChoiceReady, }); @@ -600,7 +609,7 @@ export async function POST(request: Request) { ); return response; } - if (action === "answer_choice" || action === "stop_and_review") { + if (action === "answer_choice" || action === "stop_and_review" || action === "skip_probe") { return NextResponse.json( { error: "选择题处理失败", message: "请稍后重试。" }, { status: 500 }, diff --git a/frontend/src/app/api/rectification/cases/[caseId]/route.ts b/frontend/src/app/api/rectification/cases/[caseId]/route.ts index fa295d94..7b702002 100644 --- a/frontend/src/app/api/rectification/cases/[caseId]/route.ts +++ b/frontend/src/app/api/rectification/cases/[caseId]/route.ts @@ -14,7 +14,7 @@ import { type ConversationFocusList, type V9CaseDossier, } from "@/lib/rectification-agentic/v9/tool-service"; -import { choiceCardFromCaseDossier, decideFromDossier, overlayPublicDecision } from "@/lib/rectification-agentic/v9/interview-state"; +import { choiceCardFromCaseDossier, decideFromDossier, overlayPublicDecision, nextUserActionFromDossier } from "@/lib/rectification-agentic/v9/interview-state"; import { projectCurrentQuestion } from "@/lib/rectification-agentic/v9/turn-decision"; import { attachQuestionsToTurns, attachOfferResultToTurns } from "@/lib/rectification-agentic/v9/turn-question"; import { previousInferenceFromReceipt } from "@/lib/rectification-agentic/v9/inference-adapter"; @@ -147,6 +147,7 @@ export function dossierResponse( }, current_question: projectCurrentQuestion(dossier.conversationSummary.activeFocus), choice_card: choiceCardFromCaseDossier(dossier), + next_user_action: nextUserActionFromDossier(dossier), question_source: questionSourceFromFocusList(listed), }; } diff --git a/frontend/src/components/rectification-agentic-chat.tsx b/frontend/src/components/rectification-agentic-chat.tsx index 918f03d9..15cd4fc6 100644 --- a/frontend/src/components/rectification-agentic-chat.tsx +++ b/frontend/src/components/rectification-agentic-chat.tsx @@ -70,12 +70,14 @@ import { import { userFacingRunFailure, isIncompleteRunBanner } from "@/lib/rectification-agentic/v9/run-diagnostic"; import { CHOICE_ACTION, + SKIP_PROBE_ACTION, STOP_ACTION, isStructuredChoiceUserText, shouldContinueAfterStructuredChoice, stableChoiceActionKey, type ChoiceOptionId, } from "@/lib/rectification-agentic/v9/choice-action"; +import { RECTIFICATION_USER_COPY } from "@/lib/rectification-agentic/user-copy"; import { isRectificationCaseStatus, isResumableStatus, type RectificationCaseStatus } from "@/lib/rectification-agentic/v9/case-status"; import { CHOICE_MODE, CHOICE_SKIP_QUESTION_LABEL, CHOICE_SKIP_QUESTION_MESSAGE, CHOICE_STOP_LABEL, CHOICE_STOP_MESSAGE, isPersistedFocusId, parseRectificationChoiceCard, type ChoiceKey, type RectificationChoiceCard as ChoiceCardModel } from "@/lib/rectification-agentic/v9/choice-card"; import type { PublicLanguageModel } from "@/lib/public-models"; @@ -272,7 +274,7 @@ function mergeTurnQuestions(current: RenderMessage[], turns: readonly unknown[]) function markQuestionAnswered( current: RenderMessage[], focusId: string, - selected: ChoiceKey | "stop" | "typed", + selected: ChoiceKey | "stop" | "skip_probe" | "typed", ): RenderMessage[] { return current.map((message) => { const question = message.question; @@ -329,6 +331,7 @@ function choiceCardFromQuestion( probe_id: question.probe_id, case_revision: null, focus_id: question.focus_id, + ...(live?.skip_this_probe ? { skip_this_probe: true } : {}), }; } @@ -418,8 +421,14 @@ type CaseSnapshotState = Readonly<{ caseStatus: RectificationCaseStatus | null; savedTime: string | null; savedStatus: "accepted" | "confirmed" | null; + nextUserActionId: string | null; }>; +function nextUserActionIdFromSnapshot(payload: RectificationCaseSnapshotPayload | null): string | null { + const id = payload?.next_user_action?.id; + return typeof id === "string" && id.trim() ? id.trim() : null; +} + /** The snapshot that arrived with the reveal, as initial state; nothing is fetched on mount. */ function caseSnapshotState(payload: RectificationCaseSnapshotPayload | null): CaseSnapshotState | null { if (!payload) return null; @@ -433,6 +442,7 @@ function caseSnapshotState(payload: RectificationCaseSnapshotPayload | null): Ca caseStatus: isRectificationCaseStatus(payload.case?.status) ? payload.case.status : null, savedTime: confirmedTime ?? acceptedTime, savedStatus: confirmedTime ? "confirmed" : acceptedTime ? "accepted" : null, + nextUserActionId: nextUserActionIdFromSnapshot(payload), }; } @@ -470,6 +480,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { const [currentQuestion, setCurrentQuestion] = useState(() => caseSnapshotState(initialSnapshot)?.question ?? null); const [questionSource, setQuestionSource] = useState<"focus" | "unavailable" | null>(() => caseSnapshotState(initialSnapshot)?.questionSource ?? null); const [caseStatus, setCaseStatus] = useState(() => caseSnapshotState(initialSnapshot)?.caseStatus ?? null); + const [nextUserActionId, setNextUserActionId] = useState(() => caseSnapshotState(initialSnapshot)?.nextUserActionId ?? null); const [caseSnapshotLoaded, setCaseSnapshotLoaded] = useState(initialSnapshot !== null); const [questionRetryAttempts, setQuestionRetryAttempts] = useState(0); const [openingRequested, setOpeningRequested] = useState(false); @@ -628,6 +639,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { choice_card?: unknown; turns?: unknown; question_source?: unknown; + next_user_action?: { id?: unknown }; case?: { status?: unknown; accepted_time?: unknown; @@ -643,11 +655,15 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { : null; const acceptedTime = typeof payload.case?.accepted_time === "string" ? payload.case.accepted_time : null; const confirmedTime = typeof payload.case?.confirmed_time === "string" ? payload.case.confirmed_time : null; + const nextActionId = typeof payload.next_user_action?.id === "string" + ? payload.next_user_action.id.trim() + : ""; setCandidateResult(nextCandidate); setCurrentQuestion(nextQuestion); setQuestionSource(questionSourceFromSnapshot(payload.question_source)); setChoiceCard(nextChoice); setCaseStatus(nextCaseStatus); + setNextUserActionId(nextActionId || null); setCaseSnapshotLoaded(true); if (confirmedTime) { setSavedTime(confirmedTime); @@ -1051,8 +1067,8 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { }, []); const submitStructuredChoice = useCallback(async ( - action: typeof CHOICE_ACTION | typeof STOP_ACTION, - optionId: ChoiceKey | "stop", + action: typeof CHOICE_ACTION | typeof STOP_ACTION | typeof SKIP_PROBE_ACTION, + optionId: ChoiceKey | "stop" | "skip_probe", override?: Readonly<{ focusId: string; questionId: string | null; @@ -1124,6 +1140,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { return; } const willContinue = shouldContinueAfterStructuredChoice(payload?.nextAction, payload); + const payloadNextActionId = typeof payload?.next_user_action?.id === "string" + ? payload.next_user_action.id.trim() + : ""; + if (payloadNextActionId) setNextUserActionId(payloadNextActionId); onCompleted?.(); const snapshot = await loadCaseSnapshot(); const turns = snapshot?.turns ?? []; @@ -1392,6 +1412,7 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { questionMissing: currentQuestion === null, questionLoadFailed: questionSource === "unavailable", offerAwaitingReader: showSelectionCards && !candidateResult?.selectedTime, + nextUserActionId, busy, readonly, regenerating: regeneratingMessageKey !== null, @@ -1447,6 +1468,10 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { function submitStop() { if (!choiceCard) return; + if (choiceCard.skip_this_probe) { + void submitStructuredChoice(SKIP_PROBE_ACTION, "skip_probe"); + return; + } void submitStructuredChoice(STOP_ACTION, "stop"); } @@ -1622,6 +1647,11 @@ export function RectificationAgenticChat(props: RectificationAgenticChatProps) { )} + {questionGap === "verified_idle" && ( +

+ {RECTIFICATION_USER_COPY.postAdoptVerifyDone} +

+ )} {savedTime && savedStatus === "confirmed" && (

已确认校正时间:{savedTime} diff --git a/frontend/src/components/rectification-choice-card.tsx b/frontend/src/components/rectification-choice-card.tsx index 9c0d611d..ec8dcdf6 100644 --- a/frontend/src/components/rectification-choice-card.tsx +++ b/frontend/src/components/rectification-choice-card.tsx @@ -12,7 +12,7 @@ type RectificationChoiceCardProps = Readonly<{ card: ChoiceCard; pending: boolean; disabled: boolean; - selectedKey?: ChoiceKey | "stop" | ""; + selectedKey?: ChoiceKey | "stop" | "skip_probe" | ""; onSelect: (key: ChoiceKey) => void; onStop: () => void; variant?: "card" | "embedded"; @@ -20,7 +20,7 @@ type RectificationChoiceCardProps = Readonly<{ }>; export function RectificationChoiceCard(props: RectificationChoiceCardProps) { - const [localSelected, setLocalSelected] = useState(""); + const [localSelected, setLocalSelected] = useState(""); const selectedKey = props.selectedKey || localSelected; const answered = Boolean(selectedKey); @@ -77,7 +77,7 @@ export function RectificationChoiceCard(props: RectificationChoiceCardProps) {