Files
Jyotisha/docs/tasks/TASK-rectification-stop-rescore-fix-20260907.md
T

92 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` L162183)要求 `decision_receipt.inference_state` 存在且 `candidates` 非空、`revision`/`candidate_set_id` 与最新 transition 一致;否则抛 `candidate_state_inconsistent` |
## 2. 根因
### 2.1 BUG-581P0):停止路径用的是"裸重算",把推断态整个丢了
BUG-579 让 STOP / 无焦点停止 / 闲置持久化在快照过期时先调 `rescoreStaleMinuteSnapshotIfNeeded``rescoreMinuteAfterWindowChange``block-scan-answer.ts` L160–200)。这个函数是为"时段选定 / 放宽窗口"写的:只 `runV9CandidateScore` 然后 `persistV9Candidate`**不调 `buildCaseInferenceState`,不带 `previous`,不写 `inference_state`,不写 transition**。而正规比较路径 `rectification-v9-tools.ts::scoreAndPersistCurrentEvidence`L8791190)会用上一份 `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-582P1):没有探针支撑的"区分题"退化成开场句
`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 P3holdout 改成采集题后排在带年月采集之前
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)"
```
## 验收(Claude2026-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` 验收段。