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 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}