codebase-graph 1.1.6

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use serde::de::Error as DeError;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;

#[derive(Debug, Clone, Deserialize)]
pub struct NativeSyntaxMaterializationRequest {
    pub source_root: String,
    pub repository_label: String,
    pub mode: String,
    pub parser_version: String,
    pub manifest_schema_version: u64,
    pub ontology: String,
    #[serde(default)]
    pub ontology_schema: OntologySchema,
    pub previous_manifest: Option<NativeManifest>,
    pub profiles: Vec<LanguageProfile>,
    pub excluded_parts: Vec<String>,
    #[serde(default)]
    pub include_patterns: Vec<String>,
    #[serde(default)]
    pub exclude_patterns: Vec<String>,
    #[serde(default)]
    pub ignore_patterns: Vec<String>,
    #[serde(default)]
    pub candidate_paths: Vec<String>,
    pub db_path: String,
    pub include_fts: bool,
    #[serde(default)]
    pub semantic_enrichment: bool,
    #[serde(default = "default_semantic_provider_mode")]
    pub semantic_provider_mode: String,
    #[serde(default)]
    pub schema_statements: Vec<String>,
    pub staging_dir: String,
    #[serde(default)]
    pub atomic_rebuild: bool,
    #[serde(default)]
    pub strict: bool,
    #[serde(default = "default_parallel")]
    pub parallel: bool,
    #[serde(default)]
    pub progress: bool,
}

fn default_semantic_provider_mode() -> String {
    "local_only".to_string()
}

fn default_parallel() -> bool {
    true
}

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct OntologySchema {
    #[serde(default)]
    pub relation_types: Vec<OntologyRelationType>,
}

