lean_ctx/core/context_package/
loader.rs1use crate::core::knowledge::ProjectKnowledge;
2use crate::core::memory_policy::MemoryPolicy;
3use crate::core::property_graph::{CodeGraph, Edge, EdgeKind, Node, NodeKind};
4
5use super::content::{GraphLayer, KnowledgeLayer, PackageContent, SessionLayer};
6use super::manifest::PackageManifest;
7
8#[derive(Debug, Clone, Default)]
9pub struct LoadReport {
10 pub package_name: String,
11 pub package_version: String,
12 pub knowledge_facts_merged: u32,
13 pub knowledge_facts_skipped: u32,
14 pub knowledge_patterns_merged: u32,
15 pub knowledge_insights_merged: u32,
16 pub graph_nodes_imported: u32,
17 pub graph_edges_imported: u32,
18 pub gotchas_imported: u32,
19 pub session_findings_merged: u32,
20 pub session_decisions_merged: u32,
21 pub warnings: Vec<String>,
22}
23
24impl std::fmt::Display for LoadReport {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 writeln!(
27 f,
28 "Package: {} v{}",
29 self.package_name, self.package_version
30 )?;
31 if self.knowledge_facts_merged > 0 || self.knowledge_facts_skipped > 0 {
32 writeln!(
33 f,
34 " Knowledge: {} facts merged, {} skipped (duplicates)",
35 self.knowledge_facts_merged, self.knowledge_facts_skipped
36 )?;
37 }
38 if self.knowledge_patterns_merged > 0 {
39 writeln!(
40 f,
41 " Patterns: {} imported",
42 self.knowledge_patterns_merged
43 )?;
44 }
45 if self.knowledge_insights_merged > 0 {
46 writeln!(
47 f,
48 " Insights: {} imported",
49 self.knowledge_insights_merged
50 )?;
51 }
52 if self.graph_nodes_imported > 0 || self.graph_edges_imported > 0 {
53 writeln!(
54 f,
55 " Graph: {} nodes, {} edges imported",
56 self.graph_nodes_imported, self.graph_edges_imported
57 )?;
58 }
59 if self.gotchas_imported > 0 {
60 writeln!(f, " Gotchas: {} imported", self.gotchas_imported)?;
61 }
62 if self.session_findings_merged > 0 || self.session_decisions_merged > 0 {
63 writeln!(
64 f,
65 " Session: {} findings, {} decisions imported",
66 self.session_findings_merged, self.session_decisions_merged
67 )?;
68 }
69 for w in &self.warnings {
70 writeln!(f, " WARNING: {w}")?;
71 }
72 Ok(())
73 }
74}
75
76pub fn load_package(
77 manifest: &PackageManifest,
78 content: &PackageContent,
79 project_root: &str,
80) -> Result<LoadReport, String> {
81 let mut report = LoadReport {
82 package_name: manifest.name.clone(),
83 package_version: manifest.version.clone(),
84 ..Default::default()
85 };
86
87 if let Some(ref kl) = content.knowledge {
88 if let Err(e) = merge_knowledge(kl, project_root, manifest, &mut report) {
89 report
90 .warnings
91 .push(format!("knowledge import failed: {e}"));
92 }
93 }
94
95 if let Some(ref gl) = content.graph {
96 if let Err(e) = import_graph(gl, project_root, &mut report) {
97 report.warnings.push(format!("graph import failed: {e}"));
98 }
99 }
100
101 if let Some(ref gotchas) = content.gotchas {
102 import_gotchas(gotchas, project_root, &mut report);
103 }
104
105 if let Some(ref session) = content.session {
106 import_session(session, project_root, manifest, &mut report);
107 }
108
109 Ok(report)
110}
111
112fn merge_knowledge(
113 layer: &KnowledgeLayer,
114 project_root: &str,
115 manifest: &PackageManifest,
116 report: &mut LoadReport,
117) -> Result<(), String> {
118 let mut knowledge = ProjectKnowledge::load_or_create(project_root);
119 let policy = MemoryPolicy::default();
120 let source_tag = format!("{}@{}", manifest.name, manifest.version);
121
122 for fact in &layer.facts {
123 let exists = knowledge
124 .facts
125 .iter()
126 .any(|f| f.category == fact.category && f.key == fact.key && f.value == fact.value);
127
128 if exists {
129 report.knowledge_facts_skipped += 1;
130 continue;
131 }
132
133 knowledge.remember(
134 &fact.category,
135 &fact.key,
136 &fact.value,
137 &fact.source_session,
138 fact.confidence.min(0.8),
139 &policy,
140 );
141 if let Some(last) = knowledge.facts.last_mut() {
142 last.imported_from = Some(source_tag.clone());
143 }
144 report.knowledge_facts_merged += 1;
145 }
146
147 for pattern in &layer.patterns {
148 let exists = knowledge.patterns.iter().any(|p| {
149 p.pattern_type == pattern.pattern_type && p.description == pattern.description
150 });
151
152 if !exists {
153 knowledge.patterns.push(pattern.clone());
154 report.knowledge_patterns_merged += 1;
155 }
156 }
157
158 for insight in &layer.insights {
159 let exists = knowledge
160 .history
161 .iter()
162 .any(|h| h.summary == insight.summary);
163
164 if !exists {
165 knowledge.history.push(insight.clone());
166 report.knowledge_insights_merged += 1;
167 }
168 }
169
170 knowledge.save()?;
171 Ok(())
172}
173
174fn import_graph(
175 layer: &GraphLayer,
176 project_root: &str,
177 report: &mut LoadReport,
178) -> Result<(), String> {
179 let graph = CodeGraph::open(project_root).map_err(|e| format!("graph open: {e}"))?;
180
181 for node_export in &layer.nodes {
182 let node = Node {
183 id: None,
184 kind: NodeKind::parse(&node_export.kind),
185 name: node_export.name.clone(),
186 file_path: node_export.file_path.clone(),
187 line_start: node_export.line_start,
188 line_end: node_export.line_end,
189 metadata: node_export.metadata.clone(),
190 };
191
192 match graph.upsert_node(&node) {
193 Ok(_) => report.graph_nodes_imported += 1,
194 Err(e) => {
195 report
196 .warnings
197 .push(format!("node import failed ({}): {e}", node_export.name));
198 }
199 }
200 }
201
202 for edge_export in &layer.edges {
203 let source = find_node_for_edge(&graph, &edge_export.source_path, &edge_export.source_name);
204 let target = find_node_for_edge(&graph, &edge_export.target_path, &edge_export.target_name);
205
206 match (source, target) {
207 (Some(src), Some(tgt)) => {
208 let Some(src_id) = src.id else {
209 report.warnings.push(format!(
210 "edge skipped: source node has no id ({}:{})",
211 edge_export.source_path, edge_export.source_name
212 ));
213 continue;
214 };
215 let Some(tgt_id) = tgt.id else {
216 report.warnings.push(format!(
217 "edge skipped: target node has no id ({}:{})",
218 edge_export.target_path, edge_export.target_name
219 ));
220 continue;
221 };
222
223 let edge = Edge {
224 id: None,
225 source_id: src_id,
226 target_id: tgt_id,
227 kind: EdgeKind::parse(&edge_export.kind),
228 metadata: edge_export.metadata.clone(),
229 };
230
231 match graph.upsert_edge(&edge) {
232 Ok(()) => report.graph_edges_imported += 1,
233 Err(e) => {
234 report.warnings.push(format!(
235 "edge import failed ({} -> {}): {e}",
236 edge_export.source_name, edge_export.target_name
237 ));
238 }
239 }
240 }
241 _ => {
242 report.warnings.push(format!(
243 "edge skipped: node not found ({} -> {})",
244 edge_export.source_name, edge_export.target_name
245 ));
246 }
247 }
248 }
249
250 Ok(())
251}
252
253fn find_node_for_edge(graph: &CodeGraph, file_path: &str, name: &str) -> Option<Node> {
255 if let Ok(Some(node)) = graph.get_node_by_symbol(name, file_path) {
256 return Some(node);
257 }
258 if let Ok(Some(node)) = graph.get_node_by_path(file_path) {
259 return Some(node);
260 }
261 None
262}
263
264fn import_gotchas(
265 layer: &super::content::GotchasLayer,
266 project_root: &str,
267 report: &mut LoadReport,
268) {
269 use crate::core::gotcha_tracker::{
270 Gotcha, GotchaCategory, GotchaSeverity, GotchaSource, GotchaStore,
271 };
272
273 let mut store = GotchaStore::load(project_root);
274 let before = store.gotchas.len();
275
276 for g in &layer.gotchas {
277 let dup = store.gotchas.iter().any(|e| e.id == g.id);
278 if dup {
279 continue;
280 }
281
282 let category = GotchaCategory::from_str_loose(&g.category);
283 let severity = match g.severity.as_str() {
284 "critical" => GotchaSeverity::Critical,
285 "warning" => GotchaSeverity::Warning,
286 _ => GotchaSeverity::Info,
287 };
288
289 let mut gotcha = Gotcha::new(
290 category,
291 severity,
292 &g.trigger,
293 &g.resolution,
294 GotchaSource::AgentReported {
295 session_id: "package-import".into(),
296 },
297 "package-import",
298 );
299 g.id.clone_into(&mut gotcha.id);
300 g.file_patterns.clone_into(&mut gotcha.file_patterns);
301 gotcha.confidence = g.confidence.min(0.8);
302
303 store.gotchas.push(gotcha);
304 }
305
306 report.gotchas_imported = (store.gotchas.len() - before) as u32;
307 if let Err(e) = store.save(project_root) {
308 report.warnings.push(format!("gotcha save failed: {e}"));
309 }
310}
311
312fn import_session(
313 layer: &SessionLayer,
314 project_root: &str,
315 manifest: &PackageManifest,
316 report: &mut LoadReport,
317) {
318 let mut knowledge = ProjectKnowledge::load_or_create(project_root);
319 let policy = MemoryPolicy::default();
320 let source_tag = format!("{}@{} (session)", manifest.name, manifest.version);
321
322 for finding in &layer.findings {
323 let key = finding.file.as_deref().unwrap_or("general");
324 let exists = knowledge
325 .facts
326 .iter()
327 .any(|f| f.category == "session_finding" && f.value == finding.summary);
328 if !exists {
329 knowledge.remember(
330 "session_finding",
331 key,
332 &finding.summary,
333 &source_tag,
334 0.6,
335 &policy,
336 );
337 report.session_findings_merged += 1;
338 }
339 }
340
341 for decision in &layer.decisions {
342 let value = if let Some(ref rationale) = decision.rationale {
343 format!("{} (rationale: {})", decision.summary, rationale)
344 } else {
345 decision.summary.clone()
346 };
347 let exists = knowledge
348 .facts
349 .iter()
350 .any(|f| f.category == "session_decision" && f.value == decision.summary);
351 if !exists {
352 knowledge.remember(
353 "session_decision",
354 "decision",
355 &value,
356 &source_tag,
357 0.7,
358 &policy,
359 );
360 report.session_decisions_merged += 1;
361 }
362 }
363
364 if report.session_findings_merged > 0 || report.session_decisions_merged > 0 {
365 if let Err(e) = knowledge.save() {
366 report
367 .warnings
368 .push(format!("session knowledge save failed: {e}"));
369 }
370 }
371}