fix: close rectification convergence gaps

This commit is contained in:
Jesse_Chen
2026-07-25 12:59:13 +08:00
parent 0e6ed6ba79
commit e41fae1e05
6 changed files with 118 additions and 7 deletions
@@ -65,6 +65,7 @@ const labeledYearChoicesPattern = /A\s*[.、:)]?[\s\S]{0,80}(?:19|20)\d{2}\s*
const affirmativeAnswerPattern = /^\s*(?:(?:)?|(?:)?|||||+|)\s*[.!,]?\s*$/u;
const negativeAnswerPattern = /^\s*(?:||||)\s*[.!,]?\s*$/u;
const proposedDateQuestionPattern = /(?:19|20)\d{2}\s*(?:\s*(?:1[0-2]|0?[1-9])\s*)?(?:\s*(?:3[01]|[12]\d|0?[1-9])\s*(?:|))?[\s\S]{0,30}(?:|||||)/u;
const nonScoringDetailPattern = /||||||||(?:||)||||/u;
const domainLabels = {
career: "事业",
education: "学业",
@@ -261,9 +262,11 @@ export function validateNarrativeAgainstPacket(
phase: RectificationNarrativePhase = "first",
context: RectificationNarrativeContext = {},
): NarrativeValidation {
void phase;
const issues: string[] = [];
const candidate = packet.candidate;
if (phase === "final" && output.evidenceRequest !== null) {
issues.push("final narrative must not request more evidence");
}
if (output.candidateStatus !== candidate.status) {
issues.push(`candidateStatus ${output.candidateStatus} is not packet-grounded`);
}
@@ -302,6 +305,13 @@ export function validateNarrativeAgainstPacket(
&& packet.scoredHistoricalEvidence.some((item) => item.evidenceId === followUp.evidenceId)) {
issues.push("event detail follow-up targets already scored evidence");
}
const latestActiveEvidenceId = context.eventLedger?.filter((item) => item.active).at(-1)?.id;
if (followUp?.kind === "new_event"
&& latestActiveEvidenceId
&& nonScoringDetailPattern.test(output.evidenceRequest.prompt)
&& packet.scoredHistoricalEvidence.some((item) => item.evidenceId === latestActiveEvidenceId)) {
issues.push("new-event follow-up disguises detail about already scored evidence");
}
if (proposedDateQuestionPattern.test(output.evidenceRequest.prompt)
&& (followUp?.kind !== "event_date"
|| followUp.answerMode !== "yes_no"
@@ -1024,6 +1024,77 @@ test("uses a fresh second provider request after the first attempt times out", a
assert.notEqual(signals[0], signals[1]);
});
test("retries when a final narrative asks for more evidence", async () => {
const invalid = richOutput();
const valid = {
...richOutput(),
narrative: "当前证据只能支持候选范围,本次不再要求继续提供人生事件。",
evidenceRequest: null,
} satisfies RectificationNarrativeModelOutput;
const result = await generateRectificationNarrative({
phase: "final",
packet: syntheticTechnicalPacket(),
generator: generator([invalid, valid]),
});
assert.equal(result.attempts, 2);
assert.equal(result.fallbackUsed, false);
assert.equal(result.output.evidenceRequest, null);
});
test("retries a scored-event detail question mislabeled as new_event", async () => {
const evidenceId = "00000000-0000-4000-8000-000000000709";
const packet = {
...syntheticTechnicalPacket(),
scoredHistoricalEvidence: [{
evidenceId,
domain: "education" as const,
candidateTime: "05:20",
score: 8,
ruleRefs: ["synthetic-education-rule"],
}],
};
const invalid = {
...richOutput(),
evidenceRequest: {
domains: ["career" as const],
datePrecision: "month_preferred" as const,
prompt: "这几个月里,学业压力具体体现在哪些方面,主要原因是什么?",
followUp: { kind: "new_event" as const, evidenceId: null },
},
};
const valid = {
...richOutput(),
evidenceRequest: {
domains: ["career" as const],
datePrecision: "month_preferred" as const,
prompt: "请再说一件已经发生的事业变化,并写明哪一年、哪一月。",
followUp: { kind: "new_event" as const, evidenceId: null },
},
};
const result = await generateRectificationNarrative({
phase: "intermediate",
packet,
context: {
eventLedger: [{
id: evidenceId,
rawText: "1972年12月因为学业压力正式退学",
dateLabel: "1972-12",
summary: "因为学业压力正式退学",
domain: "education",
extractionStatus: "clear",
active: true,
correctsEvidenceIds: [],
}],
},
generator: generator([invalid, valid]),
});
assert.equal(result.attempts, 2);
assert.equal(result.fallbackUsed, false);
assert.equal(result.output.evidenceRequest?.prompt, valid.evidenceRequest.prompt);
});
test("records first-turn generation timeouts separately from schema failures", async () => {
const warnings: string[] = [];
const originalWarn = console.warn;
@@ -44,6 +44,13 @@ const structuredDateConfirmationMigration = readFileSync(
),
"utf8",
);
const productionMigrationWorkflow = readFileSync(
new URL(
"../../.github/workflows/apply-production-rectification-migrations.yml",
import.meta.url,
),
"utf8",
);
test("durable rectification SQL accepts every application evidence domain", () => {
for (const validator of [
@@ -131,3 +138,10 @@ test("durable evidence requests persist strict structured date confirmation", ()
assert.match(structuredDateConfirmationMigration, /yes\/no date proposals/i);
assert.match(structuredDateConfirmationMigration, /valid_uuid_text/);
});
test("production workflow uploads and applies the structured date confirmation migration", () => {
assert.equal(
productionMigrationWorkflow.match(/20260725010000_structured_conversational_date_confirmation\.sql/g)?.length,
2,
);
});
@@ -1498,7 +1498,7 @@ test("an authored event-detail follow-up survives progress decoration and keeps
assert.doesNotMatch(completed.narrative, /大致是什么年月|只记得年份/);
});
test("a mislabeled new-event follow-up never lets program heuristics rewrite an undated reply as a correction", async () => {
test("an unscored event detail mislabeled as new_event is not heuristically merged as a correction", async () => {
const value = harness({
readyAfterEvidenceCount: 99,
continueLatestEvent: true,