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