# TASK · 生时校正修复单:停止后重算丢掉全部答题、采用报 `candidate_state_inconsistent`;无探针的区分题退化成开场句(2026-09-07) - 基线:`origin/staging` @ `63eba420`(代码头 `4e0db55f`,staging 已部署 `84a29754`,事故就是这版跑出来的) - 分支:`codex/rectification-stop-rescore-fix-20260907` - 执行方:coding agent;验收:Claude - 涉及文件:`frontend/src/lib/rectification-agentic/v9/block-scan-answer.ts`(`rescoreMinuteAfterWindowChange`)、`frontend/src/mastra/rectification-v9-tools.ts`(`scoreAndPersistCurrentEvidence` 抽出)、`v9/tool-service.ts`、`v9/server-focus.ts`(`persistSpokenChoiceFallback`)、`v9/method-followup.ts`(`spokenCollectFallbackFollowup`、`precisionStageFollowup`、holdout 排序) - BUG 编号起点:**BUG-581**(开工时 `grep -o "^## BUG-5[0-9][0-9]" docs/BUG_HISTORY.md | tail -1` 复核) - 优先级:**P0**(采用按钮点了就报错;BUG-579 的重算把用户答过的题全部作废) ## 1. 事故实证(staging `84a29754`,2026-09-07 第三次实测;只写结构) | # | 现象 | 代码定位 | | --- | --- | --- | | 1 | 五张卡答完,旁白说范围已收到 X–Y(11 分钟内的更窄一段);健康拒答后助手问回开场那句"从你最容易想起来的一件事开始就好——比如哪年上的大学…"(`GENERIC_COLLECT_QUESTION` 原文) | 见 §2.2 | | 2 | 用户点「先这样,先看当前范围」→ 旁白"再问下去也分不开 A 和 B…已从 30 分钟收到 X'–Y' 这 11 分钟"——**范围比答完最后一张卡时反而变宽了**,还多出一个原本已被淘汰、在可信区间之外的分钟 | 见 §2.1 | | 3 | 候选卡出现 4 张,"相对支持度 16 / 13 / 12 / 8"——这是引擎裸结果的 `relative_support`,不是答题后的 posterior(答题后应是 21 / 18 / 9 一类) | 同上 | | 4 | 点任一"采用此时间" → `{"error":"候选状态已更新,请刷新后重试","code":"candidate_state_inconsistent"}` | 采用 RPC `accept_agentic_rectification_candidate_for_case_v2`(`20260906020000_adopted_credible_range.sql` L162–183)要求 `decision_receipt.inference_state` 存在且 `candidates` 非空、`revision`/`candidate_set_id` 与最新 transition 一致;否则抛 `candidate_state_inconsistent` | ## 2. 根因 ### 2.1 BUG-581(P0):停止路径用的是"裸重算",把推断态整个丢了 BUG-579 让 STOP / 无焦点停止 / 闲置持久化在快照过期时先调 `rescoreStaleMinuteSnapshotIfNeeded` → `rescoreMinuteAfterWindowChange`(`block-scan-answer.ts` L160–200)。这个函数是为"时段选定 / 放宽窗口"写的:只 `runV9CandidateScore` 然后 `persistV9Candidate`,**不调 `buildCaseInferenceState`,不带 `previous`,不写 `inference_state`,不写 transition**。而正规比较路径 `rectification-v9-tools.ts::scoreAndPersistCurrentEvidence`(L879–1190)会用上一份 `inference_state` 作 `previous`,按 `semantic_key` 重放已答探针,再把 `inference_state` 放进 receipt。 后果链:停止 → 裸重算 → 最新结果没有 `inference_state` → (a) 投影退回引擎裸 `relative_support`,范围变宽、被淘汰分钟复活、卡片数字变成 16/13/12/8;(b) 采用 RPC 校验 `inference_state` 为空 → `candidate_state_inconsistent`。同样的裸重算也用于时段选定和放宽窗口之后,那两条路的采用同样会报错(尚未实测,按代码必然)。 ### 2.2 BUG-582(P1):没有探针支撑的"区分题"退化成开场句 `buildMethodFollowupPlan` 在几个分支会给出 **frameless** 的 `distinguish_candidates` followup(`precisionStageFollowup` 的 `lagna_frame` 分支 `domain: null`;`varga_observation` 分支;`else if (precisionCard) next = precisionCard`),它们没有 `choice_frame`。`persistServerOwnedFocus` 对无框区分题走 `persistSpokenChoiceFallback` → `spokenCollectFallbackFollowup`:`intent` 改成采集、`domain = collectQuestionDomain(null) = "other"`、`source = "method_coverage"`。`spokenFollowupForUser` 看到 `domain=other && source=method_coverage && !collect_retry` 判成 **开场**(`openingOther`),题干就是 `GENERIC_COLLECT_QUESTION`;`rectification-v9-tools.ts` L1447 的 `USER_COLLECT_QUESTION[""] ?? GENERIC_COLLECT_QUESTION` 是同一句的第二条来路。这就是"死问题"再次出现的机制,与 BUG-558 的 `other` 兜底无关,是另一条路。 ### 2.3 P3:holdout 改成采集题后排在带年月采集之前 BUG-580 把 holdout 改为 `collect_method_evidence`,但它在计划里仍排在 `nextDatedCollectFollowup` 之前,所以这轮先问了健康(oos 领域)再轮到搬家;健康拒答后搬家没被问到就进了 §2.2 的分支。顺序应当是:带年月采集(`DATED_COLLECT_ORDER`)→ 职业 → holdout。 ## 3. 决策记录 1. **只有一条重算路径。** 把 `scoreAndPersistCurrentEvidence` 从工具闭包抽到 `tool-service.ts`(或新文件 `v9/rescore.ts`),`rescoreMinuteAfterWindowChange` 与 `rescoreStaleMinuteSnapshotIfNeeded` 都调它。同窗口重算带 `previous`(重放已答探针);窗口变了(时段选定 / 放宽)不带 `previous`,但仍要写出新的 `inference_state`(空答案)与 transition。 2. 采用 RPC 的校验不放宽。 3. **无框区分题不得持久化为口述题。** `persistSpokenChoiceFallback` 只在 `followup.domain` 落在 `USER_COLLECT_QUESTION` 且 `intent` 原本就是采集时才落地;无框的 `distinguish_candidates` 一律 `skipped`。计划层:`precisionStageFollowup` / `varga_observation` 分支只在 `choice_frame` 存在时才当 `next`,否则记进 `dropped_probes(reason="frameless_distinguish")` 并继续往下走(带年月采集 → 职业 → holdout → 交付)。`spokenFollowupForUser` 的 `openingOther` 判定加 `followup.source === "method_coverage" && followup.method_id === "dasha_events" && !evidence.some(isConfirmedDated)`,即只有真正的开场才出 GENERIC;`rectification-v9-tools.ts` L1447 的 GENERIC 兜底删除,域不在表里就返回 `invalid_spoken_prompt` 不落地。 4. holdout 采集排到 `nextDatedCollectFollowup` 与职业之后。 5. 不动采用门、确认门、`MIN_SEPARATION_LEAD`、`_relative_support`、`minute_step=1` 指纹。 ## 4. 任务分解 ### 4.1 BUG-581 - 抽出并复用 `scoreAndPersistCurrentEvidence`;`rescoreMinuteAfterWindowChange(…, { keepAnswers: boolean })`:停止/闲置 → `keepAnswers=true`(`previous` = 上一份 inference);时段/放宽 → `false`。 - 验收:`rectification-stale-compare-fix-20260907.test.ts` 扩——(a) 5 条已答探针 + 快照过期 → STOP 后 `latest_result.decisionReceipt.inference_state.answered_probes.length === 5`,`credible_range` 与停止前一致或更窄,候选 `posterior_score` 不是引擎裸支持度;(b) 采用 RPC 夹具(`database-rectification-*.test.ts`,Docker)在 STOP 重算后能通过 `inference_state` 校验;(c) 时段选定 / 放宽后 `inference_state` 存在且 `answered_probes` 为空,采用不报 inconsistent。 ### 4.2 BUG-582 - 决策 3 三处改动;删除 GENERIC 兜底。 - 验收:形状用例——7 件事、家人/健康拒答、职业未问、`precision_stage=lagna_frame` 或 d4 观察分歧、无可渲染探针 → 下一问是搬家采集或职业口述,**不是** `GENERIC_COLLECT_QUESTION`;`persistSpokenChoiceFallback` 对 `domain=null` 的区分题返回 `skipped`;`agent-voice-copy-contract` 增加"GENERIC 只允许出现在 `collect:other:*` 开场焦点"。 ### 4.3 P3 顺序 - holdout 后移;验收:健康 oos 领域存在且搬家未问时,下一问是搬家。 ### 4.4 记录 - `docs/BUG_HISTORY.md` BUG-581(复发自 BUG-579)、BUG-582(复发自 BUG-558:同一"死问题"现象、不同来路);`CHANGELOG.md`;`PROGRESS-rectification-stop-rescore-fix-20260907.md`;`docs/testing/rectification-scenarios-20260907.md` 第 0 条加"停止后卡片数字必须等于答题后的支持度,且点采用成功"。 ## 5. 让步顺序 4.1 先做、当天部署;4.2 必做;4.3 可后置;4.4 不可省。 ## 6. 开工前置命令 ```bash git fetch origin --prune git worktree add -b codex/rectification-stop-rescore-fix-20260907 .worktrees/rectification-stop-rescore-fix-20260907 origin/staging cd .worktrees/rectification-stop-rescore-fix-20260907 ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules ln -s /workspace/Jyotisha/.venv .venv cd frontend && ls tests/rectification-*.test.ts tests/consultation-*.test.ts tests/agent-voice-copy-contract.test.ts | grep -v database | xargs npx tsx --test 2>&1 | grep -E "^# (tests|pass|fail)" ``` ## 验收(Claude,2026-09-07,`origin/staging` @ `ab57d03f`;与区间交付卡同一提交) | 门 | 结果 | | --- | --- | | tsc | **1 错**(在同提交的区间交付卡改动里:`rectification-agentic-chat.tsx` L1655 先访问 `candidateResult.resultId` 再判空,`'candidateResult' is possibly 'null'`)。执行方进度记录写 tsc 0,与实测不符——应是最后一次改动后没有复跑 | | lint | 0 error / 103 warning(比上一轮多 19 条,分散在测试文件与 `rectification-v9-tools.ts`,未清) | | 前端 rectification + consultation + birth-time + skill + voice(非 DB) | 1605 / 0 | | Python v5_services / input_contract / growth contract | 全绿 | | 引擎 `minute_step=1` 字节级不变 | 全等 | | 项 | 结论 | | --- | --- | | 4.1 BUG-581 | 通过。`scoreAndPersistCurrentEvidence` 抽到 `v9/score-persist.ts`,停止/闲置 `keepAnswers=true` 带 `previous` 重放;时段/放宽 `keepAnswers=false` 仍写出空答案的 `inference_state`;VedAstro 校验与对比包逻辑随之搬迁 | | 4.2 BUG-582 | 通过。无框区分题一律 `skipped`;工具层与 `spokenFollowupForUser` 的 GENERIC 兜底删除;`dropped_probes(frameless_distinguish)` 记录 | | 4.3 顺序 | 通过。holdout 排在带年月采集与职业之后 | | 4.4 记录 | 通过 | 结论:本单逻辑通过,但同一提交因区间交付卡的 tsc 错误**整体不可部署**,见 `TASK-rectification-range-delivery-card-20260907.md` 验收段。