Skip to main content

loong_kernel/
awareness.rs

1use std::{
2    collections::BTreeMap,
3    fs,
4    path::{Path, PathBuf},
5};
6
7use serde::{Deserialize, Serialize};
8
9use crate::{
10    architecture::{ArchitectureBoundaryPolicy, ArchitectureGuardReport},
11    errors::IntegrationError,
12    plugin::{PluginScanReport, PluginScanner},
13    plugin_ir::{
14        BridgeSupportMatrix, PluginActivationInventoryEntry, PluginActivationPlan, PluginIR,
15        PluginSetupReadinessContext, PluginTranslationReport, PluginTranslator,
16    },
17};
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct CodebaseAwarenessConfig {
21    pub roots: Vec<String>,
22    pub plugin_roots: Vec<String>,
23    pub proposed_mutations: Vec<String>,
24    pub architecture_policy: ArchitectureBoundaryPolicy,
25}
26
27impl Default for CodebaseAwarenessConfig {
28    fn default() -> Self {
29        Self {
30            roots: vec![".".to_owned()],
31            plugin_roots: Vec::new(),
32            proposed_mutations: Vec::new(),
33            architecture_policy: ArchitectureBoundaryPolicy::default(),
34        }
35    }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
39pub struct CodebaseAwarenessSnapshot {
40    pub scanned_roots: Vec<String>,
41    pub scanned_files: usize,
42    pub language_distribution: BTreeMap<String, usize>,
43    pub deterministic_fingerprint: String,
44    pub plugin_scan_reports: Vec<PluginScanReport>,
45    pub plugin_translation_reports: Vec<PluginTranslationReport>,
46    pub plugin_activation_reports: Vec<PluginActivationPlan>,
47    pub plugin_inventory: Vec<PluginIR>,
48    pub plugin_activation_inventory: Vec<PluginActivationInventoryEntry>,
49    pub architecture_guard: ArchitectureGuardReport,
50}
51
52#[derive(Debug, Clone)]
53struct FileFact {
54    path: String,
55    size_bytes: u64,
56    language: String,
57}
58
59#[derive(Debug, Default)]
60pub struct CodebaseAwarenessEngine {
61    plugin_scanner: PluginScanner,
62    plugin_translator: PluginTranslator,
63}
64
65impl CodebaseAwarenessEngine {
66    #[must_use]
67    pub fn new() -> Self {
68        Self {
69            plugin_scanner: PluginScanner::new(),
70            plugin_translator: PluginTranslator::new(),
71        }
72    }
73
74    pub fn snapshot(
75        &self,
76        config: &CodebaseAwarenessConfig,
77    ) -> Result<CodebaseAwarenessSnapshot, IntegrationError> {
78        let roots = if config.roots.is_empty() {
79            vec![".".to_owned()]
80        } else {
81            config.roots.clone()
82        };
83
84        let mut file_facts = Vec::new();
85        for root in &roots {
86            let root_path = PathBuf::from(root);
87            if !root_path.exists() {
88                return Err(IntegrationError::AwarenessRootNotFound(root.to_owned()));
89            }
90            collect_file_facts(&root_path, &mut file_facts)?;
91        }
92
93        file_facts.sort_by(|left, right| left.path.cmp(&right.path));
94
95        let mut language_distribution = BTreeMap::new();
96        for fact in &file_facts {
97            *language_distribution
98                .entry(fact.language.clone())
99                .or_insert(0) += 1;
100        }
101
102        let plugin_roots = if config.plugin_roots.is_empty() {
103            roots.clone()
104        } else {
105            config.plugin_roots.clone()
106        };
107
108        let mut plugin_scan_reports = Vec::new();
109        let mut plugin_translation_reports = Vec::new();
110        let mut plugin_activation_reports = Vec::new();
111        let mut plugin_inventory = Vec::new();
112        let mut plugin_activation_inventory = Vec::new();
113        let bridge_matrix = BridgeSupportMatrix::default();
114        let setup_readiness_context = PluginSetupReadinessContext::default();
115
116        for root in &plugin_roots {
117            let report = self.plugin_scanner.scan_path(root)?;
118            let translation = self.plugin_translator.translate_scan_report(&report);
119            let activation = self.plugin_translator.plan_activation(
120                &translation,
121                &bridge_matrix,
122                &setup_readiness_context,
123            );
124            plugin_inventory.extend(translation.entries.iter().cloned());
125            plugin_activation_inventory.extend(activation.inventory_entries(&translation));
126            plugin_scan_reports.push(report);
127            plugin_translation_reports.push(translation);
128            plugin_activation_reports.push(activation);
129        }
130
131        let architecture_guard = config
132            .architecture_policy
133            .evaluate_paths(&config.proposed_mutations);
134
135        Ok(CodebaseAwarenessSnapshot {
136            scanned_roots: roots,
137            scanned_files: file_facts.len(),
138            language_distribution,
139            deterministic_fingerprint: fingerprint(&file_facts),
140            plugin_scan_reports,
141            plugin_translation_reports,
142            plugin_activation_reports,
143            plugin_inventory,
144            plugin_activation_inventory,
145            architecture_guard,
146        })
147    }
148}
149
150fn collect_file_facts(path: &Path, acc: &mut Vec<FileFact>) -> Result<(), IntegrationError> {
151    let metadata = fs::metadata(path).map_err(|error| IntegrationError::AwarenessFileRead {
152        path: path.display().to_string(),
153        reason: error.to_string(),
154    })?;
155
156    if metadata.is_file() {
157        let normalized_path = normalize_path(path);
158        acc.push(FileFact {
159            language: detect_language(path),
160            path: normalized_path,
161            size_bytes: metadata.len(),
162        });
163        return Ok(());
164    }
165
166    for entry in fs::read_dir(path).map_err(|error| IntegrationError::AwarenessFileRead {
167        path: path.display().to_string(),
168        reason: error.to_string(),
169    })? {
170        let entry = entry.map_err(|error| IntegrationError::AwarenessFileRead {
171            path: path.display().to_string(),
172            reason: error.to_string(),
173        })?;
174        let child = entry.path();
175
176        if child.is_dir() {
177            if should_skip_dir(&child) {
178                continue;
179            }
180            collect_file_facts(&child, acc)?;
181        } else if child.is_file() {
182            let child_metadata =
183                fs::metadata(&child).map_err(|error| IntegrationError::AwarenessFileRead {
184                    path: child.display().to_string(),
185                    reason: error.to_string(),
186                })?;
187            acc.push(FileFact {
188                language: detect_language(&child),
189                path: normalize_path(&child),
190                size_bytes: child_metadata.len(),
191            });
192        }
193    }
194
195    Ok(())
196}
197
198fn detect_language(path: &Path) -> String {
199    let extension = path
200        .extension()
201        .and_then(|ext| ext.to_str())
202        .map(|ext| ext.to_ascii_lowercase())
203        .unwrap_or_else(|| "unknown".to_owned());
204
205    match extension.as_str() {
206        "rs" => "rust".to_owned(),
207        "py" => "python".to_owned(),
208        "go" => "go".to_owned(),
209        "js" => "javascript".to_owned(),
210        "ts" => "typescript".to_owned(),
211        "toml" => "toml".to_owned(),
212        "md" => "markdown".to_owned(),
213        "json" => "json".to_owned(),
214        "yaml" | "yml" => "yaml".to_owned(),
215        other => other.to_owned(),
216    }
217}
218
219fn normalize_path(path: &Path) -> String {
220    path.display()
221        .to_string()
222        .replace('\\', "/")
223        .trim_start_matches("./")
224        .to_owned()
225}
226
227fn should_skip_dir(path: &Path) -> bool {
228    matches!(
229        path.file_name().and_then(|name| name.to_str()),
230        Some(".git" | "target" | "node_modules" | ".venv" | ".idea" | ".codex")
231    )
232}
233
234fn fingerprint(file_facts: &[FileFact]) -> String {
235    const OFFSET_BASIS: u64 = 0xcbf29ce484222325;
236    const PRIME: u64 = 0x100000001b3;
237
238    let mut hash = OFFSET_BASIS;
239    for fact in file_facts {
240        let row = format!("{}:{}:{}\n", fact.path, fact.size_bytes, fact.language);
241        for byte in row.bytes() {
242            hash ^= u64::from(byte);
243            hash = hash.wrapping_mul(PRIME);
244        }
245    }
246    format!("{hash:016x}")
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use std::time::{SystemTime, UNIX_EPOCH};
253
254    fn unique_tmp_dir(prefix: &str) -> PathBuf {
255        let nanos = SystemTime::now()
256            .duration_since(UNIX_EPOCH)
257            .expect("clock should be monotonic")
258            .as_nanos();
259        std::env::temp_dir().join(format!("{}-{}", prefix, nanos))
260    }
261
262    #[test]
263    fn awareness_snapshot_captures_languages_plugins_and_guard() {
264        let root = unique_tmp_dir("loong-awareness");
265        fs::create_dir_all(&root).expect("create temp root");
266
267        fs::write(root.join("runtime.rs"), "pub fn run() {}\n").expect("write rust file");
268        fs::write(root.join("agent.py"), "print('hello')\n").expect("write python file");
269        fs::write(
270            root.join("plugin.rs"),
271            r#"
272// LOONG_PLUGIN_START
273// {
274//   "plugin_id": "openrouter-rs",
275//   "provider_id": "openrouter",
276//   "connector_name": "openrouter",
277//   "channel_id": "primary",
278//   "endpoint": "https://openrouter.ai/api/v1/chat/completions",
279//   "capabilities": ["InvokeConnector"],
280//   "metadata": {"version":"0.5.0"}
281// }
282// LOONG_PLUGIN_END
283"#,
284        )
285        .expect("write plugin file");
286
287        let engine = CodebaseAwarenessEngine::new();
288        let snapshot = engine
289            .snapshot(&CodebaseAwarenessConfig {
290                roots: vec![root.display().to_string()],
291                plugin_roots: vec![root.display().to_string()],
292                proposed_mutations: vec!["examples/spec/runtime-extension.json".to_owned()],
293                architecture_policy: ArchitectureBoundaryPolicy::default(),
294            })
295            .expect("awareness snapshot should succeed");
296
297        assert_eq!(snapshot.scanned_roots.len(), 1);
298        assert_eq!(snapshot.plugin_inventory.len(), 1);
299        assert_eq!(snapshot.plugin_activation_reports.len(), 1);
300        assert_eq!(snapshot.plugin_activation_inventory.len(), 1);
301        assert_eq!(
302            snapshot.plugin_activation_inventory[0]
303                .activation_status
304                .map(|status| status.as_str().to_owned()),
305            Some("ready".to_owned())
306        );
307        assert!(
308            snapshot
309                .language_distribution
310                .get("rust")
311                .copied()
312                .unwrap_or(0)
313                >= 1
314        );
315        assert!(
316            snapshot
317                .language_distribution
318                .get("python")
319                .copied()
320                .unwrap_or(0)
321                >= 1
322        );
323        assert!(!snapshot.architecture_guard.has_denials());
324        assert!(!snapshot.deterministic_fingerprint.is_empty());
325    }
326
327    #[test]
328    fn awareness_snapshot_detects_guard_violations() {
329        let root = unique_tmp_dir("loong-awareness-guard");
330        fs::create_dir_all(&root).expect("create temp root");
331        fs::write(root.join("main.rs"), "fn main() {}\n").expect("write rust file");
332
333        let engine = CodebaseAwarenessEngine::new();
334        let snapshot = engine
335            .snapshot(&CodebaseAwarenessConfig {
336                roots: vec![root.display().to_string()],
337                plugin_roots: Vec::new(),
338                proposed_mutations: vec!["crates/kernel/src/kernel.rs".to_owned()],
339                architecture_policy: ArchitectureBoundaryPolicy::default(),
340            })
341            .expect("awareness snapshot should succeed");
342
343        assert!(snapshot.architecture_guard.has_denials());
344        assert!(
345            snapshot
346                .architecture_guard
347                .denied_paths
348                .contains(&"crates/kernel/src/kernel.rs".to_owned())
349        );
350    }
351
352    #[test]
353    fn awareness_snapshot_skips_target_directory_noise() {
354        let root = unique_tmp_dir("loong-awareness-skip");
355        fs::create_dir_all(root.join("target")).expect("create target directory");
356        fs::write(root.join("target").join("build.bin"), [0_u8, 159, 146, 150])
357            .expect("write binary");
358        fs::write(root.join("lib.rs"), "pub fn stable() {}\n").expect("write rust file");
359
360        let engine = CodebaseAwarenessEngine::new();
361        let snapshot = engine
362            .snapshot(&CodebaseAwarenessConfig {
363                roots: vec![root.display().to_string()],
364                plugin_roots: vec![root.display().to_string()],
365                proposed_mutations: Vec::new(),
366                architecture_policy: ArchitectureBoundaryPolicy::default(),
367            })
368            .expect("awareness snapshot should succeed");
369
370        assert_eq!(snapshot.scanned_files, 1);
371        assert_eq!(snapshot.language_distribution.get("rust").copied(), Some(1));
372    }
373
374    #[test]
375    fn awareness_snapshot_projects_plugin_activation_inventory_with_slot_conflicts() {
376        let root = unique_tmp_dir("loong-awareness-slots");
377        fs::create_dir_all(&root).expect("create temp root");
378
379        fs::write(
380            root.join("first.py"),
381            r#"
382# LOONG_PLUGIN_START
383# {
384#   "plugin_id": "search-a",
385#   "provider_id": "search-a",
386#   "connector_name": "search-a",
387#   "channel_id": "primary",
388#   "endpoint": "https://example.com/a",
389#   "capabilities": ["InvokeConnector"],
390#   "slot_claims": [{"slot":"provider:web_search","key":"default","mode":"exclusive"}],
391#   "metadata": {"bridge_kind":"http_json"}
392# }
393# LOONG_PLUGIN_END
394"#,
395        )
396        .expect("write first plugin");
397        fs::write(
398            root.join("second.py"),
399            r#"
400# LOONG_PLUGIN_START
401# {
402#   "plugin_id": "search-b",
403#   "provider_id": "search-b",
404#   "connector_name": "search-b",
405#   "channel_id": "primary",
406#   "endpoint": "https://example.com/b",
407#   "capabilities": ["InvokeConnector"],
408#   "slot_claims": [{"slot":"provider:web_search","key":"default","mode":"exclusive"}],
409#   "metadata": {"bridge_kind":"http_json"}
410# }
411# LOONG_PLUGIN_END
412"#,
413        )
414        .expect("write second plugin");
415
416        let engine = CodebaseAwarenessEngine::new();
417        let snapshot = engine
418            .snapshot(&CodebaseAwarenessConfig {
419                roots: vec![root.display().to_string()],
420                plugin_roots: vec![root.display().to_string()],
421                proposed_mutations: Vec::new(),
422                architecture_policy: ArchitectureBoundaryPolicy::default(),
423            })
424            .expect("awareness snapshot should succeed");
425
426        assert_eq!(snapshot.plugin_activation_reports.len(), 1);
427        assert_eq!(snapshot.plugin_activation_reports[0].blocked_plugins, 2);
428        assert_eq!(snapshot.plugin_activation_inventory.len(), 2);
429        assert!(snapshot.plugin_activation_inventory.iter().all(|entry| {
430            entry
431                .activation_status
432                .is_some_and(|status| status.as_str() == "blocked_slot_claim_conflict")
433        }));
434    }
435}