fix(consult): split table cells by sentence and carry names inside a cell (#182 round 2, BUG-1825)
抽查补充:表格行先按 | 分格,格内再按同一套句读分句;同一格里后面的子句出现状态词时 沿用本格上文的名字(正文不跨句继承)。 验收那格「Sunaphaa、Vosi 状态为 candidate(…);Amala、Saraswati 等为 computed,不成立」 现在只有 Amala、Saraswati 两条不符。 Tests: 本单 14 条;fact-guard 13、fact-guard-stream 2、chat-reopt 4、 affliction-reading 6、professional-mode 10 全过。 Co-Authored-By: Grok (agent loop)
This commit is contained in:
co-authored by
Grok (agent loop)
parent
84f2f29f87
commit
be9b2e9131
@@ -76,3 +76,11 @@ Claude 在本分支上已跑过一次全量、指出 3 条红;我这一轮把
|
||||
给出「与 staging 逐条对比、集合差为空」的证据。等完整依赖装回来我会补跑并把名单对比写进来。
|
||||
本轮能给的替代证据:3 条被点名的测试所在的两个文件(4 条 + 6 条)全部通过;
|
||||
本单新增的 13 条、以及 fact-guard 13、fact-guard-stream 2、professional-mode 10 全部通过。
|
||||
|
||||
|
||||
### 抽查补充:表格格内按句读分句
|
||||
|
||||
表格行先按 `|` 分格,**每一格内部再按同一套句读规则分句**(`。!?;,,`,括号内不断)。
|
||||
另外,同一格里后面的子句出现状态词时,沿用本格上文提到的格局名——验收给的那格
|
||||
「Sunaphaa、Vosi 状态为 candidate(…);Amala、Saraswati 等为 computed,不成立」现在只有
|
||||
**Amala、Saraswati 两条不符**,Sunaphaa、Vosi 不出现(有测试)。正文不跨句继承。
|
||||
|
||||
@@ -353,19 +353,12 @@ export function findAnswerFactMismatches(answer: string, card: EvidenceCard): An
|
||||
});
|
||||
}
|
||||
|
||||
/** 分句:。!?;,, 与换行;括号里的标点不断句。表格行按 | 分格。 */
|
||||
function yogaSentences(line: string): Array<{ start: number; end: number }> {
|
||||
if (line.trimStart().startsWith("|")) {
|
||||
return line.split("|").reduce<Array<{ start: number; end: number }>>((acc, piece) => {
|
||||
const start = acc.length ? acc[acc.length - 1]!.end + 1 : 0;
|
||||
acc.push({ start, end: start + piece.length });
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
/** 分句:。!?;,, 与换行;括号里的标点不断句。表格行先按 | 分格,格内再按同一套句读分句。 */
|
||||
function splitProse(line: string, base = 0): Array<{ start: number; end: number }> {
|
||||
const out: Array<{ start: number; end: number }> = [];
|
||||
let depth = 0;
|
||||
let start = 0;
|
||||
const push = (end: number) => out.push({ start, end });
|
||||
const push = (end: number) => out.push({ start: base + start, end: base + end });
|
||||
for (let index = 0; index < line.length; index += 1) {
|
||||
const char = line[index]!;
|
||||
if (char === "(" || char === "(") depth += 1;
|
||||
@@ -379,6 +372,17 @@ function yogaSentences(line: string): Array<{ start: number; end: number }> {
|
||||
return out;
|
||||
}
|
||||
|
||||
function yogaSentences(line: string): Array<{ start: number; end: number }> {
|
||||
if (!line.trimStart().startsWith("|")) return splitProse(line);
|
||||
const out: Array<{ start: number; end: number }> = [];
|
||||
let base = 0;
|
||||
for (const cell of line.split("|")) {
|
||||
out.push(...splitProse(cell, base));
|
||||
base += cell.length + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 一句话里的状态词类别;同时有肯定与否定、或全是排除写法时返回 null(不检查)。 */
|
||||
function yogaWordClass(sentence: string): { kind: "affirm" | "deny"; word: string; at: number } | null {
|
||||
const { affirm, deny, exempt, conditional } = YOGA_STATUS_WORD_CLASSES;
|
||||
@@ -426,35 +430,46 @@ function yogaWordClass(sentence: string): { kind: "affirm" | "deny"; word: strin
|
||||
function checkYogaStatus(line: string, offset: number, view: CardView, push: Emit) {
|
||||
const entries = Object.entries(view.yogas ?? {});
|
||||
if (!entries.length) return;
|
||||
const table = line.trimStart().startsWith("|");
|
||||
// 表格一格内的状态词可以落在同格后面的子句里(「Amala、Saraswati 等为 computed,不成立」),
|
||||
// 所以同一格里记住刚提到的格局名;正文不跨句继承。
|
||||
let carried: Array<{ entry: YogaCardEntry; name: string }> = [];
|
||||
for (const sentence of yogaSentences(line)) {
|
||||
const text = line.slice(sentence.start, sentence.end);
|
||||
if (!text.trim()) continue;
|
||||
const wordClass = yogaWordClass(text);
|
||||
if (!wordClass) continue;
|
||||
const found: Array<{ entry: YogaCardEntry & { pattern: RegExp }; name: string }> = [];
|
||||
for (const [, entry] of entries) {
|
||||
if ("ambiguous" in entry) continue;
|
||||
for (const hit of text.matchAll(entry.pattern)) {
|
||||
const after = text.slice(hit.index! + hit[0].length);
|
||||
const bracket = /^\s*[((]([^))]*)[))]/u.exec(after);
|
||||
if (bracket && YOGA_STATUS_WORD_CLASSES.literalStatus.some((word) => bracket[1]!.includes(word))) continue;
|
||||
const matches = (entry.key === "formed_under_source" && wordClass.kind === "affirm")
|
||||
|| (entry.key === "conditions_not_met" && wordClass.kind === "deny");
|
||||
const at = offset + sentence.start + wordClass.at;
|
||||
push({
|
||||
kind: "yoga_status",
|
||||
layer: "D1",
|
||||
entity: entry.name,
|
||||
reference: "yogas_detail status",
|
||||
claimed: wordClass.word,
|
||||
card: entry.text,
|
||||
fact_id: entry.factId,
|
||||
source_path: entry.sourcePath,
|
||||
quote: wordClass.word,
|
||||
start: at,
|
||||
end: at + wordClass.word.length,
|
||||
}, matches ? "match" : "mismatch");
|
||||
found.push({ entry, name: entry.name });
|
||||
}
|
||||
}
|
||||
const wordClass = yogaWordClass(text);
|
||||
const targets = found.length ? found : (table ? carried : []);
|
||||
if (found.length) carried = found;
|
||||
if (!wordClass || !targets.length) continue;
|
||||
for (const { entry, name } of targets) {
|
||||
const matches = (entry.key === "formed_under_source" && wordClass.kind === "affirm")
|
||||
|| (entry.key === "conditions_not_met" && wordClass.kind === "deny");
|
||||
const at = offset + sentence.start + wordClass.at;
|
||||
push({
|
||||
kind: "yoga_status",
|
||||
layer: "D1",
|
||||
entity: name,
|
||||
reference: "yogas_detail status",
|
||||
claimed: wordClass.word,
|
||||
card: entry.text,
|
||||
fact_id: entry.factId,
|
||||
source_path: entry.sourcePath,
|
||||
quote: wordClass.word,
|
||||
start: at,
|
||||
end: at + wordClass.word.length,
|
||||
}, matches ? "match" : "mismatch");
|
||||
}
|
||||
if (!table) carried = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -304,3 +304,17 @@ test("usage negations with 已 in the middle are not false positives", () => {
|
||||
assert.deepEqual(mismatches(answer, evidence), []);
|
||||
assert.deepEqual(mismatches("Amala Yoga 不视为已成立。", evidence), []);
|
||||
});
|
||||
|
||||
|
||||
test("a table cell splits by sentence inside the cell, so neighbours are not blamed", () => {
|
||||
const evidence = card([
|
||||
{ name: "Sunaphaa Yoga", status: "candidate" },
|
||||
{ name: "Vosi Yoga", status: "candidate" },
|
||||
{ name: "Amala Yoga", status: "computed" },
|
||||
{ name: "Saraswati Yoga", status: "computed" },
|
||||
], ["Sunaphaa Yoga", "Vosi Yoga", "Amala Yoga", "Saraswati Yoga"]);
|
||||
const cell = "Sunaphaa、Vosi 状态为 candidate(support_only_candidate);Amala、Saraswati 等为 computed,不成立";
|
||||
const found = mismatches(`| 格局 | ${cell} |`, evidence);
|
||||
assert.deepEqual(found.map((item) => item.entity).sort(), ["Amala Yoga", "Saraswati Yoga"]);
|
||||
for (const item of found) assert.equal(item.claimed, "不成立");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user