Skip to main content

sqry_cli/commands/graph/
loader.rs

1//! Workspace graph loader for CLI graph commands.
2//!
3//! This module loads a unified `CodeGraph` either from a persisted snapshot or
4//! by invoking the core `build_unified_graph` entrypoint with the shared plugin
5//! registry.
6
7use crate::args::Cli;
8use crate::plugin_defaults::{self, PluginSelectionMode};
9use anyhow::{Context, Result, bail};
10use sqry_core::graph::CodeGraph;
11use sqry_core::graph::unified::build::{BuildConfig, build_unified_graph_with_progress};
12use sqry_core::graph::unified::persistence::{GraphStorage, load_from_path};
13// Re-export `no_op_reporter` so callers that want the silent default can
14// pull it from the same import line as `load_unified_graph_for_cli` rather
15// than reaching into `sqry_core::progress` directly.
16pub use sqry_core::progress::no_op_reporter;
17use sqry_core::progress::{ProgressStage, SharedReporter};
18use std::path::Path;
19
20/// Loader configuration derived from CLI flags.
21#[derive(Debug, Clone, Default)]
22pub struct GraphLoadConfig {
23    pub include_hidden: bool,
24    pub follow_symlinks: bool,
25    pub max_depth: Option<usize>,
26    /// Force building from source files, even if a snapshot exists.
27    /// Used by the index command to always rebuild.
28    pub force_build: bool,
29}
30
31/// Load a unified code graph using the new Arena+CSR storage architecture.
32///
33/// This is the preferred entry point for CLI graph operations. It loads a graph
34/// either from a persisted snapshot or by building from source files.
35///
36/// # Loading Strategy
37///
38/// 1. First tries to load from persisted snapshot (`.sqry/graph/snapshot.sqry`)
39/// 2. If no snapshot exists, builds from source files using language plugins
40///
41/// # Arguments
42/// * `root` - Root directory to scan for source files
43/// * `config` - Configuration for file walking (hidden files, symlinks, depth)
44///
45/// # Returns
46/// A `CodeGraph` populated with nodes and edges from all supported languages
47///
48/// # Errors
49/// Returns an error if the path is missing, the snapshot is invalid, or the graph build fails.
50///
51/// # Example
52/// ```ignore
53/// use std::path::Path;
54/// use sqry_cli::commands::graph::loader::{load_unified_graph, GraphLoadConfig};
55///
56/// let config = GraphLoadConfig::default();
57/// let graph = load_unified_graph(Path::new("."), &config)?;
58/// # Ok::<(), anyhow::Error>(())
59/// ```
60#[allow(dead_code)]
61pub fn load_unified_graph(root: &Path, config: &GraphLoadConfig) -> Result<CodeGraph> {
62    load_unified_graph_with_progress_and_plugins(
63        root,
64        config,
65        &sqry_plugin_registry::create_plugin_manager_all(),
66        no_op_reporter(),
67    )
68}
69
70/// CLI-aware graph loader that enforces manifest-backed plugin semantics.
71///
72/// `progress` selects the reporter for the snapshot-load and source-build
73/// passes. Non-search subcommands pass [`no_op_reporter`] for the pre-#238
74/// silent default; the search path passes
75/// [`crate::progress::PlainProgressReporter::for_search`] so `--verbose` /
76/// `SQRY_LOG=info` surface stage events.
77///
78/// # Errors
79///
80/// Returns an error if the workspace path is invalid, manifest-selected plugins
81/// cannot load the persisted snapshot, or the fallback source build fails.
82pub fn load_unified_graph_for_cli(
83    root: &Path,
84    config: &GraphLoadConfig,
85    cli: &Cli,
86    progress: SharedReporter,
87) -> Result<CodeGraph> {
88    let resolved_plugins =
89        plugin_defaults::resolve_plugin_selection(cli, root, PluginSelectionMode::ReadOnly)?;
90    load_unified_graph_with_progress_and_plugins(
91        root,
92        config,
93        &resolved_plugins.plugin_manager,
94        progress,
95    )
96}
97
98/// Load a unified code graph with progress reporting.
99///
100/// Same as [`load_unified_graph`] but accepts a progress reporter for tracking
101/// build progress when loading from source files.
102///
103/// # Arguments
104/// * `root` - Root directory to scan for source files
105/// * `config` - Configuration for file walking (hidden files, symlinks, depth)
106/// * `progress` - Progress reporter for build status updates
107///
108/// # Returns
109/// A `CodeGraph` populated with nodes and edges from all supported languages
110///
111/// # Errors
112/// Returns an error if the path is missing, the snapshot is invalid, or the graph build fails.
113#[allow(dead_code)]
114pub fn load_unified_graph_with_progress(
115    root: &Path,
116    config: &GraphLoadConfig,
117    progress: SharedReporter,
118) -> Result<CodeGraph> {
119    load_unified_graph_with_progress_and_plugins(
120        root,
121        config,
122        &sqry_plugin_registry::create_plugin_manager_all(),
123        progress,
124    )
125}
126
127fn load_unified_graph_with_progress_and_plugins(
128    root: &Path,
129    config: &GraphLoadConfig,
130    plugins: &sqry_core::plugin::PluginManager,
131    progress: SharedReporter,
132) -> Result<CodeGraph> {
133    if !root.exists() {
134        bail!("Path {} does not exist", root.display());
135    }
136
137    // Try to load from persisted snapshot first (unless force_build is set)
138    if config.force_build {
139        log::info!("Force build enabled, skipping snapshot load");
140    } else {
141        let storage = GraphStorage::new(root);
142        if storage.exists() {
143            log::info!(
144                "Loading unified graph from snapshot: {}",
145                storage.snapshot_path().display()
146            );
147            // Bracket the snapshot load with a stage event so verbose mode
148            // can show "[sqry] load snapshot ... / complete in N ms". Pre-#238
149            // this path was only visible via log::info, which required both
150            // RUST_LOG=info AND a configured logger — invisible to most users.
151            let stage = ProgressStage::start(&progress, "load snapshot");
152            match load_from_path(storage.snapshot_path(), Some(plugins)) {
153                Ok(mut graph) => {
154                    stage.finish();
155                    log::info!("Loaded graph from snapshot");
156
157                    // Restore confidence metadata from manifest if available
158                    // The snapshot binary format doesn't include confidence,
159                    // so we load it separately from the manifest JSON.
160                    if let Ok(manifest) = storage.load_manifest()
161                        && !manifest.confidence.is_empty()
162                    {
163                        log::debug!(
164                            "Restoring confidence metadata for {} languages",
165                            manifest.confidence.len()
166                        );
167                        graph.set_confidence(manifest.confidence);
168                    }
169
170                    return Ok(graph);
171                }
172                Err(e) => {
173                    // Manifest present but snapshot missing/corrupt → corruption.
174                    // Finish the stage before bailing so verbose mode emits
175                    // a matched `[sqry] load snapshot complete in N` pair —
176                    // an orphan `... ...` start without a completion would
177                    // break the stream contract documented in the
178                    // PlainProgressReporter tests. Do not silently rebuild;
179                    // the user should run `sqry index --force`.
180                    stage.finish();
181                    bail!(
182                        "Index at {} is corrupted or incomplete ({}). Run `sqry index --force` to rebuild.",
183                        root.display(),
184                        e
185                    );
186                }
187            }
188        }
189    }
190
191    // Build from source files
192    log::info!(
193        "Building unified graph from source files in {}",
194        root.display()
195    );
196
197    let build_config = BuildConfig {
198        include_hidden: config.include_hidden,
199        follow_links: config.follow_symlinks,
200        max_depth: config.max_depth,
201        num_threads: None,
202        ..BuildConfig::default()
203    };
204
205    let (graph, _effective_threads) =
206        build_unified_graph_with_progress(root, plugins, &build_config, progress)
207            .context("Failed to build unified graph")?;
208
209    log::info!("Built unified graph with {} nodes", graph.node_count());
210    Ok(graph)
211}