forgekit-core 0.5.0

Deterministic code intelligence SDK - Core library
Documentation
use crate::error::{ForgeError, Result};
use crate::knowledge::types::{self, CfgBlockData, GraphNode};
use crate::knowledge::KnowledgeGraph;

/// Byte range of a symbol within its source file, used when inserting symbol
/// nodes into the knowledge graph.
#[derive(Clone, Debug)]
pub struct SourceSpan {
    pub file: String,
    pub line: usize,
    pub byte_start: u32,
    pub byte_end: u32,
}

impl SourceSpan {
    pub fn new(file: impl Into<String>, line: usize, byte_start: u32, byte_end: u32) -> Self {
        Self {
            file: file.into(),
            line,
            byte_start,
            byte_end,
        }
    }
}

impl KnowledgeGraph {
    pub fn get_node(&self, node_id: i64) -> Result<GraphNode> {
        let entity = self
            .backend
            .get_node(Self::snapshot(), node_id)
            .map_err(|e| ForgeError::DatabaseError(format!("Node not found: {}", e)))?;
        Ok(GraphNode {
            id: node_id,
            kind: entity.kind,
            name: entity.name,
            file_path: entity.file_path,
            data: entity.data,
        })
    }

    pub fn find_nodes_by_kind(&self, kind: &str) -> Result<Vec<GraphNode>> {
        let snap = Self::snapshot();
        let ids = self
            .backend
            .entity_ids()
            .map_err(|e| ForgeError::DatabaseError(format!("Entity list failed: {}", e)))?;
        let mut results = Vec::new();
        for id in ids {
            if let Ok(entity) = self.backend.get_node(snap, id) {
                if entity.kind == kind {
                    results.push(GraphNode {
                        id,
                        kind: entity.kind,
                        name: entity.name,
                        file_path: entity.file_path,
                        data: entity.data,
                    });
                }
            }
        }
        Ok(results)
    }

    pub fn add_symbol(
        &self,
        name: &str,
        symbol_kind: &str,
        qualified_name: &str,
        span: &SourceSpan,
        language: &str,
        parent_id: Option<i64>,
    ) -> Result<i64> {
        let mut data = serde_json::json!({
            "symbol_kind": symbol_kind,
            "qualified_name": qualified_name,
            "file": span.file,
            "line": span.line,
            "byte_start": span.byte_start,
            "byte_end": span.byte_end,
            "language": language,
        });
        if let Some(pid) = parent_id {
            data["parent_id"] = serde_json::json!(pid);
        }
        self.insert_node(types::node::SYMBOL, name, Some(&span.file), data)
    }

    pub fn add_file(&self, path: &str, language: &str, hash: &str) -> Result<i64> {
        let data = serde_json::json!({
            "path": path,
            "language": language,
            "hash": hash,
        });
        self.insert_node(types::node::FILE, path, None, data)
    }

    pub fn add_discovery(
        &self,
        agent: &str,
        discovery_type: &str,
        target: &str,
        metadata: serde_json::Value,
    ) -> Result<i64> {
        let data = serde_json::json!({
            "discovery_type": discovery_type,
            "agent": agent,
            "timestamp": chrono::Utc::now().to_rfc3339(),
            "metadata": metadata,
        });
        self.insert_node(types::node::DISCOVERY, target, None, data)
    }

    pub fn add_issue(
        &self,
        severity: &str,
        description: &str,
        rule_id: Option<&str>,
    ) -> Result<i64> {
        let mut data = serde_json::json!({"severity": severity, "description": description,});
        if let Some(rid) = rule_id {
            data["rule_id"] = serde_json::json!(rid);
        }
        self.insert_node(types::node::ISSUE, description, None, data)
    }

    pub fn add_pattern(
        &self,
        pattern_type: &str,
        confidence: f64,
        description: &str,
    ) -> Result<i64> {
        let data = serde_json::json!({
            "pattern_type": pattern_type,
            "confidence": confidence,
            "description": description,
        });
        self.insert_node(types::node::PATTERN, pattern_type, None, data)
    }

    pub fn add_knowledge(
        &self,
        source: &str,
        title: &str,
        tags: &[String],
        summary: &str,
    ) -> Result<i64> {
        let data = serde_json::json!({
            "source": source,
            "title": title,
            "tags": tags,
            "summary": summary,
        });
        self.insert_node(types::node::KNOWLEDGE, title, None, data)
    }

    pub fn add_hotspot(
        &self,
        complexity: u32,
        risk_score: f64,
        loop_depth: u32,
        description: &str,
    ) -> Result<i64> {
        let data = serde_json::json!({
            "complexity": complexity,
            "risk_score": risk_score,
            "loop_depth": loop_depth,
            "description": description,
        });
        self.insert_node(types::node::HOTSPOT, description, None, data)
    }

    pub fn add_cfg_block(&self, function_id: i64, block: &CfgBlockData) -> Result<i64> {
        let data = serde_json::json!({
            "function_id": function_id,
            "start_byte": block.start_byte,
            "end_byte": block.end_byte,
            "block_kind": block.block_kind,
            "is_error": block.is_error,
        });
        self.insert_node(
            types::node::CFG_BLOCK,
            &format!("block_{}", block.start_byte),
            None,
            data,
        )
    }
}

#[cfg(test)]
mod tests {
    use crate::knowledge::{open_kg, CfgBlockData, SourceSpan};

