From 529fcc097fc96427fe773364438628b9cc0f5aa8 Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sat, 12 Sep 2026 06:34:35 +0800 Subject: [PATCH] fix(rectification): keep invite collect CHECK-legal after kind mapping (BUG-656) Full TAP failed because invite_more is not a target_kind enum value, and exhaustion re-decide after skipped targeted persist must reuse decideAfterInferenceChange. Co-authored-by: Cursor --- docs/BUG_HISTORY.md | 4 ++-- ...n-targeted-collect-persist-fix-20260911.md | 9 +++++++- .../rectification-agentic/v9/answer-choice.ts | 6 +++++- ...ication-collect-direction-20260904.test.ts | 9 +++++++- .../tests/rectification-collect-stall.test.ts | 9 +++++++- .../tests/rectification-server-focus.test.ts | 21 +++++++++++++++++++ 6 files changed, 52 insertions(+), 6 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index a61b550b..29865d23 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -10207,8 +10207,8 @@ - 触发条件:下一问是定向 / 锚定 / 通用采集,`kind_hint` 为 `targeted:`、`anchor:…` 或 `generic:…`。 - 根因:`targetKind` 直接写入 `kind_hint`,违反 `target_kind` CHECK;插入失败后 persist 返回 `skipped`,定向题从未成为活动焦点。 - 修复:写库前经 `collectFocusTargetKind` 映射到 CHECK 枚举(产品别名 `relationship_change`→`relationship_start`、`education_milestone`→`education_start`);原 `kind_hint` 写入 `expected_answer_schema.collect_kind`;读取侧先读 `collect_kind` 再回退旧字段。不放宽 CHECK、不加迁移。 -- 验证:`frontend/tests/rectification-server-focus.test.ts` 用真实 CHECK 列表断言三类 followup 的 `targetKind` 合法且 `status=created`;`rectification-probe-pool-exhausted-20260911.test.ts` 第六题后定向题成为活动焦点。 -- 防复发:采集焦点 `target_kind` 必须落在 CHECK 列表内;`kind_hint` 原值只进 schema,不进列。 +- 验证:`frontend/tests/rectification-server-focus.test.ts` 用真实 CHECK 列表断言三类 followup 的 `targetKind` 合法且 `status=created`,`invite_more` 写 `target_kind=null` + `collect_kind`;`rectification-probe-pool-exhausted-20260911.test.ts` 第六题后定向题成为活动焦点。门禁跟进:家人拒答后的邀请 persist 与 idle `decideFromDossier` 源码锁仍通过。 +- 防复发:采集焦点 `target_kind` 必须落在 CHECK 列表内或为 null;`kind_hint` 原值只进 schema,不进列。`invite_more` 等非枚举 hint 不得写进 `target_kind`。 - 相关记录:BUG-648、BUG-442、BUG-658 - 复发自:BUG-648(采集题号可重试,但 kind 仍未映射到表约束) - 修复版本:待发布 diff --git a/docs/tasks/PROGRESS-rectification-targeted-collect-persist-fix-20260911.md b/docs/tasks/PROGRESS-rectification-targeted-collect-persist-fix-20260911.md index da70867d..0c0170c0 100644 --- a/docs/tasks/PROGRESS-rectification-targeted-collect-persist-fix-20260911.md +++ b/docs/tasks/PROGRESS-rectification-targeted-collect-persist-fix-20260911.md @@ -23,7 +23,14 @@ - `rectification-server-focus.test.ts` + `rectification-probe-pool-exhausted-20260911.test.ts` + `rectification-surface-state.test.ts` + `rectification-replay-20260911.test.ts`:**51 / 51** - Python:`test_candidate_discriminator_contract.py` + `test_rectification_event_probes.py`,fail=0(`.venv/bin/python -m pytest`) +## 门禁跟进(2026-09-12) + +- Independent Staging Quality Gate run 2568 / SHA `eae049d0`:validate 失败,`npm test --prefix frontend` **fail 4 / 3132** +- 原因不是基础设施:全量 TAP 才跑到的既有用例被 CHECK 映射和第三次 `decideFromDossier` 打坏 + - `invite_more` 不再写入 `target_kind`(CHECK 不允许),改断言 `null` + `collect_kind=invite_more` + - `persistExhaustionCollect` 定向 persist skipped 后改走已有的 `decideAfterInferenceChange`,idle 源码切片仍是两次 `decideFromDossier`(BUG-653) +- 本机补跑上述 4 个失败套件后再推 staging + ## 未做 -- 未 commit / 未 push - 未部署后按场景 0c 真机走一遍 diff --git a/frontend/src/lib/rectification-agentic/v9/answer-choice.ts b/frontend/src/lib/rectification-agentic/v9/answer-choice.ts index 1c711c7b..0a43ff78 100644 --- a/frontend/src/lib/rectification-agentic/v9/answer-choice.ts +++ b/frontend/src/lib/rectification-agentic/v9/answer-choice.ts @@ -1625,7 +1625,11 @@ async function persistExhaustionCollect(input: { followup, }); dossier = dossierWithSkippedTargetedCollect(dossier, followup); - decision = decideFromDossier(dossier); + decision = decideAfterInferenceChange({ + dossier, + state: previousInferenceFromReceipt(receipt), + userStopped: false, + }); catalog = rectificationFollowupCatalog( dossier.latestResult ?? null, dossier.evidence, diff --git a/frontend/tests/rectification-collect-direction-20260904.test.ts b/frontend/tests/rectification-collect-direction-20260904.test.ts index d3836243..438e4a14 100644 --- a/frontend/tests/rectification-collect-direction-20260904.test.ts +++ b/frontend/tests/rectification-collect-direction-20260904.test.ts @@ -407,7 +407,14 @@ test("persistNextInterviewAfterChoice after family denial asks the invite, not o const setFocus = accounting.calls.find((item) => item.fn === "set_agentic_rectification_conversation_focus"); assert.equal(setFocus?.args.p_target_domain, "other"); assert.equal(setFocus?.args.p_question_id, "collect:invite:more"); - assert.equal(setFocus?.args.p_target_kind, "invite_more"); + // 原值: invite_more + // 新值: null,collect_kind=invite_more + // 原因: target_kind CHECK 不允许 invite_more,原 hint 只进 schema(BUG-656) + assert.equal(setFocus?.args.p_target_kind, null); + assert.equal( + (setFocus?.args.p_expected_answer_schema as { collect_kind?: string } | undefined)?.collect_kind, + "invite_more", + ); assert.match( String((setFocus?.args.p_expected_answer_schema as { prompt?: string } | undefined)?.prompt ?? ""), /^还有吗?/, diff --git a/frontend/tests/rectification-collect-stall.test.ts b/frontend/tests/rectification-collect-stall.test.ts index db65345d..f33368b3 100644 --- a/frontend/tests/rectification-collect-stall.test.ts +++ b/frontend/tests/rectification-collect-stall.test.ts @@ -573,7 +573,14 @@ test("persistServerOwnedFocus writes family as target_domain so a no answer beco assert.equal(invitePersisted.status, "created"); const call = inviteAccounting.calls.find((item) => item.fn === "set_agentic_rectification_conversation_focus"); assert.equal(call?.args.p_target_domain, "other"); - assert.equal(call?.args.p_target_kind, "invite_more"); + // 原值: invite_more + // 新值: null,collect_kind=invite_more + // 原因: target_kind CHECK 不允许 invite_more,原 hint 只进 schema(BUG-656) + assert.equal(call?.args.p_target_kind, null); + assert.equal( + (call?.args.p_expected_answer_schema as { collect_kind?: string } | undefined)?.collect_kind, + "invite_more", + ); assert.equal(call?.args.p_intent, "collect_method_evidence"); }); diff --git a/frontend/tests/rectification-server-focus.test.ts b/frontend/tests/rectification-server-focus.test.ts index 2d104e21..da9eac3f 100644 --- a/frontend/tests/rectification-server-focus.test.ts +++ b/frontend/tests/rectification-server-focus.test.ts @@ -977,4 +977,25 @@ test("T1: collect focus kinds map to the table CHECK enum", async () => { assert.equal(persisted.focus?.expectedAnswerSchema.collect_kind, item.collectKind); assert.equal(accounting.calls[0]?.args.p_target_kind, persisted.focus?.targetKind); } + const inviteMapped = collectFocusTargetKind({ kind_hint: "invite_more", domain: "other" }); + assert.equal(inviteMapped, null); + const inviteAccounting = fakeAccounting({ + set_agentic_rectification_conversation_focus: (_fn, args) => focusRowFromArgs(args), + }); + const invitePersisted = await persistServerOwnedFocus({ + accounting: inviteAccounting.client, + userId: USER_ID, + caseId: CASE_ID, + activeFocus: null, + decisionReceipt: null, + followup: collectFollowup({ + domain: "other", + kind_hint: "invite_more", + collection_key: "collect:invite:more", + spoken_prompt: "还有吗?把能想起来的都说一声。", + }), + }); + assert.equal(invitePersisted.status, "created"); + assert.equal(invitePersisted.focus?.targetKind, null); + assert.equal(invitePersisted.focus?.expectedAnswerSchema.collect_kind, "invite_more"); });