fix: close rectification input bypasses

This commit is contained in:
Jesse_Chen
2026-07-21 01:29:12 +08:00
parent c6efe283c5
commit f963f1cf9b
4 changed files with 67 additions and 6 deletions
@@ -102,13 +102,34 @@ function evidenceId(input: ExtractLifeEventEvidenceInput, index: number, summary
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
}
function splitSentenceFragments(sentence: string): string[] {
const fragments: string[] = [];
const separators = /\s*(并且|并|以及|同时|然后|后来又|又||,)\s*/g;
let cursor = 0;
let prefixForNext = "";
for (const match of sentence.matchAll(separators)) {
const index = match.index ?? cursor;
const fragment = sentence.slice(cursor, index).trim();
if (fragment) {
fragments.push(`${prefixForNext}${fragment}`);
prefixForNext = "";
}
const separator = match[1] ?? "";
if (separator === "然后" || separator === "后来又") {
prefixForNext += separator;
}
cursor = index + match[0].length;
}
const tail = sentence.slice(cursor).trim();
if (tail) fragments.push(`${prefixForNext}${tail}`);
return fragments;
}
function splitSentences(value: string): string[][] {
const sentences = value.split(/[。!?!?;]/)
.map((sentence) => sentence.trim())
.filter(Boolean)
.map((sentence) => sentence.split(/\s*(?:并且|并|以及|同时|然后|后来又|又|,|,)\s*/)
.map((fragment) => fragment.trim())
.filter(Boolean));
.map(splitSentenceFragments);
return sentences.length > 0 ? sentences : [[value.trim()]];
}
@@ -16,7 +16,8 @@ const sensitiveSemanticsPattern = /(?:敏感|变化|差异|切换|不同|sensiti
const discriminationSemanticsPattern = /(?:区分|辨别|判别|验证|差异|变化|discriminat|distinguish)/i;
const broadYearRangePattern = /(?:19|20)\d{2}\s*年?\s*(?:[-–—~~至到\/]|\.\.)\s*(?:19|20)\d{2}\s*年?/i;
const explicitYearPattern = /(?:19|20)\d{2}\s*年?/g;
const choiceQuestionPattern = /(?:哪(?:一|个)?(?:年份|年代|时间段|区间|时期)|哪个时间段|选择|选项|更符合|更匹配|A\s*[.、:)]|B\s*[.、:)]|which\s+(?:year|period|range)|options?)/i;
const proposedYearAlternativesPattern = /(?:19|20)\d{2}\s*年?\s*(?:还是|或者|或是|或|、|,|)\s*(?:19|20)\d{2}\s*年?/i;
const choiceQuestionPattern = /(?:哪(?:一|个)?(?:年|年份|年代|时间段|区间|时期)|哪个时间段|还是|选择|选项|更符合|更匹配|A\s*[.、:)]|B\s*[.、:)]|which\s+(?:year|period|range)|options?)/i;
const domainSemantics = {
career: /(?:事业|工作|职业|career)/i,
education: /(?:教育|学业|学校|education)/i,
@@ -120,8 +121,9 @@ function narrativeReferences(value: string): string[] {
function isGenericBroadYearChoiceQuestionnaire(value: string): boolean {
const distinctYears = unique((value.match(explicitYearPattern) ?? [])
.map((year) => year.replace(/\s*年$/, "")));
return choiceQuestionPattern.test(value)
&& (broadYearRangePattern.test(value) || distinctYears.length >= 2);
return proposedYearAlternativesPattern.test(value)
|| (choiceQuestionPattern.test(value)
&& (broadYearRangePattern.test(value) || distinctYears.length >= 2));
}
function proseFields(output: RectificationNarrativeModelOutput): readonly {
@@ -80,6 +80,8 @@ for (const rawText of [
"2021年7月毕业并后来工作",
"2021年7月毕业并第二年工作",
"2021年7月毕业并此前工作",
"2021年7月毕业然后工作",
"2021年7月毕业后来又工作",
]) {
test(`does not propagate a shared date through an unresolved relative clause: ${rawText}`, () => {
const evidence = extractLifeEventEvidence({ rawText, sourceTurnId, asOfDate: "2026-07-20" });
@@ -217,6 +217,42 @@ test("rejects generic individual-year options even without a written range", ()
assert.ok(result.issues.some((issue) => issue.includes("broad-year choice questionnaire")));
});
for (const prompt of [
"请提供已经发生的真实事件:2018年还是2021年?也请说明哪一月以及发生了什么。",
"请提供已经发生的真实事件,并说明哪一年:2018年、2021年;也请说明哪一月以及发生了什么。",
"请提供已经发生的真实事件:2018年或2021年,也请说明哪一月以及发生了什么。",
]) {
test(`rejects a rich first turn that proposes multiple years: ${prompt}`, () => {
const output = richOutput();
assert.ok(output.evidenceRequest);
const invalid = {
...output,
evidenceRequest: { ...output.evidenceRequest, prompt },
} satisfies RectificationNarrativeModelOutput;
const result = validateNarrativeAgainstPacket(invalid, syntheticTechnicalPacket(), "first");
assert.equal(result.valid, false);
assert.ok(result.issues.some((issue) => issue.includes("broad-year choice questionnaire")));
});
}
test("accepts a request for one real past event's year and month without proposed years", () => {
const output = richOutput();
assert.ok(output.evidenceRequest);
const legitimate = {
...output,
evidenceRequest: {
...output.evidenceRequest,
prompt: "请提供一件已经发生的真实事件,并说明是哪一年、哪一月以及发生了什么。",
},
} satisfies RectificationNarrativeModelOutput;
assert.deepEqual(
validateNarrativeAgainstPacket(legitimate, syntheticTechnicalPacket(), "first"),
{ valid: true, issues: [] },
);
});
test("rejects ungrounded layers and references nested in a domain reason", () => {
const output = richOutput();
const firstReason = output.domainReasons[0];