fix(rectification): stop discriminator followup from dropping user evidence
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:
@@ -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);
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildInferenceState } from "../src/lib/rectification-agentic/core/build-state.ts";
|
||||
import {
|
||||
inspectDiscriminatorProbes,
|
||||
withCompletedContrastOptions,
|
||||
type CandidateDiscriminatorProbe,
|
||||
} from "../src/lib/rectification-agentic/core/candidate-contrast-packet.ts";
|
||||
import type { ConflictProbe } from "../src/lib/rectification-agentic/core/types.ts";
|
||||
import {
|
||||
contrastPacketFromDossier,
|
||||
decideFromDossier,
|
||||
followupAsksRenderableDiscriminator,
|
||||
rectificationFollowupCatalog,
|
||||
type DecisionDossier,
|
||||
} from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
||||
import { buildMethodFollowupPlan } from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
|
||||
const CAREER_2020_04: ConflictProbe = {
|
||||
id: "probe:career.2020.04.dasha_boundary",
|
||||
semantic_key: "career.2020.04.dasha_boundary",
|
||||
candidate_split_hash: "career.2020.04",
|
||||
domain: "career",
|
||||
year: 2020,
|
||||
question: "2020 年 4 月前后有没有入职或换工作?",
|
||||
candidate_ids: ["05:00", "05:10"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||||
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
],
|
||||
information_gain: 0.9,
|
||||
source: "dasha_boundary",
|
||||
choice_kind: "existence",
|
||||
};
|
||||
|
||||
const REL_2024_05: ConflictProbe = {
|
||||
id: "probe:relationship.2024.05.dasha_boundary",
|
||||
semantic_key: "relationship.2024.05.dasha_boundary",
|
||||
candidate_split_hash: "relationship.2024.05",
|
||||
domain: "relationship",
|
||||
year: 2024,
|
||||
question: "2024 年 5 月前后有没有开始一段认真关系?",
|
||||
candidate_ids: ["05:00", "05:10"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||||
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
],
|
||||
information_gain: 0.85,
|
||||
source: "dasha_boundary",
|
||||
choice_kind: "existence",
|
||||
};
|
||||
|
||||
const D24: ConflictProbe = {
|
||||
id: "contrast:varga.d24.05:00/05:10",
|
||||
semantic_key: "varga.d24.05:00/05:10",
|
||||
candidate_split_hash: "varga.d24.05:00/05:10",
|
||||
domain: "education",
|
||||
year: 0,
|
||||
question: "有没有学业或考试发挥明显失常、压力特别大的时候?",
|
||||
candidate_ids: ["05:00", "05:10"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||||
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
],
|
||||
information_gain: 2.5,
|
||||
source: "varga_contrast",
|
||||
choice_kind: "event_quality",
|
||||
};
|
||||
|
||||
const D12: ConflictProbe = {
|
||||
id: "contrast:varga.d12.05:00/05:10",
|
||||
semantic_key: "varga.d12.05:00/05:10",
|
||||
candidate_split_hash: "varga.d12.05:00/05:10",
|
||||
domain: "family",
|
||||
year: 0,
|
||||
question: "家里有没有结婚、添丁或住院这类事?",
|
||||
candidate_ids: ["05:00", "05:10"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||||
{ answer_class: "no", supports: ["05:10"], conflicts: ["05:00"] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
],
|
||||
information_gain: 0.8,
|
||||
source: "varga_contrast",
|
||||
choice_kind: "existence",
|
||||
};
|
||||
|
||||
const D9_STYLE_NO_OPTIONS: ConflictProbe = {
|
||||
id: "contrast:varga.d9.巨蟹座/狮子座",
|
||||
semantic_key: "varga.d9.巨蟹座/狮子座",
|
||||
candidate_split_hash: "varga.d9.巨蟹座/狮子座",
|
||||
domain: "relationship",
|
||||
year: 0,
|
||||
question: "亲密关系里更接近下面哪一种相处方式?",
|
||||
candidate_ids: ["05:00", "05:10"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||||
{ answer_class: "weak_yes", supports: ["05:10"], conflicts: ["05:00"] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
],
|
||||
information_gain: 1.4,
|
||||
source: "varga_contrast",
|
||||
choice_kind: "varga_style",
|
||||
};
|
||||
|
||||
const D10_STYLE_NO_OPTIONS: ConflictProbe = {
|
||||
id: "contrast:varga.d10.天秤座/天蝎座",
|
||||
semantic_key: "varga.d10.天秤座/天蝎座",
|
||||
candidate_split_hash: "varga.d10.天秤座/天蝎座",
|
||||
domain: "career",
|
||||
year: 0,
|
||||
question: "平时做事更接近下面哪一种职责风格?",
|
||||
candidate_ids: ["05:00", "05:10"],
|
||||
expected_outcomes: [
|
||||
{ answer_class: "yes", supports: ["05:00"], conflicts: ["05:10"] },
|
||||
{ answer_class: "weak_yes", supports: ["05:10"], conflicts: ["05:00"] },
|
||||
{ answer_class: "unsure", supports: [], conflicts: [] },
|
||||
],
|
||||
information_gain: 1.2,
|
||||
source: "varga_contrast",
|
||||
choice_kind: "varga_style",
|
||||
};
|
||||
|
||||
const FAMILY_2021_COLLECT = {
|
||||
year: 2021,
|
||||
year_label: "2021 年前后",
|
||||
domain: "family" as const,
|
||||
event_family: "家人结婚、添丁或住院",
|
||||
source: "age_band" as const,
|
||||
tracks: ["vimshottari", "narayana"] as const,
|
||||
tracks_agree: false,
|
||||
unique_minute_claim: false as const,
|
||||
user_meaning: "时间范围锁定 2021 年前后;领域锁定 family。",
|
||||
role: "collect" as const,
|
||||
phase: "evidence_collection" as const,
|
||||
information_gain: 0,
|
||||
semantic_key: "family.2021",
|
||||
candidate_split_hash: "family:2021",
|
||||
candidate_ids: [] as const,
|
||||
expected_outcomes: [] as const,
|
||||
choice_kind: "existence" as const,
|
||||
};
|
||||
|
||||
const EVIDENCE = [
|
||||
{
|
||||
id: "e-career-entry",
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2020-04-01",
|
||||
occurredTo: null,
|
||||
eventKind: "career_entry",
|
||||
},
|
||||
{
|
||||
id: "e-career-exit",
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2020-10-01",
|
||||
occurredTo: null,
|
||||
eventKind: "career_exit",
|
||||
},
|
||||
{
|
||||
id: "e-rel-start",
|
||||
status: "confirmed",
|
||||
domain: "relationship",
|
||||
datePrecision: "month",
|
||||
occurredFrom: "2024-05-01",
|
||||
occurredTo: null,
|
||||
eventKind: "relationship_start",
|
||||
},
|
||||
{
|
||||
id: "e-rel-end",
|
||||
status: "confirmed",
|
||||
domain: "relationship",
|
||||
datePrecision: "day",
|
||||
occurredFrom: "2024-08-08",
|
||||
occurredTo: null,
|
||||
eventKind: "relationship_end",
|
||||
},
|
||||
] as const;
|
||||
|
||||
function contrastFromConflict(probe: ConflictProbe): CandidateDiscriminatorProbe {
|
||||
return {
|
||||
probeId: probe.id,
|
||||
candidateSetVersion: "05:00-05:10",
|
||||
question: probe.question,
|
||||
expectedOutcomes: probe.expected_outcomes.map((row) => ({
|
||||
outcomeId: row.answer_class,
|
||||
supportsCandidateIds: row.supports,
|
||||
conflictsCandidateIds: row.conflicts,
|
||||
})),
|
||||
candidateSplitHash: probe.candidate_split_hash,
|
||||
informationGain: probe.information_gain,
|
||||
sourceFeatures: [{ technique: probe.domain, calculationResultId: null }],
|
||||
domain: probe.domain,
|
||||
year: probe.year > 0 ? probe.year : null,
|
||||
semanticKey: probe.semantic_key,
|
||||
choiceKind: probe.choice_kind,
|
||||
};
|
||||
}
|
||||
|
||||
function deadlockDossier(): { dossier: DecisionDossier; state: ReturnType<typeof buildInferenceState> } {
|
||||
const state = buildInferenceState({
|
||||
range_start: "05:00",
|
||||
range_end: "05:10",
|
||||
candidates: [
|
||||
{ id: "05:00", time: "05:00", relative_support: 18 },
|
||||
{ id: "05:10", time: "05:10", relative_support: 16 },
|
||||
],
|
||||
events: [
|
||||
{ id: "e-career-entry", domain: "career", year: 2020, precision: "month" },
|
||||
{ id: "e-career-exit", domain: "career", year: 2020, precision: "month" },
|
||||
{ id: "e-rel-start", domain: "relationship", year: 2024, precision: "month" },
|
||||
{ id: "e-rel-end", domain: "relationship", year: 2024, precision: "day" },
|
||||
],
|
||||
probes: [CAREER_2020_04, REL_2024_05, D24, D12, D9_STYLE_NO_OPTIONS, D10_STYLE_NO_OPTIONS],
|
||||
answered_probes: [
|
||||
{
|
||||
probe_id: CAREER_2020_04.id,
|
||||
semantic_key: CAREER_2020_04.semantic_key,
|
||||
candidate_split_hash: CAREER_2020_04.candidate_split_hash,
|
||||
answer_class: "yes",
|
||||
classified_from: "choice",
|
||||
},
|
||||
{
|
||||
probe_id: REL_2024_05.id,
|
||||
semantic_key: REL_2024_05.semantic_key,
|
||||
candidate_split_hash: REL_2024_05.candidate_split_hash,
|
||||
answer_class: "yes",
|
||||
classified_from: "choice",
|
||||
},
|
||||
],
|
||||
});
|
||||
const dossier: DecisionDossier = {
|
||||
evidence: EVIDENCE,
|
||||
conversationSummary: { activeFocus: null, declinedSkippedTopics: [] },
|
||||
latestResult: {
|
||||
resultId: "55555555-5555-4555-8555-555555555555",
|
||||
candidates: state.candidates.map((item) => ({
|
||||
candidateId: item.id,
|
||||
time: item.time,
|
||||
rank: item.rank,
|
||||
relativeSupport: Math.round(item.posterior_score),
|
||||
})),
|
||||
representativeTime: state.representative_time,
|
||||
evidenceLedgerFingerprint: evidenceLedgerFingerprint(EVIDENCE as never),
|
||||
decisionReceipt: {
|
||||
inference_state: state,
|
||||
evidence_collection_probes: [FAMILY_2021_COLLECT],
|
||||
},
|
||||
},
|
||||
case: { acceptedTime: null },
|
||||
};
|
||||
return { dossier, state };
|
||||
}
|
||||
|
||||
test("deadlock case: decision and followup agree and still render a D9/D10 style card", () => {
|
||||
const { dossier } = deadlockDossier();
|
||||
const inspected = inspectDiscriminatorProbes(contrastPacketFromDossier(dossier));
|
||||
assert.equal(inspected.selected?.semanticKey, D24.semantic_key);
|
||||
|
||||
const d9 = withCompletedContrastOptions(contrastFromConflict(D9_STYLE_NO_OPTIONS));
|
||||
const d10 = withCompletedContrastOptions(contrastFromConflict(D10_STYLE_NO_OPTIONS));
|
||||
assert.equal(d9.ok, true);
|
||||
assert.equal(d10.ok, true);
|
||||
if (d9.ok) assert.equal(d9.probe.choiceKind, "varga_style");
|
||||
if (d10.ok) assert.equal(d10.probe.choiceKind, "varga_style");
|
||||
|
||||
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: dossier.evidence,
|
||||
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
|
||||
sessionOutcome: "discriminate_candidates",
|
||||
...catalog,
|
||||
candidatesSeparated: false,
|
||||
});
|
||||
assert.equal(followupAsksRenderableDiscriminator(plan.next_followup), true);
|
||||
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
|
||||
assert.ok(plan.next_followup?.choice_frame);
|
||||
assert.equal(plan.next_followup?.choice_kind, "varga_style");
|
||||
assert.match(plan.next_followup?.semantic_key ?? "", /^varga\.d(9|10)\./);
|
||||
assert.equal(
|
||||
plan.dropped_probes.some((item) => item.semantic_key.startsWith("varga.d9.") || item.semantic_key.startsWith("varga.d10.")),
|
||||
false,
|
||||
JSON.stringify(plan.dropped_probes),
|
||||
);
|
||||
|
||||
const decision = decideFromDossier(dossier);
|
||||
assert.equal(decision.nextAction, "ask_candidate_discriminator");
|
||||
assert.equal(decision.sessionOutcome, "discriminate_candidates");
|
||||
assert.equal(decision.canConfirmExactMinute, false);
|
||||
});
|
||||
|
||||
test("varga_style probes missing style_options complete the same way in inspect and followup ranking", () => {
|
||||
const probe = contrastFromConflict(D9_STYLE_NO_OPTIONS);
|
||||
const inspected = inspectDiscriminatorProbes({
|
||||
candidateSetVersion: "05:00-05:10",
|
||||
vargaDifferences: [],
|
||||
probes: [probe],
|
||||
});
|
||||
assert.ok(inspected.selected);
|
||||
assert.equal(inspected.selected?.choiceKind, "varga_style");
|
||||
assert.equal(inspected.dropped.length, 0);
|
||||
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: EVIDENCE,
|
||||
sessionOutcome: "discriminate_candidates",
|
||||
candidatesSeparated: false,
|
||||
contrastPacket: {
|
||||
candidateSetVersion: "05:00-05:10",
|
||||
vargaDifferences: [],
|
||||
probes: [probe],
|
||||
},
|
||||
});
|
||||
assert.equal(plan.next_followup?.semantic_key, probe.semanticKey);
|
||||
assert.equal(plan.next_followup?.choice_kind, "varga_style");
|
||||
assert.ok(plan.next_followup?.choice_frame);
|
||||
assert.equal(plan.dropped_probes.some((item) => item.semantic_key === probe.semanticKey), false);
|
||||
});
|
||||
|
||||
test("method coverage incomplete still asks a renderable D9 style card", () => {
|
||||
const { dossier } = deadlockDossier();
|
||||
const decision = decideFromDossier(dossier);
|
||||
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: dossier.evidence,
|
||||
declinedTopics: [],
|
||||
sessionOutcome: "discriminate_candidates",
|
||||
...catalog,
|
||||
candidatesSeparated: false,
|
||||
});
|
||||
assert.equal(plan.methods.find((item) => item.method_id === "relatives")?.status, "uncovered");
|
||||
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
|
||||
assert.equal(decision.nextAction, "ask_candidate_discriminator");
|
||||
assert.equal(plan.next_followup?.intent, "distinguish_candidates");
|
||||
assert.ok(plan.next_followup?.choice_frame);
|
||||
assert.notEqual(plan.next_followup?.source, "method_coverage");
|
||||
});
|
||||
|
||||
test("clock-key varga_style without signs fail-closes to collect instead of a dead-end discriminator", () => {
|
||||
const d9Clock: ConflictProbe = {
|
||||
...D9_STYLE_NO_OPTIONS,
|
||||
id: "contrast:varga.d9.05:00/05:10",
|
||||
semantic_key: "varga.d9.05:00/05:10",
|
||||
candidate_split_hash: "varga.d9.05:00/05:10",
|
||||
};
|
||||
const d10Clock: ConflictProbe = {
|
||||
...D10_STYLE_NO_OPTIONS,
|
||||
id: "contrast:varga.d10.05:00/05:10",
|
||||
semantic_key: "varga.d10.05:00/05:10",
|
||||
candidate_split_hash: "varga.d10.05:00/05:10",
|
||||
};
|
||||
const { dossier } = deadlockDossier();
|
||||
const state = buildInferenceState({
|
||||
range_start: "05:00",
|
||||
range_end: "05:10",
|
||||
candidates: [
|
||||
{ id: "05:00", time: "05:00", relative_support: 18 },
|
||||
{ id: "05:10", time: "05:10", relative_support: 16 },
|
||||
],
|
||||
events: [
|
||||
{ id: "e-career-entry", domain: "career", year: 2020, precision: "month" },
|
||||
{ id: "e-career-exit", domain: "career", year: 2020, precision: "month" },
|
||||
{ id: "e-rel-start", domain: "relationship", year: 2024, precision: "month" },
|
||||
{ id: "e-rel-end", domain: "relationship", year: 2024, precision: "day" },
|
||||
],
|
||||
probes: [CAREER_2020_04, REL_2024_05, D24, D12, d9Clock, d10Clock],
|
||||
answered_probes: [
|
||||
{
|
||||
probe_id: CAREER_2020_04.id,
|
||||
semantic_key: CAREER_2020_04.semantic_key,
|
||||
candidate_split_hash: CAREER_2020_04.candidate_split_hash,
|
||||
answer_class: "yes",
|
||||
classified_from: "choice",
|
||||
},
|
||||
{
|
||||
probe_id: REL_2024_05.id,
|
||||
semantic_key: REL_2024_05.semantic_key,
|
||||
candidate_split_hash: REL_2024_05.candidate_split_hash,
|
||||
answer_class: "yes",
|
||||
classified_from: "choice",
|
||||
},
|
||||
],
|
||||
});
|
||||
const clockDossier: DecisionDossier = {
|
||||
...dossier,
|
||||
latestResult: {
|
||||
...dossier.latestResult!,
|
||||
decisionReceipt: {
|
||||
inference_state: state,
|
||||
evidence_collection_probes: [FAMILY_2021_COLLECT],
|
||||
},
|
||||
},
|
||||
};
|
||||
const inspected = inspectDiscriminatorProbes(contrastPacketFromDossier(clockDossier));
|
||||
assert.equal(inspected.selected?.semanticKey, D24.semantic_key);
|
||||
assert.equal(inspected.selected?.choiceKind, "event_quality");
|
||||
const d9 = withCompletedContrastOptions(contrastFromConflict(d9Clock));
|
||||
assert.equal(d9.ok, true);
|
||||
if (d9.ok) assert.equal(d9.probe.choiceKind, "existence");
|
||||
|
||||
const catalog = rectificationFollowupCatalog(clockDossier.latestResult, clockDossier.evidence);
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: clockDossier.evidence,
|
||||
declinedTopics: [],
|
||||
sessionOutcome: "discriminate_candidates",
|
||||
...catalog,
|
||||
candidatesSeparated: false,
|
||||
});
|
||||
assert.equal(followupAsksRenderableDiscriminator(plan.next_followup), false);
|
||||
assert.equal(plan.next_followup?.intent, "collect_method_evidence");
|
||||
assert.equal(plan.next_followup?.choice_frame, null);
|
||||
assert.equal(plan.next_followup?.source, "method_coverage");
|
||||
|
||||
const decision = decideFromDossier(clockDossier);
|
||||
assert.notEqual(decision.nextAction, "ask_candidate_discriminator");
|
||||
assert.equal(decision.nextAction, "ask_fact_collection");
|
||||
assert.equal(decision.canConfirmExactMinute, false);
|
||||
});
|
||||
|
||||
test("unrenderable contrast probes are recorded in dropped_probes instead of being dropped silently", () => {
|
||||
const unrenderable = contrastFromConflict({
|
||||
...D9_STYLE_NO_OPTIONS,
|
||||
information_gain: 0,
|
||||
semantic_key: "varga.d9.dropped",
|
||||
candidate_split_hash: "varga.d9.dropped",
|
||||
id: "contrast:varga.d9.dropped",
|
||||
});
|
||||
const inspected = inspectDiscriminatorProbes({
|
||||
candidateSetVersion: "05:00-05:10",
|
||||
vargaDifferences: [],
|
||||
probes: [unrenderable],
|
||||
});
|
||||
assert.equal(inspected.selected, null);
|
||||
assert.equal(inspected.dropped.some((item) => item.semantic_key === unrenderable.semanticKey), true);
|
||||
|
||||
const plan = buildMethodFollowupPlan({
|
||||
evidence: EVIDENCE,
|
||||
sessionOutcome: "discriminate_candidates",
|
||||
candidatesSeparated: false,
|
||||
contrastPacket: {
|
||||
candidateSetVersion: "05:00-05:10",
|
||||
vargaDifferences: [],
|
||||
probes: [unrenderable],
|
||||
},
|
||||
});
|
||||
assert.equal(
|
||||
plan.dropped_probes.some((item) => item.semantic_key === unrenderable.semanticKey),
|
||||
true,
|
||||
JSON.stringify(plan.dropped_probes),
|
||||
);
|
||||
});
|
||||
|
||||
test("agent message path no longer returns the discriminator dead-end copy", () => {
|
||||
const route = readFileSync(new URL("../src/app/api/rectification/agent/route.ts", import.meta.url), "utf8");
|
||||
const fastPath = route.slice(
|
||||
route.indexOf('if (action === "message")'),
|
||||
route.indexOf("const requestTime"),
|
||||
);
|
||||
assert.doesNotMatch(fastPath, /目前没有可继续区分/);
|
||||
assert.match(route, /当前几个候选已经构成可信区间/);
|
||||
assert.match(fastPath, /DISCRIMINATOR_EXHAUSTED_NARRATION/);
|
||||
assert.match(fastPath, /!plan\.next_followup/);
|
||||
assert.ok(fastPath.includes("ask_candidate_discriminator"));
|
||||
assert.ok(fastPath.includes("open.unrenderable !== true"));
|
||||
assert.ok(route.indexOf("classifyRectificationTurnIntent") < route.indexOf("runV9AgentTurn({"));
|
||||
});
|
||||
@@ -432,3 +432,50 @@ test("contrast probe is not replaced by an already-answered education quality pr
|
||||
assert.notEqual(schema.probe_id, "probe:education.2016");
|
||||
assert.match(String(schema.probe_id), /varga\.d24/);
|
||||
});
|
||||
|
||||
test("spoken collect followup persists a collect focus without a choice card", async () => {
|
||||
const followup: MethodFollowup = {
|
||||
method_id: "relatives",
|
||||
intent: "collect_method_evidence",
|
||||
ask_theme: "family_event",
|
||||
domain: "family",
|
||||
kind_hint: "family_event",
|
||||
user_prompt_hint: "collect",
|
||||
must_not_label: false,
|
||||
choice_frame: null,
|
||||
source: "method_coverage",
|
||||
probe_year: 2021,
|
||||
year_label: "2021 年前后",
|
||||
semantic_key: "family.2021",
|
||||
};
|
||||
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-27T00:00:00.000Z",
|
||||
resolved_at: null,
|
||||
idempotent: false,
|
||||
}),
|
||||
});
|
||||
const result = await persistServerOwnedFocus({
|
||||
accounting: accounting.client,
|
||||
userId: USER_ID,
|
||||
caseId: CASE_ID,
|
||||
activeFocus: null,
|
||||
decisionReceipt: null,
|
||||
followup,
|
||||
});
|
||||
assert.equal(result.status, "created");
|
||||
assert.equal(result.focus?.intent, "collect_method_evidence");
|
||||
assert.equal(result.focus?.expectedAnswerSchema.collect, true);
|
||||
assert.equal(result.focus?.expectedAnswerSchema.choice, undefined);
|
||||
assert.match(String(result.focus?.expectedAnswerSchema.prompt ?? ""), /2021/);
|
||||
assert.equal(openQuestionFromPersistedFocus(result), null);
|
||||
});
|
||||
|
||||
@@ -105,6 +105,9 @@ test("production intent handling contains no semantic regex or positional text p
|
||||
assert.ok(route.indexOf("classifyRectificationTurnIntent") < route.indexOf("runV9AgentTurn({"));
|
||||
assert.ok(route.indexOf("persistServerOwnedFocus") < route.indexOf("runV9AgentTurn({"));
|
||||
assert.ok(fastPath.includes("ask_candidate_discriminator"));
|
||||
assert.match(fastPath, /目前没有可继续区分/);
|
||||
assert.doesNotMatch(fastPath, /目前没有可继续区分/);
|
||||
assert.match(route, /当前几个候选已经构成可信区间/);
|
||||
assert.match(fastPath, /DISCRIMINATOR_EXHAUSTED_NARRATION/);
|
||||
assert.match(fastPath, /!plan\.next_followup/);
|
||||
assert.doesNotMatch(route, /classified\.answer_class!/);
|
||||
});
|
||||
|
||||
@@ -161,6 +161,13 @@ test("varga_style B-option weak_yes counts as strong conflict; existence weak_ye
|
||||
assert.equal(existenceCounts[existenceConflicted], 0);
|
||||
});
|
||||
|
||||
test("varga_style contrast writeback keeps style_options for the next round", () => {
|
||||
const probe = styleProbeFrom(twoGroupStylePacket());
|
||||
assert.equal(probe.choice_kind, "varga_style");
|
||||
assert.ok((probe.style_options?.length ?? 0) >= 2);
|
||||
assert.ok(probe.style_options?.every((item) => item.label.trim().length > 0));
|
||||
});
|
||||
|
||||
test("engine varga.d9/d10 without style_options scores with the render effective kind", () => {
|
||||
for (const semanticKey of ["varga.d9", "varga.d10"] as const) {
|
||||
const packet = {
|
||||
|
||||
Reference in New Issue
Block a user