Skip to main content

clawft_kernel/assessment/
analyzer.rs

1//! Pluggable analyzer trait and registry for the assessment pipeline.
2
3use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6
7use super::{AssessmentReport, Finding};
8
9/// Context passed to each analyzer during a scan.
10#[derive(Debug, Clone, Default)]
11pub struct AnalysisContext {
12    /// The assessment scope (full, commit, ci, dependency).
13    pub scope: String,
14    /// The previous assessment report, if available.
15    pub previous_report: Option<AssessmentReport>,
16}
17
18/// Trait for pluggable assessment analyzers.
19///
20/// Implement this trait to add a new analyzer to the assessment pipeline.
21/// Each analyzer receives the project root, the list of scoped files, and
22/// an `AnalysisContext`, and returns zero or more `Finding` values.
23pub trait Analyzer: Send + Sync {
24    /// Unique identifier for the analyzer (e.g. "complexity").
25    fn id(&self) -> &str;
26    /// Human-readable display name.
27    fn name(&self) -> &str;
28    /// Categories of findings this analyzer may produce.
29    fn categories(&self) -> &[&str];
30    /// Run analysis over the given files and return findings.
31    fn analyze(&self, project: &Path, files: &[PathBuf], context: &AnalysisContext)
32    -> Vec<Finding>;
33}
34
35/// Registry that holds all pluggable analyzers.
36pub struct AnalyzerRegistry {
37    analyzers: Vec<Box<dyn Analyzer>>,
38}
39
40impl AnalyzerRegistry {
41    /// Create an empty registry.
42    pub fn new() -> Self {
43        Self {
44            analyzers: Vec::new(),
45        }
46    }
47
48    /// Create a registry pre-loaded with all built-in analyzers.
49    pub fn with_defaults() -> Self {
50        let mut reg = Self::new();
51        reg.register(Box::new(super::analyzers::ComplexityAnalyzer::new()));
52        reg.register(Box::new(super::analyzers::DependencyAnalyzer));
53        reg.register(Box::new(super::analyzers::SecurityAnalyzer));
54        reg.register(Box::new(super::analyzers::TopologyAnalyzer));
55        reg.register(Box::new(super::analyzers::DataSourceAnalyzer));
56        reg.register(Box::new(super::analyzers::NetworkAnalyzer));
57        reg.register(Box::new(super::analyzers::RabbitMQAnalyzer));
58        reg.register(Box::new(super::analyzers::TerraformAnalyzer));
59        reg
60    }
61
62    /// Register a new analyzer.
63    pub fn register(&mut self, analyzer: Box<dyn Analyzer>) {
64        self.analyzers.push(analyzer);
65    }
66
67    /// Run all registered analyzers and collect their findings.
68    pub fn run_all(
69        &self,
70        project: &Path,
71        files: &[PathBuf],
72        context: &AnalysisContext,
73    ) -> Vec<Finding> {
74        let mut findings = Vec::new();
75        for analyzer in &self.analyzers {
76            findings.extend(analyzer.analyze(project, files, context));
77        }
78        findings
79    }
80
81    /// Return the ids of all registered analyzers.
82    pub fn analyzer_ids(&self) -> Vec<String> {
83        self.analyzers.iter().map(|a| a.id().to_string()).collect()
84    }
85}
86
87impl Default for AnalyzerRegistry {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93/// Diff between two assessment reports.
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct AssessmentDiff {
96    /// Files present in current but not in previous.
97    pub files_added: Vec<String>,
98    /// Files present in previous but not in current.
99    pub files_removed: Vec<String>,
100    /// Findings in current that were not in previous.
101    pub findings_new: Vec<Finding>,
102    /// Findings in previous that are no longer in current.
103    pub findings_resolved: Vec<Finding>,
104    /// Change in complexity warnings (positive = more warnings).
105    pub complexity_delta: i64,
106    /// Change in coherence score (positive = improved).
107    pub coherence_delta: f64,
108}
109
110/// Compute a diff between two assessment reports.
111pub fn diff_reports(current: &AssessmentReport, previous: &AssessmentReport) -> AssessmentDiff {
112    use std::collections::HashSet;
113
114    // Collect file sets from findings + all scanned context
115    let current_files: HashSet<String> = current.findings.iter().map(|f| f.file.clone()).collect();
116    let previous_files: HashSet<String> =
117        previous.findings.iter().map(|f| f.file.clone()).collect();
118
119    let files_added: Vec<String> = current_files.difference(&previous_files).cloned().collect();
120    let files_removed: Vec<String> = previous_files.difference(&current_files).cloned().collect();
121
122    // Finding identity: (category, file, message)
123    type FindingKey = (String, String, String);
124    fn finding_key(f: &Finding) -> FindingKey {
125        (f.category.clone(), f.file.clone(), f.message.clone())
126    }
127
128    let prev_keys: HashSet<FindingKey> = previous.findings.iter().map(finding_key).collect();
129    let curr_keys: HashSet<FindingKey> = current.findings.iter().map(finding_key).collect();
130
131    let findings_new: Vec<Finding> = current
132        .findings
133        .iter()
134        .filter(|f| !prev_keys.contains(&finding_key(f)))
135        .cloned()
136        .collect();
137
138    let findings_resolved: Vec<Finding> = previous
139        .findings
140        .iter()
141        .filter(|f| !curr_keys.contains(&finding_key(f)))
142        .cloned()
143        .collect();
144
145    let complexity_delta =
146        current.summary.complexity_warnings as i64 - previous.summary.complexity_warnings as i64;
147
148    let coherence_delta = current.summary.coherence_score - previous.summary.coherence_score;
149
150    AssessmentDiff {
151        files_added,
152        files_removed,
153        findings_new,
154        findings_resolved,
155        complexity_delta,
156        coherence_delta,
157    }
158}