1use std::collections::BTreeSet;
2
3use crate::{
4 api::{
5 ApiError, ApiMetadata, CodeRepositoryFreshnessDiagnostics, CodeRepositoryFreshnessInput,
6 CodebaseViewResponse, RequestContext,
7 },
8 application::service::RelayKnowledgeService,
9 domain::{
10 BusinessKnowledgeQueryKind, BusinessKnowledgeQueryRequest, CodeRepositorySelector,
11 CodeRepositoryStatus, CodebaseViewDeclaredBusinessDomain, CodebaseViewKind,
12 CodebaseViewRequest, CodebaseViewSnapshot, FreshnessPolicy,
13 },
14};
15
16use super::super::{
17 errors::storage_api_error,
18 repository::{
19 code_status_checkpoint, ensure_worktree_overlay_matches_current_worktree,
20 required_code_repository,
21 },
22 scope::{
23 active_index_matches_request, indexed_commit_for_selector, indexed_source_scope,
24 latest_compatible_code_scope_status, missing_indexed_source_scope_error,
25 resolved_code_scope_status,
26 },
27};
28use super::{
29 affected_scope::derive_affected_scope,
30 architecture::derive_architecture_layers,
31 builder::{DerivedView, ViewBuilder},
32 business_domains::derive_business_domains,
33 dependency_tour::derive_dependency_tour,
34 process_flow::derive_process_flow,
35 rules::normalized_view_paths,
36};
37
38const SNAPSHOT_LIMIT_MULTIPLIER: usize = 20;
39const SNAPSHOT_LIMIT_MAX: usize = 2_000;
40
41impl RelayKnowledgeService {
42 pub async fn codebase_view(
44 &self,
45 request: CodebaseViewRequest,
46 context: RequestContext,
47 ) -> Result<CodebaseViewResponse, ApiError> {
48 let store = self.store().await.map_err(storage_api_error)?;
49 let status =
50 required_code_repository(store.as_ref(), &request.repository.repository).await?;
51 let requested_ref = request.repository.ref_selector.clone();
52 let mut request = view_request_at_indexed_ref(request, &status).await?;
53 if requested_ref == "worktree" {
54 ensure_worktree_overlay_matches_current_worktree(&store, &status, &request.repository)
55 .await?;
56 }
57 let requested_resolved_ref = request.repository.ref_selector.clone();
58 let freshness_target = request.repository.clone();
59 let mut stale_reason = None;
60 let scoped_status = match resolved_code_scope_status(&store, &status, &request.repository)
61 .await
62 {
63 Ok(scoped_status) => scoped_status,
64 Err(error) if request.freshness_policy == FreshnessPolicy::AllowStale => {
65 if !active_index_matches_request(&store, &status, &request.repository).await? {
66 return Err(error);
67 }
68 let Some(stale_status) =
69 latest_compatible_code_scope_status(&store, &request.repository).await?
70 else {
71 return Err(error);
72 };
73 let Some(last_indexed_commit) = stale_status.last_indexed_commit.clone() else {
74 return Err(error);
75 };
76 request.repository.ref_selector = last_indexed_commit;
77 stale_reason = Some(
78 "requested ref is not indexed yet; served last completed code index".to_owned(),
79 );
80 stale_status
81 }
82 Err(error) => return Err(error),
83 };
84 if request.freshness_policy == FreshnessPolicy::WaitUntilFresh && scoped_status.stale {
85 return Err(ApiError::invalid_argument(format!(
86 "code repository '{}' scope '{}' is stale; run repo index before deriving codebase views with wait_until_fresh",
87 scoped_status.alias,
88 scoped_status
89 .last_indexed_scope_id
90 .as_deref()
91 .unwrap_or("unscoped")
92 )));
93 }
94 let graph_version = store
95 .current_graph_version()
96 .await
97 .map_err(storage_api_error)?;
98 let source_scope = indexed_source_scope(&scoped_status)
99 .ok_or_else(|| missing_indexed_source_scope_error(&scoped_status))?;
100 let row_limit = request
101 .limit
102 .saturating_mul(SNAPSHOT_LIMIT_MULTIPLIER)
103 .min(SNAPSHOT_LIMIT_MAX);
104 let mut snapshot = store
105 .codebase_view_snapshot(source_scope.clone(), request.clone(), row_limit)
106 .await
107 .map_err(storage_api_error)?;
108 if request.view_kind == CodebaseViewKind::BusinessDomains {
109 let business_request = BusinessKnowledgeQueryRequest::new(
110 request.repository.clone(),
111 None,
112 None,
113 BusinessKnowledgeQueryKind::Terms,
114 request.freshness_policy,
115 request.limit,
116 )
117 .map_err(|error| ApiError::invalid_argument(error.to_string()))?;
118 let business = store
119 .business_knowledge_projection_for_scope(source_scope, business_request)
120 .await
121 .map_err(storage_api_error)?;
122 snapshot.declared_business_domains = business
123 .domains
124 .into_iter()
125 .map(|domain| CodebaseViewDeclaredBusinessDomain {
126 id: domain.id,
127 name: domain.name,
128 source_path: domain.evidence.source_path,
129 evidence_id: domain.evidence.evidence_id,
130 })
131 .collect();
132 }
133 let derived = derive_view(&request, snapshot, row_limit);
134 let direct_source_read_paths = view_source_read_paths(&request, &derived);
135 let degraded_reason = scoped_status
136 .degraded_reason
137 .clone()
138 .or_else(|| stale_reason.clone());
139 let scope_stale = served_scope_is_stale(&scoped_status, &stale_reason);
140 let mut metadata = ApiMetadata::graph_only(&context, graph_version);
141 if scope_stale {
142 metadata.stale = true;
143 }
144 let mut scope = crate::api::CodeRepositoryScopeMetadata::from_status(
145 &scoped_status,
146 &request.repository,
147 requested_ref.clone(),
148 );
149 if scope_stale {
150 scope.stale = true;
151 }
152 let freshness = view_freshness(ViewFreshnessInput {
153 store: &store,
154 base_status: &status,
155 scoped_status: &scoped_status,
156 request: &request,
157 requested_ref,
158 requested_resolved_ref,
159 freshness_target,
160 stale_reason,
161 degraded_reason: degraded_reason.clone(),
162 graph_version: graph_version.get(),
163 direct_source_read_paths,
164 })
165 .await?;
166
167 Ok(CodebaseViewResponse {
168 metadata,
169 scope,
170 freshness,
171 request,
172 graph_version: graph_version.get(),
173 nodes: derived.nodes,
174 edges: derived.edges,
175 sections: derived.sections,
176 evidence: derived.evidence,
177 budget: derived.budget,
178 diagnostics: derived.diagnostics,
179 degraded_reason,
180 })
181 }
182}
183
184pub(super) fn derive_view(
185 request: &CodebaseViewRequest,
186 snapshot: CodebaseViewSnapshot,
187 row_limit: usize,
188) -> DerivedView {
189 let mut builder = ViewBuilder::new(request.limit, row_limit, snapshot.truncated);
190 match request.view_kind {
191 CodebaseViewKind::ArchitectureLayers => derive_architecture_layers(&mut builder, &snapshot),
192 CodebaseViewKind::BusinessDomains => derive_business_domains(&mut builder, &snapshot),
193 CodebaseViewKind::DependencyTour => derive_dependency_tour(&mut builder, &snapshot),
194 CodebaseViewKind::ProcessFlow => derive_process_flow(&mut builder, &snapshot),
195 CodebaseViewKind::AffectedScope => derive_affected_scope(&mut builder, request, &snapshot),
196 }
197 builder.finish()
198}
199
200async fn view_request_at_indexed_ref(
201 mut request: CodebaseViewRequest,
202 status: &CodeRepositoryStatus,
203) -> Result<CodebaseViewRequest, ApiError> {
204 request.repository.ref_selector = indexed_commit_for_selector(
205 status,
206 &request.repository,
207 request.repository.ref_selector.clone(),
208 )
209 .await?;
210
211 Ok(request)
212}
213
214struct ViewFreshnessInput<'a> {
215 store: &'a std::sync::Arc<dyn crate::storage::KnowledgeStore>,
216 base_status: &'a CodeRepositoryStatus,
217 scoped_status: &'a CodeRepositoryStatus,
218 request: &'a CodebaseViewRequest,
219 requested_ref: String,
220 requested_resolved_ref: String,
221 freshness_target: CodeRepositorySelector,
222 stale_reason: Option<String>,
223 degraded_reason: Option<String>,
224 graph_version: u64,
225 direct_source_read_paths: Vec<String>,
226}
227
228async fn view_freshness(
229 input: ViewFreshnessInput<'_>,
230) -> Result<CodeRepositoryFreshnessDiagnostics, ApiError> {
231 let active_task = input
232 .store
233 .active_code_index_task(input.base_status.repository_id.clone())
234 .await
235 .map_err(storage_api_error)?;
236 let queue = input
237 .store
238 .code_index_task_queue_status()
239 .await
240 .map_err(storage_api_error)?;
241 let active_matches_request =
242 active_index_matches_request(input.store, input.base_status, &input.freshness_target)
243 .await?;
244 let pending = crate::api::CodeRepositoryPendingIndexWork::from_task_and_queue(
245 active_task.as_ref(),
246 active_matches_request,
247 queue,
248 );
249 let checkpoint = if active_matches_request {
250 code_status_checkpoint(
251 input.store.as_ref(),
252 input.scoped_status,
253 active_task.as_ref(),
254 )
255 .await?
256 } else if let Some(scope) = input.scoped_status.last_indexed_scope_id.clone() {
257 input
258 .store
259 .code_index_checkpoint(scope)
260 .await
261 .map_err(storage_api_error)?
262 } else {
263 None
264 };
265 let cursor = checkpoint
266 .as_ref()
267 .map(crate::api::CodeRepositoryFreshnessCursor::from_checkpoint);
268 let served_ref = input
269 .scoped_status
270 .last_indexed_commit
271 .clone()
272 .unwrap_or_else(|| input.request.repository.ref_selector.clone());
273
274 Ok(CodeRepositoryFreshnessDiagnostics::code_query(
275 CodeRepositoryFreshnessInput {
276 graph_version: input.graph_version,
277 freshness_policy: input.request.freshness_policy,
278 source_scope: indexed_source_scope(input.scoped_status),
279 requested_ref: input.requested_ref,
280 requested_resolved_ref: input.requested_resolved_ref,
281 served_ref,
282 scope_stale: served_scope_is_stale(input.scoped_status, &input.stale_reason),
283 stale_reason: input.stale_reason,
284 degraded_reason: input.degraded_reason,
285 pending,
286 cursor,
287 direct_source_read_paths: input.direct_source_read_paths,
288 },
289 ))
290}
291
292pub(super) fn view_source_read_paths(
293 request: &CodebaseViewRequest,
294 derived: &DerivedView,
295) -> Vec<String> {
296 let mut paths = BTreeSet::new();
297 if request.view_kind == CodebaseViewKind::AffectedScope {
298 paths.extend(normalized_view_paths(&request.changed_paths));
299 }
300 paths.extend(
301 derived
302 .evidence
303 .iter()
304 .map(|evidence| evidence.path.clone())
305 .filter(|path| !path.is_empty()),
306 );
307 paths.extend(
308 derived
309 .nodes
310 .iter()
311 .filter_map(|node| node.path.clone())
312 .filter(|path| !path.is_empty()),
313 );
314 paths.into_iter().collect()
315}
316
317pub(super) fn served_scope_is_stale(
318 status: &CodeRepositoryStatus,
319 stale_reason: &Option<String>,
320) -> bool {
321 status.stale || stale_reason.is_some()
322}