codebase-graph 1.2.2

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use super::timing::elapsed_seconds;
use crate::artifact_store::{ArtifactExpectations, ArtifactStore};
use crate::error::NativeError;
use crate::parser;
use crate::partition_builder;
use crate::protocol::{LanguageProfile, NativeSyntaxMaterializationRequest, SourceSnapshot};
use crate::scan;
use std::thread;
use std::time::Instant;

pub(super) struct PartitionBuildResult {
    pub(super) partition: partition_builder::GraphPartition,
    pub(super) diagnostics: Vec<String>,
    pub(super) parse_seconds: f64,
    pub(super) graph_build_seconds: f64,
    pub(super) reused_artifact: bool,
}

pub(super) fn build_execution_plan(
    scan: &scan::SourceScan,
    rebuild_paths: &[String],
    artifact_store: &ArtifactStore,
) -> Result<Vec<PartitionBuildResult>, NativeError> {
    let request = &scan.input;
    if request.parallel && rebuild_paths.len() > 1 {
        return thread::scope(|scope| {
            let mut handles = Vec::new();
            for path in rebuild_paths {
                let Some(snapshot) = scan.supported.get(path) else {
                    continue;
                };
                let Some(language) = snapshot.language.as_deref() else {
                    continue;
                };
                let Some(profile) = scan
                    .profiles
                    .iter()
                    .find(|profile| profile.language == language)
                else {
                    continue;
                };
                let artifact_store = artifact_store.clone();
                let previous_entry = request
                    .previous_manifest
                    .as_ref()
                    .and_then(|manifest| manifest.files.get(path))
                    .cloned();
                handles.push(scope.spawn(move || {
                    build_partition_for_snapshot(
                        request,
                        snapshot,
                        profile,
                        previous_entry.as_ref(),
                        &artifact_store,
                    )
                }));
            }
            handles
                .into_iter()
                .map(|handle| {
                    handle.join().map_err(|_| {
                        NativeError::InvalidInput("parallel parser panicked".to_string())
                    })?
                })
                .collect::<Result<Vec<_>, NativeError>>()
        });
    }

    let mut results = Vec::new();
    for path in rebuild_paths {
        results.push(build_partition_for_path(scan, path, artifact_store)?);
    }
    Ok(results)
}

pub(super) fn build_partition_for_path(
    scan: &scan::SourceScan,
    path: &str,
    artifact_store: &ArtifactStore,
) -> Result<PartitionBuildResult, NativeError> {
    let Some(snapshot) = scan.supported.get(path) else {
        return Err(NativeError::InvalidInput(format!(
            "missing scanned source for artifact rebuild: {path}"
        )));
    };
    let Some(language) = snapshot.language.as_deref() else {
        return Err(NativeError::InvalidInput(format!(
            "missing language for artifact rebuild: {path}"
        )));
    };
    let Some(profile) = scan
        .profiles
        .iter()
        .find(|profile| profile.language == language)
    else {
        return Err(NativeError::InvalidInput(format!(
            "missing language profile for artifact rebuild: {path}"
        )));
    };

    let previous_entry = scan
        .input
        .previous_manifest
        .as_ref()
        .and_then(|manifest| manifest.files.get(path));
    build_partition_for_snapshot(
        &scan.input,
        snapshot,
        profile,
        previous_entry,
        artifact_store,
    )
}

fn build_partition_for_snapshot(
    request: &NativeSyntaxMaterializationRequest,
    snapshot: &SourceSnapshot,
    profile: &LanguageProfile,
    previous_entry: Option<&crate::protocol::ManifestEntry>,
    artifact_store: &ArtifactStore,
) -> Result<PartitionBuildResult, NativeError> {
    let artifact_key = ArtifactStore::key_for_request(request, snapshot, Some(profile))?;
    if previous_entry.and_then(|entry| entry.artifact_key.as_deref()) == Some(artifact_key.as_str())
    {
        if let Some(partition) = artifact_store.load_partition(
            &artifact_key,
            &ArtifactExpectations {
                path: &snapshot.path,
                content_hash: &snapshot.content_hash,
                language: snapshot.language.as_deref().unwrap_or_default(),
            },
        )? {
            return Ok(PartitionBuildResult {
                partition,
                diagnostics: Vec::new(),
                parse_seconds: 0.0,
                graph_build_seconds: 0.0,
                reused_artifact: true,
            });
        }
    }
    let parse_started = Instant::now();
    let parse = parser::parse_file(snapshot, profile)?;
    let parse_seconds = elapsed_seconds(parse_started);
    let diagnostics = parse.diagnostics.clone();
    let graph_build_started = Instant::now();
    let mut partition = partition_builder::build_partition(request, snapshot, parse)?;
    partition.set_artifact_key(artifact_key.clone());
    let graph_build_seconds = elapsed_seconds(graph_build_started);
    let _ = artifact_store.store_partition(&artifact_key, &partition)?;
    Ok(PartitionBuildResult {
        partition,
        diagnostics,
        parse_seconds,
        graph_build_seconds,
        reused_artifact: false,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::artifact_store::ArtifactStore;
    use crate::protocol::{NativeSyntaxMaterializationRequest, OntologySchema};
    use std::fs;
    use std::time::{SystemTime, UNIX_EPOCH};

    #[test]
    fn execution_plan_uses_scanned_source_after_repository_is_removed() {
        let nonce = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("clock should be after epoch")
            .as_nanos();
        let source_root =
            std::env::temp_dir().join(format!("codebase-graph-scanned-source-{nonce}"));
        fs::create_dir_all(source_root.join("src")).expect("source directory should be created");
        fs::write(
            source_root.join("src/lib.rs"),
            "pub fn scanned_source() -> bool { true }\n",
        )
        .expect("source file should be written");

        let request = NativeSyntaxMaterializationRequest {
            source_root: source_root.to_string_lossy().into_owned(),
            repository_label: "scanned-source".to_string(),
            mode: "full".to_string(),
            parser_version: "test".to_string(),
            manifest_schema_version: 1,
            ontology: "code_ontology_v1".to_string(),
            ontology_schema: OntologySchema::default(),
            previous_manifest: None,
            profiles: Vec::new(),
            excluded_parts: Vec::new(),
            include_patterns: Vec::new(),
            exclude_patterns: Vec::new(),
            ignore_patterns: Vec::new(),
            candidate_paths: Vec::new(),
            artifact_root: source_root
                .join(".codebaseGraph/artifacts")
                .to_string_lossy()
                .into_owned(),
            db_path: source_root.join("graph").to_string_lossy().into_owned(),
            include_fts: false,
            semantic_enrichment: false,
            semantic_provider_mode: "local_only".to_string(),
            schema_statements: Vec::new(),
            staging_dir: source_root.join("staging").to_string_lossy().into_owned(),
            atomic_rebuild: false,
            strict: true,
            parallel: false,
            progress: false,
        };
        let scan = crate::scan::scan_sources(&request).expect("scan should succeed");
        let rebuild_paths = scan.diff.rebuild_paths();
        let artifact_store = ArtifactStore::new(request.resolved_artifact_root());

        fs::remove_dir_all(&source_root).expect("source repository should be removed");

        let plan = build_execution_plan(&scan, &rebuild_paths, &artifact_store)
            .expect("planning should use the scanned source payload");
        assert_eq!(plan.len(), 1);
        assert_eq!(plan[0].partition.entry.path, "src/lib.rs");
    }
}