Skip to main content

vwi_core/
auditor.rs

1use crate::report::AuditReport;
2use crate::rules::*;
3use crate::types::{Finding, Severity};
4use rayon::prelude::*;
5use serde::{Deserialize, Serialize};
6use std::collections::{HashMap, HashSet};
7
8/// Cấu hình kiểm toán ngôn ngữ học (Auditor Configuration).
9#[derive(Debug, Clone, Serialize, Deserialize, Default)]
10pub struct AuditorConfig {
11    /// Danh sách các mã luật bị vô hiệu hóa (disabled).
12    #[serde(default)]
13    pub disabled_rules: HashSet<String>,
14    /// Nhóm thể loại được kích hoạt (nếu None thì cho phép toàn bộ).
15    #[serde(default)]
16    pub enabled_categories: Option<HashSet<String>>,
17    /// Ghi đè mức độ nghiêm trọng (pattern_id -> new severity, e.g. "warning", "critical", "minor", "suggestion").
18    #[serde(default)]
19    pub severity_overrides: HashMap<String, String>,
20    /// Ngưỡng mức độ nghiêm trọng tối thiểu (nếu Some, bỏ qua các finding dưới ngưỡng này).
21    #[serde(default)]
22    pub min_severity: Option<Severity>,
23}
24
25impl AuditorConfig {
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    pub fn disable_rule(&mut self, rule_id: impl Into<String>) -> &mut Self {
31        self.disabled_rules.insert(rule_id.into());
32        self
33    }
34
35    pub fn enable_rule(&mut self, rule_id: &str) -> &mut Self {
36        self.disabled_rules.remove(rule_id);
37        self
38    }
39
40    pub fn set_enabled_categories(&mut self, categories: Vec<String>) -> &mut Self {
41        self.enabled_categories = Some(categories.into_iter().collect());
42        self
43    }
44
45    pub fn override_severity(
46        &mut self,
47        rule_id: impl Into<String>,
48        severity: impl Into<String>,
49    ) -> &mut Self {
50        self.severity_overrides
51            .insert(rule_id.into(), severity.into());
52        self
53    }
54
55    pub fn set_min_severity(&mut self, min: Severity) -> &mut Self {
56        self.min_severity = Some(min);
57        self
58    }
59
60    #[inline]
61    pub fn is_rule_enabled(&self, rule_id: &str, category: &str) -> bool {
62        if self.disabled_rules.contains(rule_id) {
63            return false;
64        }
65        if let Some(ref cats) = self.enabled_categories {
66            if !cats.contains(category) {
67                return false;
68            }
69        }
70        true
71    }
72}
73
74/// Động cơ kiểm toán ngôn ngữ học cấp cao (High-Level Auditor).
75/// Hỗ trợ cấu hình bộ lọc luật, nhóm thể loại, ghi đè mức độ nghiêm trọng (Voice Profile),
76/// và xử lý tài liệu lớn song song bằng Rayon đa luồng.
77#[derive(Debug, Clone, Default)]
78pub struct Auditor {
79    pub config: AuditorConfig,
80}
81
82impl Auditor {
83    pub fn new() -> Self {
84        Self {
85            config: AuditorConfig::default(),
86        }
87    }
88
89    pub fn with_config(config: AuditorConfig) -> Self {
90        Self { config }
91    }
92
93    pub fn config(&self) -> &AuditorConfig {
94        &self.config
95    }
96
97    pub fn config_mut(&mut self) -> &mut AuditorConfig {
98        &mut self.config
99    }
100
101    /// Kiểm toán một đoạn văn bản đơn lẻ, tự động áp dụng cấu hình kích hoạt/vô hiệu hóa và ghi đè severity.
102    pub fn audit(&self, text: &str) -> Vec<Finding> {
103        let mut findings = Vec::new();
104
105        // 1. Nhóm bị động nhân tạo & thụ hưởng dư thừa
106        if self.config.is_rule_enabled(ARTIFICIAL_PASSIVE_RULE_ID, "syntax") {
107            findings.extend(evaluate_artificial_passive_fast(text));
108        }
109        if self.config.is_rule_enabled(REDUNDANT_BENEFICIARY_RULE_ID, "syntax") {
110            findings.extend(evaluate_redundant_beneficiary_passive(text));
111        }
112
113        // 2. Nhóm cấu trúc Khung đề - Chủ đề
114        if self.config.is_rule_enabled(CRIPPLED_TOPIC_RULE_ID, "syntax") {
115            findings.extend(evaluate_crippled_prepositional_topic(text));
116        }
117        if self.config.is_rule_enabled(PREP_CONJ_RULE_ID, "syntax") {
118            findings.extend(evaluate_preposition_conjunction_conflict(text));
119        }
120
121        // 3. Nhóm trùng ngữ logic
122        if self.config.is_rule_enabled(LOGICAL_PLEONASM_RULE_ID, "pleonasm") {
123            findings.extend(evaluate_logical_pleonasm(text));
124        }
125
126        // 4. Nhóm dịch ngữ & danh từ hóa & chủ ngữ giả
127        if self.config.is_rule_enabled(NOMINALIZATION_HEAVY_RULE_ID, "translationese") {
128            findings.extend(evaluate_nominalization_heavy(text));
129        }
130        if self.config.is_rule_enabled(TEMPORAL_FRAMING_RULE_ID, "translationese") {
131            findings.extend(evaluate_temporal_framing(text));
132        }
133        if self.config.is_rule_enabled(TRANSLATIONESE_SOLEIL_RULE_ID, "translationese") {
134            findings.extend(evaluate_translationese_soleil(text));
135        }
136        if self.config.is_rule_enabled(DUMMY_IT_RULE_ID, "translationese")
137            || self.config.is_rule_enabled(ADJECTIVAL_SUFFIX_FAKE_RULE_ID, "translationese")
138        {
139            let dummy_findings = evaluate_dummy_it_translationese(text);
140            findings.extend(
141                dummy_findings
142                    .into_iter()
143                    .filter(|f| self.config.is_rule_enabled(&f.pattern_id, &f.category)),
144            );
145        }
146
147        // 5. Nhóm liên từ nhượng bộ, xung đột kép & thời thể
148        if self.config.is_rule_enabled(FACTUAL_CONCESSIVE_RULE_ID, "syntax") {
149            findings.extend(evaluate_factual_concessive_misuse(text));
150        }
151        if self.config.is_rule_enabled(HYPOTHETICAL_CONCESSIVE_RULE_ID, "syntax") {
152            findings.extend(evaluate_hypothetical_concessive_misuse(text));
153        }
154        if self.config.is_rule_enabled(CONCESSIVE_INVERSION_RULE_ID, "syntax") {
155            findings.extend(evaluate_concessive_inversion(text));
156        }
157        if self.config.is_rule_enabled(ASPECT_MECHANICS_RULE_ID, "aspect") {
158            findings.extend(evaluate_aspect_mechanics(text));
159        }
160
161        // 6. Nhóm cú pháp đối ngẫu & trần thuật
162        if self.config.is_rule_enabled(SYNTAX_REPORTING_RULE_ID, "syntax") {
163            findings.extend(evaluate_syntax_reporting(text));
164        }
165        if self.config.is_rule_enabled(SYNTAX_PARALLEL_RULE_ID, "syntax") {
166            findings.extend(evaluate_syntax_parallel(text));
167        }
168        if self.config.is_rule_enabled(SYNTAX_CLICHE_RULE_ID, "cliche") {
169            findings.extend(evaluate_syntax_cliche(text));
170        }
171
172        // 7. Nhóm từ vựng lai căng
173        if self.config.is_rule_enabled(SUPERLATIVE_CHI_IT_RULE_ID, "lexical") {
174            findings.extend(evaluate_superlative_misuse(text));
175        }
176        if self.config.is_rule_enabled(SUYT_NUA_THI_RULE_ID, "lexical") {
177            findings.extend(evaluate_suyt_nua_thi(text));
178        }
179
180        // 8. Nhóm công thức & sáo ngữ AI (Humanizer)
181        if self.config.is_rule_enabled(AI_OPENING_RULE_ID, "cliche") {
182            findings.extend(evaluate_ai_opening_cliche(text));
183        }
184        if self.config.is_rule_enabled(AI_SUMMARY_RULE_ID, "cliche") {
185            findings.extend(evaluate_ai_summary_tail(text));
186        }
187        if self.config.is_rule_enabled(FORMULAIC_METAPHOR_RULE_ID, "cliche") {
188            findings.extend(evaluate_formulaic_metaphor(text));
189        }
190
191        // 9. Nhóm sáo ngữ hành chính & đệm từ rỗng (Aho-Corasick)
192        if self.config.is_rule_enabled(BUREAUCRATIC_VERB_PADDING_RULE_ID, "bureaucracy")
193            || self.config.is_rule_enabled(REDUNDANT_CONNECTORS_RULE_ID, "bureaucracy")
194            || self.config.is_rule_enabled(VAGUE_INTENSIFIER_RULE_ID, "bureaucracy")
195        {
196            let bureau_findings = evaluate_bureaucratic_rules(text);
197            findings.extend(
198                bureau_findings
199                    .into_iter()
200                    .filter(|f| self.config.is_rule_enabled(&f.pattern_id, &f.category)),
201            );
202        }
203
204        // Áp dụng ghi đè severity nếu có
205        if !self.config.severity_overrides.is_empty() {
206            for f in &mut findings {
207                if let Some(new_sev) = self.config.severity_overrides.get(&f.pattern_id) {
208                    f.severity = new_sev.clone();
209                }
210            }
211        }
212
213        // Lọc theo min_severity nếu có
214        if let Some(min_sev) = self.config.min_severity {
215            findings.retain(|f| Severity::parse(&f.severity) >= min_sev);
216        }
217
218        // Sắp xếp các phát hiện theo thứ tự xuất hiện trong văn bản
219        findings.sort_by(|a, b| a.start.cmp(&b.start).then_with(|| a.end.cmp(&b.end)));
220        findings
221    }
222
223    /// Kiểm toán danh sách nhiều tài liệu/đoạn văn bản song song trên đa nhân CPU thông qua Rayon.
224    pub fn audit_batch(&self, texts: &[&str]) -> Vec<Vec<Finding>> {
225        texts.par_iter().map(|text| self.audit(text)).collect()
226    }
227
228    /// Kiểm toán toàn diện văn bản, tự động tách từ bằng Viterbi để đếm word count và sinh AuditReport hoàn chỉnh.
229    pub fn audit_document(&self, text: &str) -> AuditReport {
230        let start_time = std::time::Instant::now();
231        let tokens = crate::segmentation::viterbi_segmenter::tokenize_viterbi(text);
232        let word_count = tokens.len();
233        let findings = self.audit(text);
234        let elapsed = start_time.elapsed().as_secs_f64() * 1000.0;
235        AuditReport::calculate(text, findings, word_count, elapsed)
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_auditor_default() {
245        let auditor = Auditor::new();
246        let text = "Trong tình hình hiện nay đòi hỏi giải pháp tối ưu nhất được triển khai bởi nhóm.";
247        let findings = auditor.audit(text);
248
249        assert!(findings.len() >= 3);
250        let ids: Vec<&str> = findings.iter().map(|f| f.pattern_id.as_str()).collect();
251        assert!(ids.contains(&"CRIPPLED_PREPOSITIONAL_TOPIC"));
252        assert!(ids.contains(&"LOGICAL_PLEONASM"));
253        assert!(ids.contains(&"ARTIFICIAL_PASSIVE_BE_BY"));
254    }
255
256    #[test]
257    fn test_auditor_disabled_rule() {
258        let mut config = AuditorConfig::new();
259        config.disable_rule("ARTIFICIAL_PASSIVE_BE_BY");
260        let auditor = Auditor::with_config(config);
261
262        let text = "Dự án được triển khai bởi nhóm.";
263        let findings = auditor.audit(text);
264        let ids: Vec<&str> = findings.iter().map(|f| f.pattern_id.as_str()).collect();
265        assert!(!ids.contains(&"ARTIFICIAL_PASSIVE_BE_BY"));
266    }
267
268    #[test]
269    fn test_auditor_category_filter() {
270        let mut config = AuditorConfig::new();
271        config.set_enabled_categories(vec!["bureaucracy".to_string()]);
272        let auditor = Auditor::with_config(config);
273
274        let text = "Nó là rất quan trọng để tiến hành thực hiện phương án này được duyệt bởi sếp.";
275        let findings = auditor.audit(text);
276
277        // Chỉ cho phép bureaucracy, các lỗi syntax/translationese sẽ bị lọc bỏ
278        for f in &findings {
279            assert_eq!(f.category, "bureaucracy");
280        }
281        let ids: Vec<&str> = findings.iter().map(|f| f.pattern_id.as_str()).collect();
282        assert!(ids.contains(&"BUREAUCRATIC_VERB_PADDING"));
283        assert!(!ids.contains(&"DUMMY_IT_TRANSLATIONESE"));
284        assert!(!ids.contains(&"ARTIFICIAL_PASSIVE_BE_BY"));
285    }
286
287    #[test]
288    fn test_auditor_severity_override() {
289        let mut config = AuditorConfig::new();
290        config.override_severity("BUREAUCRATIC_VERB_PADDING", "critical");
291        let auditor = Auditor::with_config(config);
292
293        let text = "Chúng tôi tiến hành thực hiện kế hoạch.";
294        let findings = auditor.audit(text);
295        let finding = findings
296            .iter()
297            .find(|f| f.pattern_id == "BUREAUCRATIC_VERB_PADDING")
298            .unwrap();
299        assert_eq!(finding.severity, "critical");
300    }
301
302    #[test]
303    fn test_auditor_audit_batch_rayon() {
304        let auditor = Auditor::new();
305        let docs = vec![
306            "Trong tình hình hiện nay đòi hỏi giải pháp tối ưu nhất.",
307            "Dự án được triển khai bởi ban giám đốc.",
308            "Chúng tôi tiến hành thực hiện công tác kiểm tra.",
309        ];
310
311        let batch_findings = auditor.audit_batch(&docs);
312        assert_eq!(batch_findings.len(), 3);
313        assert!(!batch_findings[0].is_empty());
314        assert!(!batch_findings[1].is_empty());
315        assert!(!batch_findings[2].is_empty());
316    }
317
318    #[test]
319    fn test_auditor_document_report() {
320        let auditor = Auditor::new();
321        let text = "Không thể phủ nhận rằng trong tình hình hiện nay đòi hỏi giải pháp tối ưu nhất được triển khai bởi nhóm.";
322        let report = auditor.audit_document(text);
323
324        assert!(report.word_count > 10);
325        assert!(!report.findings.is_empty());
326        assert!(report.score < 90.0);
327        assert!(report.execution_time_ms >= 0.0);
328        let md = report.to_markdown();
329        assert!(md.contains("# BÁO CÁO KIỂM TOÁN VĂN BẢN"));
330        let json = report.to_json();
331        assert!(json.contains("\"score\":"));
332    }
333
334    #[test]
335    fn test_auditor_min_severity() {
336        let mut config = AuditorConfig::new();
337        config.set_min_severity(Severity::Major);
338        let auditor = Auditor::with_config(config);
339
340        // Văn bản có cả Minor (tối ưu nhất) và Major (bị động nhân tạo, khung đề)
341        let text = "Giải pháp tối ưu nhất được triển khai bởi nhóm.";
342        let findings = auditor.audit(text);
343
344        for f in &findings {
345            let sev = Severity::parse(&f.severity);
346            assert!(sev >= Severity::Major);
347        }
348    }
349}