fix(web): stop adopting tied rectification candidates after method coverage
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

Coverage complete only unlocks discrimination. A 34/33/33 window plus an
occupation note must ask a D9/D10 contrast probe instead of offering a
stale winner card.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-24 12:19:36 +08:00
parent 3b785b821d
commit 3f1998bdb6
19 changed files with 1345 additions and 80 deletions
+205 -12
View File
@@ -812,21 +812,27 @@ test("precision stage lagna_frame waits for uncovered career before asking anoth
assert.doesNotMatch(JSON.stringify(plan), UNIQUE_MINUTE_COPY);
});
test("lagna_frame after classic coverage does not keep blocking representative time cards", () => {
test("lagna_frame after classic coverage still discriminates when candidates are tied", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE,
precisionStage: "lagna_frame",
});
assert.equal(plan.next_followup?.source, "precision_stage");
assert.equal(plan.next_followup?.ask_theme, "dated_event");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), false);
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods, { separated: true }), false);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "adopt_representative");
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "discriminate_candidates");
});
test("precision stage d4 asks home change not family, and d5 asks education", () => {
@@ -1007,7 +1013,7 @@ test("career evidence does not cover occupation; occupation still blocks until a
}), "collect_evidence");
});
test("draft occupation_note without a date covers occupation and unblocks offering", () => {
test("draft occupation_note without a date covers occupation and does not adopt a tie", () => {
const plan = buildMethodFollowupPlan({
evidence: [
{ status: "confirmed", domain: "education", datePrecision: "year", occurredFrom: "2016-01-01", occurredTo: null },
@@ -1027,12 +1033,17 @@ test("draft occupation_note without a date covers occupation and unblocks offeri
});
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered");
assert.notEqual(plan.next_followup?.method_id, "occupation");
assert.equal(conversationalSessionOutcome({
assert.notEqual(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "adopt_representative");
});
@@ -1059,15 +1070,76 @@ test("stale occupation collect focus does not keep interviewing after occupation
targetKind: "occupation_note",
},
});
assert.notEqual(plan.next_followup?.method_id, "active_focus");
assert.notEqual(plan.next_followup?.method_id, "occupation");
assert.notEqual(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "adopt_representative");
});
test("D9/D10 contrast after occupation coverage asks a discriminator, not adopt", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
contrastPacket: {
candidateSetVersion: "05:00-05:02",
vargaDifferences: [
{ layer: "d9", signs: ["天秤", "天蝎", "射手"] },
{ layer: "d10", signs: ["巨蟹", "狮子", "处女"] },
],
probes: [{
probeId: "contrast:varga.d10.巨蟹|狮子|处女",
candidateSetVersion: "05:00-05:02",
question: "当前几个候选在事业盘上还分得开。请核对一段还没用进评分的职业前事。",
expectedOutcomes: [
{ outcomeId: "supports_巨蟹", supportsCandidateIds: ["巨蟹"], conflictsCandidateIds: ["狮子", "处女"] },
{ outcomeId: "supports_狮子", supportsCandidateIds: ["狮子"], conflictsCandidateIds: ["巨蟹", "处女"] },
{ outcomeId: "supports_处女", supportsCandidateIds: ["处女"], conflictsCandidateIds: ["巨蟹", "狮子"] },
],
candidateSplitHash: "varga.d10.巨蟹|狮子|处女",
informationGain: 0.12,
sourceFeatures: [{ technique: "D10", calculationResultId: RESULT_ID }],
domain: "career",
year: null,
semanticKey: "varga.d10.巨蟹|狮子|处女",
}],
},
});
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
assert.match(plan.next_followup?.user_prompt_hint ?? "", /职业前事|事业盘/);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "adopt_representative");
discriminatorProbe: {
probeId: "contrast:varga.d10",
candidateSetVersion: "05:00-05:02",
question: "核对一段还没用进评分的职业前事",
expectedOutcomes: [
{ outcomeId: "a", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:01"] },
{ outcomeId: "b", supportsCandidateIds: ["05:01"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d10",
informationGain: 0.12,
sourceFeatures: [{ technique: "D10", calculationResultId: RESULT_ID }],
domain: "career",
year: null,
semanticKey: "varga.d10",
},
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "discriminate_candidates");
});
@@ -1125,10 +1197,10 @@ test("high information_gain leftover probe still blocks offering after coverage"
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "collect_evidence");
}), "discriminate_candidates");
});
test("event_probe does not block offering once blocking methods are covered", () => {
test("event_probe still discriminates after coverage when candidates remain tied", () => {
const plan = buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
eventProbes: [{
@@ -1144,14 +1216,20 @@ test("event_probe does not block offering once blocking methods are covered", ()
role: "reverse_verify",
}],
});
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), false);
assert.equal(plan.next_followup?.source, "event_probe");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), true);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
}), "adopt_representative");
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:01", score: 33 },
{ time: "05:02", score: 33 },
],
}), "discriminate_candidates");
});
test("accepted time reverse-verifies an uncovered year in a covered domain", () => {
@@ -1206,6 +1284,12 @@ test("declining occupation covers the method; declining horary is skipped_by_pol
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 62 },
{ time: "05:01", score: 22 },
{ time: "05:02", score: 16 },
],
holdoutValidation: "passed",
}), "adopt_representative");
});
@@ -1214,12 +1298,19 @@ test("horary follow-up does not block propose once occupation is covered", () =>
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
});
assert.equal(plan.next_followup?.method_id, "horary");
assert.equal(isOfferBlockingFollowup(plan.next_followup, plan.methods), false);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: plan.next_followup,
methods: plan.methods,
candidateScores: [
{ time: "05:00", score: 62 },
{ time: "05:01", score: 22 },
{ time: "05:02", score: 16 },
],
holdoutValidation: "passed",
}), "adopt_representative");
});
@@ -1275,6 +1366,7 @@ test("user stop with selection_allowed may offer the escape hatch", async () =>
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "04:48",
evidenceLedgerFingerprint: null,
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "04:48", relative_support: 58, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "04:49", relative_support: 42, tied_minute_count: 2 },
@@ -1306,3 +1398,104 @@ test("user stop with selection_allowed may offer the escape hatch", async () =>
);
});
test("offer-candidates refuses a 34/33/33 tie after method coverage", async () => {
const accounting = fakeAccounting({
...receiptHandlers,
get_agentic_rectification_case_dossier: () => dossierFixture({
evidence: [
{ ...educationEvidence, domain: "education" },
{
id: "44444444-4444-4444-8444-444444444442",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "relationship_start",
domain: "relationship",
occurred_from: "2018-01-01",
occurred_to: null,
date_precision: "year",
summary: "感情变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:07.000Z",
},
{
id: "44444444-4444-4444-8444-444444444443",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "career_entry",
domain: "career",
occurred_from: "2019-01-01",
occurred_to: null,
date_precision: "year",
summary: "工作变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:08.000Z",
},
{
id: "44444444-4444-4444-8444-444444444446",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "family_event",
domain: "family",
occurred_from: "2020-01-01",
occurred_to: null,
date_precision: "year",
summary: "家人变化",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:09.000Z",
},
{
id: "44444444-4444-4444-8444-444444444445",
source_turn_id: TURN_ID,
subject: "self",
event_kind: "occupation_note",
domain: "occupation",
occurred_from: null,
occurred_to: null,
date_precision: "unknown",
summary: "长期一直是程序员",
status: "confirmed",
supersedes_evidence_id: null,
created_at: "2026-08-12T10:00:10.000Z",
},
],
latestResult: candidateSnapshotFixture({
selectionAllowed: true,
representativeTime: "05:00",
evidenceLedgerFingerprint: null,
decisionReceipt: {
propose_allowed: true,
window_scan: {
scanned: true,
d9_lagna_count: 3,
d10_lagna_count: 3,
d9_candidates_differ: true,
d10_candidates_differ: true,
d9_sign_names: ["天秤", "天蝎", "射手"],
d10_sign_names: ["巨蟹", "狮子", "处女"],
},
},
candidates: [
{ candidate_id: CANDIDATE_ID, rank: 1, time: "05:00", relative_support: 34, tied_minute_count: 1 },
{ candidate_id: SECOND_CANDIDATE_ID, rank: 2, time: "05:01", relative_support: 33, tied_minute_count: 1 },
{ candidate_id: THIRD_CANDIDATE_ID, rank: 3, time: "05:02", relative_support: 33, tied_minute_count: 1 },
],
}),
}),
});
const tools = createRectificationV9Tools({
userId: USER_ID,
caseId: CASE_ID,
turnId: TURN_ID,
accounting: accounting.client as never,
});
await assert.rejects(
() => (tools["rectification-offer-candidates"] as unknown as {
execute(input: unknown): Promise<unknown>;
}).execute({ caseId: CASE_ID }),
(error: unknown) => error instanceof RectificationToolServiceError && error.code === "offer_not_allowed",
);
});