Intake stores how sure the user is; rectification now searches that range, offers a one-click widen when event fit is low at the edge, and trisects windows longer than two hours before the minute grid. Co-authored-by: Cursor <cursoragent@cursor.com>
410 lines
14 KiB
TypeScript
410 lines
14 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { applyRectificationChoice } from "../src/lib/rectification-agentic/v9/answer-choice.ts";
|
|
import { CHOICE_ACTION } from "../src/lib/rectification-agentic/v9/choice-action.ts";
|
|
import { decideFromDossier } from "../src/lib/rectification-agentic/v9/decision-from-dossier.ts";
|
|
import {
|
|
buildWidenWindowFrame,
|
|
shouldSuggestWindowWiden,
|
|
} from "../src/lib/rectification-agentic/v9/window-widen.ts";
|
|
import { clockSpanMinutes, splitWindowIntoThirds } from "../src/lib/rectification-agentic/v9/search-window.ts";
|
|
import { evidenceLedgerFingerprint, parseV9CaseDossier } from "../src/lib/rectification-agentic/v9/tool-service.ts";
|
|
import { WIDEN_WINDOW_INTENT, WIDEN_WINDOW_KIND } from "../src/lib/rectification-agentic/v9/block-scan.ts";
|
|
import {
|
|
CASE_ID,
|
|
FOCUS_ID,
|
|
SESSION_ID,
|
|
TURN_ID,
|
|
USER_ID,
|
|
activeFocusFixture,
|
|
candidateSnapshotFixture,
|
|
dossierFixture,
|
|
fakeAccounting,
|
|
receiptHandlers,
|
|
} from "./rectification-v9-test-support.ts";
|
|
|
|
const TRAINING_EVIDENCE = [
|
|
{
|
|
id: "e-edu",
|
|
source_turn_id: TURN_ID,
|
|
subject: "self",
|
|
event_kind: "education_start",
|
|
domain: "education",
|
|
occurred_from: "2016-09-01",
|
|
occurred_to: "2016-09-30",
|
|
date_precision: "month",
|
|
summary: "education start",
|
|
status: "confirmed",
|
|
supersedes_evidence_id: null,
|
|
created_at: "2026-09-07T00:00:00.000Z",
|
|
},
|
|
{
|
|
id: "e-rel",
|
|
source_turn_id: TURN_ID,
|
|
subject: "self",
|
|
event_kind: "relationship_end",
|
|
domain: "relationship",
|
|
occurred_from: "2024-08-08",
|
|
occurred_to: "2024-08-08",
|
|
date_precision: "day",
|
|
summary: "relationship end",
|
|
status: "confirmed",
|
|
supersedes_evidence_id: null,
|
|
created_at: "2026-09-07T00:00:01.000Z",
|
|
},
|
|
{
|
|
id: "e-move",
|
|
source_turn_id: TURN_ID,
|
|
subject: "self",
|
|
event_kind: "relocation",
|
|
domain: "relocation",
|
|
occurred_from: "2023-07-01",
|
|
occurred_to: "2023-07-31",
|
|
date_precision: "month",
|
|
summary: "relocation",
|
|
status: "confirmed",
|
|
supersedes_evidence_id: null,
|
|
created_at: "2026-09-07T00:00:02.000Z",
|
|
},
|
|
{
|
|
id: "e-job",
|
|
source_turn_id: TURN_ID,
|
|
subject: "self",
|
|
event_kind: "career_entry",
|
|
domain: "career",
|
|
occurred_from: "2018-07-01",
|
|
occurred_to: "2018-07-31",
|
|
date_precision: "month",
|
|
summary: "career entry",
|
|
status: "confirmed",
|
|
supersedes_evidence_id: null,
|
|
created_at: "2026-09-07T00:00:03.000Z",
|
|
},
|
|
] as const;
|
|
|
|
function widenSchema(reportedTime = "05:00", window = { start_time: "04:45", end_time: "05:15" }) {
|
|
const frame = buildWidenWindowFrame({ reportedTime, currentWindow: window });
|
|
assert.ok(frame);
|
|
const windows = {
|
|
A: { radius: 30, start_time: "04:30", end_time: "05:30" },
|
|
B: { radius: 60, start_time: "04:00", end_time: "06:00" },
|
|
};
|
|
return {
|
|
choice: {
|
|
prompt: frame.prompt,
|
|
option_a: frame.option_a_hint,
|
|
option_b: frame.option_b_hint,
|
|
option_c: frame.neither_label,
|
|
option_d: frame.unsure_label,
|
|
options: [
|
|
{ key: "A", label: frame.option_a_hint, answer_class: frame.option_a_answer_class, role: "primary" },
|
|
{ key: "B", label: frame.option_b_hint, answer_class: frame.option_b_answer_class, role: "primary" },
|
|
{ key: "C", label: frame.neither_label, answer_class: frame.option_c_answer_class, role: "primary" },
|
|
{ key: "D", label: frame.unsure_label, answer_class: frame.option_d_answer_class, role: "primary" },
|
|
],
|
|
},
|
|
choice_kind: WIDEN_WINDOW_KIND,
|
|
scoring: false,
|
|
widen_windows: windows,
|
|
};
|
|
}
|
|
|
|
function widenDossier(extra: {
|
|
representativeTime?: string;
|
|
range?: { start_time: string; end_time: string };
|
|
declinedFingerprint?: string | null;
|
|
birthTimeSource?: string;
|
|
evidence?: unknown[];
|
|
withFocus?: boolean;
|
|
} = {}) {
|
|
const range = extra.range ?? { start_time: "04:45", end_time: "05:15" };
|
|
const schema = widenSchema("05:00", range);
|
|
return dossierFixture({
|
|
candidateRange: range,
|
|
stage: "minute",
|
|
reportedBirthTime: "05:00",
|
|
birthTimeSource: extra.birthTimeSource ?? "approximate",
|
|
widenDeclinedAtFingerprint: extra.declinedFingerprint ?? null,
|
|
evidence: extra.evidence ?? [...TRAINING_EVIDENCE],
|
|
latestResult: candidateSnapshotFixture({
|
|
representativeTime: extra.representativeTime ?? "04:45",
|
|
decisionReceipt: {
|
|
event_fit_rate: {
|
|
matched: 1,
|
|
total: 4,
|
|
percent: 25,
|
|
band: "low",
|
|
label: "事件吻合率",
|
|
user_meaning: "这是相对拟合,不是已确认唯一出生分钟。",
|
|
unique_minute_claim: false,
|
|
},
|
|
},
|
|
}),
|
|
conversationSummary: {
|
|
confirmed_evidence_summary: [],
|
|
pending_revisions: [],
|
|
active_focus: extra.withFocus === false
|
|
? null
|
|
: activeFocusFixture({
|
|
intent: WIDEN_WINDOW_INTENT,
|
|
targetDomain: null,
|
|
targetKind: null,
|
|
questionId: "widen_window:search_range:holdout",
|
|
expectedAnswerSchema: schema,
|
|
}),
|
|
declined_skipped_topics: [],
|
|
candidate_divergence_summary: null,
|
|
missing_evidence_categories: [],
|
|
last_result_policy: null,
|
|
summary_version: 1,
|
|
updated_at: "2026-09-07T00:00:00.000Z",
|
|
},
|
|
});
|
|
}
|
|
|
|
function choiceHandlers(current: { value: ReturnType<typeof widenDossier> }) {
|
|
return {
|
|
...receiptHandlers,
|
|
get_agentic_rectification_case_dossier: () => current.value,
|
|
get_agentic_rectification_case_compute: () => ({
|
|
case_id: CASE_ID,
|
|
skill_version: "9.0.0",
|
|
baseline_profile_fingerprint: "a".repeat(64),
|
|
baseline_birth_snapshot: {
|
|
birth_date: "1997-08-08",
|
|
latitude: 36.42,
|
|
longitude: 114.2,
|
|
timezone_id: "Asia/Shanghai",
|
|
timezone_offset: 8,
|
|
birth_time_source: current.value.case.birth_time_source,
|
|
reported_birth_time: "05:00",
|
|
},
|
|
candidate_range: current.value.case.candidate_range,
|
|
}),
|
|
append_agentic_rectification_turn: () => ({ turn_id: TURN_ID, idempotent: false }),
|
|
apply_agentic_rectification_choice_action: (_fn: string, args: Record<string, unknown>) => ({
|
|
action_id: args.p_action_id,
|
|
status: "applied",
|
|
idempotent: false,
|
|
question_id: args.p_question_id,
|
|
option_id: args.p_option_id,
|
|
revision: 1,
|
|
narration: args.p_narration,
|
|
focus_status: args.p_focus_status,
|
|
}),
|
|
set_agentic_rectification_conversation_focus: (_fn: string, args: Record<string, unknown>) => ({
|
|
focus: {
|
|
id: FOCUS_ID,
|
|
case_id: CASE_ID,
|
|
question_id: args.p_question_id,
|
|
intent: args.p_intent,
|
|
expected_answer_schema: args.p_expected_answer_schema,
|
|
status: "active",
|
|
asked_at: "2026-09-07T00:00:00.000Z",
|
|
resolved_at: null,
|
|
},
|
|
idempotent: false,
|
|
}),
|
|
persist_agentic_rectification_inference_transition: () => {
|
|
throw new Error("widen must not rewrite inference");
|
|
},
|
|
};
|
|
}
|
|
|
|
test("exclusive span 120 stays minute; 240 splits into three non-overlapping thirds", () => {
|
|
assert.equal(clockSpanMinutes("04:00", "06:00"), 120);
|
|
assert.equal(clockSpanMinutes("03:00", "07:00"), 240);
|
|
const thirds = splitWindowIntoThirds({ start_time: "03:00", end_time: "07:00" });
|
|
assert.equal(thirds.length, 3);
|
|
assert.equal(thirds[0]?.start_time, "03:00");
|
|
assert.equal(thirds[2]?.end_time, "07:00");
|
|
assert.notEqual(thirds[0]?.end_time, thirds[1]?.start_time);
|
|
});
|
|
|
|
test("fit low plus a representative on the edge asks a four-option widen card", () => {
|
|
const parsed = parseV9CaseDossier(widenDossier({ withFocus: false }));
|
|
assert.ok(parsed);
|
|
const decision = decideFromDossier(parsed);
|
|
assert.equal(decision.nextAction, "ask_window_widen");
|
|
assert.equal(decision.sessionOutcome, "widen_window");
|
|
assert.equal(decision.canAdopt, false);
|
|
const frame = buildWidenWindowFrame({
|
|
reportedTime: "05:00",
|
|
currentWindow: { start_time: "04:45", end_time: "05:15" },
|
|
});
|
|
assert.ok(frame);
|
|
assert.equal(frame.choice_kind, WIDEN_WINDOW_KIND);
|
|
assert.match(frame.option_a_hint, /半小时/);
|
|
assert.match(frame.option_b_hint, /一小时/);
|
|
assert.match(frame.neither_label, /不放宽/);
|
|
assert.ok(frame.unsure_label.length >= 4);
|
|
const labels = [frame.option_a_hint, frame.option_b_hint, frame.neither_label, frame.unsure_label];
|
|
assert.equal(new Set(labels).size, 4);
|
|
});
|
|
|
|
test("fit low with a representative in the middle of the window does not suggest widen", () => {
|
|
const parsed = parseV9CaseDossier(widenDossier({
|
|
representativeTime: "05:00",
|
|
withFocus: false,
|
|
}));
|
|
assert.ok(parsed);
|
|
assert.notEqual(decideFromDossier(parsed).nextAction, "ask_window_widen");
|
|
assert.equal(
|
|
shouldSuggestWindowWiden({
|
|
trainingGateOpen: true,
|
|
fitBand: "low",
|
|
fitTotal: 4,
|
|
representativeNearEdge: false,
|
|
stage: "minute",
|
|
accepted: false,
|
|
reportedTime: "05:00",
|
|
declinedForFingerprint: false,
|
|
currentRadius: 15,
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("answering A widens the window, patches approximate radius, and does not rewrite answered probes", async () => {
|
|
const current = { value: widenDossier() };
|
|
const accounting = fakeAccounting({
|
|
...choiceHandlers(current),
|
|
widen_agentic_rectification_case_window: (_fn, args) => {
|
|
current.value = widenDossier({
|
|
range: { start_time: String(args.p_start_time), end_time: String(args.p_end_time) },
|
|
withFocus: false,
|
|
});
|
|
return {
|
|
case_id: CASE_ID,
|
|
stage: "minute",
|
|
candidate_range: { start_time: args.p_start_time, end_time: args.p_end_time },
|
|
};
|
|
},
|
|
});
|
|
const askedBefore = accounting.calls.filter((item) => item.fn.includes("inference")).length;
|
|
const receipt = await applyRectificationChoice(accounting.client, {
|
|
userId: USER_ID,
|
|
caseId: CASE_ID,
|
|
sessionId: SESSION_ID,
|
|
actionId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa3",
|
|
action: CHOICE_ACTION,
|
|
focusId: FOCUS_ID,
|
|
optionId: "A",
|
|
expectedRevision: 0,
|
|
});
|
|
assert.equal(receipt.snapshotCurrent, false);
|
|
assert.ok(accounting.calls.some((item) => item.fn === "widen_agentic_rectification_case_window"));
|
|
const widenCall = accounting.calls.find((item) => item.fn === "widen_agentic_rectification_case_window");
|
|
assert.equal(widenCall?.args.p_start_time, "04:30");
|
|
assert.equal(widenCall?.args.p_end_time, "05:30");
|
|
assert.deepEqual(accounting.profilePatches, [{
|
|
birth_time_source: "approximate",
|
|
uncertainty_before_minutes: 30,
|
|
uncertainty_after_minutes: 30,
|
|
}]);
|
|
assert.equal(
|
|
accounting.calls.filter((item) => item.fn.includes("inference")).length,
|
|
askedBefore,
|
|
);
|
|
});
|
|
|
|
test("answering C records the fingerprint and does not ask again until evidence changes", async () => {
|
|
const current = { value: widenDossier() };
|
|
const parsedBefore = parseV9CaseDossier(current.value);
|
|
assert.ok(parsedBefore);
|
|
const fingerprint = evidenceLedgerFingerprint(parsedBefore.evidence);
|
|
const accounting = fakeAccounting({
|
|
...choiceHandlers(current),
|
|
set_agentic_rectification_widen_declined: (_fn, args) => {
|
|
current.value = widenDossier({
|
|
declinedFingerprint: String(args.p_fingerprint),
|
|
withFocus: false,
|
|
});
|
|
return {
|
|
case_id: CASE_ID,
|
|
widen_declined_at_fingerprint: args.p_fingerprint,
|
|
};
|
|
},
|
|
});
|
|
await applyRectificationChoice(accounting.client, {
|
|
userId: USER_ID,
|
|
caseId: CASE_ID,
|
|
sessionId: SESSION_ID,
|
|
actionId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa4",
|
|
action: CHOICE_ACTION,
|
|
focusId: FOCUS_ID,
|
|
optionId: "C",
|
|
expectedRevision: 0,
|
|
});
|
|
const declined = parseV9CaseDossier(current.value);
|
|
assert.ok(declined);
|
|
assert.equal(declined.case.widenDeclinedAtFingerprint, fingerprint);
|
|
assert.notEqual(decideFromDossier(declined, { currentEvidenceFingerprint: fingerprint }).nextAction, "ask_window_widen");
|
|
const freshEvidence = [
|
|
...TRAINING_EVIDENCE,
|
|
{
|
|
...TRAINING_EVIDENCE[3],
|
|
id: "e-job-2",
|
|
occurred_from: "2019-03-01",
|
|
occurred_to: "2019-03-31",
|
|
summary: "later career",
|
|
},
|
|
];
|
|
const later = parseV9CaseDossier(widenDossier({
|
|
declinedFingerprint: fingerprint,
|
|
evidence: freshEvidence,
|
|
withFocus: false,
|
|
}));
|
|
assert.ok(later);
|
|
assert.equal(decideFromDossier(later).nextAction, "ask_window_widen");
|
|
});
|
|
|
|
test("already ±120 does not suggest a widen card", () => {
|
|
assert.equal(
|
|
shouldSuggestWindowWiden({
|
|
trainingGateOpen: true,
|
|
fitBand: "low",
|
|
fitTotal: 4,
|
|
representativeNearEdge: true,
|
|
stage: "minute",
|
|
accepted: false,
|
|
reportedTime: "05:00",
|
|
declinedForFingerprint: false,
|
|
currentRadius: 120,
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("hospital_record answering A widens the window but does not rewrite the archive source", async () => {
|
|
const current = { value: widenDossier({ birthTimeSource: "hospital_record" }) };
|
|
const accounting = fakeAccounting({
|
|
...choiceHandlers(current),
|
|
widen_agentic_rectification_case_window: (_fn, args) => {
|
|
current.value = widenDossier({
|
|
birthTimeSource: "hospital_record",
|
|
range: { start_time: String(args.p_start_time), end_time: String(args.p_end_time) },
|
|
withFocus: false,
|
|
});
|
|
return {
|
|
case_id: CASE_ID,
|
|
stage: "minute",
|
|
candidate_range: { start_time: args.p_start_time, end_time: args.p_end_time },
|
|
};
|
|
},
|
|
});
|
|
await applyRectificationChoice(accounting.client, {
|
|
userId: USER_ID,
|
|
caseId: CASE_ID,
|
|
sessionId: SESSION_ID,
|
|
actionId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa5",
|
|
action: CHOICE_ACTION,
|
|
focusId: FOCUS_ID,
|
|
optionId: "A",
|
|
expectedRevision: 0,
|
|
});
|
|
assert.ok(accounting.calls.some((item) => item.fn === "widen_agentic_rectification_case_window"));
|
|
assert.deepEqual(accounting.profilePatches, []);
|
|
});
|