Files
Jyotisha/docs/tasks/TASK-rectification-question-ownership-fix-20260902.md
T
Jesse_ChenandClaude Fable 5.1 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

84 lines
9.9 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.
# 任务书 · 题目进消息修复:选择题选项回归服务端、题目加载 fail-closed、题干必含探针年份(2026-09-02)
基线:`origin/staging` `d9404976``TASK-rectification-question-in-message-20260902.md` 的实现)。本任务书是对该实现 review 后的修复单,**不改 `d9404976` 已定的形态**(一条消息承载正文+题干+选项、问题槽已删、`asked_turn_id` 关联),只修下面三个缺口和两个测试欠账。
## 0. Review 结论摘要
`d9404976` 的数据契约、前端同 article 渲染、刷新重建、复制文本、39c30b55 回归修复均已落地,tsc/lint/四组测试全绿。但有一个 P0 违反任务书 §8 红线"选项、答案→候选映射、计分全部服务端所有",两个 P1。
## 1. P0 · 消息路径的选择题选项由 Agent 自填 / 丢失
**现象(静态复现,已用脚本验证)**`rectification-set-focus``d9404976` 重新开放给 Agent`agentToolsForDecision` 不再剔除它),但 `execute` 里选择题的 `expectedAnswerSchema.choice` 仍走老逻辑——`parseAgentChoiceCopy(expectedAnswerSchemaInput)` 要求 Agent 同时给 `choice.prompt``option_a..d` **和 `options[].answer_class`**。三种可能的 Agent 行为结果都不对:
| Agent 写法 | 结果 |
|---|---|
| 只写 `spokenPrompt``method-followup.ts` 的 choice hint 就是这么教的:"用 spokenPrompt 写出题干" | focus schema = `{prompt}``parseAgentChoiceCopy` 为 null → `turnQuestionFromFocus` 判成 `collect_spoken` 无选项;`projectCurrentQuestion` 返回 **null**`current_question` 为空;随后 `persistNextInterviewIfIdle` 看到 active focus 直接 return,服务端 choice focus 永远建不出来,该探针永远不计分 |
| 按工具描述只写 `choice.option_a..d` | `parseAgentChoiceCopy` 因缺 `options` 返回 null → `hasChoice && !choiceCopy``invalid_choice_copy` |
| 把 `options[].answer_class` 也写上 | 选项文本与答案→候选映射由模型决定,违反红线 |
`d9404976` 之前 Agent 根本调不到 set-focuschoice focus 全部由 `persistNextInterviewIfIdle → persistServerOwnedFocus → expectedAnswerSchemaFor(frame)` 建立,所以这条路径没被测试覆盖。
**修法**`frontend/src/mastra/rectification-v9-tools.ts` set-focus `execute`):
1. 已经算出 `nextFollowup``sessionAwareFollowupForParsed(...).plan.next_followup`)。当 `nextFollowup?.choice_frame` 非空且 `input.questionId === stableFollowupQuestionId(nextFollowup)`(现有 `probe_mismatch` 校验已保证)时:**schema 强制取服务端**——把 `server-focus.ts``expectedAnswerSchemaFor(frame, questionId, decisionReceipt, followup)` 导出并复用,得到含 `choice.options[].answer_class``semantic_key``candidate_split_hash``probe_id``scoring` 的完整 schema;再 `withSpokenPrompt(schema, spoken.prompt)`。Agent 传入的 `expectedAnswerSchema.choice` **一律忽略**(不报错,不合并)。`expectedAnswerSchemaFor` 返回 null(frame 不可渲染)时,走与 `persistServerOwnedFocus` 相同的降级:`spokenCollectFallbackFollowup` 的 collect schema,不得建立无选项的 distinguish focus。
2.`nextFollowup` 没有 `choice_frame`(采集题)时:schema 强制带 `collect: true``COLLECT_FOCUS_SCHEMA_KEY`+ `prompt`,与 `collectFocusSchema(followup)` 同形,Agent 传入的 `choice` 同样忽略。目的:`projectCurrentQuestion` 对 Agent 建的采集 focus 也能投影出 `collect_spoken`(现在 `{prompt}``{required:[...]}` 形状会投影成 null,composer 占位丢失)。
3. `nextFollowup` 为 null(无下一问)时:工具返回 `{ ok:false, error:"no_pending_question" }`,不建 focus(指令第 4 条已写"没有下一问时不要自拟问题",服务端也要挡)。
4. 工具描述与 `method-followup.ts` 两处 hint 同步改:删掉"在 expectedAnswerSchema.choice 写入 option_a…",改为"选项、计分由服务端按 choice_frame 写入,你只写 spokenPrompt"。`expectedAnswerSchema` 输入保留为可选(兼容旧 prompt),但服务端不再读它的 `choice`
5. `keepChoice` 分支(active_focus followup`method-followup.ts:1529` 附近)的 hint 让 Agent 对**已持久化的 choice focus 再次 set-focus**。确认幂等:同 `questionId`、同 intent、服务端 schema(去 prompt 后)一致 → RPC 走 `idempotent` 路径,不 supersede;写一条测试证明"第二轮对同一 choice 探针再 set-focus 不会丢选项、focus id 不变"。若做不到幂等,就把这条 hint 改成"当前焦点已在,不要再调 set-focus"。
## 2. P1 · 题目加载 fail-open
`tool-service.ts` `listV10ConversationFocuses`RPC 报错或异常时静默 `return []`——迁移没跑、RPC 权限不对时,聊天里**所有**题目消失且没有任何日志。
**修法**
1. 失败时 `console.warn("[rectification-v9] list focuses failed case=… reason=<safeToolErrorCode>")`,并让函数返回 `{ focuses, available: boolean }`(或抛出让 GET 决定)。
2. GET `/api/rectification/cases/[caseId]` 在 focuses 不可用时响应体带 `question_source: "unavailable"`(可用时 `"focus"`),前端 `showMissingQuestion` 的 status 文案区分"服务端更新中"与"题目加载失败,请刷新"。
3. 测试:RPC 返回 error 时 GET 仍 200、`turns[].question` 全 null、`question_source === "unavailable"`、有 warn。
## 3. P1 · 题干可以不含探针年份,而嵌入卡又隐藏了 `why`
`spoken-prompt.ts``year_mismatch` 只在题干**写了** 19xx/20xx 时比对;`rectification-choice-card.tsx``variant="embedded"``why`(原本携带年份/期间)也藏掉了。结果"你有没有一段认真开始或结束的关系?"能过校验,用户看不到是哪一年,答错年份直接影响计分。
**修法**`validateSpokenPrompt`):
1. `followup.probe_year` 非空时,题干必须包含 `String(probe_year)`(允许 "2023 年"/"2023年"/"2023"),否则 `reason: "year_missing"`
2. `probe_year` 为空但 `choice_frame.period` 非空时(区间探针),题干必须包含 period 里出现的每个年份;period 无年份则不检查。
3. 采集题(无 choice_frame)不加年份要求。
4. 工具描述补一句:"题干必须写出服务端给你的年份/期间"。VOICE.md 的坏例加一条"没写年份"。
5. 测试:`rectification-spoken-prompt.test.ts``year_missing` 正反例;embedded 卡片测试断言"渲染出的题干含探针年份"(结构断言:用 `question.prompt``probe_year` 比对,不做正文匹配)。
## 4. 测试欠账
1. `tests/rectification-question-in-message.test.ts` 里的 choice fixture 缺 `choice.options[]`,实际被 `parseAgentChoiceCopy` 解析成 `collect_spoken`,测试没断言 `kind`,所以 P0 没被发现。修 fixture(用 `serverOwnedChoiceCopy` 生成或补 `options[].answer_class`),并对每个 choice 断言 `kind === "choice"``options.length === 4`
2. 补任务书 §7 第 3 条:以走查 case `a17efc37` 形状跑完整链(opening → 2 采集 → 3 区分点选 → 1 打字答区分 → 采用 → 前事核对),断言每条 assistant turn 的 `question` 形状(kind / focus_id / answer_option / status)与在线会话 `messagesFromTurns` 结果一致。用 fake accounting,不需要真实环境。
3. BUG_HISTORY 记一条已知边界:用户不答上一题直接发新消息时,旧 focus 仍挂在旧消息、新 turn 无 question(不变量 1 短暂不成立),旧卡可点,属可接受。
## 5. 硬红线
1. 问哪道题、年份、选项文本、`answer_class`、计分:服务端所有,Agent 只写 `spokenPrompt`。任何路径不得再从 Agent 输入读 `choice`
2. 不改 `d9404976` 的消息形态与 `asked_turn_id` 契约;不恢复问题槽。
3. 确认门恒 fail-closed;不得用正文字符串判断状态。
4. Python 引擎不动;不 bump Skill 10.0.14。
5. `./node_modules/.bin/tsc --noEmit` 通过(不要用 `npx tsc`);`npm run lint --prefix frontend` 0 错误;`rectification-*` / `consultation-*` / `consult-*` / `chat-*` 测试 fail=0;改动的既有断言逐条三栏说明(旧→新→保留语义)。
6. 无凭据不得声称已真实环境验证。
## 6. 开工前置
```bash
git fetch origin --prune
git worktree add -b codex/rectification-question-ownership-fix-20260902 \
../.worktrees/rectification-question-ownership-fix-20260902 origin/staging
```
`TASK-rectification-question-in-message-20260902.md``docs/BUG_HISTORY.md`BUG-490/491 及 BUG-441/449/456 链)、`frontend/docs/VOICE.md`。BUG 编号从 **BUG-492** 起(先 grep 确认最大号)。**行号是线索,按符号名定位。**
## 7. 验收标准
1. 消息路径合成测试:Agent 只传 `spokenPrompt` + 探针 `questionId``next_followup``choice_frame` → 建立的 focus `expectedAnswerSchema.choice.options``serverOwnedChoiceCopy(frame)` 逐项相等、`probe_id` 非空、`projectCurrentQuestion(...).kind === "choice"`、GET 的 `turns[last].question.options.length === 4`Agent 额外传的 `choice` 被忽略(结果与不传完全一致)。
2.`choice_frame` 时 Agent set-focus → focus 为 `collect: true` 形状,`current_question.kind === "collect_spoken"`
3. `next_followup` 为 null 时 set-focus 返回 `no_pending_question`,无 focus 写入。
4. 同一 choice 探针第二轮再 set-focus → focus id 不变、选项不丢。
5. `list_…_focuses` RPC 失败 → GET 200 + `question_source: "unavailable"` + warn。
6. `spokenPrompt` 缺探针年份 → `year_missing`;含错年份 → `year_mismatch`;含正确年份 → 通过。
7. §4 的 fixture 修正与全链等价测试全绿;tsc + lint + 四组测试 fail=0BUG_HISTORY 追加条目。
8. 真实环境人工清单(部署后):答一件事后的区分题在同一条消息里**有 A–D 四个选项且题干带年份**;点 A 后计分生效(候选比较更新);刷新后一致。