docs(rectification): add adopt-flow fix task brief (exit fallback, 改选, opening domain, probe_year)
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# 任务书 · 采用流程修复单:采集期出口兜底、「改选」可点、开场题不占 education、核对题年份持久化(2026-09-03)
|
||||
|
||||
基线:`origin/staging` `35e5781e`(`TASK-rectification-adopt-flow-20260902.md` 的实现,BUG-497~500)。本任务书是对该实现 review 后的修复单,**不改 `35e5781e` 已定的形态**(公开 `can_adopt` 按 session_outcome 收紧、accept 409、核对题沿用持久化 questionId、卡归出卡消息、状态条承接),只修 1 个 P1 和 3 个 P2。
|
||||
|
||||
## 0. Review 结论摘要
|
||||
|
||||
`35e5781e` 门禁全绿(tsc / lint 0 错 / 1109 测试 fail=0),两个 P0 的修法与任务书一致。但收紧公开 `can_adopt` 之后,**非终止轮出口检查还在看内部 `canAdopt`**,会把一种此前靠"漏出的采用卡"兜住的状态变成真正的死角;另外三处是形态没做完或副作用。
|
||||
|
||||
## 1. P1 · 采集期"无题、无卡、无出口"
|
||||
|
||||
**位置**:`frontend/src/lib/rectification-agentic/v9/answer-choice.ts` `inspectNonTerminalTurnExit`(行号线索 ~992–1013,按符号名定位)。
|
||||
|
||||
**现象(静态推演)**:`satisfied` 的最后一项是 `decision.canAdopt`(内部能力位)。当 `session_outcome = collect_evidence`、内部 `canAdopt = true`、`buildMethodFollowupPlan` 的 `next_followup` / `deferred_followup` 都为 null(题库耗尽)时:
|
||||
|
||||
1. `persistNextInterviewIfIdle` 走到 `if (!followup) return { persisted:false }`,不建题;
|
||||
2. `ensureNonTerminalTurnExit` 因 `decision.canAdopt` 判 `satisfied = true`,**不调用 `persistExhaustionCollect` 兜底**;
|
||||
3. 公开 `can_adopt` 现在为 false → 前端不出采用卡;`showCollectStop` 依赖活跃采集题 → 「先这样」也不出;
|
||||
4. 用户只剩空 composer,随便发一句话再跑一轮回到同一状态。
|
||||
|
||||
`35e5781e` 之前这个状态会漏出采用卡(就是 BUG-497 的现象),所以没人碰到过死角;现在门关了,出口必须补上。
|
||||
|
||||
**修法**:
|
||||
|
||||
1. `inspectNonTerminalTurnExit` 的 `satisfied` 把 `decision.canAdopt` 改成 `publicCanAdopt(decision)`(从 `core/rectification-decision` 导入)。语义:只有**用户在界面上真的能采用**时,"没有下一问"才算满足;否则走 `persistExhaustionCollect`(它建的题自带「先这样」,`answerChoice` 的 stop 路径会把 outcome 推到 `provisional_range_user_stopped`,公开 `can_adopt` 随之打开)。
|
||||
2. `persistNextInterviewIfIdle` 里 `shouldSkipFollowupPersist({ canAdopt: decision.canAdopt, ... })` 同步改成 `publicCanAdopt(decision)`。现有 nextAction 白名单已保证采集/区分/holdout 不会跳过持久化,这一改只是让"跳过持久化、改出 adopt 旁白"与公开门一致,不得出现"旁白说可以采用、界面却没卡"的分叉。
|
||||
3. 确认 `persistExhaustionCollect` 在 `collect_evidence` + 内部 `canAdopt = true` 下能返回一道题(`exhaustionSpokenCollectFollowup` 若也为 null,则至少给 `occupation` 兜底题——现有逻辑已有,验证即可)。
|
||||
|
||||
**测试**(`tests/rectification-adopt-flow-fix-20260903.test.ts` 新建):
|
||||
- 构造 dossier:`sessionOutcome = collect_evidence`、内部 `canAdopt = true`、无 active focus、题库耗尽(evidence 覆盖所有 collect 域或 declined 全部)。断言 `ensureNonTerminalTurnExit` 返回 `persisted = true`,写入的 focus `intent = collect_method_evidence` 且 `projectCurrentQuestion(focus).kind === "collect_spoken"`,并有 `rectification_nonterminal_exit_repaired` warn。用 fake accounting,不需要真实环境。
|
||||
- 对照:`sessionOutcome = adopt_representative`(`publicCanAdopt = true`)同样条件下 `satisfied = true`、不建题(保留既有语义)。
|
||||
|
||||
## 2. P2-a · 状态条「改选」是死的
|
||||
|
||||
**位置**:`rectification-agentic-chat.tsx` 状态条 `<span className="rectification-adopt-status__link">改选</span>`(~1501);`offerSectionRef`(~439、~1438)赋了 ref 但没有任何 `scrollIntoView` 调用。
|
||||
|
||||
**修法**:
|
||||
1. 改成 `<button type="button" className="rectification-adopt-status__link">改选</button>`,onClick:`offerSectionRef.current?.scrollIntoView({ block: "center", behavior: "smooth" })`,并给该消息一次 `rectification-message-flash`(globals.css 加一个 600ms 的背景闪烁 keyframe,复用现有 `.rectification-message-entry` 的色板)。
|
||||
2. `showSelectionCards` 为 false(出卡消息还没重建,或 `readonly`)时不渲染「改选」。
|
||||
3. `globals.css` `.rectification-adopt-status__link` 补 button reset(去边框/背景、光标 pointer、焦点环沿用 `.rectification-candidate` 的样式)。
|
||||
4. 测试:源码锁 `rectification-adopt-status__link` 出现在 `<button` 上、文件内存在 `offerSectionRef.current?.scrollIntoView`;不做 DOM 测试。
|
||||
|
||||
## 3. P2-b · 开场题把 `education` 记成"已问/已拒"
|
||||
|
||||
**位置**:`method-followup.ts` `OPENING_COLLECT_DOMAIN = "education"`(~1068)、`collectQuestionDomain`、开场 followup `domain: OPENING_COLLECT_DOMAIN`(~1800);`server-focus.ts` `persistCollectFocus` 的 `targetDomain` 兜底;`declinedDomains` / `domainCollectFocusAsked`(~351/~366)。
|
||||
|
||||
**问题**:开场题是"随便一件带大概时间的经历",不是教育题。现在它以 `target_domain = education`、`questionId = collect:education:collect_method_evidence` 持久化:
|
||||
- 用户对开场题点「先这样」或不答 → `declinedSkippedTopics` 里出现 `education` → `declinedDomains` 含 education → `exhaustionSpokenCollectFollowup` 跳过教育、D5 refine 的 `!declined.has("education")` 分支全部关闭;
|
||||
- 用户答完开场题(说的是搬家)→ `domainCollectFocusAsked(rows, "education")` 为真 → 之后真正的教育采集题被打上 `collect_retry`,文案变成"再补一件"。
|
||||
|
||||
**修法**(不改 DB check 约束,`target_domain` 只能是 education/career/relationship/relocation/finance/health/family/other):
|
||||
1. `OPENING_COLLECT_DOMAIN` 改为 `"other"`;`collectQuestionDomain(null | "unknown" | "active_focus")` 返回 `"other"`。stable id 变为 `collect:other:collect_method_evidence`,`persistableFocusDomain("other")` 本来就合法。
|
||||
2. `declinedDomains` 与 `domainCollectFocusAsked` 忽略 `target_domain === "other"` 且 `questionId` 以 `collect:other:` 开头的行(occupation 走的是 `collect:occupation:` 前缀,不受影响;`occupationCollectFocusClosed` 已按 questionId 前缀判断,确认不回归)。
|
||||
3. `tests/rectification-adopt-flow-20260902.test.ts` 里 "opening collect domain is education rather than unknown" 改为断言 `"other"`——三栏说明:旧 `education` → 新 `other` → 保留语义"不得为 unknown、stable id 不含 unknown"。
|
||||
4. 新测试:closedCollectFocuses 里只有开场题(`collect:other:…`,status `skipped`)时,`exhaustionSpokenCollectFollowup` 仍会给出 education 题;`buildMethodFollowupPlan` 的教育采集 followup 不带 `collect_retry`。
|
||||
|
||||
## 4. P2-c · 核对题年份靠正则从题干文本抠
|
||||
|
||||
**位置**:`method-followup.ts` `keepAcceptedFocus` 分支 `promptYear`(~1592)——`text.match(/19\d{2}|20\d{2}/)`。
|
||||
|
||||
**问题**:`schema.probe_year` 缺失时才走到它,但它是"从已展示文案反推数据",与"不得用字符串反推状态"的红线同一个方向;一旦题干写了两个年份("2016 年上大学、2020 年毕业")就取错。
|
||||
|
||||
**修法**:
|
||||
1. 数据源头补齐:`server-focus.ts` `expectedAnswerSchemaFor` / `persistServerOwnedFocus` 与 `rectification-v9-tools.ts` set-focus 建 reverse_verify / out_of_sample_check focus 时,schema 必写 `probe_year`(取 `followup.probe_year`,来自匹配的 verify 探针;没有就不写,不得从题干抠)。
|
||||
2. `keepAcceptedFocus` 的 `keepProbeYear = matchingVerify?.year || schemaYear`,删除 `promptYear` 兜底。年份仍缺时不给 `probe_year`,`validateSpokenPrompt` 的 `year_missing` 只在 `probe_year` 非空时触发(现有语义),不会误伤。
|
||||
3. 测试:`rectification-question-ownership.test.ts` 加一条:采用后 set-focus 建的 reverse_verify focus,`expectedAnswerSchema.probe_year` 等于探针年份;`keepAcceptedFocus` 在 schema 无 `probe_year`、无匹配探针时 followup 不带 `probe_year`,且 `method-followup.ts` 源码不再含 `/19\\d{2}|20\\d{2}/`。
|
||||
|
||||
## 5. 硬红线
|
||||
|
||||
1. 公开 `can_adopt` 只能收紧;`collect_evidence` / `discriminate_candidates` / `validate_holdout` 不得放行。确认门恒 fail-closed。
|
||||
2. 选项、answer_class、计分服务端所有,Agent 只写 `spokenPrompt`;不恢复问题槽、不改 `asked_turn_id` 契约。
|
||||
3. 不得用正文/题干字符串反推状态或数据(本单 §4 就是在删一处)。
|
||||
4. 不改 DB check 约束、不加迁移;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-adopt-flow-fix-20260903 \
|
||||
../.worktrees/rectification-adopt-flow-fix-20260903 origin/staging
|
||||
```
|
||||
|
||||
读 `TASK-rectification-adopt-flow-20260902.md`、`docs/BUG_HISTORY.md`(BUG-497~500,以及 BUG-313/323 链)。BUG 编号从 **BUG-501** 起(先 grep 确认最大号):§1 记 BUG-501(复发自:无;相关 BUG-497),§2/§3/§4 各一条或并入 BUG-499/498 的"最近更新",由实现者判断,但 §1 必须独立成条。**行号是线索,按符号名定位。**
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
1. `collect_evidence` + 内部 `canAdopt = true` + 题库耗尽 → `ensureNonTerminalTurnExit` 建出 `collect_spoken` 兜底题(含 warn);`adopt_representative` 同条件不建题。
|
||||
2. 「改选」是 `<button>`,点击滚到出卡消息;`showSelectionCards` 为 false 时不渲染。
|
||||
3. 开场题 `target_domain = other`、id `collect:other:collect_method_evidence`;跳过开场题后 education 不进 declined、教育采集题无 `collect_retry`。
|
||||
4. reverse_verify focus schema 带 `probe_year`;`method-followup.ts` 无年份正则。
|
||||
5. tsc + lint + 四组测试 fail=0;BUG_HISTORY 追加条目。
|
||||
6. 真实环境人工清单(部署后,沿用上一单 §9.7):采集阶段把题答到没题 → 出现"先这样"入口而不是空 composer;采用后点「改选」能跳回卡片。
|
||||
Reference in New Issue
Block a user