1use serde::{Deserialize, Serialize};
4
5use super::{
6 DomainError, FreshnessPolicy,
7 error::required_text,
8 repository::{CodeQueryKind, CodeRetrievalHit},
9};
10
11#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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;