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::{
5        ApiError, ApiMetadata, RequestContext, SoftwareGlobalExportResponse, SoftwareGlobalResponse,
6    },
7    application::service::RelayKnowledgeService,
8    domain::{
9        CodeRepositoryStatus, FreshnessPolicy, GraphVersion, SoftwareExportProfile,
10        SoftwareGlobalRequest, SoftwareGlobalStatus,
11    },
12};
13
14mod export;
15
16use super::{
17    errors::storage_api_error,
18    repository::{ensure_worktree_overlay_matches_current_worktree, required_code_repository},
19    scope::{
20        active_index_matches_request, indexed_commit_for_selector,
21        latest_compatible_code_scope_status, resolved_code_scope_status,
22    },
23};
24
25impl RelayKnowledgeService {
26    /// Exports the snapshot-bound ontology through a versioned interoperability profile.
27    pub async fn software_global_export(
28        &self,
29        mut request: SoftwareGlobalRequest,
30        profile: SoftwareExportProfile,
31        context: RequestContext,
32    ) -> Result<SoftwareGlobalExportResponse, ApiError> {
33        request.kind = crate::domain::SoftwareGlobalKind::Statements;
34        let response = self.software_global_projection(request, context).await?;
35        let document = export::export_document(&response, profile);
36        Ok(SoftwareGlobalExportResponse {
37            metadata: response.metadata,
38            scope: response.scope,
39            status: response.status,
40            profile,
41            media_type: profile.media_type().to_owned(),
42            document,
43        })
44    }
45
46    /// Reads the repository-scoped software global dependency and SDK projection.
47    pub async fn software_global_projection(
48        &self,
49        request: SoftwareGlobalRequest,
50        context: RequestContext,
51    ) -> Result<SoftwareGlobalResponse, ApiError> {
52        let store = self.store().await.map_err(storage_api_error)?;
53        let status =
54            required_code_repository(store.as_ref(), &request.repository.repository).await?;
55        if request.freshness_policy == FreshnessPolicy::GraphOnly {
56            let graph_version = store
57                .current_graph_version()
58                .await
59                .map_err(storage_api_error)?;
60            return Ok(SoftwareGlobalResponse {
61                metadata: ApiMetadata::graph_only(&context, graph_version),
62                scope: crate::api::CodeRepositoryScopeMetadata::from_status(
63                    &status,
64                    &request.repository,
65                    request.repository.ref_selector.clone(),
66                ),
67                request,
68                status: SoftwareGlobalStatus {
69                    repository_id: status.repository_id.clone(),
70                    source_scope: status
71                        .last_indexed_scope_id
72                        .clone()
73                        .unwrap_or_else(|| "unscoped".to_owned()),
74                    projected_graph_version: GraphVersion::ZERO,
75                    stale: true,
76                    ontology_version: crate::domain::SOFTWARE_ONTOLOGY_VERSION.to_owned(),
77                    projection_schema_version: crate::domain::SOFTWARE_PROJECTION_SCHEMA_VERSION,
78                    source_coverage: crate::domain::SoftwareSourceCoverage::default(),
79                    completeness_basis_points: 0,
80                    freshness: crate::domain::SoftwareProjectionFreshness::Stale,
81                    conflict_count: 0,
82                    entity_count: 0,
83                    statement_count: 0,
84                    diagnostic_count: 0,
85                    component_count: 0,
86                    sdk_usage_count: 0,
87                    file_count: 0,
88                    topic_count: 0,
89                    relationship_count: 0,
90                    build_target_count: 0,
91                    iac_resource_count: 0,
92                    design_element_count: 0,
93                    last_error: Some("graph_only freshness policy selected".to_owned()),
94                },
95                components: Vec::new(),
96                dependency_usages: Vec::new(),
97                sdk_usages: Vec::new(),
98                files: Vec::new(),
99                topics: Vec::new(),
100                relationships: Vec::new(),
101                build_targets: Vec::new(),
102                iac_resources: Vec::new(),
103                design_elements: Vec::new(),
104                entities: Vec::new(),
105                statements: Vec::new(),
106                diagnostics: Vec::new(),
107            });
108        }
109
110        let requested_ref = request.repository.ref_selector.clone();
111        let mut request = software_request_at_indexed_ref(request, &status).await?;
112        if requested_ref == "worktree" {
113            ensure_worktree_overlay_matches_current_worktree(&store, &status, &request.repository)
114                .await?;
115        }
116        let mut served_stale_scope = false;
117        let scoped_status =
118            match resolved_code_scope_status(&store, &status, &request.repository).await {
119                Ok(scoped_status) => scoped_status,
120                Err(error) if request.freshness_policy == FreshnessPolicy::AllowStale => {
121                    if !active_index_matches_request(&store, &status, &request.repository).await? {
122                        return Err(error);
123                    }
124                    let Some(stale_status) =
125                        latest_compatible_code_scope_status(&store, &request.repository).await?
126                    else {
127                        return Err(error);
128                    };
129                    let Some(last_indexed_commit) = stale_status.last_indexed_commit.clone() else {
130                        return Err(error);
131                    };
132                    request.repository.ref_selector = last_indexed_commit;
133                    served_stale_scope = true;
134                    stale_status
135                }
136                Err(error) => return Err(error),
137            };
138        if let Some(last_indexed_commit) = scoped_status.last_indexed_commit.clone() {
139            request.repository.ref_selector = last_indexed_commit;
140        }
141        request.repository.repository = status.repository_id.clone();
142
143        let source_scope = scoped_status.last_indexed_scope_id.clone().ok_or_else(|| {
144            ApiError::invalid_argument(format!(
145                "code repository '{}' does not have an indexed source scope",
146                scoped_status.alias
147            ))
148        })?;
149        let projection = store
150            .software_global_projection_for_scope(source_scope, request.clone())
151            .await
152            .map_err(storage_api_error)?;
153        if request.freshness_policy == FreshnessPolicy::WaitUntilFresh
154            && (projection.status.stale || scoped_status.stale)
155        {
156            return Err(ApiError::invalid_argument(format!(
157                "software global projection for repository '{}' scope '{}' is stale; run repo index before querying with wait_until_fresh",
158                status.alias, projection.status.source_scope
159            )));
160        }
161        let graph_version = store
162            .current_graph_version()
163            .await
164            .map_err(storage_api_error)?;
165        let mut metadata = ApiMetadata::graph_only(&context, graph_version);
166        if projection.status.stale || scoped_status.stale || served_stale_scope {
167            metadata.stale = true;
168        }
169
170        let mut scope_selector = request.repository.clone();
171        scope_selector.path_filters = scoped_status.path_filters.clone();
172        scope_selector.language_filters = scoped_status.language_filters.clone();
173        let mut scope = crate::api::CodeRepositoryScopeMetadata::from_status(
174            &scoped_status,
175            &scope_selector,
176            requested_ref,
177        );
178        if served_stale_scope {
179            scope.stale = true;
180        }
181
182        let mut status = projection.status;
183        if scoped_status.stale || served_stale_scope {
184            status.stale = true;
185            status.freshness = crate::domain::SoftwareProjectionFreshness::Stale;
186        } else if scoped_status.degraded_reason.is_some() {
187            status.freshness = crate::domain::SoftwareProjectionFreshness::Degraded;
188        }
189
190        Ok(SoftwareGlobalResponse {
191            metadata,
192            scope,
193            request,
194            status,
195            components: projection.components,
196            dependency_usages: projection.dependency_usages,
197            sdk_usages: projection.sdk_usages,
198            files: projection.files,
199            topics: projection.topics,
200            relationships: projection.relationships,
201            build_targets: projection.build_targets,
202            iac_resources: projection.iac_resources,
203            design_elements: projection.design_elements,
204            entities: projection.entities,
205            statements: projection.statements,
206            diagnostics: projection.diagnostics,
207        })
208    }
209}
210
211async fn software_request_at_indexed_ref(
212    mut request: SoftwareGlobalRequest,
213    status: &CodeRepositoryStatus,
214) -> Result<SoftwareGlobalRequest, ApiError> {
215    request.repository.ref_selector = indexed_commit_for_selector(
216        status,
217        &request.repository,
218        request.repository.ref_selector.clone(),
219    )
220    .await?;
221
222    Ok(request)
223}