relay_knowledge/application/code_repository/repository_set/status/
mod.rs1use crate::{
4 api::{ApiError, ApiMetadata, CodeRepositorySetStatusResponse, RequestContext},
5 application::service::RelayKnowledgeService,
6 domain::{CodeRepositorySelector, CodeRepositorySetMember, CodeRepositorySetStatus},
7 storage::CodeRepositorySetMemberSeed,
8};
9
10use super::{
11 super::scope::resolve_code_ref_for_selector, errors::storage_api_error,
12 member_freshness::refresh_fact_version_member_freshness,
13};
14
15impl RelayKnowledgeService {
16 pub async fn code_repository_set_status(
18 &self,
19 set_alias: String,
20 context: RequestContext,
21 ) -> Result<CodeRepositorySetStatusResponse, ApiError> {
22 let store = self.store().await.map_err(storage_api_error)?;
23 let status = required_set_status(&store, &set_alias).await?;
24 let graph_version = store
25 .current_graph_version()
26 .await
27 .map_err(storage_api_error)?;
28
29 Ok(CodeRepositorySetStatusResponse {
30 metadata: ApiMetadata::graph_only(&context, graph_version),
31 status,
32 })
33 }
34}
35
36pub(super) async fn required_set_status(
37 store: &std::sync::Arc<dyn crate::storage::KnowledgeStore>,
38 set_alias: &str,
39) -> Result<CodeRepositorySetStatus, ApiError> {
40 refreshed_required_set_status(store, set_alias)
41 .await
42 .map(|(status, _)| status)
43}
44
45pub(super) async fn refreshed_required_set_status(
46 store: &std::sync::Arc<dyn crate::storage::KnowledgeStore>,
47 set_alias: &str,
48) -> Result<(CodeRepositorySetStatus, Vec<CodeRepositorySetMember>), ApiError> {
49 let mut status = store
50 .code_repository_set_status(set_alias.to_owned())
51 .await
52 .map_err(storage_api_error)?
53 .ok_or_else(|| {
54 ApiError::invalid_argument(format!(
55 "code repository set '{set_alias}' is not registered"
56 ))
57 })?;
58 let fact_version_replacements =
59 refresh_fact_version_member_freshness(store, &mut status).await?;
60 refresh_moving_member_freshness(store, &mut status).await?;
61 refresh_repository_set_freshness(&mut status);
62
63 Ok((status, fact_version_replacements))
64}
65
66pub(super) async fn persist_fact_version_member_replacements(
67 store: &std::sync::Arc<dyn crate::storage::KnowledgeStore>,
68 set_alias: &str,
69 replacements: &[CodeRepositorySetMember],
70) -> Result<(), ApiError> {
71 for member in replacements {
72 store
73 .add_code_repository_set_member(CodeRepositorySetMemberSeed {
74 set_alias: set_alias.to_owned(),
75 repository_id: member.repository_id.clone(),
76 repository_alias: member.repository_alias.clone(),
77 ref_selector: member.ref_selector.clone(),
78 resolved_commit_sha: member.resolved_commit_sha.clone(),
79 source_scope: member.source_scope.clone(),
80 path_filters: member.path_filters.clone(),
81 language_filters: member.language_filters.clone(),
82 priority: member.priority,
83 })
84 .await
85 .map_err(storage_api_error)?;
86 }
87
88 Ok(())
89}
90
91async fn refresh_moving_member_freshness(
92 store: &std::sync::Arc<dyn crate::storage::KnowledgeStore>,
93 status: &mut CodeRepositorySetStatus,
94) -> Result<(), ApiError> {
95 for index in 0..status.members.len() {
96 let member = status.members[index].member.clone();
97 let Some(reason) = moving_member_stale_reason(store, &member).await? else {
98 continue;
99 };
100 status.members[index].stale = true;
101 status.members[index].freshness_state = "stale".to_owned();
102 status.members[index].degraded_reason = Some(reason);
103 }
104
105 Ok(())
106}
107
108async fn moving_member_stale_reason(
109 store: &std::sync::Arc<dyn crate::storage::KnowledgeStore>,
110 member: &crate::domain::CodeRepositorySetMember,
111) -> Result<Option<String>, ApiError> {
112 if !member_ref_tracks_repository(&member.ref_selector, &member.resolved_commit_sha) {
113 return Ok(None);
114 }
115 let repository = store
116 .code_repository_status(member.repository_id.clone())
117 .await
118 .map_err(storage_api_error)?
119 .ok_or_else(|| {
120 ApiError::invalid_argument(format!(
121 "code repository '{}' is not registered",
122 member.repository_alias
123 ))
124 })?;
125 let ref_selector = member.ref_selector.clone();
126 let selector = CodeRepositorySelector {
127 repository: member.repository_alias.clone(),
128 ref_selector: ref_selector.clone(),
129 path_filters: member.path_filters.clone(),
130 language_filters: member.language_filters.clone(),
131 };
132 let resolved = resolve_code_ref_for_selector(&repository, &selector, ref_selector).await;
133
134 match resolved {
135 Ok(current_commit) if current_commit == member.resolved_commit_sha => Ok(None),
136 Ok(current_commit) => Ok(Some(format!(
137 "repository set member '{}' ref '{}' now resolves to {}, not stored snapshot {}",
138 member.repository_alias,
139 member.ref_selector,
140 current_commit,
141 member.resolved_commit_sha
142 ))),
143 Err(error) => Ok(Some(format!(
144 "repository set member '{}' ref '{}' could not be resolved: {error}",
145 member.repository_alias,
146 member.ref_selector,
147 error = error.message
148 ))),
149 }
150}
151
152fn member_ref_tracks_repository(ref_selector: &str, resolved_commit_sha: &str) -> bool {
153 let ref_selector = ref_selector.trim();
154 !(ref_selector == resolved_commit_sha
155 || (is_git_oid_prefix(ref_selector) && resolved_commit_sha.starts_with(ref_selector)))
156}
157
158fn is_git_oid_prefix(value: &str) -> bool {
159 (7..=64).contains(&value.len()) && value.bytes().all(|byte| byte.is_ascii_hexdigit())
160}
161
162fn refresh_repository_set_freshness(status: &mut CodeRepositorySetStatus) {
163 let member_stale = status.members.iter().any(|member| member.stale);
164 if member_stale && !status.overlay.stale {
165 status.overlay.stale = true;
166 status.overlay.state = "overlay_stale".to_owned();
167 }
168 status.freshness_state = if status.members.is_empty() {
169 "incomplete"
170 } else if member_stale {
171 "stale"
172 } else if status.overlay.stale {
173 "overlay_stale"
174 } else {
175 "fresh"
176 }
177 .to_owned();
178 status.degraded_reason = status
179 .members
180 .iter()
181 .find_map(|member| member.degraded_reason.clone())
182 .or_else(|| status.overlay.degraded_reason.clone());
183}
184
185#[cfg(test)]
186#[path = "mod_tests.rs"]
187mod tests;