fix: stabilize rectification staging release
This commit is contained in:
@@ -191,6 +191,31 @@ test("Builder 的领域排序不受事件输入数组顺序影响", () => {
|
||||
assert.deepEqual(domains([education, career]), domains([career, education]));
|
||||
});
|
||||
|
||||
test("Builder 和 Reasoner 按事件创建时间承接最近经历而不是 UUID 顺序", () => {
|
||||
const older = event({
|
||||
eventId: "ffffffff-ffff-4fff-8fff-ffffffffffff",
|
||||
summary: "较早的研究院实习",
|
||||
createdAt: "2026-07-29T01:00:00.000Z",
|
||||
});
|
||||
const newer = event({
|
||||
eventId: "00000000-0000-4000-8000-000000000000",
|
||||
domain: "relocation",
|
||||
eventKind: "relocation",
|
||||
summary: "刚提到的搬到北京",
|
||||
rawText: "2018年8月搬到北京",
|
||||
dateRange: { start: "2018-08-01", end: "2018-08-31", precision: "month", label: "2018年8月" },
|
||||
createdAt: "2026-07-29T02:00:00.000Z",
|
||||
});
|
||||
const opportunities = buildQuestionOpportunities({ caseId, events: [newer, older], turns: [], snapshot: null, diagnostics: null });
|
||||
const askNewEvent = opportunities.find((item) => item.kind === "ask_new_event");
|
||||
assert.ok(askNewEvent);
|
||||
assert.match(askNewEvent.fallbackPrompt, /刚提到的搬到北京/);
|
||||
assert.doesNotMatch(askNewEvent.fallbackPrompt, /较早的研究院实习/);
|
||||
|
||||
const state = buildReasonerState({ snapshot: null, diagnostics: diagnostics(), opportunities, recentEvents: [newer, older] });
|
||||
assert.equal(state.recentEvents.at(-1)?.summary, "刚提到的搬到北京");
|
||||
});
|
||||
|
||||
test("Reasoner 状态包含最近语义上下文但不包含贡献矩阵", () => {
|
||||
const latestEvent = event();
|
||||
const opportunity = targetOpportunity(latestEvent);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import test from "node:test";
|
||||
import { validatedModelAssistedEvidence } from "../src/lib/conversational-rectification/evidence-extractor.ts";
|
||||
|
||||
test("模型辅助提取拒绝 self 与家庭 relatedPerson 的矛盾组合", () => {
|
||||
const rawText = "2020年4月老爸进了ICU。";
|
||||
const result = validatedModelAssistedEvidence({
|
||||
rawText,
|
||||
sourceTurnId: randomUUID(),
|
||||
asOfDate: "2026-07-29",
|
||||
extraction: {
|
||||
sourceSpan: "2020年4月老爸进了ICU",
|
||||
summary: "老爸进了ICU",
|
||||
domain: "health_pressure",
|
||||
eventKind: "self_health_event",
|
||||
subject: "self",
|
||||
relatedPerson: "father",
|
||||
dateText: "2020年4月",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
test("明确家庭主体不能被模型伪装为 self scoreable", () => {
|
||||
const rawText = "2021年6月家里老人病危。";
|
||||
const result = validatedModelAssistedEvidence({
|
||||
rawText,
|
||||
sourceTurnId: randomUUID(),
|
||||
asOfDate: "2026-07-29",
|
||||
extraction: {
|
||||
sourceSpan: "2021年6月家里老人病危",
|
||||
summary: "家里老人病危",
|
||||
domain: "health_pressure",
|
||||
eventKind: "self_health_event",
|
||||
subject: "self",
|
||||
relatedPerson: null,
|
||||
dateText: "2021年6月",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
test("合法家庭健康事件只作为 context_only", () => {
|
||||
const rawText = "2020年4月老爸进了ICU。";
|
||||
const result = validatedModelAssistedEvidence({
|
||||
rawText,
|
||||
sourceTurnId: randomUUID(),
|
||||
asOfDate: "2026-07-29",
|
||||
extraction: {
|
||||
sourceSpan: "2020年4月老爸进了ICU",
|
||||
summary: "老爸进了ICU",
|
||||
domain: "family",
|
||||
eventKind: "family_health_event",
|
||||
subject: "family",
|
||||
relatedPerson: "father",
|
||||
dateText: "2020年4月",
|
||||
},
|
||||
});
|
||||
|
||||
assert.ok(result);
|
||||
assert.equal(result.scoreability, "context_only");
|
||||
assert.equal(result.scoreable, false);
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import test from "node:test";
|
||||
import type { QuestionOpportunity, ValidatedDecision } from "../src/lib/rectification-agent/contracts.ts";
|
||||
import { realizePublicMessage, validateQuestionRealization } from "../src/lib/rectification-agent/renderer-agent.ts";
|
||||
import type { CandidateSnapshot, LifeEventRevision, PendingEvidence } from "../src/lib/rectification-v4/contracts.ts";
|
||||
|
||||
const caseId = "00000000-0000-4000-8000-000000000701";
|
||||
const now = "2026-07-29T00:00:00.000Z";
|
||||
|
||||
function event(): LifeEventRevision {
|
||||
return {
|
||||
id: randomUUID(),
|
||||
eventId: randomUUID(),
|
||||
revision: 1,
|
||||
domain: "career",
|
||||
eventKind: "career_change",
|
||||
subject: "self",
|
||||
relatedPerson: null,
|
||||
summary: "2020年4月研究院实习",
|
||||
rawText: "2020年4月去石油化工研究院实习做研究员。",
|
||||
dateRange: { start: "2020-04-01", end: "2020-04-30", precision: "month", label: "2020年4月" },
|
||||
scoreability: "scoreable",
|
||||
supersedesRevisionId: null,
|
||||
createdAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
function opportunity(target: LifeEventRevision): QuestionOpportunity {
|
||||
return {
|
||||
contractVersion: "semantic-question-v2",
|
||||
opportunityId: randomUUID(),
|
||||
kind: "refine_event_date",
|
||||
domain: "career",
|
||||
targetEventId: target.eventId,
|
||||
goal: "确认研究院实习发生的大概阶段。",
|
||||
requestedFields: ["event_stage"],
|
||||
anchors: [target.summary],
|
||||
contextFacts: [],
|
||||
forbiddenMoves: ["switch_target_event", "ask_multiple_questions", "claim_exact_birth_minute", "invent_event", "invent_date", "expose_private_score", "expose_internal_id", "expose_technique_trace"],
|
||||
fallbackPrompt: `关于“${target.summary}”,你更记得是开始、高峰还是结束阶段吗?`,
|
||||
reason: "当前事件仍需区分发生阶段。",
|
||||
expectedInformationGain: 0.7,
|
||||
dateSensitivity: 0.7,
|
||||
candidateSplitRelevance: 0.4,
|
||||
domainCoverageGain: 0,
|
||||
recallEase: 0.6,
|
||||
novelty: 0.8,
|
||||
repetitionPenalty: 0,
|
||||
privacyCost: 0.05,
|
||||
utility: 0.65,
|
||||
active: true,
|
||||
};
|
||||
}
|
||||
|
||||
function validated(selectedOpportunity: QuestionOpportunity): ValidatedDecision {
|
||||
return {
|
||||
decision: { action: "ask_question", opportunityId: selectedOpportunity.opportunityId, narrativeFocus: ["latest_event"] },
|
||||
mode: "agent",
|
||||
validationIssues: [],
|
||||
selectedOpportunity,
|
||||
};
|
||||
}
|
||||
|
||||
function snapshot(range: readonly [string, string]): CandidateSnapshot {
|
||||
const [startTime, endTime] = range;
|
||||
return {
|
||||
id: randomUUID(),
|
||||
caseId,
|
||||
caseVersion: 3,
|
||||
evidenceSetHash: "e".repeat(64),
|
||||
calculationSpecHash: "c".repeat(64),
|
||||
algorithmVersion: "rectification-v5-matrix-scoring-1",
|
||||
candidates: [{ time: startTime, score: 10, supportingEventIds: [], conflictingEventIds: [] }],
|
||||
clusters: [{ rank: 1, startTime, endTime, representativeTime: startTime, widthMinutes: 7, peakScore: 10, scoreMass: 1 }],
|
||||
robustness: { neighborSupportMinutes: 8, leaveOneOutRetentionRate: 0.8, dateSensitivityRetentionRate: 0.8, calculationSpecHashMatched: true },
|
||||
canConfirmExactMinute: false,
|
||||
canAcceptRange: true,
|
||||
gateReasons: [],
|
||||
createdAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
test("Renderer 对全部模型可见文本执行 exact-minute 和内部信息安全回落", () => {
|
||||
const target = event();
|
||||
const selectedOpportunity = opportunity(target);
|
||||
const input = {
|
||||
latestAnswer: target.rawText,
|
||||
acceptedEvents: [target],
|
||||
pendingEvidence: [] as PendingEvidence[],
|
||||
snapshot: null,
|
||||
previousSnapshot: null,
|
||||
validated: validated(selectedOpportunity),
|
||||
};
|
||||
|
||||
const message = realizePublicMessage({
|
||||
acknowledgement: `你提到的是“${target.summary}”,所以准确出生分钟是05:13。`,
|
||||
candidateUpdate: null,
|
||||
limitation: "准确出生分钟是五点十三分,snapshotId 已确认。",
|
||||
question: `关于${target.summary},唯一出生分钟是什么?`,
|
||||
}, input);
|
||||
|
||||
assert.equal(message.acknowledgement, `你提到的是 ${target.dateRange.label} 的“${target.summary}”。`);
|
||||
assert.equal(message.limitation, null);
|
||||
assert.equal(message.question, selectedOpportunity.fallbackPrompt);
|
||||
assert.doesNotMatch(JSON.stringify(message), /05:13|五点十三分|准确出生分钟|唯一出生分钟|snapshotId/);
|
||||
|
||||
for (const question of [
|
||||
`关于${target.summary},你是不是五点十三分出生?`,
|
||||
`关于${target.summary},eventId 是什么?`,
|
||||
]) {
|
||||
assert.equal(validateQuestionRealization(question, selectedOpportunity).valid, false, question);
|
||||
}
|
||||
});
|
||||
|
||||
test("Renderer 保留服务器生成的合法候选范围表达", () => {
|
||||
const target = event();
|
||||
const selectedOpportunity = opportunity(target);
|
||||
const message = realizePublicMessage({
|
||||
acknowledgement: `你提到的是“${target.summary}”。`,
|
||||
candidateUpdate: "模型声称出生时间就是05:13。",
|
||||
limitation: null,
|
||||
question: `关于${target.summary},你更记得是开始、高峰还是结束阶段吗?`,
|
||||
}, {
|
||||
latestAnswer: target.rawText,
|
||||
acceptedEvents: [target],
|
||||
pendingEvidence: [],
|
||||
snapshot: snapshot(["05:12", "05:18"]),
|
||||
previousSnapshot: null,
|
||||
validated: validated(selectedOpportunity),
|
||||
});
|
||||
|
||||
assert.match(message.candidateUpdate ?? "", /候选范围.*05:12.*05:18/);
|
||||
assert.match(message.candidateUpdate ?? "", /不代表其中某一分钟已被确认/);
|
||||
assert.doesNotMatch(message.candidateUpdate ?? "", /就是05:13/);
|
||||
});
|
||||
Reference in New Issue
Block a user