From b1ab7ee90be3313493ff4d2714f15991f201b0a7 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Fri, 4 Sep 2026 04:03:57 +0000 Subject: [PATCH] docs(tasks): rectification adopt narration brief Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu --- docs/tasks/README.md | 1 + ...-rectification-adopt-narration-20260904.md | 110 ++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 docs/tasks/TASK-rectification-adopt-narration-20260904.md diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 77b378d4..29c59cd7 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -44,6 +44,7 @@ | `TASK-rectification-adopt-flow-20260902.md` | — | 采用流程整治 | 已验收 | `35e5781e`(BUG-497~500) | | `TASK-rectification-adopt-flow-fix-20260903.md` | — | 采用流程修复单(含删「用这个时间看盘」) | 已验收 | `e8c98c37`(BUG-501/502) | | `TASK-rectification-uncertainty-stop-20260903.md` | — | 不确定度停止规则加样本下限 | 已验收 | `0c0df426`(BUG-503) | +| `TASK-rectification-adopt-narration-20260904.md` | — | 探针池耗尽时采用卡旁白「继续往下收」+ 已丢弃探针绕过 BUG-472 早退 + 区分题答否关线 | 待执行 | 基线 `4d0a67a2`,BUG-516 起 | | `TASK-rectification-ux-20260902.md` | `PROGRESS-rectification-ux-20260903.md` | 会话面空白假死与交互摩擦 | 已验收 | `d159f08e`(09-03 在新基线重做后合入,BUG-505~509) | ### 聊天主链路与首页 diff --git a/docs/tasks/TASK-rectification-adopt-narration-20260904.md b/docs/tasks/TASK-rectification-adopt-narration-20260904.md new file mode 100644 index 00000000..ec133143 --- /dev/null +++ b/docs/tasks/TASK-rectification-adopt-narration-20260904.md @@ -0,0 +1,110 @@ +# 任务书 · 采集题答「没有」后旁白说「继续往下收」却直接出采用卡(2026-09-04) + +基线:`origin/staging` `4d0a67a2`。本单只改 TypeScript 决策/落焦点层与测试,不改 Python 引擎、不改 Skill 包、不动确认门。与 `TASK-rectification-uncertainty-stop-20260903.md`(BUG-503,已合入)无文件级冲突。 + +## 0. 真实环境复现(用户 09-04 staging 反馈,事实) + +用户答完 5 道区分题(D9 风格、D10 风格、两道事业年份题、一道搬家年份题)后,系统出了一道口述采集题: + +> 2021 年前后,家里如果有结婚、添丁或住院这类事,记得大概哪年就行。 + +用户回「没有」。系统回一句: + +> 我按现有材料继续往下收。 + +然后**同一轮**直接出现「当前可能的出生时间」采用卡:05:00(相对支持度 21)/ 05:06(18)/ 04:53(9),composer 没有任何问题。GET 返回 `interview.type = offer_provisional_range`、`session_outcome = adopt_representative`、`precision_stage = ready_to_adopt`、`stop_reason = null`、`current_question = null`、`question_source = "focus"`。 + +用户原话:"这个问题我回答的没有 结果就直接弹出卡片了 但是不应该继续往下提问或者走流程吗"、"弹出的时候也不明不白的"、"在这个阶段不应该继续引导收敛吗"。 + +## 1. 事实核对(用本案 decision receipt 在本地重放,不是猜) + +用本案 `latest_result.decisionReceipt` 原样喂给 `decideFromDossier` → `publicNextAction` → `persistNextInterviewAfterChoice`(fake accounting),结论与线上一致: + +1. **决策层是对的。** `inference_state` 里 14 条探针,已答 5 条;剩余 9 条对活跃集合 {05:00, 05:06, 04:53} 的 `expected_outcomes` 全部把 05:00 与 05:06 放在同一侧(或根本不含活跃候选): + - 两道 `relationship.2024.known_event_quality`:`candidate_ids` 只覆盖窗口两端(04:45–04:51、05:08–05:15),不含任何活跃候选。 + - `career.2023.dasha_activation`:yes/no 都是三个活跃候选同进同退 → `no_split_among_active`。 + - `varga.d24 / d12 / d7 / d4 / d5` 五条无年份对比:至多把 04:53 单独分出来,05:00 与 05:06 永远同组;且按 BUG-444 规则作 `yearless_ungrounded_contrast` 丢弃。 + - `nakshatra-boundary`:yes 同时支持三个活跃候选 → 无区分力。 + 于是 `decideRectification` 走 `!separation.sufficient → 无 probe → holdout 因 canAdopt 已真而跳过(BUG-463 决策)→ 无 dated collect → completeWithRange("offer")`,得到 `offer_provisional_range / adopt_representative / ready_to_adopt`。**引擎手里已经没有任何一道能把 05:00 和 05:06 分开的题**;receipt `reasons` 里也写着 `adjacent_minutes_indistinguishable`。这一步不是 bug,是设计:先给范围与代表分钟,采用后再用保留的 holdout 事件和 OOS 三条线(family / finance / health_pressure)核对。 + +2. **旁白与落焦点层错了,而且错在 BUG-472 修过的那条路上。** 链路如下(符号定位,行号会漂): + - `v9/method-followup.ts` `buildMethodFollowupPlan` 的 nakshatra 分支(`if (!next && input.holdoutValidation !== "not_started" && !datedMethodCollectOpen(methods) && input.nakshatraProbe)`)只看「有没有 nakshatra 探针」,**不看决策层是否已把它丢弃**。`rectificationFollowupCatalog` 把 `inference.probes` 里的 nakshatra 探针原样传进来,于是计划产出一个 `intent=distinguish_candidates / source=nakshatra_boundary / domain="appearance" / choice_frame=null` 的 followup;因 `sessionOutcome=adopt_representative` 被放进 `deferred_followup`。 + - `v9/answer-choice.ts` `interviewToPersist` 取 `next_followup ?? deferred_followup`,`isRemainingDiscriminatorFollowup` 见 `source=nakshatra_boundary` 返回 true → `shouldSkipFollowupPersist` 返回 **false** → **BUG-472 加的「已可采用就不再落下一问、改出 adoptHostNarration」早退被绕过**。 + - 接着 `persistFocusAfterChoice` → `server-focus.ts` `persistServerOwnedFocus`:无 `choice_frame` 的 distinguish → `persistSpokenChoiceFallback` → `persistCollectFocus`,要写一个 `collect:appearance:collect_method_evidence` 焦点,题干是开场通用题「从你最容易想起来的一件事开始就好……」。 + - `persistableFocusDomain("appearance")` 落到兜底 `return domain`,而 `agentic_rectification_conversation_focus.target_domain` 的 check 只允许 `education/career/relationship/relocation/finance/health/family/other` → 写库失败(非 idempotency 冲突)→ `status: "skipped"` → 重试一次仍 skipped → `open` 为 null → followup 既非 collect 也无 frame → 落到最后一行 `spokenFollowupForUser(followup) ?? RECTIFICATION_USER_COPY.hostNarrationFallback` = **「我按现有材料继续往下收。」** + - 本地重放(fake accounting 不做 check 约束)得到的是更糟的形状:真的落下了 `collect:appearance` 焦点并把**开场题**再问一遍,同时采用卡照出。两种结果都是错的;线上只是碰巧被 DB 约束挡成了空旁白。 + +3. **`rectification-collect-stall.test.ts` 的「live five-evidence case opens adopt」为什么没拦住**:该 fixture 的 `inference_state.probes` 里没有 nakshatra 探针、receipt 里没有 `window_scan`,所以计划 `deferred_followup=null`,早退成立,断言 `/可以从下面选一个先用着/` 通过。真实 receipt 两者都有。 + +4. **潜在的第二个问题(本案未改变结局,列为 P2)**:`choice-action.ts` `focusStatusForAnswer` 把区分题选 C「明确没有发生」记为 `declined`;`v_declined_skipped` 投影不带 question_id;`method-followup.ts` `declinedDomains` 把任何 declined 焦点的 `target_domain` 当成「用户拒答该领域」。本案用户对事业 2023 题、搬家 2015 题都选了 C(这两个答案**都参与了计分**,把 05:00 从 23 压到 21、04:53 从 7 抬到 9),却顺带把 career、relocation 两条线整体标成 declined:后续 `remainingConflictProbes`、D10/D4 分盘观察分支、`d10_refine / d4_refine` 精度阶段分支都会因 `declined.has(domain)` 跳过。用本案 receipt 重放,declined 只留 family 与留 {career, relocation, family} 决策相同,所以本案没有被它改写;但换一个还有事业年份题可问的案子,一次「没发生」就会把整条线关掉。 + +## 2. 决策记录(产品授权) + +1. **采用门语义不变。** 候选分不开、探针池空时先出范围 + 代表分钟 + 采用卡,采用后再核对(BUG-463 / BUG-472 / BUG-501 的既定设计)。本单**不**把 holdout / OOS 提前到采用前——引擎剩余探针与保留事件都不能把相邻 6 分钟分开,提前问只是表演。若产品负责人希望改成「采用前先问保留事件」,另开任务书并在此处覆盖本条。 +2. 用户看到的应当是 **BUG-472 设计的交付旁白**(`deliveryAdoptNarration`:「已经从最初的 … 收到 05:00–05:06 … 代表分钟是 05:00 … 可以从下面选一个先用着。」),并且在同一句里说清**为什么停**:剩下的问题分不开 05:00 和 05:06。 +3. 「没有」= 对这一道题的否定,不等于「这条线以后都别问」。仅当焦点本身是**采集题**(`collect_method_evidence`)时,否定才算拒答该领域;对计分区分题的否定只计分、不关线。这条覆盖 BUG-440 里「答 C declined 计分」的表述中「declined 即覆盖」的那一半:D12 同领域卡「没有」仍计分,但 career/relocation 等**有年份**的区分题答「没有」不再写成领域拒答。 + +## 3. 硬红线 + +1. 不改 `decideRectification` 分支顺序、`publicCanAdopt`、`sessionOutcomeAllowsAdopt`、确认门、`MIN_SEPARATION_LEAD`、`minUncertaintyAnswers`、`maxPlateauRounds`。 +2. 不改 Python 引擎、不 bump Skill 版本(保持 10.0.14)、不改探针身份 / hash / `dropped_probes` reason 集合。 +3. 不得为了让 `collect:appearance` 能落库而放宽 DB check;`appearance / marks / horary / nakshatra` 这类非证据领域不得成为口述采集焦点。 +4. 不得再新增一处「先落焦点、失败了再兜底旁白」的路径;早退判定只在 `shouldSkipFollowupPersist` 一处。 +5. 测试总数不得低于 `origin/staging` `4d0a67a2` 的实测;改既有断言写「原值 / 新值 / 原因」。 + +## 4. 任务分解 + +### 4.1 P0 · 已丢弃的区分探针不得绕过采用早退(BUG-516) + +1. `buildMethodFollowupPlan` 的 nakshatra 分支必须与决策层同一口径:只有 `nakshatraProbeIfFollowupCanAsk` 会选中的探针才产出 followup。实现方式二选一,实现者定并写进 BUG 条目:(a) `rectificationFollowupCatalog` 传入的 `nakshatraProbe` 先过 `inspectDiscriminatorProbes`(同 `askedKeys / topCandidateTimes`),不可渲染即传 null;(b) 分支内 `makeFollowup(..., true, true)` 产出无 `choice_frame` 时直接丢弃并进 `dropped_probes`。**两种都要保证** `plan.deferred_followup` 不再含无 `choice_frame` 的 `distinguish_candidates`。 +2. `answer-choice.ts` `isRemainingDiscriminatorFollowup`:无 `choice_frame` 的 distinguish followup 不算「剩余区分题」(它本来就渲染不出来)。`interviewToPersist` 对 `deferred_followup` 只在其可渲染时才取。 +3. `server-focus.ts` `persistableFocusDomain`:非白名单领域返回 `null`(不得返回 `appearance / horary / nakshatra`);`persistSpokenChoiceFallback` 只允许 `collectQuestionDomain` 后仍在白名单内的领域,否则返回 `status: "skipped"` 且**不**触发 `persistFocusAfterChoice` 的二次重试。既有断言 `persistableFocusDomain("horary") === "horary"` 改为 `null`:原值 `"horary"` / 新值 `null` / 原因:DB check 不含 horary,原断言锁的是一条必然写库失败的路径。 +4. 交付旁白带停止原因:`adoptHostNarration` / `deliveryAdoptNarration` 在 `stopReason` 为空但 `nextAction.type === "offer_provisional_range"` 且决策层 `droppedProbes` 非空、`probe` 为 null 时,在进度句之前加一句(放 `user-copy.ts`,进 `listUserVisibleCopy`):`剩下的问题已经分不开 05:00 和 05:06,先给范围。`(分钟由 `credible_range` 填入;两端相同则只说代表分钟)。`decisionReceipt` 已有 `dropped_probes`,不需要新字段;若需要在 `publicNextAction` 增加只读 `exhausted_probe_pool: boolean`,允许。 +5. 采用卡出现的那一轮,`current_question` 必须为 null 且不得有任何 `set_agentic_rectification_conversation_focus` 调用(BUG-472 断言原样搬到新 fixture)。 + +验收: +- 新测试 `frontend/tests/rectification-adopt-narration-20260904.test.ts`:fixture 复刻本案形状——五件已确认证据(education×1 作 holdout、relationship×2、career×2)、`inference_state` 含 14 条探针(其中 nakshatra 一条、五条 yearless varga、一条 dasha_activation、两条 candidate_ids 不含活跃候选的 known_event_quality)、5 条 answered、活跃 {05:00, 05:06, 04:53} 分数 21/18/9、receipt 含 `window_scan.d*_candidates_differ=true`、`declined_skipped_topics=[family]`。断言:`decideFromDossier` 为 `offer_provisional_range / adopt_representative / canAdopt=true / probe=null`;`plan.next_followup` 与 `plan.deferred_followup` 均为 null 或均带 `choice_frame`;`persistNextInterviewAfterChoice` 返回 `persisted=false`、`choiceReady=false`、`hostNarration` 匹配 `/分不开 05:00 和 05:06/` 且 `/可以从下面选一个先用着/`、不匹配 `/继续往下收/`;`accounting.calls` 无 `set_agentic_rectification_conversation_focus`。 +- 同一 fixture 走 `applyCollectFocusDenial`(family 采集焦点 + 否定)得到同样旁白。 +- `rectification-server-focus`:`persistableFocusDomain("appearance") === null`、`("horary") === null`、`("nakshatra") === null`;对 `domain="appearance"` 的 distinguish followup(无 frame)调用 `persistServerOwnedFocus` 返回 `skipped` 且零次 RPC。 +- `rectification-collect-stall` 的「live five-evidence case」fixture 补上 nakshatra 探针与 `window_scan`,原断言不变仍需通过(这是回归锁)。 +- `docs/BUG_HISTORY.md` 新增 BUG-516:复发自 BUG-472(同一早退被 nakshatra deferred followup 绕过);相关 BUG-440、BUG-444、BUG-463。 + +### 4.2 P2 · 区分题答「明确没有发生」不再把整条领域标成拒答(BUG-517) + +1. `focusStatusForAnswer` 保持返回 `declined`(DB 状态语义与 `uncertaintyHighFromAnswers` 的 `classified_from="declined"` 计数都依赖它,不动)。 +2. 改消费端:`method-followup.ts` `declinedDomains` 只把 **`intent === "collect_method_evidence"`** 的 declined/skipped 焦点算成领域拒答;`intent === "distinguish_candidates"` 的 declined 只是那一题的答案。`v_declined_skipped` 投影已带 `intent` 字段,不需要动迁移。 +3. `dossierWithClosedFocus`(`answer-choice.ts`)追加的行必须带 `intent`,否则消费端无法区分。 +4. `domainCollectFocusAsked` 语义不变。 + +验收: +- `rectification-collect-stall`「denying the dated family collect declines relatives」原断言不变(采集题否定仍算拒答)。 +- 新增:同一 dossier,把 `declined_skipped_topics` 换成 `[{ target_domain: "career", intent: "distinguish_candidates", status: "declined" }]`,`buildMethodFollowupPlan` 的 `methods` 中 `d10_career` 不得因此变成 `covered`,且 career 域的剩余有年份探针仍可被 `remainingConflictProbes` 选中。 +- 用本案 receipt 形状重放,declined 为 `[family(collect)]` 与 `[family(collect), career(distinguish), relocation(distinguish)]` 两种输入,决策与旁白相同(本案不受影响,作为不变量锁住)。 +- `docs/BUG_HISTORY.md` 新增 BUG-517,关联 BUG-440(说明 BUG-440 的「答 C declined 计分」为什么当时对 D12 同领域卡成立,而对有年份区分题不成立)。 + +## 5. 让步顺序 + +1. P0 全做。若 4.1.4 的停止原因句与 BUG-503 已加的 `stopReasonPrefix` 排版冲突,先保留 BUG-503 的原因句,新句放其后,不得删任何一句。 +2. P2 若时间不够可拆成 `-fix` 单独做,但 BUG-517 条目必须以 `investigating` 状态先写进 `docs/BUG_HISTORY.md`。 +3. 任何一步需要放宽采用门 / 确认门才能通过测试,立即停手写 `BLOCKED.md`。 + +## 6. 开工前置命令 + +```bash +git fetch origin --prune +git worktree add -b codex/rectification-adopt-narration-20260904 .worktrees/rectification-adopt-narration-20260904 origin/staging +cd .worktrees/rectification-adopt-narration-20260904/frontend +./node_modules/.bin/tsc --noEmit +npx tsx --test tests/rectification-collect-stall.test.ts tests/rectification-server-focus.test.ts tests/rectification-provisional-adopt.test.ts tests/rectification-adopt-flow-fix-20260903.test.ts +npx tsx --test tests/*.test.ts 2>&1 | tail -12 # 记下开工时总数与失败清单 +``` + +验收命令(交付前):`tsc --noEmit` 0 错;`npm run lint` 0 error;`tests/rectification-*.test.ts tests/agentic-rectification-*.test.ts tests/birth-time-rectification-contract.test.ts` fail=0;全量 `tests/*.test.ts` 失败清单与开工基线逐条一致;`next build` 后 `/` 仍 Static。 + +## 7. BUG 编号起点 + +开工时核对 `docs/BUG_HISTORY.md` 最大号;截至本单撰写为 **BUG-515**,本单从 **BUG-516** 起。 + +## 8. 真实环境复核清单(实现者做不了的,交给产品负责人) + +见 `docs/testing/`(实现者补一条 `rectification-adopt-narration-20260904.md`):在 staging 新开一个校正会话,走到探针池耗尽;预期采用卡出现的那一轮旁白说出「分不开 A 和 B,先给范围……可以从下面选一个先用着」,composer 旁有「先这样」而没有再冒出开场题;采用后进入核对题(BUG-501/502 流程)。