Skip to main content

vwi_core/rules/
syntax.rs

1use crate::types::Finding;
2use crate::unicode_utils::{normalize_nfc, CharOffsetMap};
3use regex::RegexBuilder;
4use std::sync::LazyLock;
5
6static SYNTAX_REPORTING_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
7    RegexBuilder::new(r"^(phải nói là|cho là|cho rằng)\s+(?:một\s+)?(?:cú|sự|cuộc|việc|quyết định)\s+[^,.]+[.?!]?$")
8        .case_insensitive(true)
9        .build()
10        .expect("Regex CXH_REPORTING_VERB_INCOMPLETE_CLAUSE không hợp lệ")
11});
12
13static SYNTAX_PARALLEL_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
14    RegexBuilder::new(r"^(một\s+[^,]+?),?\s+nhưng\s+(không|chưa|thiếu)\b")
15        .case_insensitive(true)
16        .build()
17        .expect("Regex CXH_PARALLEL_CONJUNCTION_ASYMMETRY không hợp lệ")
18});
19
20static SYNTAX_CLICHE_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
21    RegexBuilder::new(r"\bnhư\s+[^,]+?\s+đã\s+biết\s+(?:là|rằng)\b")
22        .case_insensitive(true)
23        .build()
24        .expect("Regex CXH_HYBRID_COMPLEX_CONJUNCTION không hợp lệ")
25});
26
27pub const SYNTAX_REPORTING_RULE_ID: &str = "CXH_REPORTING_VERB_INCOMPLETE_CLAUSE";
28pub const SYNTAX_PARALLEL_RULE_ID: &str = "CXH_PARALLEL_CONJUNCTION_ASYMMETRY";
29pub const SYNTAX_CLICHE_RULE_ID: &str = "CXH_HYBRID_COMPLEX_CONJUNCTION";
30
31/// Bắt lỗi sau vị từ trần thuật dùng một danh ngữ đơn độc thay vì mệnh đề trọn vẹn.
32pub fn evaluate_syntax_reporting(text: &str) -> Vec<Finding> {
33    let norm = normalize_nfc(text);
34    let mapper = CharOffsetMap::new(&norm);
35    let mut findings = Vec::new();
36
37    for m in SYNTAX_REPORTING_RE.find_iter(&norm) {
38        let (start_char, end_char) = mapper.convert_range(m.start(), m.end());
39        findings.push(Finding::new(
40            SYNTAX_REPORTING_RULE_ID,
41            "syntax_reporting",
42            "major",
43            start_char,
44            end_char,
45            0.95,
46            "Sau 'phải nói là' bắt buộc phải là một mệnh đề trọn vẹn, không dùng một danh ngữ đơn độc.",
47            "linguistic",
48            norm[m.start()..m.end()].to_string(),
49        ));
50    }
51
52    findings
53}
54
55/// Bắt lỗi liên từ 'nhưng' nối hai vế bất đối xứng (danh ngữ đối lập vị từ cụt).
56pub fn evaluate_syntax_parallel(text: &str) -> Vec<Finding> {
57    let norm = normalize_nfc(text);
58    let mapper = CharOffsetMap::new(&norm);
59    let mut findings = Vec::new();
60
61    for m in SYNTAX_PARALLEL_RE.find_iter(&norm) {
62        let (start_char, end_char) = mapper.convert_range(m.start(), m.end());
63        findings.push(Finding::new(
64            SYNTAX_PARALLEL_RULE_ID,
65            "syntax_parallel",
66            "major",
67            start_char,
68            end_char,
69            0.95,
70            "Liên từ 'nhưng' nối hai vế bất đối xứng: Danh ngữ đứng trước đối lập với cụm vị từ cụt phía sau.",
71            "linguistic",
72            norm[m.start()..m.end()].to_string(),
73        ));
74    }
75
76    findings
77}
78
79/// Bắt lỗi ghép nhầm hai cấu trúc diễn ngôn 'Như... đã biết' và '... đã biết rằng'.
80pub fn evaluate_syntax_cliche(text: &str) -> Vec<Finding> {
81    let norm = normalize_nfc(text);
82    let mapper = CharOffsetMap::new(&norm);
83    let mut findings = Vec::new();
84
85    for m in SYNTAX_CLICHE_RE.find_iter(&norm) {
86        let (start_char, end_char) = mapper.convert_range(m.start(), m.end());
87        findings.push(Finding::new(
88            SYNTAX_CLICHE_RULE_ID,
89            "syntax_cliche",
90            "minor",
91            start_char,
92            end_char,
93            0.95,
94            "Ghép nhầm hai kiểu câu: 'Như... đã biết' và '... đã biết rằng'.",
95            "linguistic",
96            norm[m.start()..m.end()].to_string(),
97        ));
98    }
99
100    findings
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_syntax_reporting() {
109        let text = "Phải nói là một quyết định sáng suốt.";
110        let findings = evaluate_syntax_reporting(text);
111        assert_eq!(findings.len(), 1);
112        assert_eq!(findings[0].pattern_id, SYNTAX_REPORTING_RULE_ID);
113    }
114
115    #[test]
116    fn test_syntax_parallel() {
117        let text = "Một kế hoạch hoàn hảo nhưng không khả thi.";
118        let findings = evaluate_syntax_parallel(text);
119        assert_eq!(findings.len(), 1);
120        assert_eq!(findings[0].pattern_id, SYNTAX_PARALLEL_RULE_ID);
121    }
122
123    #[test]
124    fn test_syntax_cliche() {
125        let text = "Như chúng ta đã biết rằng việc này rất khó.";
126        let findings = evaluate_syntax_cliche(text);
127        assert_eq!(findings.len(), 1);
128        assert_eq!(findings[0].pattern_id, SYNTAX_CLICHE_RULE_ID);
129    }
130}