Skip to main content

relay_knowledge/application/code_repository/views/
service.rs

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    /// Builds a deterministic, evidence-backed repository understanding view.
43    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}