fix: close rectification convergence gaps
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user