fix(rectification): keep invite collect CHECK-legal after kind mapping (BUG-656)
Independent Staging Quality Gate / validate (push) Successful in 9m14s
Independent Staging Quality Gate / publish (push) Successful in 2m12s

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 <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-12 06:34:35 +08:00
co-authored by Cursor
parent eae049d0ea
commit 529fcc097f
6 changed files with 52 additions and 6 deletions
+2 -2
View File
@@ -10207,8 +10207,8 @@
- 触发条件:下一问是定向 / 锚定 / 通用采集,`kind_hint``targeted:<domain>``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 仍未映射到表约束)
- 修复版本:待发布
@@ -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 真机走一遍
@@ -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,
@@ -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
// 新值: nullcollect_kind=invite_more
// 原因: target_kind CHECK 不允许 invite_more,原 hint 只进 schemaBUG-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 ?? ""),
/^还有吗?/,
@@ -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
// 新值: nullcollect_kind=invite_more
// 原因: target_kind CHECK 不允许 invite_more,原 hint 只进 schemaBUG-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");
});
@@ -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");
});