fix(web): restore rectification discriminator cards and stop-offer path

Coverage-complete ties never persisted A/B/C/D because contrast probes were stamped with an answered education quality probe, remaining minutes were asked as window D10 signs, and 「没有了」 missed the stop pattern.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-08-25 14:41:48 +08:00
co-authored by Cursor
parent 20ede7cadf
commit d7afe5b50d
19 changed files with 924 additions and 48 deletions
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { buildMethodFollowupPlan, buildNextUserAction, conversationalSessionOutcome, isOfferBlockingFollowup } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import { buildMethodFollowupPlan, buildNextUserAction, conversationalSessionOutcome, isOfferBlockingFollowup, latestUserStoppedCollecting } from "../src/lib/rectification-agentic/v9/method-followup.ts";
import {
internalObservationsFromWindowScan,
parseWindowScan,
@@ -1144,6 +1144,72 @@ test("D9/D10 contrast after occupation coverage asks a discriminator, not adopt"
}), "discriminate_candidates");
});
test("answered duty language skips window D10 and uses remaining D24", () => {
const packet = {
candidateSetVersion: "05:00-05:07",
vargaDifferences: [
{ layer: "d10", signs: ["巨蟹座", "狮子座", "处女座"] },
{ layer: "d24", signs: ["05:00", "05:06|05:07"] },
],
probes: [{
probeId: "contrast:varga.d24.05:00/05:06|05:07",
candidateSetVersion: "05:00-05:07",
question: "当前几个候选在学业盘上还分得开。请核对一段还没用进评分的学业前事。",
expectedOutcomes: [
{ outcomeId: "yes", supportsCandidateIds: ["05:00"], conflictsCandidateIds: ["05:06", "05:07"] },
{ outcomeId: "no", supportsCandidateIds: ["05:06", "05:07"], conflictsCandidateIds: ["05:00"] },
],
candidateSplitHash: "varga.d24.05:00/05:06|05:07",
informationGain: 0.16,
sourceFeatures: [{ technique: "D24", calculationResultId: RESULT_ID }],
domain: "education",
year: null,
semanticKey: "varga.d24.05:00/05:06|05:07",
}],
};
const evidence = [
...CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
{
status: "draft" as const,
domain: "occupation",
datePrecision: "unknown" as const,
occurredFrom: null,
occurredTo: null,
eventKind: "occupation_note",
summary: "医疗器械算法,第三个(技术执行)",
},
];
const plan = buildMethodFollowupPlan({
evidence,
contrastPacket: packet,
});
assert.equal(plan.next_followup?.domain, "education");
assert.equal(plan.next_followup?.semantic_key, "varga.d24.05:00/05:06|05:07");
assert.doesNotMatch(plan.next_followup?.semantic_key ?? "", /varga\.d10/);
});
test("没有了 is a user stop", () => {
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "没有了" }]), true);
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "没了" }]), true);
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "想不起来了" }]), true);
assert.equal(latestUserStoppedCollecting([{ role: "user", text: "还有一件升学" }]), false);
assert.equal(conversationalSessionOutcome({
selectionAllowed: true,
proposeAllowed: true,
confirmationAllowed: false,
nextFollowup: null,
methods: buildMethodFollowupPlan({
evidence: CLASSIC_COVERAGE.filter((item) => item.domain !== "horary"),
}).methods,
userStopped: true,
candidateScores: [
{ time: "05:00", score: 34 },
{ time: "05:06", score: 33 },
{ time: "05:07", score: 33 },
],
}), "provisional_range");
});
test("same domain different year still asks a conflict probe", () => {
const plan = buildMethodFollowupPlan({