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