lean_ctx/core/
knowledge_bootstrap.rs1use std::path::Path;
2
3use crate::core::graph_provider::GraphProvider;
4use crate::core::knowledge::ProjectKnowledge;
5use crate::core::memory_policy::MemoryPolicy;
6
7const BOOTSTRAP_SESSION_ID: &str = "auto-bootstrap";
8const BOOTSTRAP_CONFIDENCE: f32 = 0.95;
9
10pub fn bootstrap_if_empty(
15 knowledge: &mut ProjectKnowledge,
16 project_root: &str,
17 graph: Option<&GraphProvider>,
18 policy: &MemoryPolicy,
19) -> bool {
20 if !knowledge.facts.is_empty() {
21 return false;
22 }
23
24 let mut changed = false;
25
26 changed |= remember_fact(knowledge, "workflow", "project_root", project_root, policy);
28
29 if let Some(identity) = crate::core::project_hash::project_identity(project_root) {
30 changed |= remember_fact(
31 knowledge,
32 "architecture",
33 "project_identity",
34 &identity,
35 policy,
36 );
37 if let Some(url) = identity.strip_prefix("git:") {
38 changed |= remember_fact(knowledge, "deployment", "git_remote", url, policy);
39 }
40 }
41
42 let markers = detect_build_markers(project_root);
43 if !markers.is_empty() {
44 changed |= remember_fact(
45 knowledge,
46 "architecture",
47 "build_markers",
48 &markers.join(", "),
49 policy,
50 );
51 }
52
53 if let Some(gp) = graph {
54 let file_count = gp.file_count();
55 let symbol_count = gp.symbol_count();
56 let edge_count = gp.edge_count().unwrap_or(0);
57 changed |= remember_fact(
58 knowledge,
59 "workflow",
60 "index_stats",
61 &format!("files={file_count}, symbols={symbol_count}, edges={edge_count}"),
62 policy,
63 );
64
65 let last_scan = gp.last_scan();
66 if !last_scan.trim().is_empty() {
67 changed |= remember_fact(knowledge, "workflow", "index_last_scan", &last_scan, policy);
68 }
69
70 let (langs, total_tokens) = summarize_languages_and_tokens(gp);
71 if !langs.is_empty() {
72 changed |= remember_fact(knowledge, "architecture", "languages_top", &langs, policy);
73 }
74 if total_tokens > 0 {
75 changed |= remember_fact(
76 knowledge,
77 "performance",
78 "tokens_indexed",
79 &total_tokens.to_string(),
80 policy,
81 );
82 }
83 }
84
85 changed
86}
87
88fn remember_fact(
89 knowledge: &mut ProjectKnowledge,
90 category: &str,
91 key: &str,
92 value: &str,
93 policy: &MemoryPolicy,
94) -> bool {
95 if value.trim().is_empty() {
96 return false;
97 }
98 knowledge.remember(
99 category,
100 key,
101 value,
102 BOOTSTRAP_SESSION_ID,
103 BOOTSTRAP_CONFIDENCE,
104 policy,
105 );
106 true
107}
108
109fn detect_build_markers(project_root: &str) -> Vec<&'static str> {
110 let root = Path::new(project_root);
111 let mut out: Vec<&'static str> = Vec::new();
112
113 if !crate::core::pathutil::may_probe_path(root) {
117 return out;
118 }
119
120 if root.join(".git").exists() {
121 out.push("git");
122 }
123 if root.join("Cargo.toml").exists() {
124 out.push("cargo");
125 }
126 if root.join("package.json").exists() {
127 out.push("npm");
128 }
129 if root.join("pyproject.toml").exists() {
130 out.push("python");
131 }
132 if root.join("go.mod").exists() {
133 out.push("go");
134 }
135 if root.join("pom.xml").exists() {
136 out.push("maven");
137 }
138 if root.join("build.gradle").exists() || root.join("build.gradle.kts").exists() {
139 out.push("gradle");
140 }
141 if root.join("CMakeLists.txt").exists() {
142 out.push("cmake");
143 }
144
145 if let Ok(entries) = std::fs::read_dir(root)
146 && entries
147 .flatten()
148 .any(|e| e.path().extension().is_some_and(|ext| ext == "sln"))
149 {
150 out.push("dotnet");
151 }
152
153 out
154}
155
156fn summarize_languages_and_tokens(gp: &GraphProvider) -> (String, u64) {
157 let mut total_tokens: u64 = 0;
158 let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
159
160 for f in gp.file_entries() {
161 total_tokens = total_tokens.saturating_add(f.token_count as u64);
162 let lang = if f.language.trim().is_empty() {
163 "unknown".to_string()
164 } else {
165 f.language
166 };
167 *counts.entry(lang).or_insert(0) += 1;
168 }
169
170 let mut entries: Vec<(String, usize)> = counts.into_iter().collect();
171 entries.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
172
173 let langs = entries
174 .into_iter()
175 .take(6)
176 .map(|(lang, count)| format!("{lang}:{count}"))
177 .collect::<Vec<_>>()
178 .join(", ");
179
180 (langs, total_tokens)
181}