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:
@@ -574,7 +574,7 @@ test("rectification Agent output stays natural and keeps tool execution silent",
|
||||
assert.match(agent, /本轮做了什么/);
|
||||
assert.match(agent, /完成凭证完全由服务端公开 Activity\/receipt 展示/);
|
||||
assert.match(agent, /禁止只说记下了、会话会保留、以后再继续/);
|
||||
assert.match(agent, /暂时想不到了 \/ 没有更多 \/ 先这样/);
|
||||
assert.match(agent, /暂时想不到了 \/ 没有更多 \/ 没有了 \/ 没了 \/ 没有其它 \/ 想不起来了 \/ 先这样/);
|
||||
assert.match(agent, /on_user_stop/);
|
||||
assert.match(agent, /skill_verification_report/);
|
||||
assert.match(agent, /D9\/D10 类型对照/);
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
parseRectificationChoiceCard,
|
||||
} from "../src/lib/rectification-agentic/v9/choice-card.ts";
|
||||
import { buildMethodFollowupPlan, projectRectificationChoiceCard } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import { choiceCardFromCaseDossier } from "../src/lib/rectification-agentic/v9/interview-state.ts";
|
||||
import type { DiscriminatingEventProbe } from "../src/lib/rectification-agentic/v9/refinement-packet.ts";
|
||||
|
||||
const SAMPLE_COPY = {
|
||||
@@ -516,3 +517,152 @@ test("choice card user messages expose A/B/C/D as a leading key", () => {
|
||||
assert.equal(parseChoiceKeyFromUserMessage(`${HOLDOUT_MESSAGE_PREFIX}:B. 有类似但年份不对`), "B");
|
||||
assert.equal(parseChoiceKeyFromUserMessage("没有明显发生"), null);
|
||||
});
|
||||
|
||||
test("GET choice_card stays after coverage when remaining minutes still split on D24", () => {
|
||||
const card = choiceCardFromCaseDossier({
|
||||
evidence: [{
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2016-01-01",
|
||||
occurredTo: null,
|
||||
summary: "2016 年大学入学",
|
||||
}, {
|
||||
status: "confirmed",
|
||||
domain: "relationship",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2018-01-01",
|
||||
occurredTo: null,
|
||||
}, {
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2019-01-01",
|
||||
occurredTo: null,
|
||||
}, {
|
||||
status: "confirmed",
|
||||
domain: "family",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2020-01-01",
|
||||
occurredTo: null,
|
||||
}, {
|
||||
status: "draft",
|
||||
domain: "occupation",
|
||||
datePrecision: "unknown",
|
||||
occurredFrom: null,
|
||||
occurredTo: null,
|
||||
eventKind: "occupation_note",
|
||||
summary: "医疗器械算法,第三个(技术执行)",
|
||||
}],
|
||||
conversationSummary: {
|
||||
activeFocus: {
|
||||
id: "abababab-abab-4bab-8bab-abababababab",
|
||||
intent: "distinguish_candidates",
|
||||
targetDomain: "education",
|
||||
targetKind: "education_milestone",
|
||||
expectedAnswerSchema: {
|
||||
choice: SAMPLE_COPY,
|
||||
probe_id: "contrast:varga.d24.05:00/05:06|05:07",
|
||||
semantic_key: "varga.d24.05:00/05:06|05:07",
|
||||
},
|
||||
},
|
||||
declinedSkippedTopics: [],
|
||||
},
|
||||
latestResult: {
|
||||
resultId: "55555555-5555-4555-8555-555555555555",
|
||||
selectionAllowed: true,
|
||||
candidates: [
|
||||
{ time: "05:00", relativeSupport: 34 },
|
||||
{ time: "05:06", relativeSupport: 33 },
|
||||
{ time: "05:07", relativeSupport: 33 },
|
||||
],
|
||||
decisionReceipt: {
|
||||
propose_allowed: true,
|
||||
window_scan: {
|
||||
scanned: true,
|
||||
d10_candidates_differ: true,
|
||||
d10_sign_names: ["巨蟹座", "狮子座", "处女座"],
|
||||
d24_candidates_differ: true,
|
||||
transitions: [
|
||||
{ layer: "d10", at: "05:00" },
|
||||
{ layer: "d10", at: "05:15" },
|
||||
{ layer: "d24", at: "05:00" },
|
||||
{ layer: "d24", at: "05:06" },
|
||||
],
|
||||
},
|
||||
inference_state: {
|
||||
answered_probes: [{
|
||||
probe_id: "probe:education.2016",
|
||||
semantic_key: "education.2016",
|
||||
answer_class: "yes",
|
||||
}],
|
||||
probes: [{
|
||||
id: "probe:education.2016",
|
||||
semantic_key: "education.2016",
|
||||
information_gain: 0,
|
||||
source: "known_event_quality",
|
||||
expected_outcomes: [],
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
case: { acceptedTime: null },
|
||||
turns: [],
|
||||
});
|
||||
assert.ok(card);
|
||||
assert.equal(card.probe_id, "contrast:varga.d24.05:00/05:06|05:07");
|
||||
assert.equal(card.prompt, SAMPLE_COPY.prompt);
|
||||
});
|
||||
|
||||
test("GET choice_card stays hidden after 没有了 when selection is allowed", () => {
|
||||
const card = choiceCardFromCaseDossier({
|
||||
evidence: [{
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2016-01-01",
|
||||
occurredTo: null,
|
||||
}, {
|
||||
status: "confirmed",
|
||||
domain: "relationship",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2018-01-01",
|
||||
occurredTo: null,
|
||||
}, {
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2019-01-01",
|
||||
occurredTo: null,
|
||||
}, {
|
||||
status: "confirmed",
|
||||
domain: "family",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2020-01-01",
|
||||
occurredTo: null,
|
||||
}, {
|
||||
status: "draft",
|
||||
domain: "occupation",
|
||||
datePrecision: "unknown",
|
||||
occurredFrom: null,
|
||||
occurredTo: null,
|
||||
eventKind: "occupation_note",
|
||||
}],
|
||||
conversationSummary: {
|
||||
activeFocus: null,
|
||||
declinedSkippedTopics: [],
|
||||
},
|
||||
latestResult: {
|
||||
selectionAllowed: true,
|
||||
candidates: [
|
||||
{ time: "05:00", relativeSupport: 34 },
|
||||
{ time: "05:06", relativeSupport: 33 },
|
||||
{ time: "05:07", relativeSupport: 33 },
|
||||
],
|
||||
decisionReceipt: { propose_allowed: true },
|
||||
},
|
||||
case: { acceptedTime: null },
|
||||
turns: [{ role: "user", text: "没有了" }],
|
||||
});
|
||||
assert.equal(card, null);
|
||||
});
|
||||
|
||||
@@ -127,6 +127,45 @@ test("D9/D10 sign differences synthesize a contrast probe when engine probes are
|
||||
assert.match(probe.question, /职业前事|事业盘/);
|
||||
});
|
||||
|
||||
test("remaining-candidate D24 split beats window D10 signs", () => {
|
||||
const packet = buildCandidateContrastPacket({
|
||||
candidateSetVersion: "05:00-05:07",
|
||||
calculationResultId: "22222222-2222-4222-8222-222222222222",
|
||||
vargaDifferences: [
|
||||
{ layer: "d10", signs: ["巨蟹座", "狮子座", "处女座"] },
|
||||
],
|
||||
candidateTimes: ["05:00", "05:06", "05:07"],
|
||||
transitions: [
|
||||
{ layer: "d10", at: "05:00" },
|
||||
{ layer: "d10", at: "05:15" },
|
||||
{ layer: "d24", at: "05:00" },
|
||||
{ layer: "d24", at: "05:06" },
|
||||
],
|
||||
askedKeys: ["education.2016", "varga.d10"],
|
||||
});
|
||||
const probe = selectDiscriminatorProbe(packet);
|
||||
assert.ok(probe);
|
||||
assert.match(probe.semanticKey, /varga\.d24/);
|
||||
assert.equal(probe.domain, "education");
|
||||
assert.deepEqual(probe.expectedOutcomes[0]?.supportsCandidateIds, ["05:00"]);
|
||||
assert.ok(probe.expectedOutcomes[0]?.conflictsCandidateIds.includes("05:06"));
|
||||
assert.ok(probe.expectedOutcomes.every((row) => (
|
||||
row.supportsCandidateIds.every((id) => id.includes(":"))
|
||||
)));
|
||||
});
|
||||
|
||||
test("user stop with selection_allowed offers a provisional range", () => {
|
||||
const next = decideNextAction({
|
||||
methodCoverageAll: true,
|
||||
proposeAllowed: true,
|
||||
selectionAllowed: true,
|
||||
userStopped: true,
|
||||
candidateScores: TIED,
|
||||
discriminatorProbe: CONTRAST_PROBE,
|
||||
});
|
||||
assert.equal(next.type, "offer_provisional_range");
|
||||
});
|
||||
|
||||
test("non-scoreable occupation note does not change the scoreable evidence fingerprint", () => {
|
||||
const dated = [{
|
||||
id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -322,6 +322,15 @@ test("holdout events stay out of training and a winner must stay stable for two
|
||||
[{ id: "job", domain: "career", year: 2019, precision: "year" }],
|
||||
);
|
||||
assert.equal(matching[0]?.classified_from, "evidence");
|
||||
const quality = {
|
||||
...probe({ id: "p-quality", domain: "education", year: 2016, gain: 0, yesSupports: ["05:00"], yesConflicts: ["05:10"] }),
|
||||
source: "known_event_quality",
|
||||
};
|
||||
const skippedQuality = answersFromEvidence(
|
||||
[quality],
|
||||
[{ id: "enroll", domain: "education", year: 2016, precision: "year" }],
|
||||
);
|
||||
assert.equal(skippedQuality.length, 0);
|
||||
});
|
||||
|
||||
test("C without new evidence updates the posterior immediately and D only marks the split asked", () => {
|
||||
@@ -385,6 +394,50 @@ test("C without new evidence updates the posterior immediately and D only marks
|
||||
})], unsure.state.answered_probes)?.id, "p-other");
|
||||
});
|
||||
|
||||
test("A/B/C/D on a remaining-minute contrast probe moves the posterior", () => {
|
||||
const contrast: ConflictProbe = {
|
||||
id: "contrast:varga.d24.05:00/05:06|05:07",
|
||||
semantic_key: "varga.d24.05:00/05:06|05:07",
|
||||
candidate_split_hash: "varga.d24.05:00/05:06|05:07",
|
||||
domain: "education",
|
||||
year: 0,
|
||||
question: "学业盘还分得开",
|
||||
candidate_ids: ["05:00", "05:06", "05:07"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:06", "05:07"] },
|
||||
{ answer_class: "no", supports: ["05:06", "05:07"], conflicts: ["05:00"] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
],
|
||||
information_gain: 0.16,
|
||||
source: "varga_contrast",
|
||||
};
|
||||
const state = buildInferenceState({
|
||||
range_start: "04:45",
|
||||
range_end: "05:15",
|
||||
candidates: [
|
||||
{ id: "05:00", time: "05:00", relative_support: 34 },
|
||||
{ id: "05:06", time: "05:06", relative_support: 33 },
|
||||
{ id: "05:07", time: "05:07", relative_support: 33 },
|
||||
],
|
||||
events: [{ id: "e1", domain: "education", year: 2016, precision: "year" }],
|
||||
probes: [contrast],
|
||||
});
|
||||
const before = posteriorMap(state.candidates);
|
||||
const applied = applyChoiceWithoutEvidence(state, {
|
||||
choiceKey: "C",
|
||||
schema: {
|
||||
choice: { prompt: "那次考试有没有发挥失常?" },
|
||||
probe_id: contrast.id,
|
||||
semantic_key: contrast.semantic_key,
|
||||
},
|
||||
});
|
||||
assert.equal(applied.applied, true);
|
||||
assert.equal(applied.answerClass, "no");
|
||||
assert.notDeepEqual(posteriorMap(applied.state.candidates), before);
|
||||
assert.ok((applied.state.candidates.find((item) => item.time === "05:06")?.posterior_score ?? 0)
|
||||
> (applied.state.candidates.find((item) => item.time === "05:00")?.posterior_score ?? 0));
|
||||
});
|
||||
|
||||
test("holdout and collection declines do not write a probe answer", () => {
|
||||
const conflict = probe({
|
||||
id: "p-holdout",
|
||||
|
||||
@@ -109,3 +109,64 @@ test("duplicate focus conflict does not throw", async () => {
|
||||
});
|
||||
assert.equal(result.status, "duplicate_focus");
|
||||
});
|
||||
|
||||
test("contrast probe is not replaced by an already-answered education quality probe", async () => {
|
||||
const followup = discriminatorFollowup({
|
||||
domain: "education",
|
||||
ask_theme: "education_style",
|
||||
information_gain: 0.16,
|
||||
semantic_key: "varga.d24.05:00/05:06|05:07",
|
||||
candidate_split_hash: "varga.d24.05:00/05:06|05:07",
|
||||
probe_year: undefined,
|
||||
});
|
||||
const accounting = fakeAccounting({
|
||||
set_agentic_rectification_conversation_focus: (_fn, args) => ({
|
||||
id: FOCUS_ID,
|
||||
case_id: CASE_ID,
|
||||
question_id: args.p_question_id,
|
||||
intent: args.p_intent,
|
||||
target_evidence_id: null,
|
||||
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-25T00:00:00.000Z",
|
||||
resolved_at: null,
|
||||
idempotent: false,
|
||||
}),
|
||||
});
|
||||
const result = await persistServerOwnedFocus({
|
||||
accounting: accounting.client,
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
activeFocus: null,
|
||||
decisionReceipt: {
|
||||
inference_state: {
|
||||
answered_probes: [{
|
||||
probe_id: "probe:education.2016",
|
||||
semantic_key: "education.2016",
|
||||
answer_class: "yes",
|
||||
}],
|
||||
probes: [{
|
||||
id: "probe:education.2016",
|
||||
semantic_key: "education.2016",
|
||||
candidate_split_hash: "education:2016",
|
||||
domain: "education",
|
||||
year: 2016,
|
||||
question: "发挥失常",
|
||||
candidate_ids: [],
|
||||
expected_outcomes: [],
|
||||
information_gain: 0,
|
||||
source: "known_event_quality",
|
||||
}],
|
||||
},
|
||||
},
|
||||
followup,
|
||||
});
|
||||
assert.equal(result.status, "created");
|
||||
assert.ok(result.focus);
|
||||
const schema = result.focus.expectedAnswerSchema;
|
||||
assert.equal(schema.semantic_key, "varga.d24.05:00/05:06|05:07");
|
||||
assert.notEqual(schema.probe_id, "probe:education.2016");
|
||||
assert.match(String(schema.probe_id), /varga\.d24/);
|
||||
});
|
||||
|
||||
@@ -113,6 +113,30 @@ test("Chinese interview planning after tools stays out of the spoken answer", ()
|
||||
assert.equal(isRectificationProcessNarration(spoken), false);
|
||||
});
|
||||
|
||||
test("planning about candidate_contrast_packet stays out of the spoken answer", () => {
|
||||
const processTalk = [
|
||||
"这意味着:方法资料已齐,还不能出牌。",
|
||||
"服务器给了 candidate_contrast_packet,choice_frame 已写好。",
|
||||
"第 7 条边界:id=ask_candidate_discriminator 时不得 offer。",
|
||||
].join("\n\n");
|
||||
const spoken = "那次高考或重要考试,发挥有没有明显失常、压力很大?说有或没有就行。";
|
||||
|
||||
const split = splitRectificationSpokenAndThinking(`${processTalk}\n\n${spoken}`);
|
||||
assert.equal(split.spoken, spoken);
|
||||
assert.match(split.thinking, /这意味着/);
|
||||
assert.match(split.thinking, /candidate_contrast_packet/);
|
||||
assert.match(split.thinking, /不得 offer/);
|
||||
assert.match(split.thinking, /第 7 条边界/);
|
||||
assert.doesNotMatch(split.thinking, /发挥有没有明显失常/);
|
||||
assert.equal(isRectificationProcessNarration("这意味着:方法资料已齐"), true);
|
||||
assert.equal(isRectificationProcessNarration("服务器给了 candidate_contrast_packet"), true);
|
||||
assert.equal(isRectificationProcessNarration(spoken), false);
|
||||
|
||||
const settled = settleRectificationSpokenAndThinking(`${processTalk}\n\n${spoken}`, "");
|
||||
assert.equal(settled.spoken, spoken);
|
||||
assert.doesNotMatch(settled.spoken, /candidate_contrast_packet/);
|
||||
});
|
||||
|
||||
test("leaked process text on the answer channel is not mixed into native thinking", () => {
|
||||
const processTalk = "用户在上一轮里提供了两件带日期的经历。我需要用批量工具写入这些证据。用户";
|
||||
const spoken = "记下了升学这两件。接下来有没有一件带大概年份的工作变化?";
|
||||
|
||||
Reference in New Issue
Block a user