Skip to main content

relay_knowledge/application/code_repository/software_projection/
mod.rs

1//! Coordinates repository software-projection reads and scope validation.
2
3use crate::{
4    api::{ApiError, ApiMetadata, RequestContext, SoftwareGlobalResponse},
5    application::service::RelayKnowledgeService,
6    domain::{
7        CodeRepositoryStatus, FreshnessPolicy, GraphVersion, SoftwareGlobalRequest,
8        SoftwareGlobalStatus,
9    },
10};
11
12use super::{
13    errors::storage_api_error,
14    repository::{ensure_worktree_overlay_matches_current_worktree, required_code_repository},
15    scope::{
16        active_index_matches_request, indexed_commit_for_selector,
17        latest_compatible_code_scope_status, resolved_code_scope_status,
18    },
19};
20
21impl RelayKnowledgeService {
22    /// Reads the repository-scoped software global dependency and SDK projection.
23    pub async fn software_global_projection(
24        &self,
25        request: SoftwareGlobalRequest,
26        context: RequestContext,
27    ) -> Result<SoftwareGlobalResponse, ApiError> {
28        let store = self.store().await.map_err(storage_api_error)?;
29        let status =
30            required_code_repository(store.as_ref(), &request.repository.repository).await?;
31        if request.freshness_policy == FreshnessPolicy::GraphOnly {
32            let graph_version = store
33                .current_graph_version()
34                .await
35                .map_err(storage_api_error)?;
36            return Ok(SoftwareGlobalResponse {
37                metadata: ApiMetadata::graph_only(&context, graph_version),
38                scope: crate::api::CodeRepositoryScopeMetadata::from_status(
39                    &status,
40                    &request.repository,
41                    request.repository.ref_selector.clone(),
42                ),
43                request,
44                status: SoftwareGlobalStatus {
45                    repository_id: status.repository_id.clone(),
46                    source_scope: status
47                        .last_indexed_scope_id
48                        .clone()
49                        .unwrap_or_else(|| "unscoped".to_owned()),
50                    projected_graph_version: GraphVersion::ZERO,
51                    stale: true,
52                    component_count: 0,
53                    sdk_usage_count: 0,
54                    file_count: 0,
55                    topic_count: 0,
56                    relationship_count: 0,
57                    build_target_count: 0,
58                    iac_resource_count: 0,
59                    design_element_count: 0,
60                    last_error: Some("graph_only freshness policy selected".to_owned()),
61                },
62                components: Vec::new(),
63                dependency_usages: Vec::new(),
64                sdk_usages: Vec::new(),
65                files: Vec::new(),
66                topics: Vec::new(),
67                relationships: Vec::new(),
68                build_targets: Vec::new(),
69                iac_resources: Vec::new(),
70                design_elements: Vec::new(),
71            });
72        }
73
74        let requested_ref = request.repository.ref_selector.clone();
75        let mut request = software_request_at_indexed_ref(request, &status).await?;
76        if requested_ref == "worktree" {
77            ensure_worktree_overlay_matches_current_worktree(&store, &status, &request.repository)
78                .await?;
79        }
80        let mut served_stale_scope = false;
81        let scoped_status =
82            match resolved_code_scope_status(&store, &status, &request.repository).await {
83                Ok(scoped_status) => scoped_status,
84                Err(error) if request.freshness_policy == FreshnessPolicy::AllowStale => {
85                    if !active_index_matches_request(&store, &status, &request.repository).await? {
86                        return Err(error);
87                    }
88                    let Some(stale_status) =
89                        latest_compatible_code_scope_status(&store, &request.repository).await?
90                    else {
91                        return Err(error);
92                    };
93                    let Some(last_indexed_commit) = stale_status.last_indexed_commit.clone() else {
94                        return Err(error);
95                    };
96                    request.repository.ref_selector = last_indexed_commit;
97                    served_stale_scope = true;
98                    stale_status
99                }
100                Err(error) => return Err(error),
101            };
102        if let Some(last_indexed_commit) = scoped_status.last_indexed_commit.clone() {
103            request.repository.ref_selector = last_indexed_commit;
104        }
105        request.repository.repository = status.repository_id.clone();
106
107        let source_scope = scoped_status.last_indexed_scope_id.clone().ok_or_else(|| {
108            ApiError::invalid_argument(format!(
109                "code repository '{}' does not have an indexed source scope",
110                scoped_status.alias
111            ))
112        })?;
113        let projection = store
114            .software_global_projection_for_scope(source_scope, request.clone())
115            .await
116            .map_err(storage_api_error)?;
117        if request.freshness_policy == FreshnessPolicy::WaitUntilFresh
118            && (projection.status.stale || scoped_status.stale)
119        {
120            return Err(ApiError::invalid_argument(format!(
121                "software global projection for repository '{}' scope '{}' is stale; run repo index before querying with wait_until_fresh",
122                status.alias, projection.status.source_scope
123            )));
124        }
125        let graph_version = store
126            .current_graph_version()
127            .await
128            .map_err(storage_api_error)?;
129        let mut metadata = ApiMetadata::graph_only(&context, graph_version);
130        if projection.status.stale || scoped_status.stale || served_stale_scope {
131            metadata.stale = true;
132        }
133
134        let mut scope_selector = request.repository.clone();
135        scope_selector.path_filters = scoped_status.path_filters.clone();
136        scope_selector.language_filters = scoped_status.language_filters.clone();
137        let mut scope = crate::api::CodeRepositoryScopeMetadata::from_status(
138            &scoped_status,
139            &scope_selector,
140            requested_ref,
141        );
142        if served_stale_scope {
143            scope.stale = true;
144        }
145
146        let mut status = projection.status;
147        if scoped_status.stale || served_stale_scope {
148            status.stale = true;
149        }
150
151        Ok(SoftwareGlobalResponse {
152            metadata,
153            scope,
154            request,
155            status,
156            components: projection.components,
157            dependency_usages: projection.dependency_usages,
158            sdk_usages: projection.sdk_usages,
159            files: projection.files,
160            topics: projection.topics,
161            relationships: projection.relationships,
162            build_targets: projection.build_targets,
163            iac_resources: projection.iac_resources,
164            design_elements: projection.design_elements,
165        })
166    }
167}
168
169async fn software_request_at_indexed_ref(
170    mut request: SoftwareGlobalRequest,
171    status: &CodeRepositoryStatus,
172) -> Result<SoftwareGlobalRequest, ApiError> {
173    request.repository.ref_selector = indexed_commit_for_selector(
174        status,
175        &request.repository,
176        request.repository.ref_selector.clone(),
177    )
178    .await?;
179
180    Ok(request)
181}