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(
40            Finding::new(
41                SYNTAX_REPORTING_RULE_ID,
42                "syntax_reporting",
43                "major",
44                start_char,
45                end_char,
46                0.95,
47                "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.",
48                "linguistic",
49                norm[m.start()..m.end()].to_string(),
50            )
51            .with_suggestion("Bổ sung vị ngữ để tạo mệnh đề hoàn chỉnh sau vị từ trần thuật"),
52        );
53    }
54
55    findings
56}
57
58/// 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).
59pub fn evaluate_syntax_parallel(text: &str) -> Vec<Finding> {
60    let norm = normalize_nfc(text);
61    let mapper = CharOffsetMap::new(&norm);
62    let mut findings = Vec::new();
63
64    for m in SYNTAX_PARALLEL_RE.find_iter(&norm) {
65        let (start_char, end_char) = mapper.convert_range(m.start(), m.end());
66        findings.push(
67            Finding::new(
68                SYNTAX_PARALLEL_RULE_ID,
69                "syntax_parallel",
70                "major",
71                start_char,
72                end_char,
73                0.95,
74                "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.",
75                "linguistic",
76                norm[m.start()..m.end()].to_string(),
77            )
78            .with_suggestion("Cân bằng hai vế liên từ 'nhưng' cùng là danh ngữ hoặc cùng là vị ngữ trọn vẹn"),
79        );
80    }
81
82    findings
83}
84
85/// 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'.
86pub fn evaluate_syntax_cliche(text: &str) -> Vec<Finding> {
87    let norm = normalize_nfc(text);
88    let mapper = CharOffsetMap::new(&norm);
89    let mut findings = Vec::new();
90
91    for m in SYNTAX_CLICHE_RE.find_iter(&norm) {
92        let (start_char, end_char) = mapper.convert_range(m.start(), m.end());
93        findings.push(
94            Finding::new(
95                SYNTAX_CLICHE_RULE_ID,
96                "syntax_cliche",
97                "minor",
98                start_char,
99                end_char,
100                0.95,
101                "Ghép nhầm hai kiểu câu: 'Như... đã biết' và '... đã biết rằng'.",
102                "linguistic",
103                norm[m.start()..m.end()].to_string(),
104            )
105            .with_suggestion("Chỉ dùng 'Như... đã biết,' hoặc 'Chúng ta đều biết rằng...'"),
106        );
107    }
108
109    findings
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_syntax_reporting() {
118        let text = "Phải nói là một quyết định sáng suốt.";
119        let findings = evaluate_syntax_reporting(text);
120        assert_eq!(findings.len(), 1);
121        assert_eq!(findings[0].pattern_id, SYNTAX_REPORTING_RULE_ID);
122    }
123
124    #[test]
125    fn test_syntax_parallel() {
126        let text = "Một kế hoạch hoàn hảo nhưng không khả thi.";
127        let findings = evaluate_syntax_parallel(text);
128        assert_eq!(findings.len(), 1);
129        assert_eq!(findings[0].pattern_id, SYNTAX_PARALLEL_RULE_ID);
130    }
131
132    #[test]
133    fn test_syntax_cliche() {
134        let text = "Như chúng ta đã biết rằng việc này rất khó.";
135        let findings = evaluate_syntax_cliche(text);
136        assert_eq!(findings.len(), 1);
137        assert_eq!(findings[0].pattern_id, SYNTAX_CLICHE_RULE_ID);
138    }
139}