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