Skip to main content

relay_knowledge/domain/code/repository_set/
mod.rs

1//! Owns repository-set membership, refresh, query, and status contracts.
2
3use serde::{Deserialize, Serialize};
4
5use super::{
6    DomainError, FreshnessPolicy,
7    error::required_text,
8    repository::{CodeQueryKind, CodeRetrievalHit},
9};
10
11/// Repository-set creation request shared by API adapters.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct CodeRepositorySetCreateRequest {
14    pub alias: String,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub description: Option<String>,
17    pub default_ref_policy_json: String,
18}
19
20impl CodeRepositorySetCreateRequest {
21    pub fn new(
22        alias: impl Into<String>,
23        description: Option<String>,
24        default_ref_policy_json: Option<String>,
25    ) -> Result<Self, DomainError> {
26        Ok(Self {
27            alias: required_text("set_alias", alias)?,
28            description: optional_text("description", description)?,
29            default_ref_policy_json: default_ref_policy_json
30                .filter(|value| !value.trim().is_empty())
31                .unwrap_or_else(|| "{\"default_ref\":\"HEAD\"}".to_owned()),
32        })
33    }
34}
35
36/// Persisted repository-set metadata.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct CodeRepositorySet {
39    pub set_id: String,
40    pub alias: String,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub description: Option<String>,
43    pub default_ref_policy_json: String,
44    pub created_at_ms: u64,
45    pub updated_at_ms: u64,
46}
47
48/// Request to attach one indexed repository snapshot to a repository set.
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct CodeRepositorySetAddMemberRequest {
51    pub set_alias: String,
52    pub repository_alias: String,
53    pub ref_selector: String,
54    pub path_filters: Vec<String>,
55    pub language_filters: Vec<String>,
56    pub priority: i32,
57}
58
59impl CodeRepositorySetAddMemberRequest {
60    pub fn new(
61        set_alias: impl Into<String>,
62        repository_alias: impl Into<String>,
63        ref_selector: impl Into<String>,
64        path_filters: Vec<String>,
65        language_filters: Vec<String>,
66        priority: i32,
67    ) -> Result<Self, DomainError> {
68        Ok(Self {
69            set_alias: required_text("set_alias", set_alias)?,
70            repository_alias: required_text("repository_alias", repository_alias)?,
71            ref_selector: required_text("ref_selector", ref_selector)?,
72            path_filters: normalize_filter_list("path_filter", path_filters)?,
73            language_filters: normalize_filter_list("language_filter", language_filters)?,
74            priority,
75        })
76    }
77}
78
79/// Request to remove one repository snapshot from a repository set.
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct CodeRepositorySetRemoveMemberRequest {
82    pub set_alias: String,
83    pub repository_alias: String,
84}
85
86impl CodeRepositorySetRemoveMemberRequest {
87    pub fn new(
88        set_alias: impl Into<String>,
89        repository_alias: impl Into<String>,
90    ) -> Result<Self, DomainError> {
91        Ok(Self {
92            set_alias: required_text("set_alias", set_alias)?,
93            repository_alias: required_text("repository_alias", repository_alias)?,
94        })
95    }
96}
97
98/// Persisted repository-set membership pointing at a real repository snapshot.
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct CodeRepositorySetMember {
101    pub set_id: String,
102    pub repository_id: String,
103    pub repository_alias: String,
104    pub ref_selector: String,
105    pub resolved_commit_sha: String,
106    pub source_scope: String,
107    pub path_filters: Vec<String>,
108    pub language_filters: Vec<String>,
109    pub priority: i32,
110}
111
112/// Status for one repository-set member snapshot.
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114pub struct CodeRepositorySetMemberStatus {
115    pub member: CodeRepositorySetMember,
116    pub tree_hash: String,
117    #[serde(default, skip_serializing)]
118    pub indexed_path_filters: Vec<String>,
119    #[serde(default, skip_serializing)]
120    pub indexed_language_filters: Vec<String>,
121    pub freshness_state: String,
122    pub stale: bool,
123    pub indexed_file_count: usize,
124    pub symbol_count: usize,
125    pub reference_count: usize,
126    pub chunk_count: usize,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub degraded_reason: Option<String>,
129}
130
131/// Overlay freshness and task-independent diagnostics.
132#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
133pub struct CodeRepositorySetOverlayStatus {
134    pub state: String,
135    pub stale: bool,
136    pub edge_count: usize,
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub refreshed_at_ms: Option<u64>,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub degraded_reason: Option<String>,
141}
142
143/// Aggregated repository-set status.
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145pub struct CodeRepositorySetStatus {
146    pub repository_set: CodeRepositorySet,
147    pub members: Vec<CodeRepositorySetMemberStatus>,
148    pub overlay: CodeRepositorySetOverlayStatus,
149    pub freshness_state: String,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub degraded_reason: Option<String>,
152}
153
154/// Multi-repository query request.
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct CodeRepositorySetQueryRequest {
157    pub set_alias: String,
158    pub query: String,
159    pub code_query_kind: CodeQueryKind,
160    pub limit: usize,
161    pub freshness_policy: FreshnessPolicy,
162    pub path_filters: Vec<String>,
163    pub language_filters: Vec<String>,
164    #[serde(default)]
165    pub exclude_generated: bool,
166}
167
168impl CodeRepositorySetQueryRequest {
169    pub fn new(
170        set_alias: impl Into<String>,
171        query: impl Into<String>,
172        code_query_kind: CodeQueryKind,
173        limit: usize,
174        freshness_policy: FreshnessPolicy,
175        path_filters: Vec<String>,
176        language_filters: Vec<String>,
177    ) -> Result<Self, DomainError> {
178        let limit = match limit {
179            1..=50 => limit,
180            0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
181            _ => return Err(DomainError::invalid("limit", "must be 50 or less")),
182        };
183
184        Ok(Self {
185            set_alias: required_text("set_alias", set_alias)?,
186            query: required_text("query", query)?,
187            code_query_kind,
188            limit,
189            freshness_policy,
190            path_filters: normalize_filter_list("path_filter", path_filters)?,
191            language_filters: normalize_filter_list("language_filter", language_filters)?,
192            exclude_generated: false,
193        })
194    }
195}
196
197/// Cross-repository overlay edge derived after member snapshots are indexed.
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199pub struct CodeRepositoryCrossEdge {
200    pub edge_id: String,
201    pub set_id: String,
202    pub from_source_scope: String,
203    pub from_repository_id: String,
204    pub from_record_kind: String,
205    pub from_record_id: String,
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub to_source_scope: Option<String>,
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub to_repository_id: Option<String>,
210    pub to_record_kind: String,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub to_record_id: Option<String>,
213    pub edge_kind: String,
214    pub resolution_state: String,
215    pub confidence_basis_points: u16,
216    pub confidence_tier: String,
217    pub evidence_json: String,
218    pub created_at_ms: u64,
219}
220
221/// Query hit with explicit repository-set provenance.
222#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
223pub struct CodeRepositorySetQueryHit {
224    pub member: CodeRepositorySetMember,
225    pub hit: CodeRetrievalHit,
226    pub overlay_evidence: Vec<CodeRepositoryCrossEdge>,
227    pub score: f64,
228}
229
230/// Summary returned after rebuilding a repository-set overlay.
231#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
232pub struct CodeRepositorySetRefreshSummary {
233    pub set_id: String,
234    pub alias: String,
235    pub edge_count: usize,
236    pub resolved_edge_count: usize,
237    pub ambiguous_edge_count: usize,
238    pub unresolved_edge_count: usize,
239    pub refreshed_at_ms: u64,
240}
241
242/// Durable state for a repository-set overlay refresh task.
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(rename_all = "snake_case")]
245pub enum CodeRepositorySetRefreshTaskState {
246    Queued,
247    Running,
248    Succeeded,
249    Retrying,
250    DeadLetter,
251}
252
253impl CodeRepositorySetRefreshTaskState {
254    pub const fn as_str(self) -> &'static str {
255        match self {
256            Self::Queued => "queued",
257            Self::Running => "running",
258            Self::Succeeded => "succeeded",
259            Self::Retrying => "retrying",
260            Self::DeadLetter => "dead_letter",
261        }
262    }
263
264    pub fn parse(value: &str) -> Result<Self, DomainError> {
265        match value {
266            "queued" => Ok(Self::Queued),
267            "running" => Ok(Self::Running),
268            "succeeded" => Ok(Self::Succeeded),
269            "retrying" => Ok(Self::Retrying),
270            "dead_letter" => Ok(Self::DeadLetter),
271            _ => Err(DomainError::invalid(
272                "repository_set_refresh_task_state",
273                "unknown repository set refresh task state",
274            )),
275        }
276    }
277
278    pub const fn is_unfinished(self) -> bool {
279        matches!(self, Self::Queued | Self::Running | Self::Retrying)
280    }
281}
282
283/// Durable repository-set overlay refresh task record.
284#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
285pub struct CodeRepositorySetRefreshTaskRecord {
286    pub task_id: String,
287    pub set_id: String,
288    pub set_alias: String,
289    pub state: CodeRepositorySetRefreshTaskState,
290    #[serde(skip_serializing_if = "Option::is_none")]
291    pub lease_owner: Option<String>,
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub lease_expires_at_ms: Option<u64>,
294    pub attempt_count: u32,
295    pub next_retry_at_ms: u64,
296    pub input_fingerprint: String,
297    #[serde(skip_serializing_if = "Option::is_none")]
298    pub last_error_kind: Option<String>,
299    #[serde(skip_serializing_if = "Option::is_none")]
300    pub last_error_message: Option<String>,
301    pub created_at_ms: u64,
302    pub updated_at_ms: u64,
303}
304
305fn optional_text(
306    field: &'static str,
307    value: Option<String>,
308) -> Result<Option<String>, DomainError> {
309    value.map(|value| required_text(field, value)).transpose()
310}
311
312fn normalize_filter_list(
313    field: &'static str,
314    values: Vec<String>,
315) -> Result<Vec<String>, DomainError> {
316    let mut normalized = Vec::new();
317    for value in values {
318        let value = required_text(field, value)?;
319        if !normalized.contains(&value) {
320            normalized.push(value);
321        }
322    }
323
324    Ok(normalized)
325}
326
327#[cfg(test)]
328mod mod_tests;