diff --git a/docs/tasks/PROGRESS-yoga-status-wording-20261109.md b/docs/tasks/PROGRESS-yoga-status-wording-20261109.md index f79d1b1c..95de26d9 100644 --- a/docs/tasks/PROGRESS-yoga-status-wording-20261109.md +++ b/docs/tasks/PROGRESS-yoga-status-wording-20261109.md @@ -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 不出现(有测试)。正文不跨句继承。 diff --git a/frontend/src/lib/consultation-answer-fact-guard.ts b/frontend/src/lib/consultation-answer-fact-guard.ts index db8fcac0..778d6de4 100644 --- a/frontend/src/lib/consultation-answer-fact-guard.ts +++ b/frontend/src/lib/consultation-answer-fact-guard.ts @@ -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>((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 = []; } } diff --git a/frontend/tests/consult-yoga-status-wording-1825.test.ts b/frontend/tests/consult-yoga-status-wording-1825.test.ts index b3839492..79f88248 100644 --- a/frontend/tests/consult-yoga-status-wording-1825.test.ts +++ b/frontend/tests/consult-yoga-status-wording-1825.test.ts @@ -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, "不成立"); +});