Files
Jyotisha/docs/tasks/TASK-rectification-adopt-flow-20260902.md
T
Jesse_Chen 8db71aaf81 docs: product-level README, AGENTS.md split into code/reading parts, add CLAUDE.md, move task briefs to docs/tasks
- README.md is now the product/repo front door (architecture, repo map,
  local dev, test tiers, delivery flow, doc map). Engine positioning,
  VedAstro/Codex setup and the oracle/benchmark command reference move
  verbatim to docs/engine/README.md, docs/engine/vedastro-gateway.md and
  docs/benchmark/README.md. Capability badges realigned with the registry
  (91/78/8/0); tests/test_readme_badges.py was red on staging.
- AGENTS.md: Part A (environment truth, delivery, worktrees, record
  placement, bug workflow, growth freeze, frontend red lines, privacy,
  pre-work check, test tiers) and Part B (reading-rigor constraints).
  GitHub issue-tracker/triage boilerplate removed: GitHub is a read-only
  mirror. All strings locked by tests/ are preserved.
- CLAUDE.md added: roles, three working modes, task-brief sections,
  acceptance criteria, session discipline; imports AGENTS.md.
- 50 tracked TASK-*/PROGRESS-* files and 3 never-committed briefs move to
  docs/tasks/ with an index; REPO_LAYOUT.md merged into README.

Docs-only change (no gated path touched).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
2026-09-03 06:56:06 +00:00

