adfc75af8c
Co-authored-by: Cursor <cursoragent@cursor.com>
198 lines
6.5 KiB
TypeScript
198 lines
6.5 KiB
TypeScript
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);
|
|
});
|