Skip to main content

gobby_code/commands/
index.rs

1use crate::config;
2use crate::config::Context;
3use crate::index::api::{self, IndexDegradation, IndexOutcome, IndexRequest};
4use crate::output::{self, Format};
5use crate::projection::sync::{self, ProjectionSyncReports};
6use crate::utils::short_id;
7use serde::Serialize;
8
9pub fn run(
10    ctx: &Context,
11    path: Option<String>,
12    files: Option<Vec<String>>,
13    full: bool,
14    require_cpp_semantics: bool,
15    sync_projections: bool,
16    format: Format,
17) -> anyhow::Result<()> {
18    let (target_ctx, path_filter) = resolve_index_context(ctx, path.as_deref())?;
19    let explicit_files: Vec<std::path::PathBuf> = files
20        .unwrap_or_default()
21        .into_iter()
22        .map(std::path::PathBuf::from)
23        .collect();
24    let request = IndexRequest {
25        project_root: target_ctx.project_root.clone(),
26        path_filter: if explicit_files.is_empty() {
27            path_filter
28        } else {
29            None
30        },
31        explicit_files,
32        full,
33        require_cpp_semantics,
34        sync_projections,
35    };
36
37    let outcome = api::index_files(request, &target_ctx)?;
38    if sync_projections {
39        let projections = sync::sync_after_index(&target_ctx, &outcome.indexed_file_paths)?;
40        let payload = sync_projections_payload(&outcome, projections);
41        return match format {
42            Format::Json => output::print_json(&payload),
43            Format::Text => output::print_text(&sync_projections_text(&payload)?),
44        };
45    }
46
47    match format {
48        Format::Json => output::print_json(&outcome),
49        Format::Text => output::print_text(&format!(
50            "Indexed {} files ({} skipped), {} symbols, {} chunks in {}ms",
51            outcome.indexed_files,
52            outcome.skipped_files,
53            outcome.symbols_indexed,
54            outcome.chunks_indexed,
55            outcome.durations.total_ms
56        )),
57    }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
61pub(crate) struct IndexSyncProjectionsOutput {
62    pub indexed_files: usize,
63    pub skipped_files: usize,
64    pub symbols_indexed: usize,
65    pub chunks_indexed: usize,
66    #[serde(default, skip_serializing_if = "Vec::is_empty")]
67    pub degraded: Vec<IndexDegradation>,
68    pub projections: ProjectionSyncReports,
69}
70
71pub(crate) fn sync_projections_payload(
72    outcome: &IndexOutcome,
73    projections: ProjectionSyncReports,
74) -> IndexSyncProjectionsOutput {
75    IndexSyncProjectionsOutput {
76        indexed_files: outcome.indexed_files,
77        skipped_files: outcome.skipped_files,
78        symbols_indexed: outcome.symbols_indexed,
79        chunks_indexed: outcome.chunks_indexed,
80        degraded: outcome.degraded.clone(),
81        projections,
82    }
83}
84
85pub(crate) fn sync_projections_text(
86    payload: &IndexSyncProjectionsOutput,
87) -> anyhow::Result<String> {
88    Ok(serde_json::to_string(payload)?)
89}
90
91fn resolve_index_context(
92    ctx: &Context,
93    path: Option<&str>,
94) -> anyhow::Result<(Context, Option<std::path::PathBuf>)> {
95    let Some(p) = path else {
96        return Ok((
97            clone_context(ctx, ctx.project_root.clone(), ctx.project_id.clone()),
98            None,
99        ));
100    };
101
102    // Resolve root and project_id. If the path belongs to a different project
103    // than the CWD-derived context, re-resolve identity for that project.
104    let target = std::path::PathBuf::from(p);
105    let target_root = crate::config::detect_project_root_from(&target)?;
106    let target_filter = path_filter_for(&target_root, &target);
107    if target_root != ctx.project_root {
108        let identity = crate::config::resolve_project_identity(
109            &target_root,
110            crate::config::MissingIdentity::Generate,
111        )?;
112        crate::config::warn_project_identity(&identity, ctx.quiet);
113        if !ctx.quiet {
114            eprintln!(
115                "Warning: path '{}' belongs to project {} (not {}), re-resolving context",
116                p,
117                short_id(&identity.project_id),
118                short_id(&ctx.project_id)
119            );
120        }
121        if identity.should_write_gcode_json {
122            crate::project::ensure_gcode_json(&target_root)?;
123        }
124        Ok((
125            clone_context(ctx, target_root, identity.project_id),
126            target_filter,
127        ))
128    } else {
129        Ok((
130            clone_context(ctx, target_root, ctx.project_id.clone()),
131            target_filter,
132        ))
133    }
134}
135
136fn clone_context(ctx: &Context, project_root: std::path::PathBuf, project_id: String) -> Context {
137    config::Context {
138        database_url: ctx.database_url.clone(),
139        project_root,
140        project_id,
141        quiet: ctx.quiet,
142        falkordb: ctx.falkordb.clone(),
143        qdrant: ctx.qdrant.clone(),
144        embedding: ctx.embedding.clone(),
145        code_vectors: ctx.code_vectors.clone(),
146        daemon_url: ctx.daemon_url.clone(),
147    }
148}
149
150fn path_filter_for(
151    project_root: &std::path::Path,
152    target: &std::path::Path,
153) -> Option<std::path::PathBuf> {
154    let target_abs = if target.is_absolute() {
155        target.to_path_buf()
156    } else {
157        std::env::current_dir()
158            .map(|cwd| cwd.join(target))
159            .unwrap_or_else(|_| project_root.join(target))
160    };
161
162    let root_abs = project_root
163        .canonicalize()
164        .unwrap_or_else(|_| project_root.to_path_buf());
165    let target_abs = target_abs.canonicalize().unwrap_or(target_abs);
166
167    if target_abs == root_abs {
168        None
169    } else {
170        Some(target_abs)
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::index::api::IndexOutcome;
178    use crate::projection::sync::{
179        ProjectionStatus, ProjectionSyncError, ProjectionSyncReport, ProjectionSyncReports,
180    };
181    use serde_json::{Value, json};
182
183    fn sample_outcome() -> IndexOutcome {
184        IndexOutcome {
185            indexed_files: 12,
186            skipped_files: 0,
187            symbols_indexed: 348,
188            chunks_indexed: 921,
189            ..IndexOutcome::default()
190        }
191    }
192
193    fn sample_reports() -> ProjectionSyncReports {
194        ProjectionSyncReports {
195            graph: ProjectionSyncReport {
196                status: ProjectionStatus::Ok,
197                synced_files: 12,
198                synced_symbols: 348,
199                degraded: false,
200                error: None,
201            },
202            vector: ProjectionSyncReport {
203                status: ProjectionStatus::Degraded,
204                synced_files: 0,
205                synced_symbols: 0,
206                degraded: true,
207                error: Some(ProjectionSyncError {
208                    kind: "missing_qdrant_config".to_string(),
209                    message: "Qdrant config is required".to_string(),
210                }),
211            },
212        }
213    }
214
215    #[test]
216    fn sync_projections_json_contract() {
217        let payload = sync_projections_payload(&sample_outcome(), sample_reports());
218        assert_eq!(
219            serde_json::to_value(&payload).expect("payload serializes"),
220            json!({
221                "indexed_files": 12,
222                "skipped_files": 0,
223                "symbols_indexed": 348,
224                "chunks_indexed": 921,
225                "projections": {
226                    "graph": {
227                        "status": "ok",
228                        "synced_files": 12,
229                        "synced_symbols": 348,
230                        "degraded": false,
231                        "error": null
232                    },
233                    "vector": {
234                        "status": "degraded",
235                        "synced_files": 0,
236                        "synced_symbols": 0,
237                        "degraded": true,
238                        "error": {
239                            "kind": "missing_qdrant_config",
240                            "message": "Qdrant config is required"
241                        }
242                    }
243                }
244            })
245        );
246    }
247
248    #[test]
249    fn sync_projections_text_contract() {
250        let payload = sync_projections_payload(&sample_outcome(), sample_reports());
251        let text = sync_projections_text(&payload).expect("text payload");
252        let parsed: Value = serde_json::from_str(&text).expect("text mode is structured JSON");
253        assert_eq!(parsed["indexed_files"], 12);
254        assert_eq!(parsed["projections"]["graph"]["status"], "ok");
255        assert_eq!(parsed["projections"]["vector"]["status"], "degraded");
256    }
257}