Skip to main content

relay_knowledge/application/code_repository/repository_set/status/
mod.rs

1//! Assembles repository-set status and overlay freshness diagnostics.
2
3use 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    /// Returns repository-set freshness and member diagnostics.
17    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;