Files
Jyotisha/frontend/tests/rectification-director.test.ts

961 lines
46 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import test from "node:test";
import { diagnosticsSummarySchema, type RectificationTurnPlan } from "../src/lib/rectification-agent/contracts.ts";
import { buildRectificationCaseDossier, regenerateDirectorQuestion, runRectificationDirector, validateRectificationTurnPlan } from "../src/lib/rectification-agent/director-agent.ts";
import { composeRectificationPublicTurn, mergeDirectorReconciliation } from "../src/lib/rectification-agent/orchestrator.ts";
import type { CalculationSpec, CandidateSnapshot, LifeEventRevision, PendingEvidence, RectificationV4Case, RectificationV4Turn } from "../src/lib/rectification-v4/contracts.ts";
import { stageAgentEvidenceProposals } from "../src/lib/rectification-v4/extraction.ts";
import { calculationSpecHash } from "../src/lib/rectification-v4/fingerprints.ts";
const now = "2026-07-30T00:00:00.000Z";
const caseId = "00000000-0000-4000-8000-000000000701";
const spec: CalculationSpec = {
version: "rectification-calculation-spec-v4",
birthDate: "1993-04-17",
candidateRange: { start: "05:00", end: "06:00" },
latitude: 36.683333,
longitude: 114.35,
timezoneId: "Asia/Shanghai",
timezoneOffsetHours: 8,
birthTimeSource: "approximate",
ayanamsa: "lahiri",
nodeMode: "mean",
minuteStep: 1,
};
const caseValue: RectificationV4Case = {
id: caseId,
userId: "00000000-0000-4000-8000-000000000702",
protocol: "rectification-evidence-v5",
version: 3,
status: "processing",
phase: "reasoning",
calculationSpec: spec,
calculationSpecHash: calculationSpecHash(spec),
evidenceSetHash: "e".repeat(64),
currentQuestion: null,
latestSnapshot: null,
orchestrationModelId: null,
narrationModelId: null,
skillVersion: "birth-time-rectification-v8",
promptVersion: "rectification-director-v4",
algorithmVersion: "rectification-v5-matrix-scoring-1",
deploymentMode: "v5_agent",
agentMode: "agent",
featureSnapshotId: null,
latestDiagnosticsId: null,
acceptedRange: null,
createdAt: now,
updatedAt: now,
};
function event(overrides: Partial<LifeEventRevision> = {}): LifeEventRevision {
return {
id: randomUUID(),
eventId: randomUUID(),
revision: 1,
domain: "education",
eventKind: "education_milestone",
subject: "self",
relatedPerson: null,
summary: "2016年9月大学入学",
rawText: "2016年9月大学入学",
dateRange: { start: "2016-09-01", end: "2016-09-30", precision: "month", label: "2016年9月" },
scoreability: "scoreable",
supersedesRevisionId: null,
createdAt: now,
...overrides,
};
}
function turn(index: number, overrides: Partial<RectificationV4Turn> = {}): RectificationV4Turn {
return {
id: randomUUID(),
caseId,
caseVersion: index + 1,
questionId: null,
questionDomain: null,
questionTargetEventId: null,
question: `问题${index}`,
answer: `回答${index}`,
modelId: null,
actionId: randomUUID(),
createdAt: now,
...overrides,
};
}
function plan(overrides: Partial<RectificationTurnPlan> = {}): RectificationTurnPlan {
return {
contractVersion: "rectification-turn-plan-v1",
targetDisposition: "not_applicable",
evidenceProposals: [],
action: {
type: "ask_question",
focus: {
mode: "collect_independent_event",
targetEventId: null,
domain: null,
requestedFacts: ["independent_event"],
rationaleCodes: ["need_independent_event"],
},
question: "你愿意从哪段变化继续聊?",
optionalQuickReplies: [],
},
publicReply: {
acknowledgement: "我已按你的描述整理这轮线索。",
evidenceExplanation: null,
candidateCommentary: null,
limitation: "目前仍不足以确认具体出生分钟。",
},
publicExplanationGrounding: [],
...overrides,
};
}
function dossier(events: readonly LifeEventRevision[] = [], turns: readonly RectificationV4Turn[] = [], pendingEvidence: readonly PendingEvidence[] = []) {
return buildRectificationCaseDossier({
caseValue,
turns,
events,
pendingEvidence,
snapshot: null,
diagnostics: null,
targetDisposition: "not_applicable",
currentTargetEventId: null,
});
}
const diagnostics = diagnosticsSummarySchema.parse({
id: "00000000-0000-4000-8000-000000000703",
caseId,
snapshotId: "00000000-0000-4000-8000-000000000704",
primaryClusterRetentionRate: 0.8,
leaveOneEventOutRetentionRate: 0.8,
leaveOneDomainOutRetentionRate: 0.8,
dateSensitivityRetentionRate: 0.8,
neighborSupportMinutes: 3,
primarySecondaryMarginPercent: 8,
clusterMassRatio: 0.6,
unstableEventIds: [],
mostDiscriminatingLayers: ["D9"],
eventDateSensitivity: [],
candidateSplits: [],
calculationHash: "d".repeat(64),
createdAt: now,
});
test("dossier keeps recent raw turns, useful earlier context, refusals, pending evidence, and the complete revision ledger", () => {
const sharedEventId = randomUUID();
const events = Array.from({ length: 15 }, (_, index) => event({
eventId: index < 2 ? sharedEventId : randomUUID(),
revision: index < 2 ? index + 1 : 1,
supersedesRevisionId: index === 1 ? randomUUID() : null,
summary: `事件${index}`,
rawText: `事件${index}`,
}));
const targetEventId = events.at(-1)!.eventId;
const turns = Array.from({ length: 14 }, (_, index) => turn(index));
turns[1] = turn(1, { questionDomain: "relationship", questionTargetEventId: targetEventId, answer: "这件事不方便说,换个方向。" });
const pending: PendingEvidence = {
id: randomUUID(), caseId, turnId: turns[0]!.id, rawText: "后来搬过一次家", reasonCode: "date_unresolved",
targetEventId, resolvedEventId: null, createdAt: now, resolvedAt: null,
};
const value = dossier(events, turns, [pending]);
assert.equal(value.conversation.recentRawTurns.length, 12);
assert.equal(value.conversation.recentRawTurns[0]?.question, "问题2");
assert.match(value.conversation.earlierConversationSummary ?? "", /问题0/);
assert.match(value.conversation.earlierConversationSummary ?? "", /不方便说/);
assert.deepEqual(value.interviewState.declinedDomains, ["relationship"]);
assert.equal(value.interviewState.pendingEvidence[0]?.reasonCode, "date_unresolved");
assert.equal(value.interviewState.pendingEvidence[0]?.targetEventId, targetEventId);
assert.equal(value.eventLedger.length, 15);
assert.equal(value.eventLedger[0]?.status, "superseded");
assert.equal(value.eventLedger[1]?.status, "active");
assert.equal(value.case.location.timezoneId, "Asia/Shanghai");
});
test("a declined domain is a server-owned boundary, not a keyword rule", () => {
const turns = [turn(0, {
questionDomain: "relationship",
question: "如果你愿意,可以聊聊一段关系变化吗?",
answer: "不想回答,换个方向。",
})];
const value = dossier([], turns);
assert.deepEqual(value.interviewState.declinedDomains, ["relationship"]);
const reopened = plan({
action: {
type: "ask_question",
focus: {
mode: "collect_independent_event",
targetEventId: null,
domain: "relationship",
requestedFacts: ["independent_event"],
rationaleCodes: ["agent_selected_direction"],
},
question: "有没有一段关系状态变化的经历?",
optionalQuickReplies: [],
},
});
assert.ok(validateRectificationTurnPlan({
plan: reopened,
dossier: value,
latestAnswer: turns[0]!.answer,
phase: "final",
}).issues.includes("declined_domain_reopened"));
const changedDirection = plan({
action: {
type: "ask_question",
focus: {
mode: "collect_independent_event",
targetEventId: null,
domain: null,
requestedFacts: ["independent_event"],
rationaleCodes: ["agent_selected_direction"],
},
question: "你愿意从另一段时间大致明确的经历继续吗?",
optionalQuickReplies: [],
},
});
assert.deepEqual(validateRectificationTurnPlan({
plan: changedDirection,
dossier: value,
latestAnswer: turns[0]!.answer,
phase: "final",
}).issues, []);
});
test("a natural question and multiple grounded event proposals pass without domain keywords or anchors", () => {
const latestAnswer = "2018年9月搬到北京,2020年4月开始第一份工作。";
const value = plan({
evidenceProposals: [
{ operation: "create", targetEventId: null, sourceSpan: "2018年9月搬到北京", dateText: "2018年9月", proposedSummary: "搬到北京", proposedDomain: "relocation", proposedEventKind: "relocation", proposedSubject: "self", proposedRelatedPerson: null, confidence: "high" },
{ operation: "create", targetEventId: null, sourceSpan: "2020年4月开始第一份工作", dateText: "2020年4月", proposedSummary: "开始第一份工作", proposedDomain: "career", proposedEventKind: "career_change", proposedSubject: "self", proposedRelatedPerson: null, confidence: "high" },
],
});
assert.deepEqual(validateRectificationTurnPlan({ plan: value, dossier: dossier(), latestAnswer, phase: "evidence" }).issues, []);
const staged = stageAgentEvidenceProposals({ caseId, rawText: latestAnswer, sourceTurnId: randomUUID(), asOfDate: "2026-07-30", existing: [], proposals: value.evidenceProposals, now: new Date(now) });
assert.equal(staged.revisions.length, 2);
assert.deepEqual(new Set(staged.revisions.map((item) => item.domain)), new Set(["relocation", "career"]));
});
test("evidence proposal survives a repeated provisional question", () => {
const target = event({
summary: "离家去外地上大学",
rawText: "2016 年离家去外地上大学",
dateRange: { start: "2016-01-01", end: "2016-12-31", precision: "year", label: "2016年" },
});
const repeatedQuestion = "你还记得“离家去外地上大学”大概发生在哪个月,或一年中的哪个时间段吗?";
const targetDossier = buildRectificationCaseDossier({
caseValue,
turns: [turn(0, { question: repeatedQuestion, answer: target.rawText, questionTargetEventId: target.eventId })],
events: [target],
pendingEvidence: [],
snapshot: null,
diagnostics: null,
targetDisposition: "unresolved",
currentTargetEventId: target.eventId,
});
const value = plan({
targetDisposition: "resolved",
evidenceProposals: [{
operation: "revise_date",
targetEventId: target.eventId,
sourceSpan: "9 月",
dateText: "9 月",
proposedSummary: target.summary,
proposedDomain: target.domain,
proposedEventKind: target.eventKind,
proposedSubject: target.subject,
proposedRelatedPerson: target.relatedPerson,
confidence: "high",
}],
action: {
type: "ask_question",
focus: { mode: "clarify_existing_event", targetEventId: target.eventId, domain: target.domain, requestedFacts: ["month"], rationaleCodes: ["provisional"] },
question: repeatedQuestion,
optionalQuickReplies: [],
},
});
const validated = validateRectificationTurnPlan({ plan: value, dossier: targetDossier, latestAnswer: "9 月", phase: "evidence" });
assert.deepEqual(validated.issues, []);
assert.equal(validated.plan?.evidenceProposals[0]?.operation, "revise_date");
});
test("server rejects invented sources, private details, exact minutes, and ungated ranges", () => {
const latestAnswer = "2018年9月搬到北京。";
const invented = plan({ evidenceProposals: [{ operation: "create", targetEventId: null, sourceSpan: "2020年工作", dateText: "2020年", proposedSummary: "开始工作", proposedDomain: "career", proposedEventKind: "career_change", proposedSubject: "self", proposedRelatedPerson: null, confidence: "low" }] });
assert.ok(validateRectificationTurnPlan({ plan: invented, dossier: dossier(), latestAnswer, phase: "evidence" }).issues.includes("evidence_source_not_in_latest_answer"));
const unsafe = plan({
action: { type: "ask_question", focus: { mode: "collect_independent_event", targetEventId: null, domain: null, requestedFacts: ["independent_event"], rationaleCodes: ["test"] }, question: "内部 eventId 是 00000000-0000-4000-8000-000000000799,出生时间是05:13吗?", optionalQuickReplies: [] },
});
const unsafeResult = validateRectificationTurnPlan({ plan: unsafe, dossier: dossier(), latestAnswer, phase: "final" });
assert.ok(unsafeResult.issues.includes("private_detail_exposed"));
assert.ok(unsafeResult.issues.includes("exact_minute_claimed"));
assert.equal(unsafeResult.plan, null);
const range = plan({ action: { type: "offer_candidate_range", snapshotId: "00000000-0000-4000-8000-000000000704" } });
assert.ok(validateRectificationTurnPlan({ plan: range, dossier: dossier(), latestAnswer, phase: "final" }).issues.includes("candidate_range_gate_failed"));
});
test("revisions keep the server-owned event id and append revision history", () => {
const target = event({ eventId: "00000000-0000-4000-8000-000000000705" });
const rawText = "其实是2016年10月大学入学。";
const staged = stageAgentEvidenceProposals({
caseId,
rawText,
sourceTurnId: randomUUID(),
asOfDate: "2026-07-30",
existing: [target],
proposals: [{ operation: "revise_date", targetEventId: target.eventId, sourceSpan: "2016年10月", dateText: "2016年10月", proposedSummary: "2016年10月大学入学", proposedDomain: "education", proposedEventKind: "education_milestone", proposedSubject: "self", proposedRelatedPerson: null, confidence: "high" }],
now: new Date(now),
});
assert.equal(staged.revisions.length, 1);
assert.equal(staged.revisions[0]?.eventId, target.eventId);
assert.equal(staged.revisions[0]?.revision, 2);
assert.equal(staged.revisions[0]?.dateRange.start, "2016-10-01");
assert.equal(staged.revisions[0]?.dateRange.end, "2016-10-31");
assert.equal(staged.revisions[0]?.summary, target.summary);
});
test("date revisions ignore model attempts to replace event identity", () => {
const target = event({ eventId: "00000000-0000-4000-8000-000000000708" });
const rawText = "2018年9月搬到北京。";
const staged = stageAgentEvidenceProposals({
caseId,
rawText,
sourceTurnId: randomUUID(),
asOfDate: "2026-07-30",
existing: [target],
proposals: [{ operation: "revise_date", targetEventId: target.eventId, sourceSpan: "2018年9月", dateText: "2018年9月", proposedSummary: "2018年9月创办公司", proposedDomain: "relocation", proposedEventKind: "relocation", proposedSubject: "self", proposedRelatedPerson: null, confidence: "high" }],
now: new Date(now),
});
assert.equal(staged.revisions.length, 1);
assert.equal(staged.pending.length, 0);
assert.equal(staged.revisions[0]?.eventId, target.eventId);
assert.equal(staged.revisions[0]?.domain, target.domain);
assert.equal(staged.revisions[0]?.eventKind, target.eventKind);
assert.equal(staged.revisions[0]?.subject, target.subject);
assert.equal(staged.revisions[0]?.summary, target.summary);
});
test("server-closed target dispositions cannot be overwritten by the Director", () => {
const target = event();
const pending: PendingEvidence = {
id: randomUUID(), caseId, turnId: randomUUID(), rawText: "不想说了,换一个。", reasonCode: "event_unparsed",
targetEventId: target.eventId, resolvedEventId: null, createdAt: now, resolvedAt: null,
};
for (const targetDisposition of ["unknown", "declined", "direction_change"] as const) {
const merged = mergeDirectorReconciliation({
server: { revisions: [], pending: [], unansweredTargetEventId: null, targetDisposition },
staged: { revisions: [], pending: [pending], unansweredTargetEventId: target.eventId, targetDisposition: "unresolved" },
proposedDisposition: "unresolved",
currentTargetEventId: target.eventId,
});
assert.equal(merged.targetDisposition, targetDisposition);
assert.equal(merged.unansweredTargetEventId, null);
assert.deepEqual(merged.pending, []);
}
});
test("target date-only replies inherit identity and missing year", () => {
const target = event();
const cases = [
["2020年4月", "2020-04-01", "2020-04-30"],
["4月", "2016-04-01", "2016-04-30"],
["8月8号", "2016-08-08", "2016-08-08"],
["上半年", "2016-01-01", "2016-06-30"],
["10月至12月", "2016-10-01", "2016-12-31"],
] as const;
for (const [rawText, start, end] of cases) {
const staged = stageAgentEvidenceProposals({
caseId, rawText, sourceTurnId: randomUUID(), asOfDate: "2026-07-30", existing: [target],
proposals: [{ operation: "revise_date", targetEventId: target.eventId, sourceSpan: rawText, dateText: rawText, proposedSummary: "模型不得改写", proposedDomain: "other", proposedEventKind: "other", proposedSubject: "family", proposedRelatedPerson: "mother", confidence: "high" }],
now: new Date(now),
});
assert.equal(staged.pending.length, 0, rawText);
assert.equal(staged.revisions.length, 1, rawText);
assert.equal(staged.revisions[0]?.eventId, target.eventId, rawText);
assert.equal(staged.revisions[0]?.domain, target.domain, rawText);
assert.equal(staged.revisions[0]?.eventKind, target.eventKind, rawText);
assert.equal(staged.revisions[0]?.subject, target.subject, rawText);
assert.equal(staged.revisions[0]?.summary, target.summary, rawText);
assert.equal(staged.revisions[0]?.dateRange.start, start, rawText);
assert.equal(staged.revisions[0]?.dateRange.end, end, rawText);
}
});
test("explicit event-kind correction appends a revision to the same event", () => {
const target = event({ domain: "relationship", eventKind: "relationship_start", subject: "self", relatedPerson: "partner", summary: "关系开始", rawText: "2020年关系开始" });
const rawText = "不是关系开始,是分手";
const staged = stageAgentEvidenceProposals({
caseId, rawText, sourceTurnId: randomUUID(), asOfDate: "2026-07-30", existing: [target],
proposals: [{ operation: "reclassify", targetEventId: target.eventId, sourceSpan: rawText, dateText: null, proposedSummary: "分手", proposedDomain: "relationship", proposedEventKind: "relationship_end", proposedSubject: "self", proposedRelatedPerson: "partner", confidence: "high" }],
now: new Date(now),
});
assert.equal(staged.pending.length, 0);
assert.equal(staged.revisions[0]?.eventId, target.eventId);
assert.equal(staged.revisions[0]?.revision, 2);
assert.equal(staged.revisions[0]?.eventKind, "relationship_end");
});
test("explicit subject correction is grounded and enters pending review", () => {
const target = event({ domain: "health_pressure", eventKind: "self_health_event", subject: "self", relatedPerson: null, summary: "手术", rawText: "2020年我做了手术" });
const rawText = "这次是母亲手术,不是我";
const staged = stageAgentEvidenceProposals({
caseId, rawText, sourceTurnId: randomUUID(), asOfDate: "2026-07-30", existing: [target],
proposals: [{ operation: "reclassify", targetEventId: target.eventId, sourceSpan: rawText, dateText: null, proposedSummary: "母亲手术", proposedDomain: "family", proposedEventKind: "family_health_event", proposedSubject: "family", proposedRelatedPerson: "mother", confidence: "high" }],
now: new Date(now),
});
assert.equal(staged.pending.length, 0);
assert.equal(staged.revisions[0]?.eventId, target.eventId);
assert.equal(staged.revisions[0]?.subject, "family");
assert.equal(staged.revisions[0]?.relatedPerson, "mother");
assert.equal(staged.revisions[0]?.scoreability, "pending_review");
});
test("declined targets cannot be reopened and the Director adapts through server-owned observations", async () => {
const target = event({ eventId: "00000000-0000-4000-8000-000000000706" });
const targetDossier = buildRectificationCaseDossier({ caseValue, turns: [], events: [target], snapshot: null, diagnostics: null, targetDisposition: "declined", currentTargetEventId: target.eventId });
const reopened = plan({ targetDisposition: "declined", action: { type: "ask_question", focus: { mode: "clarify_existing_event", targetEventId: target.eventId, domain: target.domain, requestedFacts: ["month"], rationaleCodes: ["retry"] }, question: "再说说那件事?", optionalQuickReplies: [] } });
assert.ok(validateRectificationTurnPlan({ plan: reopened, dossier: targetDossier, latestAnswer: "不想说", phase: "final" }).issues.includes("declined_target_reopened"));
const reopenedWithoutId = plan({ targetDisposition: "declined", action: { type: "ask_question", focus: { mode: "clarify_existing_event", targetEventId: null, domain: target.domain, requestedFacts: ["month"], rationaleCodes: ["retry"] }, question: "再说说那件事?", optionalQuickReplies: [] } });
assert.ok(validateRectificationTurnPlan({ plan: reopenedWithoutId, dossier: targetDossier, latestAnswer: "不想说", phase: "final" }).issues.includes("declined_target_reopened"));
const phases: string[] = [];
const prompts: string[] = [];
const requests = [
{ tool: "case_read", diagnostic: null },
{ tool: "candidate_scan", diagnostic: null },
{ tool: "evidence_gap", diagnostic: null },
{ tool: "diagnostic_read", diagnostic: "candidate_split" },
] as const;
const result = await runRectificationDirector({
caseValue,
dossier: dossier(),
latestAnswer: "",
phase: "final",
diagnostics,
generatePlan: async (prompt, phase) => {
phases.push(phase);
prompts.push(prompt);
const request = requests[phases.length - 1];
return { object: request ? plan({ action: { type: "request_tool", ...request } }) : plan() };
},
});
assert.equal(result.mode, "agent");
assert.deepEqual(phases, ["final", "after_observation", "after_observation", "after_observation", "after_observation"]);
assert.deepEqual(result.toolCalls.map(({ tool, diagnostic }) => [tool, diagnostic]), [
["case_read", null],
["candidate_scan", null],
["evidence_gap", null],
["diagnostic_read", "candidate_split"],
]);
assert.equal(dossier().capabilities.maxToolRounds, 10);
const firstPrompt = JSON.parse(prompts[0]!);
assert.equal(firstPrompt.dossier.eventLedger, undefined);
assert.equal(firstPrompt.dossier.candidateState, undefined);
assert.equal(firstPrompt.dossier.runtime.hypotheses, undefined);
assert.deepEqual(firstPrompt.dossier.interviewState.askedTopics, []);
assert.deepEqual(firstPrompt.dossier.availableTools.readOnly, ["case_read", "candidate_scan", "evidence_gap"]);
const caseObservationPrompt = JSON.parse(prompts[1]!);
assert.ok(Array.isArray(caseObservationPrompt.latestObservation.result.eventLedger));
assert.equal(caseObservationPrompt.dossier.runtime.observations[0].tool, "case_read");
const candidateObservationPrompt = JSON.parse(prompts[2]!);
assert.equal(candidateObservationPrompt.latestObservation.result.candidateState.hasSnapshot, false);
assert.ok(Array.isArray(candidateObservationPrompt.latestObservation.result.hypotheses));
const finalPrompt = JSON.parse(prompts[4]!);
assert.equal(finalPrompt.dossier.runtime.revision, 4);
assert.deepEqual(finalPrompt.dossier.runtime.observations.map((item: { tool: string }) => item.tool), requests.map(({ tool }) => tool));
assert.equal(result.dossier.runtime.revision, 4);
assert.equal(result.plan.action.type, "ask_question");
});
test("repeated immutable tools are not executed twice and force one convergence decision", async () => {
const phases: string[] = [];
const prompts: string[] = [];
const result = await runRectificationDirector({
caseValue,
dossier: dossier(),
latestAnswer: "",
phase: "final",
diagnostics,
generatePlan: async (prompt, phase) => {
phases.push(phase);
prompts.push(prompt);
return { object: phase === "converge" ? plan() : plan({ action: { type: "request_tool", tool: "candidate_scan", diagnostic: null } }) };
},
});
assert.equal(result.mode, "agent");
assert.deepEqual(phases, ["final", "after_observation", "converge"]);
assert.equal(result.toolCalls.length, 1);
assert.deepEqual(JSON.parse(prompts[2]!).loopState, {
round: 1,
maxRounds: 10,
observedTools: ["candidate_scan:"],
convergenceReason: "tool_repeated",
});
assert.equal(result.plan.action.type, "ask_question");
});
test("the same Director gets one repair attempt before deterministic fallback", async () => {
const phases: string[] = [];
const result = await runRectificationDirector({
caseValue,
dossier: dossier(),
latestAnswer: "2018年9月搬到北京。",
phase: "evidence",
diagnostics,
generatePlan: async (_prompt, phase) => {
phases.push(phase);
return { object: phase === "repair" ? plan() : plan({ evidenceProposals: [{ operation: "create", targetEventId: null, sourceSpan: "不存在的内容", dateText: "2020年", proposedSummary: "开始工作", proposedDomain: "career", proposedEventKind: "career_change", proposedSubject: "self", proposedRelatedPerson: null, confidence: "low" }] }) };
},
});
assert.equal(result.mode, "agent");
assert.deepEqual(phases, ["evidence", "repair"]);
assert.equal(result.fallbackReason, null);
});
test("final Director repairs a generic acknowledgement and missing evidence-value explanation", async () => {
const internship = event({
domain: "career",
eventKind: "career_change",
summary: "2020年4月去石油化工研究院实习做研究员",
rawText: "2020年4月去石油化工研究院实习做研究员",
dateRange: { start: "2020-04-01", end: "2020-04-30", precision: "month", label: "2020年4月" },
});
const phases: string[] = [];
const prompts: string[] = [];
const result = await runRectificationDirector({
caseValue,
dossier: dossier([internship]),
latestAnswer: internship.rawText,
phase: "final",
diagnostics,
generatePlan: async (prompt, phase) => {
phases.push(phase);
prompts.push(prompt);
return {
object: phase === "repair"
? plan({ action: { type: "ask_question", focus: { mode: "collect_independent_event", targetEventId: null, domain: null, requestedFacts: ["independent_event"], rationaleCodes: ["need_independent_event"] }, question: "这条职业变化已经有明确月份;为了交叉核对,你还愿意讲一件时间大致确定、与它不同的经历吗?", optionalQuickReplies: [] }, publicReply: { acknowledgement: "你提到2020年4月去石油化工研究院实习做研究员。", evidenceExplanation: "这是一条职业状态变化线索,方法层可参考 D10 与 Vimshottari;目前只是方法映射,不是实际计算结果。", candidateCommentary: null, limitation: null }, publicExplanationGrounding: [{ source: "capability_matrix", factKey: "domain:career" }] })
: plan(),
};
},
});
assert.equal(result.mode, "agent");
assert.deepEqual(phases, ["final", "repair"]);
assert.match(JSON.parse(prompts[0]!).publicReplyRequirement, /acknowledge the exact event/);
assert.deepEqual(JSON.parse(prompts[1]!).validationIssues, ["event_explanation_grounding_missing", "event_acknowledgement_generic"]);
assert.equal(result.plan.publicReply.acknowledgement, "你提到2020年4月去石油化工研究院实习做研究员。");
assert.equal(result.plan.publicReply.evidenceExplanation, "这是一条职业状态变化线索,方法层可参考 D10 与 Vimshottari;目前只是方法映射,不是实际计算结果。");
assert.equal(result.plan.action.type === "ask_question" ? result.plan.action.question : null, "这条职业变化已经有明确月份;为了交叉核对,你还愿意讲一件时间大致确定、与它不同的经历吗?");
assert.equal(result.plan.publicReply.candidateCommentary, null);
});
test("manual question regeneration preserves safe Agent wording and repairs unsafe output", async () => {
const eventValue = event({
summary: "2016年9月离家去外地上大学",
rawText: "2016年9月离家去外地上大学",
dateRange: { start: "2016-09-01", end: "2016-09-30", precision: "month", label: "2016年9月" },
});
const safeQuestion = "在这段求学经历之外,你还愿意讲一件时间大致确定、对你影响明显的事情吗?";
const phases: string[] = [];
const question = await regenerateDirectorQuestion({
caseValue,
currentQuestion: "你还记得另一件经历吗?",
latestAnswer: eventValue.rawText,
acceptedEvents: [eventValue],
focus: {
mode: "collect_independent_event",
targetEventId: null,
domain: null,
requestedFacts: ["independent_event"],
rationaleCodes: ["candidate_contrast"],
},
generateQuestion: async (_prompt, phase) => {
phases.push(phase);
return phase === "regenerate"
? { object: { question: "D10 已经证明较早候选更可信,你还能说一件事吗?" } }
: { object: { question: safeQuestion } };
},
});
assert.deepEqual(phases, ["regenerate", "repair"]);
assert.equal(question, safeQuestion);
});
test("manual question regeneration falls back only after two unsafe Agent attempts", async () => {
const eventValue = event({
summary: "2016年9月离家去外地上大学",
rawText: "2016年9月离家去外地上大学",
dateRange: { start: "2016-09-01", end: "2016-09-30", precision: "month", label: "2016年9月" },
});
let calls = 0;
const question = await regenerateDirectorQuestion({
caseValue,
currentQuestion: "你还记得另一件经历吗?",
latestAnswer: eventValue.rawText,
acceptedEvents: [eventValue],
focus: {
mode: "collect_independent_event",
targetEventId: null,
domain: "career",
requestedFacts: ["independent_event"],
rationaleCodes: ["candidate_contrast"],
},
generateQuestion: async () => {
calls += 1;
return { object: { question: "分盘已表明甲组更有把握,你还能说一件事吗?" } };
},
});
assert.equal(calls, 2);
assert.match(question, /工作状态变化/);
assert.doesNotMatch(question, /D10|分盘|测算|推演|甲组|头一组|前者占优/);
});
test("the dossier exposes compound public signals without duplicating scoreable events", () => {
const university = event({
summary: "2016年9月离家去外地上大学",
rawText: "2016年9月离家去外地上大学",
});
const built = dossier([university]);
assert.equal(built.eventLedger.length, 1);
assert.deepEqual(built.eventLedger[0]?.publicSignals, [
{ domain: "education", role: "primary", techniqueLayers: ["D24", "vimshottari", "narayana"] },
{ domain: "relocation", role: "secondary", techniqueLayers: ["D4", "vimshottari", "narayana"] },
]);
});
test("valid grounded Agent copy is preserved verbatim", () => {
const internship = event({
domain: "career",
eventKind: "career_change",
summary: "2020年4月去研究院实习做研究员",
rawText: "2020年4月去研究院实习做研究员",
dateRange: { start: "2020-04-01", end: "2020-04-30", precision: "month", label: "2020年4月" },
});
const candidate = plan({
action: {
type: "ask_question",
focus: { mode: "collect_independent_event", targetEventId: null, domain: null, requestedFacts: ["independent_event"], rationaleCodes: ["cross_check"] },
question: "这条职业变化已有明确月份;为了交叉核对,你还愿意讲一件时间大致确定、与它不同的经历吗?",
optionalQuickReplies: [],
},
publicReply: {
acknowledgement: "你提到2020年4月去研究院实习并担任研究员,这条时间线索很清楚。",
evidenceExplanation: "工作角色开始变化时,方法层会参考 D10、A10 和 Vimshottari;这里只说明核对层面,还不是候选结论。",
candidateCommentary: null,
limitation: null,
},
publicExplanationGrounding: [{ source: "capability_matrix", factKey: "domain:career" }],
});
const result = validateRectificationTurnPlan({ plan: candidate, dossier: dossier([internship]), latestAnswer: internship.rawText, phase: "final" });
assert.deepEqual(result.issues, []);
assert.deepEqual(result.plan, candidate);
});
test("compound education and relocation explanations may cite D24 and D4", () => {
const university = event({ summary: "2016年9月离家去外地上大学", rawText: "2016年9月离家去外地上大学" });
const candidate = plan({
action: {
type: "ask_question",
focus: { mode: "collect_independent_event", targetEventId: null, domain: null, requestedFacts: ["independent_event"], rationaleCodes: ["cross_check"] },
question: "这次经历同时有学习与离开原居住地两层变化;你还愿意讲一件时间大致确定、与它不同的经历吗?",
optionalQuickReplies: [],
},
publicReply: {
acknowledgement: "你提到2016年9月离家去外地上大学。",
evidenceExplanation: "上大学属于教育路径变化,可参考 D24;离家到外地长期生活也包含居住基地变化,可参考 D4。这里只说明核对层面,不代表已经形成候选结论。",
candidateCommentary: null,
limitation: null,
},
publicExplanationGrounding: [
{ source: "capability_matrix", factKey: "domain:education" },
{ source: "capability_matrix", factKey: "domain:relocation" },
],
});
const result = validateRectificationTurnPlan({ plan: candidate, dossier: dossier([university]), latestAnswer: university.rawText, phase: "final" });
assert.deepEqual(result.issues, []);
assert.deepEqual(result.plan, candidate);
});
test("public reply rejects ungrounded methods, private internals, and candidate conclusions", () => {
const internship = event({
domain: "career",
eventKind: "career_change",
summary: "2020年4月去研究院实习做研究员",
rawText: "2020年4月去研究院实习做研究员",
dateRange: { start: "2020-04-01", end: "2020-04-30", precision: "month", label: "2020年4月" },
});
const base = {
acknowledgement: "你提到2020年4月去研究院实习做研究员。",
evidenceExplanation: "这条职业变化可参考 D10。",
candidateCommentary: null,
limitation: null,
} as const;
const wrongDomain = validateRectificationTurnPlan({
plan: plan({ publicReply: { ...base, evidenceExplanation: "这条职业变化可参考 D24。" }, publicExplanationGrounding: [{ source: "capability_matrix", factKey: "domain:education" }] }),
dossier: dossier([internship]), latestAnswer: internship.rawText, phase: "final",
});
assert.ok(wrongDomain.issues.includes("public_grounding_invalid"));
assert.ok(wrongDomain.issues.includes("event_explanation_grounding_missing"));
assert.ok(wrongDomain.issues.includes("public_technique_ungrounded"));
for (const privateDetail of ["原始评分 8.7", "权重 0.4", "贡献矩阵如下", "tool_call 原始输出", "我先调用 case_read,再调用 candidate_scan 和 diagnostic_read"]) {
const result = validateRectificationTurnPlan({
plan: plan({
action: { type: "ask_question", focus: { mode: "collect_independent_event", targetEventId: null, domain: null, requestedFacts: ["independent_event"], rationaleCodes: ["test"] }, question: `${privateDetail},你还记得另一件经历吗?`, optionalQuickReplies: [] },
}),
dossier: dossier(), latestAnswer: "", phase: "final",
});
assert.ok(result.issues.includes("private_detail_exposed"), privateDetail);
}
for (const candidateClaim of [
"D10 已经显示这次职业变化支持较早的候选区间。",
"这只是方法映射;D10 已经证明这次经历更符合较早出生。",
"D10 说明 A 方案更可信,你还记得另一件经历吗?",
]) {
const result = validateRectificationTurnPlan({
plan: plan({
action: { type: "ask_question", focus: { mode: "collect_independent_event", targetEventId: null, domain: null, requestedFacts: ["independent_event"], rationaleCodes: ["test"] }, question: candidateClaim.includes("吗") ? candidateClaim : "你还愿意讲一件时间大致确定的经历吗?", optionalQuickReplies: [] },
publicReply: { ...base, evidenceExplanation: candidateClaim.includes("吗") ? base.evidenceExplanation : candidateClaim },
publicExplanationGrounding: [{ source: "capability_matrix", factKey: "domain:career" }],
}),
dossier: dossier([internship]), latestAnswer: internship.rawText, phase: "final",
});
assert.ok(result.issues.includes("ungrounded_candidate_conclusion"), candidateClaim);
}
});
test("the final plan cannot repeat a previously asked question", () => {
const previous = "你愿意再讲一件与已有记录不同、时间大致明确的经历吗?没有、记不清或不想回答也可以换个方向。";
const repeated = plan({
action: {
type: "ask_question",
focus: { mode: "collect_independent_event", targetEventId: null, domain: null, requestedFacts: ["independent_event"], rationaleCodes: ["test"] },
question: previous,
optionalQuickReplies: [],
},
});
assert.ok(validateRectificationTurnPlan({ plan: repeated, dossier: dossier([], [turn(0, { question: previous })]), latestAnswer: "", phase: "final" }).issues.includes("question_repeated"));
const prefixedRepeat = plan({
action: {
type: "ask_question",
focus: { mode: "collect_independent_event", targetEventId: null, domain: null, requestedFacts: ["independent_event"], rationaleCodes: ["test"] },
question: `目前已有 0 条可继续核对的经历。${previous}`,
optionalQuickReplies: [],
},
});
assert.ok(validateRectificationTurnPlan({ plan: prefixedRepeat, dossier: dossier([], [turn(0, { question: previous })]), latestAnswer: "", phase: "final" }).issues.includes("question_repeated"));
const storedPublicReply = composeRectificationPublicTurn({
acknowledgement: "你提到的是2020年4月去研究院实习。",
evidenceExplanation: "这条经历有明确月份,可以和其他经历交叉比较。",
candidateUpdate: null,
limitation: null,
question: previous,
});
assert.ok(validateRectificationTurnPlan({ plan: repeated, dossier: dossier([], [turn(0, { question: storedPublicReply })]), latestAnswer: "", phase: "final" }).issues.includes("question_repeated"));
});
test("candidate range requires the current approved snapshot id", () => {
const snapshot: CandidateSnapshot = {
id: diagnostics.snapshotId, caseId, caseVersion: 3, evidenceSetHash: "e".repeat(64), calculationSpecHash: "c".repeat(64), algorithmVersion: "rectification-v5-matrix-scoring-1",
candidates: [{ time: "05:12", score: 10, supportingEventIds: [], conflictingEventIds: [] }],
clusters: [{ rank: 1, startTime: "05:12", endTime: "05:18", representativeTime: "05:13", widthMinutes: 7, peakScore: 10, scoreMass: 1 }],
robustness: { neighborSupportMinutes: 7, leaveOneOutRetentionRate: .8, leaveOneDomainOutRetentionRate: .8, dateSensitivityRetentionRate: .8, calculationSpecHashMatched: true },
canConfirmExactMinute: false, canAcceptRange: true, gateReasons: [], createdAt: now,
};
const approvedDossier = buildRectificationCaseDossier({ caseValue, turns: [], events: [], snapshot, diagnostics, targetDisposition: "not_applicable", currentTargetEventId: null });
const accepted = plan({ action: { type: "offer_candidate_range", snapshotId: snapshot.id } });
assert.deepEqual(validateRectificationTurnPlan({ plan: accepted, dossier: approvedDossier, latestAnswer: "", phase: "final" }).issues, []);
const wrongSnapshot = plan({ action: { type: "offer_candidate_range", snapshotId: randomUUID() } });
assert.ok(validateRectificationTurnPlan({ plan: wrongSnapshot, dossier: approvedDossier, latestAnswer: "", phase: "final" }).issues.includes("candidate_range_gate_failed"));
const legacyRelationshipEnd = event({ domain: "relationship", eventKind: "relationship_end", scoreability: "scoreable" });
const staleDossier = buildRectificationCaseDossier({ caseValue, turns: [], events: [legacyRelationshipEnd], snapshot, diagnostics, targetDisposition: "not_applicable", currentTargetEventId: null });
assert.equal(staleDossier.candidateState.publicRangeAllowed, false);
assert.ok(validateRectificationTurnPlan({ plan: accepted, dossier: staleDossier, latestAnswer: "", phase: "final" }).issues.includes("candidate_range_gate_failed"));
});
test("an unresolved broad-date target cannot be abandoned for a new event", () => {
const target = event({
dateRange: { start: "2016-01-01", end: "2016-12-31", precision: "year", label: "2016年" },
});
const targetDossier = buildRectificationCaseDossier({
caseValue,
turns: [],
events: [target],
snapshot: null,
diagnostics: null,
targetDisposition: "unresolved",
currentTargetEventId: target.eventId,
});
const abandoned = plan({
targetDisposition: "unresolved",
action: {
type: "ask_question",
focus: {
mode: "collect_independent_event",
targetEventId: null,
domain: null,
requestedFacts: ["independent_event"],
rationaleCodes: ["need_independent_event"],
},
question: "你还能想到另一件时间大致确定的重要经历吗?",
optionalQuickReplies: [],
},
});
assert.ok(validateRectificationTurnPlan({
plan: abandoned,
dossier: targetDossier,
latestAnswer: target.rawText,
phase: "final",
}).issues.includes("unresolved_target_abandoned"));
});
test("deterministic fallback refines the known year before collecting another event", async () => {
const target = event({
summary: "2016年离家去外地上大学",
rawText: "2016 年离家去外地上大学",
dateRange: { start: "2016-01-01", end: "2016-12-31", precision: "year", label: "2016年" },
});
const result = await runRectificationDirector({
caseValue,
dossier: buildRectificationCaseDossier({
caseValue,
turns: [],
events: [target],
snapshot: null,
diagnostics: null,
targetDisposition: "unresolved",
currentTargetEventId: target.eventId,
}),
latestAnswer: target.rawText,
phase: "final",
diagnostics,
generatePlan: async () => { throw new Error("forced_fallback"); },
});
assert.equal(result.mode, "deterministic_fallback");
assert.equal(result.plan.action.type, "ask_question");
if (result.plan.action.type !== "ask_question") return;
assert.equal(result.plan.action.focus.targetEventId, target.eventId);
assert.deepEqual(result.plan.action.focus.requestedFacts, ["month"]);
assert.match(result.plan.action.question, /2016年离家去外地上大学/);
assert.match(result.plan.action.question, /哪个月|时间段/);
assert.doesNotMatch(result.plan.action.question, /另一件|还能想到一件/);
});
test("deterministic fallback pauses instead of repeating a persisted public question", async () => {
const repeatedQuestion = "你愿意再讲一件与已有记录不同、时间大致明确的经历吗?没有、记不清或不想回答也可以换个方向。";
const persisted = composeRectificationPublicTurn({
acknowledgement: "本轮信息已经保留。",
evidenceExplanation: null,
candidateUpdate: null,
limitation: null,
question: repeatedQuestion,
});
const result = await runRectificationDirector({
caseValue,
dossier: buildRectificationCaseDossier({
caseValue,
turns: [turn(0, { question: persisted, answer: "2016年9月去外地上大学" })],
events: [],
snapshot: null,
diagnostics: null,
targetDisposition: "not_applicable",
currentTargetEventId: null,
}),
latestAnswer: "",
phase: "final",
diagnostics,
generatePlan: async () => { throw new Error("forced_fallback"); },
});
assert.equal(result.mode, "deterministic_fallback");
assert.equal(result.plan.action.type, "stop_low_confidence");
if (result.plan.action.type !== "stop_low_confidence") return;
assert.deepEqual(result.plan.action.reasonCodes, ["deterministic_fallback_rejected"]);
assert.match(result.fallbackReason ?? "", /forced_fallback/);
assert.match(result.fallbackReason ?? "", /fallback_rejected:question_repeated/);
});
test("deterministic fallback stays domain-neutral when the Agent is unavailable", async () => {
const university = event({
summary: "2016年9月离家去外地上大学",
rawText: "2016年9月离家去外地上大学",
createdAt: "2026-07-30T01:00:00.000Z",
});
const internship = event({
domain: "career",
eventKind: "career_change",
summary: "2020年4月去石油化工研究院实习做研究员",
rawText: "2020年4月去石油化工研究院实习做研究员",
dateRange: { start: "2020-04-01", end: "2020-04-30", precision: "month", label: "2020年4月" },
createdAt: "2026-07-30T02:00:00.000Z",
});
const turns = [
turn(0, { questionDomain: "education", question: "请说一件时间比较确定的经历。", answer: university.rawText }),
turn(1, { questionDomain: "career", question: "你是否有过工作状态变化?", answer: internship.rawText }),
];
const result = await runRectificationDirector({
caseValue,
dossier: buildRectificationCaseDossier({
caseValue,
turns,
events: [university, internship],
snapshot: null,
diagnostics: null,
targetDisposition: "not_applicable",
currentTargetEventId: null,
}),
latestAnswer: internship.rawText,
phase: "final",
diagnostics,
generatePlan: async () => { throw new Error("forced_fallback"); },
});
assert.equal(result.mode, "deterministic_fallback");
assert.equal(result.plan.action.type, "ask_question");
if (result.plan.action.type !== "ask_question") return;
assert.equal(result.plan.action.focus.targetEventId, null);
assert.equal(result.plan.action.focus.domain, null);
assert.deepEqual(result.plan.action.focus.rationaleCodes, ["model_unavailable_neutral_fallback"]);
assert.match(result.plan.action.question, /愿意.*再讲一件/);
assert.match(result.plan.action.question, /没有|记不清|不想回答|换/);
assert.doesNotMatch(result.plan.action.question.split("之外").at(-1) ?? result.plan.action.question, /教育|迁居|关系|职业|财务|健康|大学|实习|搬家/);
assert.match(result.plan.publicReply.acknowledgement, /2020年4月|石油化工研究院|实习|研究员/);
});
test("the visible assistant turn includes acknowledgement, evidence value, and exactly one question", () => {
const text = composeRectificationPublicTurn({
acknowledgement: "你提到的是“2020年4月去石油化工研究院实习做研究员”。",
evidenceExplanation: "这条职业变化可参考 D10 与 Vimshottari;目前只是方法映射。",
candidateUpdate: "这条线索有明确时间,也说明了具体发生的变化,可以和其他独立经历交叉比较候选范围。",
limitation: null,
question: "如果你愿意,有没有一次长期资格或身份发生变化的经历,大概是哪年哪月?",
});
assert.match(text, /石油化工研究院/);
assert.match(text, /交叉比较候选范围/);
assert.match(text, /长期资格|身份发生变化/);
assert.equal((text.match(/[?]/g) ?? []).length, 1);
});