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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
10pub struct AuditorConfig {
11 #[serde(default)]
13 pub disabled_rules: HashSet<String>,
14 #[serde(default)]
16 pub enabled_categories: Option<HashSet<String>>,
17 #[serde(default)]
19 pub severity_overrides: HashMap<String, String>,
20 #[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#[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 pub fn audit(&self, text: &str) -> Vec<Finding> {
103 let mut findings = Vec::new();
104
105 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 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 if self.config.is_rule_enabled(LOGICAL_PLEONASM_RULE_ID, "pleonasm") {
123 findings.extend(evaluate_logical_pleonasm(text));
124 }
125
126 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 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 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 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 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 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 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 if let Some(min_sev) = self.config.min_severity {
215 findings.retain(|f| Severity::parse(&f.severity) >= min_sev);
216 }
217
218 findings.sort_by(|a, b| a.start.cmp(&b.start).then_with(|| a.end.cmp(&b.end)));
220 findings
221 }
222
223 pub fn audit_batch(&self, texts: &[&str]) -> Vec<Vec<Finding>> {
225 texts.par_iter().map(|text| self.audit(text)).collect()
226 }
227
228 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 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 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}