Skip to main content

lean_ctx/core/context_package/
loader.rs

1use std::path::Path;
2
3use crate::core::knowledge::ProjectKnowledge;
4use crate::core::memory_policy::MemoryPolicy;
5use crate::core::property_graph::{CodeGraph, Edge, EdgeKind, Node, NodeKind};
6
7use super::content::{GraphLayer, KnowledgeLayer, PackageContent};
8use super::manifest::PackageManifest;
9
10#[derive(Debug, Clone, Default)]
11pub struct LoadReport {
12    pub package_name: String,
13    pub package_version: String,
14    pub knowledge_facts_merged: u32,
15    pub knowledge_facts_skipped: u32,
16    pub knowledge_patterns_merged: u32,
17    pub graph_nodes_imported: u32,
18    pub graph_edges_imported: u32,
19    pub gotchas_imported: u32,
20    pub warnings: Vec<String>,
21}
22
23impl std::fmt::Display for LoadReport {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        writeln!(
26            f,
27            "Package: {} v{}",
28            self.package_name, self.package_version
29        )?;
30        if self.knowledge_facts_merged > 0 || self.knowledge_facts_skipped > 0 {
31            writeln!(
32                f,
33                "  Knowledge: {} facts merged, {} skipped (duplicates)",
34                self.knowledge_facts_merged, self.knowledge_facts_skipped
35            )?;
36        }
37        if self.knowledge_patterns_merged > 0 {
38            writeln!(
39                f,
40                "  Patterns:  {} imported",
41                self.knowledge_patterns_merged
42            )?;
43        }
44        if self.graph_nodes_imported > 0 || self.graph_edges_imported > 0 {
45            writeln!(
46                f,
47                "  Graph:     {} nodes, {} edges imported",
48                self.graph_nodes_imported, self.graph_edges_imported
49            )?;
50        }
51        if self.gotchas_imported > 0 {
52            writeln!(f, "  Gotchas:   {} imported", self.gotchas_imported)?;
53        }
54        for w in &self.warnings {
55            writeln!(f, "  WARNING: {w}")?;
56        }
57        Ok(())
58    }
59}
60
61pub fn load_package(
62    manifest: &PackageManifest,
63    content: &PackageContent,
64    project_root: &str,
65) -> Result<LoadReport, String> {
66    let mut report = LoadReport {
67        package_name: manifest.name.clone(),
68        package_version: manifest.version.clone(),
69        ..Default::default()
70    };
71
72    if let Some(ref kl) = content.knowledge {
73        merge_knowledge(kl, project_root, manifest, &mut report)?;
74    }
75
76    if let Some(ref gl) = content.graph {
77        import_graph(gl, project_root, &mut report)?;
78    }
79
80    if let Some(ref gotchas) = content.gotchas {
81        import_gotchas(gotchas, project_root, &mut report);
82    }
83
84    Ok(report)
85}
86
87fn merge_knowledge(
88    layer: &KnowledgeLayer,
89    project_root: &str,
90    manifest: &PackageManifest,
91    report: &mut LoadReport,
92) -> Result<(), String> {
93    let mut knowledge = ProjectKnowledge::load_or_create(project_root);
94    let policy = MemoryPolicy::default();
95    let source_tag = format!("{}@{}", manifest.name, manifest.version);
96
97    for fact in &layer.facts {
98        let exists = knowledge
99            .facts
100            .iter()
101            .any(|f| f.category == fact.category && f.key == fact.key && f.value == fact.value);
102
103        if exists {
104            report.knowledge_facts_skipped += 1;
105            continue;
106        }
107
108        knowledge.remember(
109            &fact.category,
110            &fact.key,
111            &fact.value,
112            &fact.source_session,
113            fact.confidence.min(0.8),
114            &policy,
115        );
116        if let Some(last) = knowledge.facts.last_mut() {
117            last.imported_from = Some(source_tag.clone());
118        }
119        report.knowledge_facts_merged += 1;
120    }
121
122    for pattern in &layer.patterns {
123        let exists = knowledge.patterns.iter().any(|p| {
124            p.pattern_type == pattern.pattern_type && p.description == pattern.description
125        });
126
127        if !exists {
128            knowledge.patterns.push(pattern.clone());
129            report.knowledge_patterns_merged += 1;
130        }
131    }
132
133    knowledge.save()?;
134    Ok(())
135}
136
137fn import_graph(
138    layer: &GraphLayer,
139    project_root: &str,
140    report: &mut LoadReport,
141) -> Result<(), String> {
142    let project_path = Path::new(project_root);
143    let graph = CodeGraph::open(project_path).map_err(|e| format!("graph open: {e}"))?;
144
145    for node_export in &layer.nodes {
146        let node = Node {
147            id: None,
148            kind: NodeKind::parse(&node_export.kind),
149            name: node_export.name.clone(),
150            file_path: node_export.file_path.clone(),
151            line_start: node_export.line_start,
152            line_end: node_export.line_end,
153            metadata: node_export.metadata.clone(),
154        };
155
156        match graph.upsert_node(&node) {
157            Ok(_) => report.graph_nodes_imported += 1,
158            Err(e) => {
159                report
160                    .warnings
161                    .push(format!("node import failed ({}): {e}", node_export.name));
162            }
163        }
164    }
165
166    for edge_export in &layer.edges {
167        let source = graph
168            .get_node_by_path(&edge_export.source_path)
169            .map_err(|e| e.to_string())?;
170        let target = graph
171            .get_node_by_path(&edge_export.target_path)
172            .map_err(|e| e.to_string())?;
173
174        if let (Some(src), Some(tgt)) = (source, target) {
175            let edge = Edge {
176                id: None,
177                source_id: src.id.unwrap_or(0),
178                target_id: tgt.id.unwrap_or(0),
179                kind: EdgeKind::parse(&edge_export.kind),
180                metadata: edge_export.metadata.clone(),
181            };
182
183            match graph.upsert_edge(&edge) {
184                Ok(()) => report.graph_edges_imported += 1,
185                Err(e) => {
186                    report.warnings.push(format!(
187                        "edge import failed ({} -> {}): {e}",
188                        edge_export.source_name, edge_export.target_name
189                    ));
190                }
191            }
192        }
193    }
194
195    Ok(())
196}
197
198fn import_gotchas(
199    layer: &super::content::GotchasLayer,
200    project_root: &str,
201    report: &mut LoadReport,
202) {
203    use crate::core::gotcha_tracker::{
204        Gotcha, GotchaCategory, GotchaSeverity, GotchaSource, GotchaStore,
205    };
206
207    let mut store = GotchaStore::load(project_root);
208    let before = store.gotchas.len();
209
210    for g in &layer.gotchas {
211        let dup = store.gotchas.iter().any(|e| e.id == g.id);
212        if dup {
213            continue;
214        }
215
216        let category = GotchaCategory::from_str_loose(&g.category);
217        let severity = match g.severity.as_str() {
218            "critical" => GotchaSeverity::Critical,
219            "warning" => GotchaSeverity::Warning,
220            _ => GotchaSeverity::Info,
221        };
222
223        let mut gotcha = Gotcha::new(
224            category,
225            severity,
226            &g.trigger,
227            &g.resolution,
228            GotchaSource::AgentReported {
229                session_id: "package-import".into(),
230            },
231            "package-import",
232        );
233        g.id.clone_into(&mut gotcha.id);
234        g.file_patterns.clone_into(&mut gotcha.file_patterns);
235        gotcha.confidence = g.confidence.min(0.8);
236
237        store.gotchas.push(gotcha);
238    }
239
240    report.gotchas_imported = (store.gotchas.len() - before) as u32;
241    let _ = store.save(project_root);
242}