Files
Jyotisha/docs/tasks/TASK-rectification-question-in-message-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

141 lines
16 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.
# 任务书 · 题目进消息:一条助手消息承载正文+题干+选项,Agent 自己写题干,删除问题槽(2026-09-02)
基线:`origin/staging` @ `33d55b3a`(含 BUG-480488 全部)。
## 0. 决策记录(产品负责人 2026-09-02 拍板)
产品负责人的原话:"我要的是这部分是一条消息出来,而不是一条消息加下面的卡片;这些问题不能让 Agent 直接说出来吗?那是不是就不需要这个槽了。"
**结论:不需要槽。** 生时校正的每一道题(采集题、区分题、前事核对)都必须作为**最新一条助手消息本身**的一部分出现——正文(Agent 承接)→ 题干(Agent 用自己的话写)→ 选项(服务端,画在同一个气泡里)。已答之后这条消息原地保留(选项变灰、标出选的那个),刷新后从 turn 重建一模一样。`.rectification-question-slot` 整体删除。
这次显式**废止**下列防复发条款中与"槽"或"正文不得含题干"绑定的部分(其余条款保留):
| 条款 | 废止内容 | 保留内容 |
|---|---|---|
| BUG-441 | "不得为无选项的问题新造视觉容器" | 同一焦点在所有投影里形态一致;不得用正文判断"有没有问过" |
| BUG-461 | "题干只留在正文"/legend sr-only | 问题与已答卡共用 `--assistant-content-inset` |
| BUG-471 | "口述采集真源是 GET 直播槽,必须在问题槽可见" | 有选项时题干必须可见;不得用正文判断"有没有问过" |
| BUG-485 | "口述采集直播题干只由问题槽展示" | 开场正文不得并行提问;不得追加第二条 opening 题干消息 |
| BUG-487/488 | "仅在最新正文尚未带该题干时渲染采集槽"/"模型仍不自己提问" | 题干必须在同一条已完成助手消息里、刷新后仍在;不得用 `includes`/正则扫正文 |
| `agentic-rectification.ts` 指令第 4 条 | "题干与选项完全由结构化槽位和 UI 承担,正文不得提问" | 见 §3 新指令 |
**不变的红线**:问哪道题、年份、选项语义、答案→候选映射、计分,全部服务端所有;确认门恒 fail-closed;不得用正文字符串判断任何状态;Python 引擎不动;不 bump Skill 10.0.14。
## 1. 目标形态
```
┌ 助手气泡(一条消息,一个 <article>)────────────────┐
│ 2016 年 9 月上大学,记下了——这种带月份的节点对校正 │ ← 正文:Agent 承接(2-4 句,不提问)
│ 特别有用。接下来想核对一下感情这边。 │
│ │
│ 2023 年前后,你有没有一段认真开始或结束的关系? │ ← 题干:Agent 通过工具写的句子(focus.prompt
│ ┌────────────────────────────────────────┐ │
│ │ A 明确发生且时间吻合 │ │ ← 选项:服务端,choice 才有
│ │ B 发生过但时间偏了 │ │
│ │ C 没有这回事 D 记不清 │ │
│ └────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
👍 👎 📋 🔄 ← 动作图标在整条消息之下
```
- 采集题(`collect_spoken`)= 同一结构,没有选项框;输入框占位由 `current_question` 驱动,不变。
- 已答:同一条消息不移动、不复制;选项禁用,选中项高亮;用户点选的那行 "A. …" 仍不作为独立用户气泡出现(保留 `isStructuredChoiceUserText` 隐藏语义)。
- 流式期间:正文先流出;题干+选项在 `run.completed` 后随 Case 快照进入**同一条**消息(沿用 BUG-488 的 `bubbleMessage` 合成思路,扩展到 choice)。不允许在流式中出现第二个气泡或临时槽。
## 2. 数据合同:题目挂在 turn 上,刷新后可重建
现状:`agentic_rectification_conversation_focuses` 已持久化 `question_id / intent / expected_answer_schema(prompt, choice.options…) / status / asked_at`,但**没有与 turn 的链接,也没有记录用户选了哪个**。`agentic_rectification_turns` 只有 `user_message / assistant_message`。前端 `messagesFromTurns``rectification-agentic-chat.tsx:267`)只能重建纯文本。
新增(一条 migration`frontend/supabase/migrations/`):
1. focuses 表加 `asked_turn_id uuid null references agentic_rectification_turns(id) on delete set null``answer_option text null check (answer_option is null or answer_option in ('A','B','C','D','stop'))`
2. 创建 focus 的 RPC`set_v10_conversation_focus` 或现名,按符号找)接受 `p_asked_turn_id``resolve` RPC 接受 `p_answer_option`
3. dossier / GET `/api/rectification/cases/[caseId]``turns[]` 每条 assistant turn 增加 `question` 字段(null 或 `{ focus_id, question_id, kind: "choice"|"collect_spoken"|"reverse_verify", prompt, options[] | null, status, answer_option, probe_id }`),由服务端按 `asked_turn_id` 连接生成。**禁止**按 `asked_at``created_at` 时间关系推断归属。
4. `current_question` / `choice_card` 保留为**提交契约**`question_id``focus_id``probe_id``case_revision`),不再驱动任何独立 UI。
链接时机:
- Agent 路径:turn 行在 run 开始就已创建(`appendV9Turn` 返回 `turnId`),工具创建 focus 时直接带 `askedTurnId`;服务端兜底(§4)同样带当前 `turnId`
- 点选/确定性路径:先 `persistV9DeterministicTurn` 拿到 `turnId`,再创建下一问 focus 并链接;或创建后 update。二选一,PR 说明。
- 采用后前事核对(BUG-483 的 reverse_verify)走同一规则。
## 3. Agent 协议:题干由 Agent 写,但通过工具而不是正文
原则:**Agent 写题干,服务端出选项和计分;"是否问过"的信号是工具调用,不是正文。** 正文继续不提问——题干不是正文的一部分,而是同一条消息里正文之后的独立段落,其文本来自 focus。这样正文里永远不会出现"两句相近的问法",且不需要读正文。
1. `rectification-set-focus``mastra/rectification-v9-tools.ts:1173`)增加输入 `spokenPrompt: string`(必填,8–120 字,单句,可含一个问号)。服务端校验:不得含选项字面("A."、"选项")、不得含"唯一/确切的出生分钟"、不得含内部 tokenprobe/focus/candidate id 字样)与机器腔词表词条(词表在 `tests/agent-voice-copy-contract.test.ts`,抽成可复用常量)、choice 时年份/域必须与 `method_followup_plan.next_followup` 的探针一致(结构字段比对,不是正文)。校验失败 → 工具返回 `invalid_spoken_prompt`,Agent 重写一次;两次失败落服务端模板(§4)。
2. 通过校验的 `spokenPrompt` 写入 `expected_answer_schema.prompt`collect)或 `choice.prompt`choice)。`projectCurrentQuestion` / `choiceCardFromCaseDossier` 已优先读这里,不用改语义;`serverOwnedChoiceCopy` 只在 Agent 没写时使用。
3. 记证据类工具(`rectification-record-evidence-batch` / `confirm` / `revise`)的返回已带 `agentVisibleLatestProjection``current_question``method_followup_plan.next_followup`;确认返回里含**服务端已决定的下一问的结构信息**(探针 id、年份/期间、域、choice_frame 的选项文本、`why`),足够 Agent 写题干。缺什么补什么,不新造第二套投影。
4. 系统指令(`mastra/agentic-rectification.ts` 第 4 条)改写为:
> 每轮在记录证据后,用 `rectification-set-focus` 的 `spokenPrompt` 写出服务端给你的下一问:用自己的话、结合用户刚说的事,问出**同一个年份/期间和同一个事件家族**;不得改年份、不得改选项含义、不得合并两道题。正文只做承接(2-4 句),不提问、不复述题干、不预告选项——题干会作为同一条消息的下一段自动出现。开场轮:先 set-focus 写采集题的 `spokenPrompt`,正文只打招呼。没有下一问(服务端返回 `next_followup=null`)时不要自拟问题。
`frontend/docs/VOICE.md` 同步:加 3 组"服务端探针 → Agent 题干"好/坏对照(坏:逐字复读模板;坏:改了年份;好:接着用户上一句、同年份同家族)。
5. 只读重写(regenerate)不得改 focus;重生成后的消息仍挂同一个 `question``regenerate-turn.ts` 已有回贴逻辑,改为按 `asked_turn_id` 回贴)。
## 4. 服务端兜底:模板题干,同一条消息
Agent 没调 set-focus(或两次校验失败)时:轮末 `persistNextInterviewIfIdle` 照常用 `USER_COLLECT_QUESTION` / `serverOwnedChoiceCopy` 创建 focus**带当前 `turnId`**。消息渲染规则与 Agent 写的题干完全相同(都是"这条 turn 的 question"),所以兜底也在同一个气泡里。
删除:
- `shouldPersistFocusPromptTurn``turn-exit.ts:82-107` 把题干写成**第二条** assistant turn 的整段逻辑。题干不再是 turn 文本,而是 turn 的 `question`
- `composeCollectSpokenAssistantText` / `detachCollectSpokenAssistantText``agent-run.ts:507-531` 的"把题干拼进 `p_assistant_message`"BUG-487)。`assistant_message` 只存正文;已存在的历史 turn 里拼过题干的文本按精确后缀一次性 detach(读取时,`asked_turn_id` 命中且文本以 `\n\n${prompt}` 结尾才剥,仅此一处允许精确字符串比较,且只作用于本次迁移前的旧数据)。
顺带修掉 `39c30b55` 引入的回归:`finalizeSuccessfulTurnExit``focusCreated` 早退,导致 agent-run 预先建好的 choice focus 在 message 路径上从不写题干 turn。新合同下这个问题自然消失(题干不依赖 turn 文本),但要加不变量测试锁住(§7 第 2 条)。
## 5. 点选路径
点选是"immediate"确定性执行,没有模型轮。本轮**保持确定性**:`applyRectificationChoice``persistV9DeterministicTurn`(正文 = 现有 `hostNarration` 里的承接/进度句)→ 下一问 focus 带 `askedTurnId` + 模板题干。`resolve` 时写 `answer_option`
用户可见效果:点 A 之后,上一条消息原地变成已答态,紧接着出现一条新消息"记下了…(进度)+ 下一题干 + 选项"。
**可选升级(本轮不做,留接口)**:点选后跑一次小预算模型轮只写 `spokenPrompt`,让区分题链也带 Agent 口吻。数据合同与 UI 不需要为此再改。PR 里记为 follow-up。
## 6. 前端
`rectification-agentic-chat.tsx`
1. 删除 `.rectification-question-slot`(三处渲染 `:1253/:1281``showQuestionSlot``showMissingQuestion`/`showUnavailableQuestion` 文案)、`collectSpokenPrompt` 合成、`attachCollectSpokenStem``bubbleText` 拼接。
2. `RenderMessage` 增加 `question?: MessageQuestion`(形状同 §2.3)。`messagesFromTurns``turn.question` 直接重建;`choiceAttachment` 并入 `question``answer_option` 非空即已答)。
3. 气泡:`ChatMessageRow` 之内(同一个 `<article>`)渲染 `message.question`:题干段落 + 可选 `RectificationChoiceCard`(改为无外框/无 legend 的"选项组"变体,题干由段落承担,不再重复)。动作图标在整条消息之下。
4. 流式结束:`run.completed``loadCaseSnapshot()` → 用返回的 `turns[last].question` 写入最新消息(不是拼文本)。`send()``submitStructuredChoice` 共用一个 `markQuestionAnswered(focusId, option | "typed")`:打字回答选择题时,上一条消息的 question 标 `answer_option: null, status: "resolved"`(灰掉选项,不高亮),不得让它消失。
5. 缺题状态:"当前没有可回答的问题,正在等待服务端更新"只在**输入框上方**以一行状态显示(`role=status`),不再是消息列表里的块。
6. 复制:复制文本 = 正文 + 空行 + 题干(+ 选项行)。
7. `globals.css`:删除 `.rectification-question-slot*`;已答/未答选项组共用 `--assistant-content-inset`BUG-461 保留条款)。
## 7. 不变量与测试(fail=0
1. **一条消息**:任意 active focus ⇒ 最新 assistant turn 的 `question.focus_id` 等于它;DOM 中题干与选项在同一个 `article` 内;不存在 `.rectification-question-slot`
2. **历史必有题**opening / message / answer_choice / accept 后建立的每个 focus 都有 `asked_turn_id`,且 GET `turns[]` 里恰有一条 assistant turn 带该 `question`(覆盖 `39c30b55` 回归形状:message 路径下 agent-run 预建 choice focus)。
3. **刷新等价**:以走查 case `a17efc37` 形状跑完整链(opening → 2 采集 → 3 区分点选 → 1 打字答区分 → 采用 → 前事核对),`messagesFromTurns(GET.turns)` 渲染结果与在线会话的消息列表结构相同(题干、选项、已答标记、条数)。
4. **Agent 题干校验**:年份/域与探针不符、含选项字面、含"唯一出生分钟"、超长 → `invalid_spoken_prompt`;两次失败落模板且仍挂在同一 turn。
5. **点选行隐藏不变**`isStructuredChoiceUserText` 语义保留;已答消息里选中项可见。
6. **正文不提问**`agent-voice-copy-contract` 的开场/承接锁保留(正文样本不得含问号结尾的题干句——这是对**模型输出样本**的测试断言,不是运行时判定)。
7. 既有 `rectification-spoken-collect` / `rectification-collect-prompt` / `rectification-walkthrough-polish` 中锁槽位与拼接的断言:逐条三栏(旧→新→保留语义)迁移到新合同,不得整文件删除。
## 8. 硬红线
1. 问哪道题、年份、选项、计分、候选/采用/确认门语义不变;确认门恒 fail-closed。
2. 运行时不得读正文字符串做任何判定(§4 的旧数据 detach 是唯一例外,且只在读取旧 turn 时)。
3. Python 引擎不动;Skill 10.0.14 不 bumpskill 文本"正文只做承接与解释"与本合同兼容)。
4. `./node_modules/.bin/tsc --noEmit` 通过(不要用 `npx tsc`);`npm run lint --prefix frontend` 0 error`rectification-*` / `consultation-*` / `consult-*` / `chat-*` fail=0。
5. migration 必须可在已有数据上执行(新列可空);旧 focus 无 `asked_turn_id` 的历史消息退化为纯文本,不报错。
6. 无凭据不得声称真实环境验证;PR 贴走查 case 形状的前后对照(在线截图形状 + 刷新后重建)。
## 9. 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/rectification-question-in-message-20260902 \
../.worktrees/rectification-question-in-message-20260902 origin/staging
```
`frontend/AGENTS.md``frontend/docs/VOICE.md``docs/BUG_HISTORY.md` 的 BUG-441/449/456/461/469/471/480/485/487/488 链(本任务书 §0 已列出各条废止/保留)。**行号是线索,按符号名定位。** BUG_HISTORY 新条目编号从 BUG-489 起(先 grep 确认最大号);本任务建议拆两条:一条记"题目进消息+删槽"的结构变更(引用 §0 决策记录),一条记 `39c30b55` 的 message 路径题干丢失回归。
## 10. 验收标准
1. §7 七条不变量全绿,改动断言逐条三栏说明。
2. 走查 case 形状前后对照:改后每一道题都在助手气泡内;刷新后历史与在线一致;打字回答区分题后上一题不消失。
3. Agent 题干样本(PR 贴 ≥5 道:2 采集、2 区分、1 前事核对):同年份同家族、接着用户上一句、无模板复读。
4. tsc + lint + 四组测试 fail=0BUG_HISTORY 两条落库。
5. 真实环境人工清单(部署后):开场一条消息(打招呼+采集题);答一件事后一条消息(承接+区分题+选项);点 A 后上一条原地变已答;打字答选择题后上一条仍在;刷新后全程一致;复制文本含题干。