    #[test]
    fn test_add_symbol_node() {
        let (_temp, kg) = open_kg();
        let id = kg
            .add_symbol(
                "my_func",
                "Function",
                "crate::module::my_func",
                &SourceSpan::new("src/lib.rs", 42, 100, 200),
                "Rust",
                None,
            )
            .expect("invariant: fresh graph accepts inserts");
        assert!(id > 0);
        let node = kg
            .get_node(id)
            .expect("invariant: just-inserted node is retrievable");
        assert_eq!(node.kind, "symbol");
        assert_eq!(node.name, "my_func");
        assert_eq!(node.prop_str("symbol_kind"), Some("Function"));
        assert_eq!(
            node.prop_str("qualified_name"),
            Some("crate::module::my_func")
        );
        assert_eq!(node.prop_str("file"), Some("src/lib.rs"));
        assert_eq!(node.prop_u64("line"), Some(42));
    }

    #[test]
    fn test_add_file_node() {
        let (_temp, kg) = open_kg();
        let id = kg
            .add_file("src/lib.rs", "Rust", "abc123")
            .expect("invariant: fresh graph accepts inserts");
        let node = kg
            .get_node(id)
            .expect("invariant: just-inserted node is retrievable");
        assert_eq!(node.kind, "file");
        assert_eq!(node.name, "src/lib.rs");
        assert_eq!(node.prop_str("language"), Some("Rust"));
        assert_eq!(node.prop_str("hash"), Some("abc123"));
    }

    #[test]
    fn test_add_discovery_node() {
        let (_temp, kg) = open_kg();
        let id = kg
            .add_discovery(
                "claude1",
                "Symbol",
                "my_func",
                serde_json::json!({"complexity": 8}),
            )
            .expect("invariant: fresh graph accepts inserts");
        let node = kg
            .get_node(id)
            .expect("invariant: just-inserted node is retrievable");
        assert_eq!(node.kind, "discovery");
        assert_eq!(node.name, "my_func");
        assert_eq!(node.prop_str("agent"), Some("claude1"));
        assert_eq!(node.prop_str("discovery_type"), Some("Symbol"));
    }

    #[test]
    fn test_add_issue_node() {
        let (_temp, kg) = open_kg();
        let id = kg
            .add_issue("high", "unwrap in production code", Some("M001"))
            .expect("invariant: fresh graph accepts inserts");
        let node = kg
            .get_node(id)
            .expect("invariant: just-inserted node is retrievable");
        assert_eq!(node.kind, "issue");
        assert_eq!(node.prop_str("severity"), Some("high"));
        assert_eq!(node.prop_str("rule_id"), Some("M001"));
    }

    #[test]
    fn test_add_pattern_node() {
        let (_temp, kg) = open_kg();
        let id = kg
            .add_pattern("builder", 0.92, "builder pattern detected")
            .expect("invariant: fresh graph accepts inserts");
        let node = kg
            .get_node(id)
            .expect("invariant: just-inserted node is retrievable");
        assert_eq!(node.kind, "pattern");
        assert_eq!(node.prop_str("pattern_type"), Some("builder"));
        assert_eq!(node.prop_f64("confidence"), Some(0.92));
    }

    #[test]
    fn test_add_knowledge_node() {
        let (_temp, kg) = open_kg();
        let tags = vec!["auth".to_string(), "middleware".to_string()];
        let id = kg
            .add_knowledge("wiki", "Auth Architecture", &tags, "Overview of auth flow")
            .expect("invariant: fresh graph accepts inserts");
        let node = kg
            .get_node(id)
            .expect("invariant: just-inserted node is retrievable");
        assert_eq!(node.kind, "knowledge");
        assert_eq!(node.prop_str("source"), Some("wiki"));
        assert_eq!(node.prop_str("title"), Some("Auth Architecture"));
    }

    #[test]
    fn test_add_hotspot_node() {
        let (_temp, kg) = open_kg();
        let id = kg
            .add_hotspot(15, 0.85, 3, "high complexity loop")
            .expect("invariant: fresh graph accepts inserts");
        let node = kg
            .get_node(id)
            .expect("invariant: just-inserted node is retrievable");
        assert_eq!(node.kind, "hotspot");
        assert_eq!(node.prop_u64("complexity"), Some(15));
        assert_eq!(node.prop_f64("risk_score"), Some(0.85));
    }

    #[test]
    fn test_add_cfg_block_node() {
        let (_temp, kg) = open_kg();
        let block = CfgBlockData {
            start_byte: 100,
            end_byte: 200,
            block_kind: "Basic".to_string(),
            is_error: false,
        };
        let id = kg
            .add_cfg_block(42, &block)
            .expect("invariant: fresh graph accepts inserts");
        let node = kg
            .get_node(id)
            .expect("invariant: just-inserted node is retrievable");
        assert_eq!(node.kind, "cfg_block");
        assert_eq!(node.prop_u64("function_id"), Some(42));
        assert_eq!(node.prop_str("block_kind"), Some("Basic"));
    }

    #[test]
    fn test_find_nodes_by_kind() {
        let (_temp, kg) = open_kg();
        kg.add_symbol(
            "func_a",
            "Function",
            "a",
            &SourceSpan::new("f.rs", 1, 0, 10),
            "Rust",
            None,
        )
        .expect("invariant: fresh graph accepts inserts");
        kg.add_symbol(
            "func_b",
            "Function",
            "b",
            &SourceSpan::new("f.rs", 2, 0, 10),
            "Rust",
            None,
        )
        .expect("invariant: fresh graph accepts inserts");
        kg.add_file("f.rs", "Rust", "hash")
            .expect("invariant: fresh graph accepts inserts");

        let symbols = kg
            .find_nodes_by_kind("symbol")
            .expect("invariant: query on valid graph succeeds");
        assert_eq!(symbols.len(), 2);
        let files = kg
            .find_nodes_by_kind("file")
            .expect("invariant: query on valid graph succeeds");
        assert_eq!(files.len(), 1);
    }

    #[test]
    fn test_get_node_not_found() {
        let (_temp, kg) = open_kg();
        let result = kg.get_node(99999);
        assert!(result.is_err());
    }
}