1use crate::{
2 api::{
3 ApiError, ApiMetadata, CodeRepositorySetAddResponse, CodeRepositorySetCreateResponse,
4 CodeRepositorySetQueryResponse, CodeRepositorySetRefreshResponse,
5 CodeRepositorySetStatusResponse, RequestContext,
6 },
7 code::{CodeIndexError, resolve_repository_snapshot},
8 domain::{
9 CodeRepositorySelector, CodeRepositorySetAddMemberRequest, CodeRepositorySetCreateRequest,
10 CodeRepositorySetQueryHit, CodeRepositorySetQueryRequest, CodeRepositorySetStatus,
11 CodeRetrievalRequest, FreshnessPolicy,
12 },
13 storage::{
14 CodeRepositorySetMemberSeed, CodeRepositorySetRefreshTaskClaimRequest,
15 CodeRepositorySetRefreshTaskCompletion, CodeRepositorySetRefreshTaskFailure,
16 CodeRepositorySetRefreshTaskSeed, CodeRepositorySetSeed, StorageError,
17 },
18};
19use std::path::PathBuf;
20
21use super::{
22 RelayKnowledgeService,
23 code_repository_set_plan::{
24 dependency_symbol_plan_needs_hybrid_fallback, repository_set_member_query_kind,
25 },
26 code_repository_set_query::{
27 OverlayEvidenceIndex, apply_bridge_support_bonus, dedupe_sort_truncate,
28 per_member_candidate_limit, prune_returned_overlay_evidence, repository_set_score,
29 },
30};
31
32const REPOSITORY_SET_REFRESH_TASK_LEASE_MS: u64 = 10 * 60 * 1000;
33const REPOSITORY_SET_REFRESH_TASK_MAX_ATTEMPTS: u32 = 3;
34const REPOSITORY_SET_REFRESH_TASK_RETRY_BACKOFF_MS: u64 = 60_000;
35
36impl RelayKnowledgeService {
37 pub async fn create_code_repository_set(
39 &self,
40 request: CodeRepositorySetCreateRequest,
41 context: RequestContext,
42 ) -> Result<CodeRepositorySetCreateResponse, ApiError> {
43 let store = self.store().await.map_err(storage_api_error)?;
44 let repository_set = store
45 .create_code_repository_set(CodeRepositorySetSeed {
46 alias: request.alias.clone(),
47 description: request.description.clone(),
48 default_ref_policy_json: request.default_ref_policy_json.clone(),
49 now_ms: now_millis(),
50 })
51 .await
52 .map_err(storage_api_error)?;
53 let graph_version = store
54 .current_graph_version()
55 .await
56 .map_err(storage_api_error)?;
57
58 Ok(CodeRepositorySetCreateResponse {
59 metadata: ApiMetadata::graph_only(&context, graph_version),
60 request,
61 repository_set,
62 })
63 }
64
65 pub async fn add_code_repository_set_member(
67 &self,
68 request: CodeRepositorySetAddMemberRequest,
69 context: RequestContext,
70 ) -> Result<CodeRepositorySetAddResponse, ApiError> {
71 let store = self.store().await.map_err(storage_api_error)?;
72 let repository = store
73 .code_repository_status(request.repository_alias.clone())
74 .await
75 .map_err(storage_api_error)?
76 .ok_or_else(|| {
77 ApiError::invalid_argument(format!(
78 "code repository '{}' is not registered",
79 request.repository_alias
80 ))
81 })?;
82 let root_path = repository.root_path.clone();
83 let ref_selector = request.ref_selector.clone();
84 let (resolved_commit_sha, _tree_hash) =
85 run_blocking_code(move || resolve_repository_snapshot(root_path, &ref_selector))
86 .await?;
87 let path_filters = merged_filters(&repository.path_filters, &request.path_filters);
88 let language_filters =
89 merged_filters(&repository.language_filters, &request.language_filters);
90 let scope = store
91 .code_repository_scope_status(
92 request.repository_alias.clone(),
93 resolved_commit_sha.clone(),
94 path_filters.clone(),
95 language_filters.clone(),
96 )
97 .await
98 .map_err(storage_api_error)?
99 .ok_or_else(|| {
100 ApiError::invalid_argument(format!(
101 "code repository '{}' has no indexed scope for ref {} and requested filters",
102 request.repository_alias, request.ref_selector
103 ))
104 })?;
105 let source_scope = scope.last_indexed_scope_id.clone().ok_or_else(|| {
106 ApiError::invalid_argument(format!(
107 "code repository '{}' matching scope has no source scope",
108 request.repository_alias
109 ))
110 })?;
111 let member = store
112 .add_code_repository_set_member(CodeRepositorySetMemberSeed {
113 set_alias: request.set_alias.clone(),
114 repository_id: repository.repository_id,
115 repository_alias: request.repository_alias.clone(),
116 ref_selector: request.ref_selector.clone(),
117 resolved_commit_sha,
118 source_scope,
119 path_filters,
120 language_filters,
121 priority: request.priority,
122 })
123 .await
124 .map_err(storage_api_error)?;
125 let status = required_set_status(&store, &request.set_alias).await?;
126 let graph_version = store
127 .current_graph_version()
128 .await
129 .map_err(storage_api_error)?;
130
131 Ok(CodeRepositorySetAddResponse {
132 metadata: ApiMetadata::graph_only(&context, graph_version),
133 request,
134 member,
135 status,
136 })
137 }
138
139 pub async fn query_code_repository_set(
141 &self,
142 request: CodeRepositorySetQueryRequest,
143 context: RequestContext,
144 ) -> Result<CodeRepositorySetQueryResponse, ApiError> {
145 let store = self.store().await.map_err(storage_api_error)?;
146 let status = required_set_status(&store, &request.set_alias).await?;
147 let graph_version = store
148 .current_graph_version()
149 .await
150 .map_err(storage_api_error)?;
151 if request.freshness_policy == FreshnessPolicy::GraphOnly {
152 return Ok(CodeRepositorySetQueryResponse {
153 metadata: ApiMetadata::graph_only(&context, graph_version),
154 request,
155 status,
156 results: Vec::new(),
157 truncated: false,
158 degraded_reason: Some("graph_only freshness policy selected".to_owned()),
159 });
160 }
161 if let Some(error) = unfresh_set_error_for_wait_policy(&request, &status) {
162 return Err(error);
163 }
164 let edges = store
165 .code_repository_set_cross_edges(status.repository_set.set_id.clone())
166 .await
167 .map_err(storage_api_error)?;
168 let edge_index = OverlayEvidenceIndex::new(&edges);
169 let mut results = Vec::new();
170 let candidate_limit = per_member_candidate_limit(request.limit, status.members.len());
171 let highest_priority = status
172 .members
173 .iter()
174 .map(|member| member.member.priority)
175 .max()
176 .unwrap_or(0);
177 for member_status in &status.members {
178 let member = &member_status.member;
179 let selector = CodeRepositorySelector::new(
180 member.repository_alias.clone(),
181 member.resolved_commit_sha.clone(),
182 request.path_filters.clone(),
183 request.language_filters.clone(),
184 )
185 .map_err(|error| ApiError::invalid_argument(error.to_string()))?;
186 let member_query_kind =
187 repository_set_member_query_kind(&request, member_status, highest_priority);
188 let search_request = CodeRetrievalRequest::new(
189 request.query.clone(),
190 selector.clone(),
191 member_query_kind,
192 candidate_limit,
193 FreshnessPolicy::AllowStale,
194 )
195 .map_err(|error| ApiError::invalid_argument(error.to_string()))?;
196 let mut hits = store
197 .search_code_scope(member.source_scope.clone(), search_request)
198 .await
199 .map_err(storage_api_error)?;
200 if dependency_symbol_plan_needs_hybrid_fallback(&request, member_query_kind, &hits) {
201 let fallback_request = CodeRetrievalRequest::new(
202 request.query.clone(),
203 selector,
204 request.code_query_kind,
205 candidate_limit,
206 FreshnessPolicy::AllowStale,
207 )
208 .map_err(|error| ApiError::invalid_argument(error.to_string()))?;
209 hits = store
210 .search_code_scope(member.source_scope.clone(), fallback_request)
211 .await
212 .map_err(storage_api_error)?;
213 }
214 for hit in hits {
215 let overlay_evidence = edge_index.evidence_for_hit(&hit);
216 let score = repository_set_score(&hit, member_status, &overlay_evidence);
217 results.push(CodeRepositorySetQueryHit {
218 member: member.clone(),
219 hit,
220 overlay_evidence,
221 score,
222 });
223 }
224 }
225 apply_bridge_support_bonus(&mut results);
226 let truncated = dedupe_sort_truncate(&mut results, request.limit);
227 prune_returned_overlay_evidence(&mut results);
228 let degraded_reason = status.degraded_reason.clone().or_else(|| {
229 status
230 .overlay
231 .stale
232 .then(|| "repository set overlay is stale".to_owned())
233 });
234
235 Ok(CodeRepositorySetQueryResponse {
236 metadata: ApiMetadata::graph_only(&context, graph_version),
237 request,
238 status,
239 results,
240 truncated,
241 degraded_reason,
242 })
243 }
244
245 pub async fn code_repository_set_status(
247 &self,
248 set_alias: String,
249 context: RequestContext,
250 ) -> Result<CodeRepositorySetStatusResponse, ApiError> {
251 let store = self.store().await.map_err(storage_api_error)?;
252 let status = required_set_status(&store, &set_alias).await?;
253 let graph_version = store
254 .current_graph_version()
255 .await
256 .map_err(storage_api_error)?;
257
258 Ok(CodeRepositorySetStatusResponse {
259 metadata: ApiMetadata::graph_only(&context, graph_version),
260 status,
261 })
262 }
263
264 pub async fn refresh_code_repository_set(
266 &self,
267 set_alias: String,
268 context: RequestContext,
269 ) -> Result<CodeRepositorySetRefreshResponse, ApiError> {
270 let store = self.store().await.map_err(storage_api_error)?;
271 let summary = store
272 .refresh_code_repository_set_overlay(set_alias.clone(), now_millis())
273 .await
274 .map_err(storage_api_error)?;
275 let status = required_set_status(&store, &set_alias).await?;
276 let graph_version = store
277 .current_graph_version()
278 .await
279 .map_err(storage_api_error)?;
280
281 Ok(CodeRepositorySetRefreshResponse {
282 metadata: ApiMetadata::graph_only(&context, graph_version),
283 status,
284 summary: Some(summary),
285 task: None,
286 })
287 }
288
289 pub async fn start_code_repository_set_refresh(
291 &self,
292 set_alias: String,
293 context: RequestContext,
294 ) -> Result<CodeRepositorySetRefreshResponse, ApiError> {
295 let store = self.store().await.map_err(storage_api_error)?;
296 let status = required_set_status(&store, &set_alias).await?;
297 let fingerprint = repository_set_refresh_fingerprint(&status);
298 let task = store
299 .queue_code_repository_set_refresh_task(CodeRepositorySetRefreshTaskSeed {
300 set_id: status.repository_set.set_id.clone(),
301 set_alias: status.repository_set.alias.clone(),
302 input_fingerprint: fingerprint,
303 now_ms: now_millis(),
304 })
305 .await
306 .map_err(storage_api_error)?;
307 let graph_version = store
308 .current_graph_version()
309 .await
310 .map_err(storage_api_error)?;
311
312 Ok(CodeRepositorySetRefreshResponse {
313 metadata: ApiMetadata::graph_only(&context, graph_version),
314 status,
315 summary: None,
316 task: Some(task),
317 })
318 }
319
320 pub async fn run_code_repository_set_refresh_task_once(
322 &self,
323 task_id: Option<String>,
324 context: RequestContext,
325 ) -> Result<Option<crate::domain::CodeRepositorySetRefreshTaskRecord>, ApiError> {
326 let store = self.store().await.map_err(storage_api_error)?;
327 let lease_owner = format!("code-repository-set-refresh-worker-{}", std::process::id());
328 let Some(task) = store
329 .claim_code_repository_set_refresh_task(CodeRepositorySetRefreshTaskClaimRequest {
330 task_id,
331 lease_owner: lease_owner.clone(),
332 lease_duration_ms: REPOSITORY_SET_REFRESH_TASK_LEASE_MS,
333 max_attempts: REPOSITORY_SET_REFRESH_TASK_MAX_ATTEMPTS,
334 now_ms: now_millis(),
335 })
336 .await
337 .map_err(storage_api_error)?
338 else {
339 return Ok(None);
340 };
341 let result = self
342 .refresh_code_repository_set(task.set_alias.clone(), context)
343 .await;
344 match result {
345 Ok(_) => store
346 .complete_code_repository_set_refresh_task(CodeRepositorySetRefreshTaskCompletion {
347 task_id: task.task_id,
348 lease_owner,
349 attempt_count: task.attempt_count,
350 now_ms: now_millis(),
351 })
352 .await
353 .map(Some)
354 .map_err(storage_api_error),
355 Err(error) => {
356 let _ = store
357 .fail_code_repository_set_refresh_task(CodeRepositorySetRefreshTaskFailure {
358 task_id: task.task_id,
359 lease_owner,
360 attempt_count: task.attempt_count,
361 error_kind: "repository_set_overlay".to_owned(),
362 error_message: error.message.clone(),
363 retry_backoff_ms: REPOSITORY_SET_REFRESH_TASK_RETRY_BACKOFF_MS,
364 max_attempts: REPOSITORY_SET_REFRESH_TASK_MAX_ATTEMPTS,
365 now_ms: now_millis(),
366 })
367 .await;
368 Err(error)
369 }
370 }
371 }
372
373 pub(crate) async fn code_repository_set_member_scopes(
374 &self,
375 set_alias: String,
376 ) -> Result<Option<Vec<(String, String)>>, ApiError> {
377 let store = self.store().await.map_err(storage_api_error)?;
378 store
379 .code_repository_set_status(set_alias)
380 .await
381 .map(|status| {
382 status.map(|status| {
383 status
384 .members
385 .into_iter()
386 .map(|member| (member.member.repository_alias, member.member.source_scope))
387 .collect()
388 })
389 })
390 .map_err(storage_api_error)
391 }
392}
393
394pub(super) async fn required_set_status(
395 store: &std::sync::Arc<dyn crate::storage::KnowledgeStore>,
396 set_alias: &str,
397) -> Result<CodeRepositorySetStatus, ApiError> {
398 let mut status = store
399 .code_repository_set_status(set_alias.to_owned())
400 .await
401 .map_err(storage_api_error)?
402 .ok_or_else(|| {
403 ApiError::invalid_argument(format!(
404 "code repository set '{set_alias}' is not registered"
405 ))
406 })?;
407 refresh_moving_member_freshness(store, &mut status).await?;
408 refresh_repository_set_freshness(&mut status);
409
410 Ok(status)
411}
412
413async fn refresh_moving_member_freshness(
414 store: &std::sync::Arc<dyn crate::storage::KnowledgeStore>,
415 status: &mut CodeRepositorySetStatus,
416) -> Result<(), ApiError> {
417 for index in 0..status.members.len() {
418 let member = status.members[index].member.clone();
419 let Some(reason) = moving_member_stale_reason(store, &member).await? else {
420 continue;
421 };
422 status.members[index].stale = true;
423 status.members[index].freshness_state = "stale".to_owned();
424 status.members[index].degraded_reason = Some(reason);
425 }
426
427 Ok(())
428}
429
430async fn moving_member_stale_reason(
431 store: &std::sync::Arc<dyn crate::storage::KnowledgeStore>,
432 member: &crate::domain::CodeRepositorySetMember,
433) -> Result<Option<String>, ApiError> {
434 if !member_ref_tracks_repository(&member.ref_selector, &member.resolved_commit_sha) {
435 return Ok(None);
436 }
437 let repository = store
438 .code_repository_status(member.repository_id.clone())
439 .await
440 .map_err(storage_api_error)?
441 .ok_or_else(|| {
442 ApiError::invalid_argument(format!(
443 "code repository '{}' is not registered",
444 member.repository_alias
445 ))
446 })?;
447 let root_path = PathBuf::from(repository.root_path);
448 let ref_selector = member.ref_selector.clone();
449 let resolved =
450 tokio::task::spawn_blocking(move || resolve_repository_snapshot(root_path, &ref_selector))
451 .await
452 .map_err(|error| ApiError::storage_unavailable(error.to_string()))?;
453
454 match resolved {
455 Ok((current_commit, _)) if current_commit == member.resolved_commit_sha => Ok(None),
456 Ok((current_commit, _)) => Ok(Some(format!(
457 "repository set member '{}' ref '{}' now resolves to {}, not stored snapshot {}",
458 member.repository_alias,
459 member.ref_selector,
460 current_commit,
461 member.resolved_commit_sha
462 ))),
463 Err(error) => Ok(Some(format!(
464 "repository set member '{}' ref '{}' could not be resolved: {error}",
465 member.repository_alias, member.ref_selector
466 ))),
467 }
468}
469
470fn member_ref_tracks_repository(ref_selector: &str, resolved_commit_sha: &str) -> bool {
471 let ref_selector = ref_selector.trim();
472 !(ref_selector == resolved_commit_sha
473 || (is_git_oid_prefix(ref_selector) && resolved_commit_sha.starts_with(ref_selector)))
474}
475
476fn is_git_oid_prefix(value: &str) -> bool {
477 (7..=64).contains(&value.len()) && value.bytes().all(|byte| byte.is_ascii_hexdigit())
478}
479
480fn refresh_repository_set_freshness(status: &mut CodeRepositorySetStatus) {
481 let member_stale = status.members.iter().any(|member| member.stale);
482 if member_stale && !status.overlay.stale {
483 status.overlay.stale = true;
484 status.overlay.state = "overlay_stale".to_owned();
485 }
486 status.freshness_state = if status.members.is_empty() {
487 "incomplete"
488 } else if member_stale {
489 "stale"
490 } else if status.overlay.stale {
491 "overlay_stale"
492 } else {
493 "fresh"
494 }
495 .to_owned();
496 status.degraded_reason = status
497 .members
498 .iter()
499 .find_map(|member| member.degraded_reason.clone())
500 .or_else(|| status.overlay.degraded_reason.clone());
501}
502
503fn unfresh_set_error_for_wait_policy(
504 request: &CodeRepositorySetQueryRequest,
505 status: &CodeRepositorySetStatus,
506) -> Option<ApiError> {
507 if request.freshness_policy != FreshnessPolicy::WaitUntilFresh {
508 return None;
509 }
510 if status.members.is_empty() {
511 return Some(ApiError::invalid_argument(format!(
512 "code repository set '{}' has no members",
513 status.repository_set.alias
514 )));
515 }
516 if let Some(member) = status.members.iter().find(|member| member.stale) {
517 return Some(ApiError::invalid_argument(format!(
518 "code repository set '{}' member '{}' scope '{}' is stale",
519 status.repository_set.alias, member.member.repository_alias, member.member.source_scope
520 )));
521 }
522 if status.overlay.stale {
523 return Some(ApiError::invalid_argument(format!(
524 "code repository set '{}' overlay is stale; run repo-set refresh before querying with wait_until_fresh",
525 status.repository_set.alias
526 )));
527 }
528
529 None
530}
531
532fn repository_set_refresh_fingerprint(status: &CodeRepositorySetStatus) -> String {
533 let mut parts = vec![status.repository_set.set_id.clone()];
534 parts.extend(status.members.iter().map(|member| {
535 format!(
536 "{}:{}:{}:{}:{}",
537 member.member.repository_id,
538 member.member.source_scope,
539 member.member.resolved_commit_sha,
540 member.tree_hash,
541 member.stale
542 )
543 }));
544 parts.join("|")
545}
546
547fn merged_filters(left: &[String], right: &[String]) -> Vec<String> {
548 let mut merged = Vec::new();
549 for value in left.iter().chain(right.iter()) {
550 if !merged.contains(value) {
551 merged.push(value.clone());
552 }
553 }
554
555 merged
556}
557
558fn now_millis() -> u64 {
559 std::time::SystemTime::now()
560 .duration_since(std::time::UNIX_EPOCH)
561 .map_or(0, |duration| {
562 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
563 })
564}
565
566async fn run_blocking_code<T, F>(operation: F) -> Result<T, ApiError>
567where
568 T: Send + 'static,
569 F: FnOnce() -> Result<T, CodeIndexError> + Send + 'static,
570{
571 tokio::task::spawn_blocking(operation)
572 .await
573 .map_err(|error| ApiError::storage_unavailable(error.to_string()))?
574 .map_err(code_api_error)
575}
576
577fn code_api_error(error: CodeIndexError) -> ApiError {
578 match error {
579 CodeIndexError::InvalidInput(message) => ApiError::invalid_argument(message),
580 CodeIndexError::Git { .. } | CodeIndexError::Io(_) | CodeIndexError::TreeSitter(_) => {
581 ApiError::storage_unavailable(error.to_string())
582 }
583 }
584}
585
586pub(super) fn storage_api_error(error: StorageError) -> ApiError {
587 match error {
588 StorageError::InvalidInput(message) => ApiError::invalid_argument(message),
589 other => ApiError::storage_unavailable(other.to_string()),
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596 use crate::{
597 api::ErrorKind,
598 domain::CodeRepositorySetMemberStatus,
599 domain::{CodeRepositorySet, CodeRepositorySetMember, CodeRepositorySetOverlayStatus},
600 storage::SqliteGraphStore,
601 };
602 use std::sync::Arc;
603
604 #[test]
605 fn helper_policy_reports_wait_until_fresh_blockers() {
606 let request = CodeRepositorySetQueryRequest::new(
607 "workspace",
608 "serve",
609 crate::domain::CodeQueryKind::Definition,
610 5,
611 FreshnessPolicy::WaitUntilFresh,
612 Vec::new(),
613 Vec::new(),
614 )
615 .expect("request should validate");
616 let empty = status_with_members(Vec::new(), overlay(true));
617 assert!(
618 unfresh_set_error_for_wait_policy(&request, &empty)
619 .expect("empty set should block")
620 .message
621 .contains("has no members")
622 );
623
624 let mut stale_member = member_status("app", "scope-app", 0);
625 stale_member.stale = true;
626 let stale_status = status_with_members(vec![stale_member], overlay(false));
627 assert!(
628 unfresh_set_error_for_wait_policy(&request, &stale_status)
629 .expect("stale member should block")
630 .message
631 .contains("member 'app'")
632 );
633
634 let overlay_status =
635 status_with_members(vec![member_status("app", "scope-app", 0)], overlay(true));
636 assert!(
637 unfresh_set_error_for_wait_policy(&request, &overlay_status)
638 .expect("stale overlay should block")
639 .message
640 .contains("overlay is stale")
641 );
642
643 let allow_stale = CodeRepositorySetQueryRequest::new(
644 "workspace",
645 "serve",
646 crate::domain::CodeQueryKind::Definition,
647 5,
648 FreshnessPolicy::AllowStale,
649 Vec::new(),
650 Vec::new(),
651 )
652 .expect("request should validate");
653 assert!(unfresh_set_error_for_wait_policy(&allow_stale, &overlay_status).is_none());
654 }
655
656 #[test]
657 fn helper_fingerprint_and_error_mapping_are_stable() {
658 let status = status_with_members(
659 vec![
660 member_status("app", "scope-app", 1),
661 member_status("svc", "scope-svc", 0),
662 ],
663 overlay(false),
664 );
665 let fingerprint = repository_set_refresh_fingerprint(&status);
666 assert!(fingerprint.contains("set-workspace"));
667 assert!(fingerprint.contains("repo-app:scope-app:commit-scope-app:tree-scope-app:false"));
668 assert_eq!(
669 merged_filters(&["src".to_owned()], &["src".to_owned(), "tests".to_owned()]),
670 ["src".to_owned(), "tests".to_owned()]
671 );
672 assert_eq!(
673 code_api_error(CodeIndexError::InvalidInput("bad ref".to_owned())).error_kind,
674 ErrorKind::InvalidArgument
675 );
676 assert_eq!(
677 code_api_error(CodeIndexError::Io(std::io::Error::other("disk"))).error_kind,
678 ErrorKind::StorageUnavailable
679 );
680 assert_eq!(
681 storage_api_error(StorageError::InvalidInput("bad storage".to_owned())).error_kind,
682 ErrorKind::InvalidArgument
683 );
684 }
685
686 #[tokio::test]
687 async fn helper_required_status_reports_missing_sets() {
688 let store: Arc<dyn crate::storage::KnowledgeStore> =
689 Arc::new(SqliteGraphStore::open_in_memory().expect("store should open"));
690 let error = required_set_status(&store, "missing")
691 .await
692 .expect_err("missing set should fail");
693
694 assert_eq!(error.error_kind, ErrorKind::InvalidArgument);
695 assert!(error.message.contains("is not registered"));
696 }
697
698 fn status_with_members(
699 members: Vec<CodeRepositorySetMemberStatus>,
700 overlay: CodeRepositorySetOverlayStatus,
701 ) -> CodeRepositorySetStatus {
702 CodeRepositorySetStatus {
703 repository_set: CodeRepositorySet {
704 set_id: "set-workspace".to_owned(),
705 alias: "workspace".to_owned(),
706 description: None,
707 default_ref_policy_json: "{\"default_ref\":\"HEAD\"}".to_owned(),
708 created_at_ms: 1,
709 updated_at_ms: 1,
710 },
711 members,
712 overlay,
713 freshness_state: "fresh".to_owned(),
714 degraded_reason: None,
715 }
716 }
717
718 fn member_status(
719 repository_alias: &str,
720 source_scope: &str,
721 priority: i32,
722 ) -> CodeRepositorySetMemberStatus {
723 CodeRepositorySetMemberStatus {
724 member: CodeRepositorySetMember {
725 set_id: "set-workspace".to_owned(),
726 repository_id: format!("repo-{repository_alias}"),
727 repository_alias: repository_alias.to_owned(),
728 ref_selector: "HEAD".to_owned(),
729 resolved_commit_sha: format!("commit-{source_scope}"),
730 source_scope: source_scope.to_owned(),
731 path_filters: vec!["src".to_owned()],
732 language_filters: vec!["rust".to_owned()],
733 priority,
734 },
735 tree_hash: format!("tree-{source_scope}"),
736 freshness_state: "fresh".to_owned(),
737 stale: false,
738 indexed_file_count: 1,
739 symbol_count: 1,
740 reference_count: 0,
741 chunk_count: 1,
742 degraded_reason: None,
743 }
744 }
745
746 fn overlay(stale: bool) -> CodeRepositorySetOverlayStatus {
747 CodeRepositorySetOverlayStatus {
748 state: if stale { "overlay_stale" } else { "fresh" }.to_owned(),
749 stale,
750 edge_count: usize::from(!stale),
751 refreshed_at_ms: (!stale).then_some(10),
752 degraded_reason: None,
753 }
754 }
755}