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