From 855cc71ca94376e950eac2d00dde967695869ac8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 14:15:55 +0800 Subject: [PATCH] =?UTF-8?q?fix(consult):=20option=20B=20for=20#182=20-=20c?= =?UTF-8?q?ells=20split=20on=20=E3=80=82=EF=BC=81=EF=BC=9F=EF=BC=9B=20only?= =?UTF-8?q?,=20no=20name=20carry=20(#182=20needs=20ruling,=20BUG-1825)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit be9b2e91 carried names across cells and past semicolons, which flagged 9 phrases in staging answers the human check rated correct (b4yoga spot set). Option B: inside a table cell split on 。!?; only (not commas) and never carry names. Spot set: new arm 0 mismatches, old arm keeps the 2 real ones (Amala, Saraswati). Also fixes the 5 tsc TS2339 errors. Prototype for the owner's ruling; not merged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8 --- .../src/lib/consultation-answer-fact-guard.ts | 21 +++++++------------ .../consult-yoga-status-wording-1825.test.ts | 14 +++++++++++++ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/frontend/src/lib/consultation-answer-fact-guard.ts b/frontend/src/lib/consultation-answer-fact-guard.ts index 778d6de4..31580ca5 100644 --- a/frontend/src/lib/consultation-answer-fact-guard.ts +++ b/frontend/src/lib/consultation-answer-fact-guard.ts @@ -353,8 +353,8 @@ export function findAnswerFactMismatches(answer: string, card: EvidenceCard): An }); } -/** 分句:。!?;,, 与换行;括号里的标点不断句。表格行先按 | 分格,格内再按同一套句读分句。 */ -function splitProse(line: string, base = 0): Array<{ start: number; end: number }> { +/** 分句:正文按 。!?;,, 与换行;表格行先按 | 分格,格内只按 。!?; 与换行分句(不按逗号,也不跨句沿用名字)。括号里的标点不断句。 */ +function splitProse(line: string, base = 0, breaks = /[。!?;,,\n]/u): Array<{ start: number; end: number }> { const out: Array<{ start: number; end: number }> = []; let depth = 0; let start = 0; @@ -363,7 +363,7 @@ function splitProse(line: string, base = 0): Array<{ start: number; end: number const char = line[index]!; if (char === "(" || char === "(") depth += 1; else if (char === ")" || char === ")") depth = Math.max(0, depth - 1); - else if (depth === 0 && /[。!?;,,\n]/u.test(char)) { + else if (depth === 0 && breaks.test(char)) { push(index); start = index + 1; } @@ -377,7 +377,7 @@ function yogaSentences(line: string): Array<{ start: number; end: number }> { const out: Array<{ start: number; end: number }> = []; let base = 0; for (const cell of line.split("|")) { - out.push(...splitProse(cell, base)); + out.push(...splitProse(cell, base, /[。!?;\n]/u)); base += cell.length + 1; } return out; @@ -430,14 +430,10 @@ 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 found: Array<{ entry: YogaCardEntry & { pattern: RegExp }; name: string }> = []; + const found: Array<{ entry: Exclude; name: string }> = []; for (const [, entry] of entries) { if ("ambiguous" in entry) continue; for (const hit of text.matchAll(entry.pattern)) { @@ -448,10 +444,8 @@ function checkYogaStatus(line: string, offset: number, view: CardView, push: Emi } } 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) { + if (!wordClass || !found.length) continue; + for (const { entry, name } of found) { 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; @@ -469,7 +463,6 @@ function checkYogaStatus(line: string, offset: number, view: CardView, push: Emi 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 79f88248..53adda43 100644 --- a/frontend/tests/consult-yoga-status-wording-1825.test.ts +++ b/frontend/tests/consult-yoga-status-wording-1825.test.ts @@ -318,3 +318,17 @@ test("a table cell splits by sentence inside the cell, so neighbours are not bla assert.deepEqual(found.map((item) => item.entity).sort(), ["Amala Yoga", "Saraswati Yoga"]); for (const item of found) assert.equal(item.claimed, "不成立"); }); + +// 验收第 2 次(Claude 2026-11-11):be9b2e91 的「同格沿用名字」跨格、跨分号沿用, +// 在 b4yoga 抽查的 staging 正确回答上误拦 9 条。下面三句都取自那批回答,均应 0 条不符。 +test("names never carry into the next cell or past a semicolon", () => { + 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"]); + assert.deepEqual(mismatches("| Yoga | Amala、Saraswati 为检测器检出、待核实;其余 computed 行同 | 都不升格为「成立」,只作观察 |", evidence), []); + assert.deepEqual(mismatches("| Yoga | Vosi、Sunaphaa 为候选;三条规则未实现 | 不升级为格局成立,只作方向观察 |", evidence), []); + assert.deepEqual(mismatches("| Yoga | conditional | hits 仅 Vosi、Sunaphaa,均 candidate;其余为 computed,不成立 | 只作支持层 |", evidence), []); +});