#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct OntologyRelationType {
    #[serde(default)]
    pub name: String,
    #[serde(default)]
    pub source_types: Vec<String>,
    #[serde(default)]
    pub target_types: Vec<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NativeManifest {
    pub schema_version: u64,
    pub ontology: String,
    pub parser_version: String,
    #[serde(default, deserialize_with = "manifest_files_from_any")]
    pub files: BTreeMap<String, ManifestEntry>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ManifestEntry {
    pub path: String,
    pub content_hash: String,
    pub language: String,
    pub partition_id: String,
    #[serde(default)]
    pub node_ids: Vec<String>,
    #[serde(default)]
    pub edge_ids: Vec<String>,
    #[serde(default)]
    pub node_types: BTreeMap<String, String>,
    #[serde(default)]
    pub edge_types: BTreeMap<String, String>,
    #[serde(default)]
    pub materialized_at: String,
}

#[derive(Debug, Clone, Deserialize)]
pub struct LanguageProfile {
    pub language: String,
    #[serde(default)]
    pub suffixes: Vec<String>,
    #[serde(default)]
    pub grammar_package: String,
    #[serde(default)]
    pub root_node_types: Vec<String>,
    #[serde(default)]
    pub capture_mappings: Vec<CaptureMapping>,
}

#[derive(Debug, Clone, Deserialize)]
pub struct CaptureMapping {
    pub capture_name: String,
    #[serde(default)]
    pub parser_node_types: Vec<String>,
    pub target_node_type: String,
    #[serde(default)]
    pub relation_types: Vec<String>,
    #[serde(default)]
    pub context_rule: String,
    #[serde(default)]
    pub construct: String,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SourceSnapshot {
    pub path: String,
    pub absolute_path: String,
    pub content_hash: String,
    pub language: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ManifestDiff {
    pub added: Vec<String>,
    pub modified: Vec<String>,
    pub unchanged: Vec<String>,
    pub deleted: Vec<String>,
    pub force_rebuild: bool,
}

impl ManifestDiff {
    pub fn rebuild_paths(&self) -> Vec<String> {
        let mut paths = self.added.clone();
        paths.extend(self.modified.clone());
        paths.sort();
        paths
    }
}

#[derive(Debug, Clone, Serialize)]
pub struct NativeSyntaxMaterializationResponse {
    pub snapshots: BTreeMap<String, SourceSnapshot>,
    pub diff: ManifestDiff,
    pub diagnostics: Vec<String>,
    pub rebuilt_entries: BTreeMap<String, ManifestEntry>,
    pub copy_statements: Vec<String>,
    pub node_rows: usize,
    pub edge_rows: usize,
    pub connector_rows: usize,
    pub copy_calls: usize,
    pub graph_summary: GraphSummary,
    pub progress_events: Vec<ProgressEvent>,
    pub phase_timings: BTreeMap<String, f64>,
    pub skipped: bool,
    pub database_written: bool,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct GraphSummary {
    pub node_count: usize,
    pub edge_count: usize,
}

#[derive(Debug, Clone, Serialize)]
pub struct ProgressEvent {
    pub phase: String,
    pub current: usize,
    pub total: usize,
    pub path: Option<String>,
}

impl NativeSyntaxMaterializationResponse {
    pub fn skipped(
        snapshots: BTreeMap<String, SourceSnapshot>,
        diff: ManifestDiff,
        diagnostics: Vec<String>,
        progress_events: Vec<ProgressEvent>,
        phase_timings: BTreeMap<String, f64>,
    ) -> Self {
        Self {
            snapshots,
            diff,
            diagnostics,
            rebuilt_entries: BTreeMap::new(),
            copy_statements: Vec::new(),
            node_rows: 0,
            edge_rows: 0,
            connector_rows: 0,
            copy_calls: 0,
            graph_summary: GraphSummary::default(),
            progress_events,
            phase_timings,
            skipped: true,
            database_written: false,
        }
    }

    pub(crate) fn from_parts(
        snapshots: BTreeMap<String, SourceSnapshot>,
        diff: ManifestDiff,
        diagnostics: Vec<String>,
        rebuilt_entries: BTreeMap<String, ManifestEntry>,
        graph_summary: GraphSummary,
        staging: crate::staging_writer::StagingResult,
        phase_timings: BTreeMap<String, f64>,
    ) -> Self {
        Self {
            snapshots,
            diff,
            diagnostics,
            rebuilt_entries,
            copy_statements: staging.copy_statements,
            node_rows: staging.node_rows,
            edge_rows: staging.edge_rows,
            connector_rows: staging.connector_rows,
            copy_calls: staging.copy_calls,
            graph_summary,
            progress_events: Vec::new(),
            phase_timings,
            skipped: false,
            database_written: false,
        }
    }

    pub(crate) fn add_phase_timing(&mut self, phase: &str, seconds: f64) {
        self.phase_timings.insert(phase.to_string(), seconds);
    }
}

fn manifest_files_from_any<'de, D>(
    deserializer: D,
) -> Result<BTreeMap<String, ManifestEntry>, D::Error>
where
    D: Deserializer<'de>,
{
    let value = Value::deserialize(deserializer)?;
    match value {
        Value::Null => Ok(BTreeMap::new()),
        Value::Array(items) => {
            let mut files = BTreeMap::new();
            for item in items {
                let entry = ManifestEntry::deserialize(item).map_err(D::Error::custom)?;
                files.insert(entry.path.clone(), entry);
            }
            Ok(files)
        }
        Value::Object(values) => values
            .into_iter()
            .map(|(path, value)| {
                let mut entry = ManifestEntry::deserialize(value).map_err(D::Error::custom)?;
                if entry.path.is_empty() {
                    entry.path = path.clone();
                }
                Ok((path, entry))
            })
            .collect(),
        _ => Err(D::Error::custom("manifest files must be a list or object")),
    }
}

#[cfg(test)]
mod tests {
    use super::NativeSyntaxMaterializationRequest;

    fn request_json(parallel_field: &str) -> String {
        format!(
            r#"{{
  "source_root": "/repo",
  "repository_label": "repo",
  "mode": "changed",
  "parser_version": "native-test",
  "manifest_schema_version": 1,
  "ontology": "code_ontology_v1",
  "profiles": [],
  "excluded_parts": [],
  "db_path": "/repo/.codebaseGraph/graph.lbug",
  "include_fts": true,
  "staging_dir": "/repo/.codebaseGraph/native-staging"{parallel_field}
}}"#
        )
    }

    #[test]
    fn native_materialization_request_defaults_parallel_to_true() {
        let request: NativeSyntaxMaterializationRequest =
            serde_json::from_str(&request_json("")).unwrap();

        assert!(request.parallel);
    }

    #[test]
    fn native_materialization_request_preserves_explicit_parallel_false() {
        let request: NativeSyntaxMaterializationRequest =
            serde_json::from_str(&request_json(r#", "parallel": false"#)).unwrap();

        assert!(!request.parallel);
    }
}