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}