Skip to content

Commit e3c14af

Browse files
committed
fix(cards): 步骤信号收紧防叙事误判(REQ-199)
1 parent 12c9b40 commit e3c14af

2 files changed

Lines changed: 53 additions & 5 deletions

File tree

‎app/src-tauri/src/card_generate.rs‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ pub const KIND_FACT: &str = "fact";
1515
pub const KIND_ACTION: &str = "action";
1616

1717
/// 步骤语义元词(命中任一即判 action——中文教程/操作高频词)。
18-
const STEP_META_WORDS: [&str; 6] = ["第一步", "步骤", "做法", "流程", "操作步骤", "教程"];
18+
const STEP_META_WORDS: [&str; 7] = ["第一步", "步骤", "做法", "流程", "操作步骤", "教程", "首先"];
19+
/// "先…然后"成对信号的误判守卫词(普通叙事高频,非步骤语义)。
20+
const PAIR_SIGNAL_GUARD: [&str; 4] = ["先生", "先后", "优先", "先修"];
1921

2022
/// 生成的卡片候选(front/back + 内容分型 kind)。
2123
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -130,11 +132,25 @@ pub fn card_from_fragment(text: &str) -> Option<CardCandidate> {
130132

131133
/// 步骤语义信号判定(REQ-199):步骤元词命中,或"先…然后"成对出现。
132134
///
133-
/// @ai-context: "先/然后"单现不算信号(普通叙事高频词,误判会把知识卡
134-
/// 伪装成动作卡);成对出现才隐含操作顺序。
135+
/// @ai-context: 审查修复(2026-08-22):"先"单字高频(先生/先后/优先/先修),
136+
/// 且"先…然后"普通叙事(如"先修课…然后…")不隐含操作顺序——
137+
/// 成对信号须满足:守卫词不命中 + 至少 3 句 + 两词跨句(步骤
138+
/// 序列的最小形态),元词命中不受限(词义明确)。
135139
fn has_step_signal(text: &str) -> bool {
136-
STEP_META_WORDS.iter().any(|w| text.contains(w))
137-
|| (text.contains("先") && text.contains("然后"))
140+
if STEP_META_WORDS.iter().any(|w| text.contains(w)) {
141+
return true;
142+
}
143+
if PAIR_SIGNAL_GUARD.iter().any(|w| text.contains(w)) {
144+
return false;
145+
}
146+
let sentences: Vec<&str> = text
147+
.split(|c| "。!?;\n".contains(c))
148+
.map(|s| s.trim())
149+
.filter(|s| !s.is_empty())
150+
.collect();
151+
let xian_at = sentences.iter().position(|s| s.contains("先"));
152+
let ranhou_at = sentences.iter().position(|s| s.contains("然后"));
153+
sentences.len() >= 3 && xian_at.is_some() && ranhou_at.is_some() && xian_at != ranhou_at
138154
}
139155

140156
#[cfg(test)]

‎app/src-tauri/src/card_generate_tests.rs‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,38 @@ fn lone_ranhou_is_not_action_signal() {
118118
assert!(!card.back.starts_with("1. "));
119119
}
120120

121+
#[test]
122+
fn xiu_ke_narrative_not_action() {
123+
// Arrange:"先修课…然后…"——"先"是课程名一部分,守卫词命中且非步骤
124+
let text = "先修课是线性代数。然后学微积分。最后是概率论。";
125+
// Act
126+
let card = card_from_fragment(text).expect("出卡");
127+
// Assert:维持 fact(守卫词拦截——防"先修/先后/优先/先生"误判)
128+
assert_eq!(card.kind, KIND_FACT);
129+
assert!(!card.back.starts_with("1. "));
130+
}
131+
132+
#[test]
133+
fn two_sentence_xian_ranhou_not_action() {
134+
// Arrange:"先…然后"同现但仅 2 句——步骤序列的最小形态是 3 句
135+
let text = "先复习错题。然后做新题。";
136+
// Act
137+
let card = card_from_fragment(text).expect("出卡");
138+
// Assert:2 句不成序列(不足 3 句),维持 fact 诚实
139+
assert_eq!(card.kind, KIND_FACT);
140+
}
141+
142+
#[test]
143+
fn shou_xian_meta_word_counts_as_action() {
144+
// Arrange:"首先"入元词表——列表/步骤描述高频,词义明确不受句数限制
145+
let text = "首先打开软件。接着导入素材。最后点击导出。";
146+
// Act
147+
let card = card_from_fragment(text).expect("出卡");
148+
// Assert:元词命中 → action(跨句信号不需要,词义已定)
149+
assert_eq!(card.kind, KIND_ACTION);
150+
assert!(card.back.starts_with("1. 首先打开软件"));
151+
}
152+
121153
#[test]
122154
fn single_sentence_with_signal_still_no_card() {
123155
// Arrange:单句含步骤信号——无多句结构,步骤清单无从谈起

0 commit comments

Comments
 (0)