fix(consult): option B for #182 - cells split on 。!?; only, no name carry (#182 needs ruling, BUG-1825)

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbBXoyn7Hj3LCW3BUs5bZ8
This commit is contained in:
2026-11-11 14:15:55 +08:00
co-authored by Claude Opus 5.5
parent be9b2e9131
commit 855cc71ca9
2 changed files with 21 additions and 14 deletions
@@ -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<YogaCardEntry, { ambiguous: true }>; 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 = [];
}
}
@@ -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), []);
});