fix(rectification): stop discriminator followup from dropping user evidence
Independent Staging Quality Gate / validate (push) Successful in 10m10s
Independent Staging Quality Gate / publish (push) Successful in 7m25s

Decision and question ranking now share contrast option completion, so a
missing style card cannot deadlock the interview with a dead-end reply.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-29 10:23:34 +08:00
co-authored by Cursor
parent 31b54aa71d
commit 86d6923e6e
15 changed files with 1054 additions and 124 deletions
@@ -644,11 +644,24 @@ test("answering a discriminator persists the next dated card so GET still has a
assert.doesNotMatch(card.prompt, /2015/);
});
test("answering the last discriminator persists a year-locked family collect, not a yearless D24 card", async () => {
test("answering the last discriminator persists a year-locked family collect focus, not a yearless D24 card", async () => {
const accounting = persistChoiceAccounting(familyCollectDossier(), {
set_agentic_rectification_conversation_focus: () => {
throw new Error("yearless D24 must not persist a scoring focus");
},
set_agentic_rectification_conversation_focus: (_fn, args) => ({
focus: {
id: NEXT_FOCUS_ID,
case_id: CASE_ID,
question_id: args.p_question_id,
intent: args.p_intent,
target_evidence_id: args.p_target_evidence_id,
target_domain: args.p_target_domain,
target_kind: args.p_target_kind,
expected_answer_schema: args.p_expected_answer_schema,
status: "active",
asked_at: "2026-08-28T07:37:50.000Z",
resolved_at: null,
},
idempotent: false,
}),
});
const applied = await applyRectificationChoice(accounting.client, {
userId: USER_ID,
@@ -662,11 +675,14 @@ test("answering the last discriminator persists a year-locked family collect, no
optionId: "C",
expectedRevision: familyCollectInference().revision,
});
assert.equal(
accounting.calls.some((call) => call.fn === "set_agentic_rectification_conversation_focus"),
false,
JSON.stringify(accounting.calls.map((call) => call.fn)),
);
const setFocus = accounting.calls.find((call) => call.fn === "set_agentic_rectification_conversation_focus");
assert.ok(setFocus, JSON.stringify(accounting.calls.map((call) => call.fn)));
assert.equal(setFocus.args.p_intent, "collect_method_evidence");
const schema = setFocus.args.p_expected_answer_schema as { choice?: unknown; prompt?: string; collect?: boolean };
assert.equal(schema.choice, undefined);
assert.equal(schema.collect, true);
assert.match(schema.prompt ?? "", /2021/);
assert.match(schema.prompt ?? "", /家人|结婚|添丁|住院/);
assert.equal(applied.nextInterviewPersisted, true);
assert.equal(applied.nextChoiceReady, false);
assert.equal(shouldContinueAfterStructuredChoice(applied.nextAction, applied), false);
@@ -801,6 +817,39 @@ test("collection focus without choice copy is not an unrenderable current_questi
assert.equal(projection.current_question, null);
});
test("failed card persist still leaves a spoken collect next step", () => {
const source = readFileSync(new URL("../src/lib/rectification-agentic/v9/answer-choice.ts", import.meta.url), "utf8");
assert.match(source, /if \(followup\?\.choice_frame\)/);
assert.match(source, /intent: "collect_method_evidence"/);
assert.match(source, /choice_frame: null/);
assert.match(source, /请再说一件记得大概时间的经历/);
assert.match(source, /当前几个候选已经构成可信区间/);
});
test("spoken collect focus keeps current_question after refresh", () => {
const snapshot = candidateSnapshotFixture();
Object.assign(snapshot.decision_receipt, { inference_state: inferenceState() });
const dossier = parseV9CaseDossier(dossierFixture({
latestResult: snapshot,
conversationSummary: conversationSummaryFixture({
activeFocus: activeFocusFixture({
intent: "collect_method_evidence",
targetDomain: "family",
expectedAnswerSchema: {
prompt: "2021 年前后,家里有没有结婚、添丁或住院这类记得住时间的事?不记得具体日子也可以先说有没有。",
collect: true,
},
}),
}),
}));
assert.ok(dossier);
const projection = projectTurnDecision(dossier);
const currentQuestion = projection.current_question as { prompt?: string; unrenderable?: boolean } | null;
assert.equal(currentQuestion?.unrenderable, undefined);
assert.match(currentQuestion?.prompt ?? "", /2021/);
assert.equal(projection.current_question && typeof projection.current_question === "object", true);
});
test("turn_decision hides current_probe unless a valid current_question exists", () => {
const withFocus = projectTurnDecision(parseV9CaseDossier(choiceDossier())!);
assert.ok(withFocus.current_question);