test(rectification): keep collection asking when scores separate before method coverage
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { decideFromDossier, rectificationFollowupCatalog } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
||||
import { buildCaseInferenceState } from "../src/lib/rectification-agentic/v9/inference-adapter.ts";
|
||||
import {
|
||||
blockingMethodsCovered,
|
||||
buildMethodFollowupPlan,
|
||||
} from "../src/lib/rectification-agentic/v9/method-followup.ts";
|
||||
import { trainingScoreableGate } from "../src/lib/rectification-agentic/v9/evidence-model.ts";
|
||||
import { evidenceLedgerFingerprint } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
||||
import type { InferenceState } from "../src/lib/rectification-agentic/core/types.ts";
|
||||
|
||||
const UNCOVERED_BLOCKING = new Set(["relatives", "occupation"]);
|
||||
|
||||
const TRAINING_EVIDENCE = [
|
||||
{
|
||||
id: "e-edu",
|
||||
status: "confirmed",
|
||||
domain: "education",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2016-01-01",
|
||||
occurredTo: null,
|
||||
eventKind: "education_milestone",
|
||||
},
|
||||
{
|
||||
id: "e-career-a",
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2018-01-01",
|
||||
occurredTo: null,
|
||||
eventKind: "career_entry",
|
||||
},
|
||||
{
|
||||
id: "e-career-b",
|
||||
status: "confirmed",
|
||||
domain: "career",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2020-01-01",
|
||||
occurredTo: null,
|
||||
eventKind: "career_change",
|
||||
},
|
||||
{
|
||||
id: "e-rel",
|
||||
status: "confirmed",
|
||||
domain: "relationship",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2024-01-01",
|
||||
occurredTo: null,
|
||||
eventKind: "relationship_start",
|
||||
},
|
||||
] as const;
|
||||
|
||||
const FAMILY_EVIDENCE = {
|
||||
id: "e-fam",
|
||||
status: "confirmed",
|
||||
domain: "family",
|
||||
datePrecision: "year",
|
||||
occurredFrom: "2023-01-01",
|
||||
occurredTo: null,
|
||||
eventKind: "family_event",
|
||||
} as const;
|
||||
|
||||
const OCCUPATION_EVIDENCE = {
|
||||
id: "e-occ",
|
||||
status: "confirmed",
|
||||
domain: "occupation",
|
||||
datePrecision: "unknown",
|
||||
occurredFrom: null,
|
||||
occurredTo: null,
|
||||
eventKind: "occupation_note",
|
||||
} as const;
|
||||
|
||||
function producedDossier(
|
||||
evidence: readonly Readonly<{
|
||||
id: string;
|
||||
status: string;
|
||||
domain: string;
|
||||
datePrecision: string;
|
||||
occurredFrom: string | null;
|
||||
occurredTo: string | null;
|
||||
eventKind?: string | null;
|
||||
}>[],
|
||||
options: {
|
||||
previous?: InferenceState | null;
|
||||
declinedTopics?: readonly Readonly<Record<string, unknown>>[];
|
||||
} = {},
|
||||
) {
|
||||
const state = buildCaseInferenceState({
|
||||
range: { start_time: "04:55", end_time: "05:02" },
|
||||
candidates: [
|
||||
{ candidateId: "05:02", time: "05:02", relativeSupport: 58 },
|
||||
{ candidateId: "04:55", time: "04:55", relativeSupport: 42 },
|
||||
],
|
||||
evidence,
|
||||
probes: [],
|
||||
previous: options.previous,
|
||||
});
|
||||
const latest = {
|
||||
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 },
|
||||
};
|
||||
return {
|
||||
state,
|
||||
dossier: {
|
||||
evidence,
|
||||
conversationSummary: {
|
||||
activeFocus: null,
|
||||
declinedSkippedTopics: options.declinedTopics ?? [],
|
||||
},
|
||||
latestResult: latest,
|
||||
case: { acceptedTime: null },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function collectionFollowup(
|
||||
dossier: ReturnType<typeof producedDossier>["dossier"],
|
||||
sessionOutcome: "collect_evidence" | "validate_holdout" | "adopt_representative" = "collect_evidence",
|
||||
) {
|
||||
const catalog = rectificationFollowupCatalog(dossier.latestResult, dossier.evidence);
|
||||
return buildMethodFollowupPlan({
|
||||
evidence: dossier.evidence,
|
||||
declinedTopics: dossier.conversationSummary.declinedSkippedTopics,
|
||||
sessionOutcome,
|
||||
...catalog,
|
||||
candidatesSeparated: true,
|
||||
});
|
||||
}
|
||||
|
||||
test("separated scores with uncovered relatives stay in collection and still ask a blocking method", () => {
|
||||
const { dossier } = producedDossier(TRAINING_EVIDENCE);
|
||||
const gate = trainingScoreableGate(TRAINING_EVIDENCE);
|
||||
assert.equal(gate.open, true);
|
||||
assert.equal(gate.trainingCount, 3);
|
||||
assert.ok(gate.trainingDomainCount >= 2);
|
||||
|
||||
const decision = decideFromDossier(dossier);
|
||||
assert.equal(decision.separation.sufficient, true);
|
||||
assert.equal(decision.nextAction, "ask_fact_collection");
|
||||
assert.equal(decision.canConfirmExactMinute, false);
|
||||
|
||||
const plan = collectionFollowup(dossier);
|
||||
assert.equal(blockingMethodsCovered(plan.methods), false);
|
||||
assert.ok(plan.next_followup, "collection must still have a question");
|
||||
assert.ok(
|
||||
UNCOVERED_BLOCKING.has(plan.next_followup.method_id),
|
||||
`expected relatives or occupation, got ${plan.next_followup.method_id}`,
|
||||
);
|
||||
assert.equal(plan.methods.find((item) => item.method_id === "relatives")?.status, "uncovered");
|
||||
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "uncovered");
|
||||
});
|
||||
|
||||
test("covering family and occupation leaves collection after scores already separated", () => {
|
||||
const { state, dossier: collecting } = producedDossier(TRAINING_EVIDENCE);
|
||||
assert.equal(decideFromDossier(collecting).nextAction, "ask_fact_collection");
|
||||
|
||||
const covered = [...TRAINING_EVIDENCE, FAMILY_EVIDENCE, OCCUPATION_EVIDENCE];
|
||||
const { dossier } = producedDossier(covered, { previous: state });
|
||||
const decision = decideFromDossier(dossier);
|
||||
assert.notEqual(decision.nextAction, "ask_fact_collection");
|
||||
assert.ok(
|
||||
decision.nextAction === "ask_holdout_validation" || decision.nextAction === "ready_to_adopt",
|
||||
decision.nextAction,
|
||||
);
|
||||
assert.equal(decision.canConfirmExactMinute, false);
|
||||
assert.equal(decision.separation.sufficient, true);
|
||||
});
|
||||
|
||||
test("declining family and occupation also leaves collection after scores already separated", () => {
|
||||
const declinedTopics = [
|
||||
{ targetDomain: "family" },
|
||||
{ target_domain: "occupation" },
|
||||
];
|
||||
const { dossier } = producedDossier(TRAINING_EVIDENCE, { declinedTopics });
|
||||
const plan = collectionFollowup(dossier);
|
||||
assert.equal(blockingMethodsCovered(plan.methods), true);
|
||||
assert.equal(plan.methods.find((item) => item.method_id === "relatives")?.status, "covered");
|
||||
assert.equal(plan.methods.find((item) => item.method_id === "occupation")?.status, "covered");
|
||||
|
||||
const decision = decideFromDossier(dossier);
|
||||
assert.notEqual(decision.nextAction, "ask_fact_collection");
|
||||
assert.ok(
|
||||
decision.nextAction === "ask_holdout_validation" || decision.nextAction === "ready_to_adopt",
|
||||
decision.nextAction,
|
||||
);
|
||||
assert.equal(decision.canConfirmExactMinute, false);
|
||||
});
|
||||
Reference in New Issue
Block a user