Skip to main content

relay_knowledge/application/code_repository/query/
mod.rs

1//! Coordinates repository retrieval and feature-flag query workflows.
2
3use crate::{
4    api::{
5        ApiError, ApiMetadata, CodeRepositoryFeatureFlagsResponse,
6        CodeRepositoryFrameworkGraphResponse, CodeRepositoryQueryResponse, RequestContext,
7    },
8    domain::{
9        CodeFeatureFlagRequest, CodeRetrievalRequest, FrameworkGraph, FrameworkGraphRequest,
10        FreshnessPolicy,
11    },
12};
13
14use crate::application::service::RelayKnowledgeService;
15
16use super::{
17    errors::storage_api_error,
18    freshness::{
19        CodeFeatureFlagFreshnessContext, CodeQueryFreshnessContext, FrameworkGraphFreshnessContext,
20        code_feature_flag_freshness_diagnostics, code_query_freshness_diagnostics,
21        framework_graph_freshness_diagnostics,
22    },
23    repository::{
24        annotate_query_result_staleness, ensure_worktree_overlay_matches_current_worktree,
25        required_code_repository,
26    },
27    scope::{
28        active_index_matches_request, feature_flag_request_at_indexed_ref,
29        framework_graph_request_at_indexed_ref, indexed_source_scope,
30        latest_compatible_code_scope_status, missing_indexed_source_scope_error,
31        resolved_code_scope_status, retrieval_request_at_indexed_ref,
32    },
33    source_fallback::apply_code_grep_fallback,
34};
35
36impl RelayKnowledgeService {
37    /// Queries indexed symbols, references, imports, calls, and code chunks.
38    pub async fn query_code_repository(
39        &self,
40        request: CodeRetrievalRequest,
41        context: RequestContext,
42    ) -> Result<CodeRepositoryQueryResponse, ApiError> {
43        let store = self.store().await.map_err(storage_api_error)?;
44        let status =
45            required_code_repository(store.as_ref(), &request.repository.repository).await?;
46        if request.freshness_policy == FreshnessPolicy::GraphOnly {
47            let graph_version = store
48                .current_graph_version()
49                .await
50                .map_err(storage_api_error)?;
51            let degraded_reason = "graph_only freshness policy selected".to_owned();
52            return Ok(CodeRepositoryQueryResponse {
53                metadata: ApiMetadata::graph_only(&context, graph_version),
54                scope: crate::api::CodeRepositoryScopeMetadata::from_status(
55                    &status,
56                    &request.repository,
57                    request.repository.ref_selector.clone(),
58                ),
59                freshness: crate::api::CodeRepositoryFreshnessDiagnostics::graph_only(
60                    graph_version.get(),
61                    request.freshness_policy,
62                    indexed_source_scope(&status),
63                    request.repository.ref_selector.clone(),
64                    degraded_reason.clone(),
65                ),
66                request,
67                results: Vec::new(),
68                degraded_reason: Some(degraded_reason),
69            });
70        }
71        let requested_ref = request.repository.ref_selector.clone();
72        let mut request = retrieval_request_at_indexed_ref(request, &status).await?;
73        if requested_ref == "worktree" {
74            ensure_worktree_overlay_matches_current_worktree(&store, &status, &request.repository)
75                .await?;
76        }
77        let requested_resolved_ref = request.repository.ref_selector.clone();
78        let freshness_target = request.repository.clone();
79        let mut served_stale_scope = false;
80        let mut stale_reason = None;
81        let scoped_status = match resolved_code_scope_status(&store, &status, &request.repository)
82            .await
83        {
84            Ok(scoped_status) => scoped_status,
85            Err(error) if request.freshness_policy == FreshnessPolicy::AllowStale => {
86                if !active_index_matches_request(&store, &status, &request.repository).await? {
87                    return Err(error);
88                }
89                let Some(stale_status) =
90                    latest_compatible_code_scope_status(&store, &request.repository).await?
91                else {
92                    return Err(error);
93                };
94                let Some(last_indexed_commit) = stale_status.last_indexed_commit.clone() else {
95                    return Err(error);
96                };
97                request.repository.ref_selector = last_indexed_commit;
98                served_stale_scope = true;
99                stale_reason = Some(
100                    "requested ref is not indexed yet; served last completed code index".to_owned(),
101                );
102                stale_status
103            }
104            Err(error) => return Err(error),
105        };
106        if request.freshness_policy == FreshnessPolicy::WaitUntilFresh && scoped_status.stale {
107            return Err(ApiError::invalid_argument(format!(
108                "code repository '{}' scope '{}' is stale; run repo index or repo update before querying with wait_until_fresh",
109                scoped_status.alias,
110                scoped_status
111                    .last_indexed_scope_id
112                    .as_deref()
113                    .unwrap_or("unscoped")
114            )));
115        }
116        let graph_version = store
117            .current_graph_version()
118            .await
119            .map_err(storage_api_error)?;
120        let source_scope = indexed_source_scope(&scoped_status)
121            .ok_or_else(|| missing_indexed_source_scope_error(&scoped_status))?;
122        let mut results = store
123            .search_code_scope(source_scope, request.clone())
124            .await
125            .map_err(storage_api_error)?;
126        let fallback_degraded_reason =
127            apply_code_grep_fallback(&store, &status, &scoped_status, &request, &mut results)
128                .await?;
129        let degraded_reason = results
130            .iter()
131            .find_map(|hit| hit.degraded_reason.clone())
132            .or(fallback_degraded_reason)
133            .or_else(|| scoped_status.degraded_reason.clone())
134            .or_else(|| stale_reason.clone());
135        let mut scope = crate::api::CodeRepositoryScopeMetadata::from_status(
136            &scoped_status,
137            &request.repository,
138            requested_ref.clone(),
139        );
140        if served_stale_scope {
141            scope.stale = true;
142        }
143        let mut metadata = ApiMetadata::graph_only(&context, graph_version);
144        if served_stale_scope {
145            metadata.stale = true;
146        }
147        let freshness = code_query_freshness_diagnostics(
148            &store,
149            CodeQueryFreshnessContext {
150                base_status: &status,
151                scoped_status: &scoped_status,
152                request: &request,
153                requested_ref,
154                requested_resolved_ref,
155                freshness_target,
156                stale_reason,
157                degraded_reason: degraded_reason.clone(),
158                results: &results,
159                graph_version: graph_version.get(),
160            },
161        )
162        .await?;
163        annotate_query_result_staleness(&mut results, &freshness);
164
165        Ok(CodeRepositoryQueryResponse {
166            metadata,
167            scope,
168            freshness,
169            request,
170            results,
171            degraded_reason,
172        })
173    }
174
175    /// Lists configuration-driven feature flags and their code graph relationships.
176    pub async fn query_code_repository_feature_flags(
177        &self,
178        request: CodeFeatureFlagRequest,
179        context: RequestContext,
180    ) -> Result<CodeRepositoryFeatureFlagsResponse, ApiError> {
181        let store = self.store().await.map_err(storage_api_error)?;
182        let status =
183            required_code_repository(store.as_ref(), &request.repository.repository).await?;
184        if request.freshness_policy == FreshnessPolicy::GraphOnly {
185            let graph_version = store
186                .current_graph_version()
187                .await
188                .map_err(storage_api_error)?;
189            let degraded_reason = "graph_only freshness policy selected".to_owned();
190            return Ok(CodeRepositoryFeatureFlagsResponse {
191                metadata: ApiMetadata::graph_only(&context, graph_version),
192                scope: crate::api::CodeRepositoryScopeMetadata::from_status(
193                    &status,
194                    &request.repository,
195                    request.repository.ref_selector.clone(),
196                ),
197                freshness: crate::api::CodeRepositoryFreshnessDiagnostics::graph_only(
198                    graph_version.get(),
199                    request.freshness_policy,
200                    indexed_source_scope(&status),
201                    request.repository.ref_selector.clone(),
202                    degraded_reason.clone(),
203                ),
204                request,
205                flags: Vec::new(),
206                degraded_reason: Some(degraded_reason),
207            });
208        }
209        let requested_ref = request.repository.ref_selector.clone();
210        let mut request = feature_flag_request_at_indexed_ref(request, &status).await?;
211        if requested_ref == "worktree" {
212            ensure_worktree_overlay_matches_current_worktree(&store, &status, &request.repository)
213                .await?;
214        }
215        let requested_resolved_ref = request.repository.ref_selector.clone();
216        let freshness_target = request.repository.clone();
217        let mut served_stale_scope = false;
218        let mut stale_reason = None;
219        let scoped_status = match resolved_code_scope_status(&store, &status, &request.repository)
220            .await
221        {
222            Ok(scoped_status) => scoped_status,
223            Err(error) if request.freshness_policy == FreshnessPolicy::AllowStale => {
224                if !active_index_matches_request(&store, &status, &request.repository).await? {
225                    return Err(error);
226                }
227                let Some(stale_status) =
228                    latest_compatible_code_scope_status(&store, &request.repository).await?
229                else {
230                    return Err(error);
231                };
232                let Some(last_indexed_commit) = stale_status.last_indexed_commit.clone() else {
233                    return Err(error);
234                };
235                request.repository.ref_selector = last_indexed_commit;
236                served_stale_scope = true;
237                stale_reason = Some(
238                    "requested ref is not indexed yet; served last completed code index".to_owned(),
239                );
240                stale_status
241            }
242            Err(error) => return Err(error),
243        };
244        if request.freshness_policy == FreshnessPolicy::WaitUntilFresh && scoped_status.stale {
245            return Err(ApiError::invalid_argument(format!(
246                "code repository '{}' scope '{}' is stale; run repo index or repo update before querying feature flags with wait_until_fresh",
247                scoped_status.alias,
248                scoped_status
249                    .last_indexed_scope_id
250                    .as_deref()
251                    .unwrap_or("unscoped")
252            )));
253        }
254        let graph_version = store
255            .current_graph_version()
256            .await
257            .map_err(storage_api_error)?;
258        let source_scope = indexed_source_scope(&scoped_status)
259            .ok_or_else(|| missing_indexed_source_scope_error(&scoped_status))?;
260        let flags = store
261            .search_code_feature_flags_scope(source_scope, request.clone())
262            .await
263            .map_err(storage_api_error)?;
264        let mut scope = crate::api::CodeRepositoryScopeMetadata::from_status(
265            &scoped_status,
266            &request.repository,
267            requested_ref.clone(),
268        );
269        if served_stale_scope {
270            scope.stale = true;
271        }
272        let degraded_reason = scoped_status
273            .degraded_reason
274            .clone()
275            .or_else(|| stale_reason.clone());
276        let mut metadata = ApiMetadata::graph_only(&context, graph_version);
277        if served_stale_scope {
278            metadata.stale = true;
279        }
280        let freshness = code_feature_flag_freshness_diagnostics(
281            &store,
282            CodeFeatureFlagFreshnessContext {
283                base_status: &status,
284                scoped_status: &scoped_status,
285                request: &request,
286                requested_ref,
287                requested_resolved_ref,
288                freshness_target,
289                stale_reason,
290                degraded_reason: degraded_reason.clone(),
291                flags: &flags,
292                graph_version: graph_version.get(),
293            },
294        )
295        .await?;
296
297        Ok(CodeRepositoryFeatureFlagsResponse {
298            metadata,
299            scope,
300            freshness,
301            request,
302            flags,
303            degraded_reason,
304        })
305    }
306
307    /// Reads the bounded Angular/Vue component and template graph for an indexed scope.
308    pub async fn query_code_repository_framework_graph(
309        &self,
310        request: FrameworkGraphRequest,
311        context: RequestContext,
312    ) -> Result<CodeRepositoryFrameworkGraphResponse, ApiError> {
313        let store = self.store().await.map_err(storage_api_error)?;
314        let status =
315            required_code_repository(store.as_ref(), &request.repository.repository).await?;
316        if request.freshness_policy == FreshnessPolicy::GraphOnly {
317            let graph_version = store
318                .current_graph_version()
319                .await
320                .map_err(storage_api_error)?;
321            let degraded_reason = "graph_only freshness policy selected".to_owned();
322            return Ok(CodeRepositoryFrameworkGraphResponse {
323                metadata: ApiMetadata::graph_only(&context, graph_version),
324                scope: crate::api::CodeRepositoryScopeMetadata::from_status(
325                    &status,
326                    &request.repository,
327                    request.repository.ref_selector.clone(),
328                ),
329                freshness: crate::api::CodeRepositoryFreshnessDiagnostics::graph_only(
330                    graph_version.get(),
331                    request.freshness_policy,
332                    indexed_source_scope(&status),
333                    request.repository.ref_selector.clone(),
334                    degraded_reason.clone(),
335                ),
336                request,
337                graph: FrameworkGraph {
338                    nodes: Vec::new(),
339                    edges: Vec::new(),
340                    truncated: false,
341                },
342                degraded_reason: Some(degraded_reason),
343            });
344        }
345        let requested_ref = request.repository.ref_selector.clone();
346        let mut request = framework_graph_request_at_indexed_ref(request, &status).await?;
347        if requested_ref == "worktree" {
348            ensure_worktree_overlay_matches_current_worktree(&store, &status, &request.repository)
349                .await?;
350        }
351        let requested_resolved_ref = request.repository.ref_selector.clone();
352        let freshness_target = request.repository.clone();
353        let mut served_stale_scope = false;
354        let mut stale_reason = None;
355        let scoped_status = match resolved_code_scope_status(&store, &status, &request.repository)
356            .await
357        {
358            Ok(scoped_status) => scoped_status,
359            Err(error) if request.freshness_policy == FreshnessPolicy::AllowStale => {
360                if !active_index_matches_request(&store, &status, &request.repository).await? {
361                    return Err(error);
362                }
363                let Some(stale_status) =
364                    latest_compatible_code_scope_status(&store, &request.repository).await?
365                else {
366                    return Err(error);
367                };
368                let Some(last_indexed_commit) = stale_status.last_indexed_commit.clone() else {
369                    return Err(error);
370                };
371                request.repository.ref_selector = last_indexed_commit;
372                served_stale_scope = true;
373                stale_reason = Some(
374                    "requested ref is not indexed yet; served last completed code index".to_owned(),
375                );
376                stale_status
377            }
378            Err(error) => return Err(error),
379        };
380        if request.freshness_policy == FreshnessPolicy::WaitUntilFresh && scoped_status.stale {
381            return Err(ApiError::invalid_argument(format!(
382                "code repository '{}' scope '{}' is stale; run repo index or repo update before querying framework facts with wait_until_fresh",
383                scoped_status.alias,
384                scoped_status
385                    .last_indexed_scope_id
386                    .as_deref()
387                    .unwrap_or("unscoped")
388            )));
389        }
390        let graph_version = store
391            .current_graph_version()
392            .await
393            .map_err(storage_api_error)?;
394        let source_scope = indexed_source_scope(&scoped_status)
395            .ok_or_else(|| missing_indexed_source_scope_error(&scoped_status))?;
396        let graph = store
397            .search_framework_graph_scope(source_scope, request.clone())
398            .await
399            .map_err(storage_api_error)?;
400        let mut scope = crate::api::CodeRepositoryScopeMetadata::from_status(
401            &scoped_status,
402            &request.repository,
403            requested_ref.clone(),
404        );
405        if served_stale_scope {
406            scope.stale = true;
407        }
408        let degraded_reason = scoped_status
409            .degraded_reason
410            .clone()
411            .or_else(|| stale_reason.clone());
412        let mut metadata = ApiMetadata::graph_only(&context, graph_version);
413        if served_stale_scope {
414            metadata.stale = true;
415        }
416        let freshness = framework_graph_freshness_diagnostics(
417            &store,
418            FrameworkGraphFreshnessContext {
419                base_status: &status,
420                scoped_status: &scoped_status,
421                request: &request,
422                requested_ref,
423                requested_resolved_ref,
424                freshness_target,
425                stale_reason,
426                degraded_reason: degraded_reason.clone(),
427                graph: &graph,
428                graph_version: graph_version.get(),
429            },
430        )
431        .await?;
432
433        Ok(CodeRepositoryFrameworkGraphResponse {
434            metadata,
435            scope,
436            freshness,
437            request,
438            graph,
439            degraded_reason,
440        })
441    }
442}