107 lines
14 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.
# 任务书 · 采用流程整治:采集阶段不出采用卡、采用后核对题可答、采用卡沉淀进历史、运行可观测(2026-09-02)
基线:`origin/staging` `8dfe457f``TASK-rectification-question-ownership-fix-20260902.md` 的实现,已验收)。本任务书来自真实环境 case `bf7a8de6-4873-4e74-94fe-bfaa3b368e32` 的两份 GET 快照与用户操作记录,覆盖两组问题:**A. 采集阶段同时出现采集题和三张"采用此时间"卡****B. 点"采用此时间"之后卡片消失、弹出一道不可答的题和一个裸按钮、运行 178 秒**。两组问题的根都在"卡片/题目/采用状态各自为政",一起修。
## 0. 真实环境复现(事实,不要重新猜)
快照一(采用前,`case_revision 5`):
- `interview.session_outcome = "collect_evidence"``propose_allowed=false``can_adopt=true``selection_allowed=true``current_question = probe:family.2021``collect_spoken`active)。
- 候选 05:06 / 04:53 / 05:03 支持度 14 / 13 / 13`margin_percent 2.45``unique_top.passed=false``diagnostic_quality.passed=false`
- 界面:最新助手消息里是采集题"2021 年前后,家里……"**同一条消息下面**是"当前可能的出生时间"+ 三张"采用此时间"卡。
用户点了 05:06 的"采用此时间",随后:
- 三张卡消失(续跑期间 `busy`),列表末尾出现"用这个时间看盘"按钮。
- 续跑 turn `3fe40677``started_at 13:27:05 → completed_at 13:30:03`178 秒,上限 210 秒),`tool_activities`: read-case completed → compare-candidates completed → **set-focus failed**;正文"前面几轮的作答都记下了,候选之间的差异在一步步变清楚。我们继续——……"(没有一句承认已采用)。
- 该 turn 的 `question``question_id reverse_verify:education_style:score``kind reverse_verify`,四个选项,status active;题干是服务端模板原文。
- 快照二 `choice_card: null``current_question.kind = "choice"`。前端嵌入卡 `disabled`,用户"这个选择题没法选择"。卡片底部仍显示"先这样,先看当前范围"。
- `probe:family.2021` 被静默标成 `skipped``persistNextInterviewIfIdle``isStalePreAdoptFocus` 分支)。
## 1. P0-A · `collect_evidence` 阶段出采用卡(BUG-313 / BUG-323 复发)
**根因**`core/rectification-decision.ts``collect()``capability`(含 `canAdopt`)原样展开进 decision`publicDecisionFields` 再把 `canAdopt` 直接投影成 `can_adopt`。于是 `session_outcome=collect_evidence``can_adopt` 仍是 true。前端 `showSelectionCards``rectification-agentic-chat.tsx`)只看 `selectionAllowed && canAdopt && canRenderRectificationSelectionCards && !showLiveChoiceCard && !busy``showLiveChoiceCard` 只在 AD 卡活着时为真,采集题(`collect_spoken`)不算,卡片就露出来了。v9 删除 `rectification-offer-candidates` 门后,BUG-313/323 的防复发条款失效。
**修法**
1. 服务端单一真值:`publicDecisionFields``can_adopt = decision.canAdopt && ADOPT_OUTCOMES.has(decision.sessionOutcome)``ADOPT_OUTCOMES = {adopt_representative, provisional_range_user_stopped, awaiting_confirmation}``validated_range` / `exact_minute_confirmed` / `completed_with_range` 按现有语义处理,不得新增放行)。`collect_evidence` / `discriminate_candidates` / `validate_holdout` 一律 false。内部 `capability.canAdopt` 保留给 decision 逻辑用,不外泄。
2. `POST /candidates/accept` 复用同一判断:`can_adopt=false` 时返回 `409 { error: "adoption_not_allowed", session_outcome }`,不调 RPC。
3. 前端 `parseRectificationCandidateResult``session_outcome` 解析进 `RectificationCandidateResult.sessionOutcome``showSelectionCards` 追加 `ADOPT_OUTCOMES.has(sessionOutcome)`(双保险,前端不得单靠 `selection_allowed`)。
4. `collect_evidence` / `discriminate_candidates``credible_range` 非空时,只允许一行只读文案(如"目前范围 04:5305:06,还在收窄"),放在 `RectificationCandidateCards` 同位置,无按钮、无支持度数字。
5. 采集题(`collect_spoken`)也要有"先这样,先看当前范围"的出口:composer 旁加一个次要按钮,走现有 `stop` 动作(`CHOICE_STOP_MESSAGE`),服务端把当前 collect focus 标 `skipped``userStopped=true`,进入 `provisional_range_user_stopped` 后才出卡。
## 2. P0-B · 采用后的核对题永远不可答
**根因**:采用后服务端建核对 focus 用的是 `method-followup.ts` `input.accepted` 分支(`method_id "reverse_verify"``ask_theme``education_style`),`stableFollowupQuestionId` 落成 `reverse_verify:education_style:score`。之后每次 GET / set-focus 重算 followup 时,active focus 已是 reverse_verify,走的是"承接当前焦点"分支(`keepAcceptedFocus``makeFollowup({ method_id: "active_focus", ask_theme: "active_focus", ... }, true, keepChoice)`),`choice_frame.question_id` 变成 `active_focus:active_focus:score``projectRectificationChoiceCard` 对没有 `semantic_key` / `candidate_split_hash` 的 followup 要求 `activeFocus.questionId === frame.question_id`,不等 → 返回 null → GET `choice_card: null` → 前端 `liveQuestion=false` → 嵌入卡 `disabled`。同一 id 不一致极可能也是本轮 `set-focus failed``probe_mismatch`)的原因。
**修法**
1. `keepAcceptedFocus` 分支(以及 `out_of_sample_check` 同路径)承接时**沿用已持久化 focus 的 `questionId``ask_theme``domain`**`makeFollowup` 前先从 `focus.questionId` / `focus.expectedAnswerSchema` 还原 `method_id`/`ask_theme`reverse_verify 的 question_id 形状是 `${method_id}:${ask_theme}:score`),保证 `stableFollowupQuestionId(keepNext) === focus.questionId`。不要靠放宽 `projectRectificationChoiceCard` 的校验来绕过——校验是防串题的,要保留。
2. `distinguish_candidates` 的 keep 分支不动(它有 `semantic_key`),但补一条断言测试防止同类回归。
3. set-focus 在 active focus 为 reverse_verify 时:`validateSpokenPrompt``questionId` 比对必须能通过(同上),且题干年份要求按 `probe_year`(核对探针有 year,如 2016)执行。
4. 前端 `liveQuestion``kind === "reverse_verify"` 的题也走同一条件;`choiceCardFromQuestion``scoring: question.kind !== "reverse_verify"` 语义保留。
## 3. P1 · 采用卡沉淀进历史、采用后的界面形态
现状:三张卡永远锚在 `latestSettledAssistant``busy` 时隐藏;采用后的续跑把它们抹掉,跑完又出现在新消息下(05:06 标"已采用")——再次与核对题并存。"用这个时间看盘"是 `savedStatus==="accepted"` 时挂在列表末尾的裸按钮(`rectification-consult-handoff`)。
**目标形态**(产品拍板):
1. 出卡的那条消息**拥有**这组卡:`RenderMessage` 增加 `candidateOffer`result_id + 三候选 + 出卡时的 session_outcome),出卡时写入该消息,刷新后由 `turns[].receipt` / `latest_result.resultId` 重建(服务端 GET 给每条 assistant turn 补 `offer_result_id`,或在 turn receipt 里记 `candidates.offered`)。卡只在拥有它的消息里渲染,不随最新消息漂移。
2. 点"采用"后,这组卡原地变成结算态:05:06 "已采用",其余两张保留"改选为此时间";不再在后续消息里重复出卡。
3. 采用后在会话顶部(或 composer 上方)出一条常驻状态条:"已采用 05:06 · 范围 04:53–05:06 · 改选 · 用这个时间看盘"。"用这个时间看盘"从列表末尾移进状态条;`rectification-consult-handoff` 删除。
4. 核对题(reverse_verify / out_of_sample_check)的卡片不显示"先这样,先看当前范围",改为"这题跳过"(走现有 `skipped`)。
5. 采用瞬间被 `isStalePreAdoptFocus` 关掉的采集题:对应消息里的题干保留、卡片灰掉并标"已跳过(已采用 05:06)",不能像现在这样无痕消失。
## 4. P1 · 采用后的续跑话术与 set-focus 失败
1. 续跑(`send("read_only","")`)的 Agent 正文必须承认已采用并说明接下来是核对:`method-followup.ts` accepted 分支的 `user_prompt_hint` 已写"当前排盘已采用……",但 `active_focus` 承接分支的 hint 没有;两处统一,并在 `agentic-rectification.ts` 指令里加一条"case.accepted_time 非空时,正文第一句要说明已按该时间采用、现在在核对"。
2. §2 修好后,在 `rectification-question-ownership.test.ts` 追加"采用后续跑:Agent 只传 spokenPrompt + 服务端 questionId → set-focus `ok:true`、focus id 不变、选项 4 个"。
3. set-focus `failed` 的 receipt 目前只有 `safeErrorCode`;把 `reason``year_missing` / `probe_mismatch` 等)也写进 `tool_activities[].detail`,后台可查。
4. 同一 case 的两处旁证一并处理:
- 开场轮 `rectification-set-focus` 也是 `failed`,题目由服务端兜底模板建立,`question_id = collect:unknown:collect_method_evidence`domain 落成 `unknown`)。用第 3 条的 `reason` 落库后查明失败原因;开场采集题的兜底 domain 不得是 `unknown`——`spokenCollectFallbackFollowup` / `persistableFocusDomain` 在开场时应取 followup 轮转的第一个领域(如 `education`)。
- 第三轮正文"选一个最贴近你实际情况的就行,答不上来也可以选「一时说不好」":没有承接用户刚说的 2024 年感情事件,还预告了选项。属 prompt 层:`agentic-rectification.ts` 指令里"正文不要提问、不要复述选项"要补"必须先用一句话承接用户本轮给出的事实(年份+事件)",VOICE.md 坏例加一条"预告选项"。不做运行时正文字符串校验。
## 5. P2 · 运行可观测与超时
178 秒的运行从回执上分不出时间花在哪(`tool_activities` 只有 tool/status/methods)。
1. `tool_activities[]` 增加 `started_at` / `elapsed_ms``rectification-compare-candidates` 额外记引擎各段耗时(engine compare / vedastro-validate / persist),来源用 `engine-client.ts` 已有的调用边界,不改 Python。
2. 前端时间线:某一步超过 45 秒时,状态文案从"读取校正记录/正在分析"切成"引擎在比较候选,可能需要一两分钟"(按当前活跃步骤,不是固定第一步);超过 `RECTIFICATION_AGENT_ATTEMPT_TIMEOUT_MS` 前 20 秒提示"即将超时,会自动重试或提示"。
3. 采用后的续跑是 `read_only`:确认 `compare-candidates` 在该动作下不重复调用 `vedastro-validate`(上一轮 receipt 已有 `vedastro_event_validation``evidenceLedgerFingerprint` 未变时直接沿用);若现有代码已如此,写一条测试固定它;若没有,加这条快路径。不得改确认门语义(沿用的结果仍是 failed 就仍 failed)。
## 6. 测试
- `rectification-decide-next-action.test.ts` / `rectification-confirmation-gate.test.ts``collect_evidence` + `capability.canAdopt=true``can_adopt=false``selection_allowed` 不变;`provisional_range_user_stopped` → true`adopt_representative` → true。
- accept route 测试:`session_outcome=collect_evidence` 时 POST 返回 409 且 RPC 未调用。
- `rectification-candidate-result.test.ts`:快照一形状 → `showSelectionCards` 为 false,只读范围行可渲染;快照二形状(accepted)→ 不再出新卡。
- `rectification-server-focus.test.ts` / `rectification-choice-card.test.ts`accepted + active reverse_verify focus → `projectRectificationChoiceCard` 非空、`focus_id` 一致、`question_id === focus.questionId`;连续两次 GET 结果一致。
- `rectification-question-in-message.test.ts`:采用后消息里的核对题 `liveQuestion=true`(结构断言:`choice_card.focus_id === turns[last].question.focus_id`)。
- 组件/渲染测试:采用后卡片留在原消息、05:06 "已采用"、新消息无卡;状态条含"用这个时间看盘";`rectification-consult-handoff` 不再渲染。
- `rectification-activity-receipt.test.ts``elapsed_ms` 序列化与时间线行映射。
- 用 §0 的两份快照做 fixture(脱敏后放 `frontend/tests/fixtures/rectification-case-bf7a8de6-*.json`)。
## 7. 硬红线
1. 确认门恒 fail-closed`can_adopt` 只能收紧不能放宽;不得用正文字符串判断状态。
2. 选项、answer_class、计分仍全部服务端所有;Agent 只写 `spokenPrompt`
3. 不恢复问题槽、不改 `asked_turn_id` 契约;不改 `d9404976` / `8dfe457f` 已定的消息形态。
4. Python 引擎不动;Skill 10.0.14 不 bump。
5. `./node_modules/.bin/tsc --noEmit` 通过(不用 `npx tsc`);`npm run lint --prefix frontend` 0 错误;`rectification-*` / `consultation-*` / `consult-*` / `chat-*` 测试 fail=0;改动的既有断言逐条三栏说明(旧→新→保留语义)。
6. 无凭据不得声称已真实环境验证。
## 8. 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/rectification-adopt-flow-20260902 \
../.worktrees/rectification-adopt-flow-20260902 origin/staging
```
`TASK-rectification-question-in-message-20260902.md``TASK-rectification-question-ownership-fix-20260902.md``docs/BUG_HISTORY.md`BUG-313、BUG-323 及 BUG-492 起的链)、`frontend/docs/VOICE.md`。BUG 编号从 **BUG-495** 起(先 grep 确认最大号):495 = 采集阶段出采用卡(复发自 313/323),496 = 采用后核对题不可答,497 = 采用卡不沉淀/裸按钮,498 = 运行耗时不可观测。**行号是线索,按符号名定位。**
## 9. 验收标准
1. 快照一形状:GET `can_adopt=false`,页面只有采集题 + 只读范围行,无"采用此时间"composer 旁有"先这样"入口,点后 `session_outcome=provisional_range_user_stopped`、三张卡出现。
2. 快照二形状:GET `choice_card` 非空且 `focus_id` 与最新题一致;AD 可点,点后 `answer_option` 落库、候选比较更新。
3. 采用后:卡片留在原消息且 05:06 标"已采用";新消息无卡;状态条含"改选"和"用这个时间看盘";列表末尾无裸按钮;被关掉的采集题标"已跳过"。
4. 采用后续跑:正文第一句承认已采用;set-focus `completed`;核对题卡片无"先这样",有"这题跳过"。
5. 回执含每步 `elapsed_ms`45 秒后文案切换。
6. tsc + lint + 四组测试 fail=0BUG_HISTORY 追加 495498。
7. 真实环境人工清单(部署后):走一遍"两件事 → 三道区分题 → 采集题阶段无卡 → 先这样 → 出卡 → 采用 → 核对题可答 → 看盘"。