Skip to main content

lean_ctx/core/context_package/
loader.rs

1use 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
253/// Find a node by symbol name+path first, then fall back to path-only lookup.
254fn 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}