1use serde::{Deserialize, Serialize};
2
3use super::{
4 CodeParseStatus, CodeParseStatusCounts, DomainError, FreshnessPolicy, error::required_text,
5};
6
7pub fn code_snapshot_scope_id(
9 repository_id: &str,
10 tree_hash: &str,
11 path_filters: &[String],
12 language_filters: &[String],
13) -> String {
14 let mut input = Vec::new();
15 append_hash_part(&mut input, "git_snapshot");
16 append_hash_part(&mut input, repository_id);
17 append_hash_part(&mut input, tree_hash);
18 append_hash_list(&mut input, path_filters);
19 append_hash_list(&mut input, language_filters);
20
21 format!("git_snapshot:{:016x}", stable_hash64(&input))
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct RepositoryCodeRange {
27 pub start: u32,
28 pub end: u32,
29}
30
31impl RepositoryCodeRange {
32 pub fn new(field: &'static str, start: usize, end: usize) -> Result<Self, DomainError> {
34 if end < start {
35 return Err(DomainError::invalid(
36 field,
37 "end must be greater than or equal to start",
38 ));
39 }
40
41 Ok(Self {
42 start: checked_u32(field, start)?,
43 end: checked_u32(field, end)?,
44 })
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct CodeRepositoryRegistration {
51 pub repository_id: String,
52 pub alias: String,
53 pub root_path: String,
54 pub path_filters: Vec<String>,
55 pub language_filters: Vec<String>,
56}
57
58impl CodeRepositoryRegistration {
59 pub fn new(
61 repository_id: impl Into<String>,
62 alias: impl Into<String>,
63 root_path: impl Into<String>,
64 path_filters: Vec<String>,
65 language_filters: Vec<String>,
66 ) -> Result<Self, DomainError> {
67 Ok(Self {
68 repository_id: required_text("repository_id", repository_id)?,
69 alias: required_text("alias", alias)?,
70 root_path: required_text("root_path", root_path)?,
71 path_filters: normalize_filter_list("path_filter", path_filters)?,
72 language_filters: normalize_filter_list("language_filter", language_filters)?,
73 })
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct CodeRepositorySelector {
80 pub repository: String,
81 pub ref_selector: String,
82 pub path_filters: Vec<String>,
83 pub language_filters: Vec<String>,
84}
85
86impl CodeRepositorySelector {
87 pub fn new(
89 repository: impl Into<String>,
90 ref_selector: impl Into<String>,
91 path_filters: Vec<String>,
92 language_filters: Vec<String>,
93 ) -> Result<Self, DomainError> {
94 Ok(Self {
95 repository: required_text("repository", repository)?,
96 ref_selector: required_text("ref_selector", ref_selector)?,
97 path_filters: normalize_filter_list("path_filter", path_filters)?,
98 language_filters: normalize_filter_list("language_filter", language_filters)?,
99 })
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "snake_case")]
106pub enum CodeIndexMode {
107 Full,
108 Incremental { base_ref: String, head_ref: String },
109 WorktreeOverlay,
110}
111
112impl CodeIndexMode {
113 pub fn incremental(
115 base_ref: impl Into<String>,
116 head_ref: impl Into<String>,
117 ) -> Result<Self, DomainError> {
118 Ok(Self::Incremental {
119 base_ref: required_text("base_ref", base_ref)?,
120 head_ref: required_text("head_ref", head_ref)?,
121 })
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct CodeIndexRequest {
128 pub repository: CodeRepositorySelector,
129 pub mode: CodeIndexMode,
130 pub freshness_policy: FreshnessPolicy,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum CodeQueryKind {
137 Hybrid,
138 Symbol,
139 Definition,
140 References,
141 Callers,
142 Callees,
143 Imports,
144 Impact,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149pub struct CodeRetrievalRequest {
150 pub query: String,
151 pub repository: CodeRepositorySelector,
152 pub code_query_kind: CodeQueryKind,
153 pub limit: usize,
154 pub freshness_policy: FreshnessPolicy,
155}
156
157impl CodeRetrievalRequest {
158 pub fn new(
160 query: impl Into<String>,
161 repository: CodeRepositorySelector,
162 code_query_kind: CodeQueryKind,
163 limit: usize,
164 freshness_policy: FreshnessPolicy,
165 ) -> Result<Self, DomainError> {
166 let limit = match limit {
167 1..=50 => limit,
168 0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
169 _ => return Err(DomainError::invalid("limit", "must be 50 or less")),
170 };
171
172 Ok(Self {
173 query: required_text("query", query)?,
174 repository,
175 code_query_kind,
176 limit,
177 freshness_policy,
178 })
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub struct CodeImpactRequest {
185 pub repository: CodeRepositorySelector,
186 pub base_ref: String,
187 pub head_ref: String,
188 pub limit: usize,
189}
190
191impl CodeImpactRequest {
192 pub fn new(
194 repository: CodeRepositorySelector,
195 base_ref: impl Into<String>,
196 head_ref: impl Into<String>,
197 limit: usize,
198 ) -> Result<Self, DomainError> {
199 let limit = match limit {
200 1..=100 => limit,
201 0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
202 _ => return Err(DomainError::invalid("limit", "must be 100 or less")),
203 };
204
205 Ok(Self {
206 repository,
207 base_ref: required_text("base_ref", base_ref)?,
208 head_ref: required_text("head_ref", head_ref)?,
209 limit,
210 })
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(rename_all = "snake_case")]
217pub enum CodeRetrievalLayer {
218 Lexical,
219 Symbol,
220 Definition,
221 Reference,
222 CallGraph,
223 ImportGraph,
224 Impact,
225 TextFallback,
226}
227
228impl CodeRetrievalLayer {
229 pub const fn as_str(self) -> &'static str {
231 match self {
232 Self::Lexical => "lexical",
233 Self::Symbol => "symbol",
234 Self::Definition => "definition",
235 Self::Reference => "reference",
236 Self::CallGraph => "call_graph",
237 Self::ImportGraph => "import_graph",
238 Self::Impact => "impact",
239 Self::TextFallback => "text_fallback",
240 }
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246pub struct CodeRepositoryStatus {
247 pub repository_id: String,
248 pub alias: String,
249 pub root_path: String,
250 pub path_filters: Vec<String>,
251 pub language_filters: Vec<String>,
252 #[serde(skip_serializing_if = "Option::is_none")]
253 pub last_indexed_scope_id: Option<String>,
254 pub last_indexed_commit: Option<String>,
255 pub tree_hash: Option<String>,
256 pub state: String,
257 pub indexed_file_count: usize,
258 pub symbol_count: usize,
259 pub reference_count: usize,
260 pub chunk_count: usize,
261 pub stale: bool,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 pub degraded_reason: Option<String>,
264}
265
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268pub struct RepositoryCodeFileRecord {
269 pub repository_id: String,
270 pub source_scope: String,
271 pub file_id: String,
272 pub path: String,
273 pub language_id: String,
274 pub blob_hash: String,
275 pub byte_len: usize,
276 pub line_count: usize,
277 pub parse_status: CodeParseStatus,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub degraded_reason: Option<String>,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
284pub struct CodeFileFingerprint {
285 pub path: String,
286 pub blob_hash: String,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
291pub struct RepositoryCodeSymbolRecord {
292 pub repository_id: String,
293 pub source_scope: String,
294 pub symbol_snapshot_id: String,
295 pub canonical_symbol_id: String,
296 pub file_id: String,
297 pub path: String,
298 pub language_id: String,
299 pub name: String,
300 pub qualified_name: String,
301 pub kind: String,
302 pub signature: String,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub doc_comment: Option<String>,
305 pub byte_range: RepositoryCodeRange,
306 pub line_range: RepositoryCodeRange,
307}
308
309#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311pub struct RepositoryCodeReferenceRecord {
312 pub repository_id: String,
313 pub source_scope: String,
314 pub reference_id: String,
315 pub file_id: String,
316 pub path: String,
317 pub name: String,
318 pub kind: String,
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub target_symbol_snapshot_id: Option<String>,
321 #[serde(skip_serializing_if = "Option::is_none")]
322 pub target_hint: Option<String>,
323 pub resolution_state: String,
324 pub confidence_basis_points: u16,
325 pub confidence_tier: String,
326 pub byte_range: RepositoryCodeRange,
327 pub line_range: RepositoryCodeRange,
328}
329
330#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
332pub struct CodeImportRecord {
333 pub repository_id: String,
334 pub source_scope: String,
335 pub import_id: String,
336 pub file_id: String,
337 pub path: String,
338 pub module: String,
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub target_hint: Option<String>,
341 pub resolution_state: String,
342 pub confidence_basis_points: u16,
343 pub confidence_tier: String,
344 pub line_range: RepositoryCodeRange,
345}
346
347#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
349pub struct CodeCallRecord {
350 pub repository_id: String,
351 pub source_scope: String,
352 pub call_id: String,
353 pub file_id: String,
354 pub path: String,
355 pub caller_symbol_snapshot_id: Option<String>,
356 pub caller_name: Option<String>,
357 #[serde(skip_serializing_if = "Option::is_none")]
358 pub callee_symbol_snapshot_id: Option<String>,
359 pub callee_name: String,
360 #[serde(skip_serializing_if = "Option::is_none")]
361 pub target_hint: Option<String>,
362 pub resolution_state: String,
363 pub confidence_basis_points: u16,
364 pub confidence_tier: String,
365 pub line_range: RepositoryCodeRange,
366}
367
368#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
370pub struct RepositoryCodeChunkRecord {
371 pub repository_id: String,
372 pub source_scope: String,
373 pub chunk_id: String,
374 pub file_id: String,
375 pub path: String,
376 pub language_id: String,
377 pub content: String,
378 pub byte_range: RepositoryCodeRange,
379 pub line_range: RepositoryCodeRange,
380 pub symbol_snapshot_id: Option<String>,
381}
382
383#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
385pub struct CodeFileDiagnostic {
386 pub repository_id: String,
387 pub source_scope: String,
388 pub path: String,
389 pub parse_status: CodeParseStatus,
390 pub message: String,
391}
392
393#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
395pub struct CodePathTombstone {
396 pub repository_id: String,
397 pub source_scope: String,
398 pub old_path: String,
399 pub new_path: Option<String>,
400 pub base_ref: String,
401 pub head_ref: String,
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
406pub struct CodeIndexSnapshot {
407 pub repository_id: String,
408 pub source_scope: String,
409 #[serde(skip_serializing_if = "Option::is_none")]
410 pub base_resolved_commit_sha: Option<String>,
411 pub resolved_commit_sha: String,
412 pub tree_hash: String,
413 pub path_filters: Vec<String>,
414 pub language_filters: Vec<String>,
415 pub full_replace: bool,
416 pub changed_path_count: usize,
417 pub skipped_unchanged_count: usize,
418 pub deleted_paths: Vec<String>,
419 pub tombstones: Vec<CodePathTombstone>,
420 pub files: Vec<RepositoryCodeFileRecord>,
421 pub symbols: Vec<RepositoryCodeSymbolRecord>,
422 pub references: Vec<RepositoryCodeReferenceRecord>,
423 pub imports: Vec<CodeImportRecord>,
424 pub calls: Vec<CodeCallRecord>,
425 pub chunks: Vec<RepositoryCodeChunkRecord>,
426 pub diagnostics: Vec<CodeFileDiagnostic>,
427}
428
429#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
431pub struct CodeIndexResourceBudget {
432 pub max_files_per_batch: usize,
433 pub max_bytes_per_batch: usize,
434 pub max_rows_per_batch: usize,
435}
436
437impl CodeIndexResourceBudget {
438 pub const DEFAULT_MAX_FILES_PER_BATCH: usize = 256;
439 pub const DEFAULT_MAX_BYTES_PER_BATCH: usize = 16 * 1024 * 1024;
440 pub const DEFAULT_MAX_ROWS_PER_BATCH: usize = 50_000;
441
442 pub fn new(
444 max_files_per_batch: usize,
445 max_bytes_per_batch: usize,
446 max_rows_per_batch: usize,
447 ) -> Result<Self, DomainError> {
448 if max_files_per_batch == 0 {
449 return Err(DomainError::invalid(
450 "max_files_per_batch",
451 "must be greater than zero",
452 ));
453 }
454 if max_bytes_per_batch == 0 {
455 return Err(DomainError::invalid(
456 "max_bytes_per_batch",
457 "must be greater than zero",
458 ));
459 }
460 if max_rows_per_batch == 0 {
461 return Err(DomainError::invalid(
462 "max_rows_per_batch",
463 "must be greater than zero",
464 ));
465 }
466
467 Ok(Self {
468 max_files_per_batch,
469 max_bytes_per_batch,
470 max_rows_per_batch,
471 })
472 }
473}
474
475impl Default for CodeIndexResourceBudget {
476 fn default() -> Self {
477 Self {
478 max_files_per_batch: Self::DEFAULT_MAX_FILES_PER_BATCH,
479 max_bytes_per_batch: Self::DEFAULT_MAX_BYTES_PER_BATCH,
480 max_rows_per_batch: Self::DEFAULT_MAX_ROWS_PER_BATCH,
481 }
482 }
483}
484
485#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
487pub struct CodeIndexSession {
488 pub repository_id: String,
489 pub source_scope: String,
490 #[serde(skip_serializing_if = "Option::is_none")]
491 pub base_resolved_commit_sha: Option<String>,
492 pub resolved_commit_sha: String,
493 pub tree_hash: String,
494 pub path_filters: Vec<String>,
495 pub language_filters: Vec<String>,
496 pub full_replace: bool,
497 pub total_path_count: usize,
498 pub changed_path_count: usize,
499 pub skipped_unchanged_count: usize,
500 pub deleted_paths: Vec<String>,
501 pub tombstones: Vec<CodePathTombstone>,
502 pub resource_budget: CodeIndexResourceBudget,
503}
504
505#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
507pub struct CodeIndexBatch {
508 pub repository_id: String,
509 pub source_scope: String,
510 pub batch_index: usize,
511 pub parsed_byte_count: usize,
512 pub files: Vec<RepositoryCodeFileRecord>,
513 pub symbols: Vec<RepositoryCodeSymbolRecord>,
514 pub references: Vec<RepositoryCodeReferenceRecord>,
515 pub imports: Vec<CodeImportRecord>,
516 pub chunks: Vec<RepositoryCodeChunkRecord>,
517 pub diagnostics: Vec<CodeFileDiagnostic>,
518}
519
520impl CodeIndexBatch {
521 pub fn row_count(&self) -> usize {
523 self.files
524 .len()
525 .saturating_add(self.symbols.len())
526 .saturating_add(self.references.len())
527 .saturating_add(self.imports.len())
528 .saturating_add(self.chunks.len())
529 .saturating_add(self.diagnostics.len())
530 }
531}
532
533#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
535pub struct CodeIndexCheckpoint {
536 pub repository_id: String,
537 pub source_scope: String,
538 pub state: String,
539 pub total_path_count: usize,
540 pub parsed_file_count: usize,
541 pub committed_file_count: usize,
542 pub committed_symbol_count: usize,
543 pub committed_reference_count: usize,
544 pub committed_chunk_count: usize,
545 pub batch_count: usize,
546 #[serde(skip_serializing_if = "Option::is_none")]
547 pub last_path: Option<String>,
548 pub resource_budget: CodeIndexResourceBudget,
549}
550
551#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
553#[serde(rename_all = "snake_case")]
554pub enum CodeIndexTaskState {
555 Queued,
556 Running,
557 Succeeded,
558 Retrying,
559 Failed,
560 DeadLetter,
561 Cancelled,
562}
563
564impl CodeIndexTaskState {
565 pub const fn as_str(self) -> &'static str {
567 match self {
568 Self::Queued => "queued",
569 Self::Running => "running",
570 Self::Succeeded => "succeeded",
571 Self::Retrying => "retrying",
572 Self::Failed => "failed",
573 Self::DeadLetter => "dead_letter",
574 Self::Cancelled => "cancelled",
575 }
576 }
577
578 pub fn parse(value: &str) -> Result<Self, DomainError> {
580 match value {
581 "queued" => Ok(Self::Queued),
582 "running" => Ok(Self::Running),
583 "succeeded" => Ok(Self::Succeeded),
584 "retrying" => Ok(Self::Retrying),
585 "failed" => Ok(Self::Failed),
586 "dead_letter" => Ok(Self::DeadLetter),
587 "cancelled" => Ok(Self::Cancelled),
588 _ => Err(DomainError::invalid(
589 "code_index_task_state",
590 "unknown code index task state",
591 )),
592 }
593 }
594
595 pub const fn is_unfinished(self) -> bool {
597 matches!(self, Self::Queued | Self::Running | Self::Retrying)
598 }
599}
600
601#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
603pub struct CodeIndexTaskRecord {
604 pub task_id: String,
605 pub repository_id: String,
606 pub alias: String,
607 pub ref_selector: String,
608 pub resolved_commit_sha: String,
609 pub tree_hash: String,
610 pub source_scope: String,
611 pub path_filters: Vec<String>,
612 pub language_filters: Vec<String>,
613 pub mode: CodeIndexMode,
614 pub state: CodeIndexTaskState,
615 #[serde(skip_serializing_if = "Option::is_none")]
616 pub lease_owner: Option<String>,
617 #[serde(skip_serializing_if = "Option::is_none")]
618 pub lease_expires_at_ms: Option<u64>,
619 pub attempt_count: u32,
620 pub next_retry_at_ms: u64,
621 pub input_fingerprint: String,
622 pub resource_budget: CodeIndexResourceBudget,
623 pub payload_json: String,
624 #[serde(skip_serializing_if = "Option::is_none")]
625 pub last_error_kind: Option<String>,
626 #[serde(skip_serializing_if = "Option::is_none")]
627 pub last_error_message: Option<String>,
628 pub created_at_ms: u64,
629 pub updated_at_ms: u64,
630}
631
632#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
634pub struct CodeScopeRetentionSummary {
635 pub repository_id: String,
636 pub retained_scope_count: usize,
637 pub prunable_scope_count: usize,
638 pub pruned_scope_count: usize,
639 pub retained_scopes: Vec<String>,
640 pub prunable_scopes: Vec<String>,
641 pub pruned_scopes: Vec<String>,
642}
643
644#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
646pub struct CodeIndexProgressSummary {
647 pub git_file_count: usize,
648 pub blob_read_count: usize,
649 pub parsed_file_count: usize,
650 pub sqlite_write_count: usize,
651 pub skipped_file_count: usize,
652 pub degraded_file_count: usize,
653 pub batch_count: usize,
654 pub checkpoint_file_count: usize,
655 pub resource_budget: CodeIndexResourceBudget,
656}
657
658#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
660pub struct CodeIndexSummary {
661 pub repository_id: String,
662 pub source_scope: String,
663 pub resolved_commit_sha: String,
664 pub tree_hash: String,
665 pub indexed_file_count: usize,
666 pub changed_path_count: usize,
667 pub skipped_unchanged_count: usize,
668 pub deleted_path_count: usize,
669 pub symbol_count: usize,
670 pub reference_count: usize,
671 pub chunk_count: usize,
672 pub degraded_file_count: usize,
673 pub progress: CodeIndexProgressSummary,
674}
675
676#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
678pub struct CodeRepositoryLanguagePreview {
679 pub language_id: String,
680 pub file_count: usize,
681 pub byte_count: usize,
682}
683
684#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
686pub struct CodeRepositoryLargestFile {
687 pub path: String,
688 pub byte_count: usize,
689}
690
691#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
693pub struct CodeRepositoryExcludedPath {
694 pub path: String,
695 pub reason: String,
696}
697
698#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
700pub struct CodeRepositoryScopePreview {
701 pub repository_id: String,
702 pub alias: String,
703 pub requested_ref: String,
704 pub resolved_commit_sha: String,
705 pub tree_hash: String,
706 pub selected_file_count: usize,
707 pub selected_byte_count: usize,
708 pub unsupported_file_count: usize,
709 pub generated_or_heavy_file_count: usize,
710 pub expected_degraded_file_count: usize,
711 pub language_distribution: Vec<CodeRepositoryLanguagePreview>,
712 pub largest_files: Vec<CodeRepositoryLargestFile>,
713 pub excluded_paths: Vec<CodeRepositoryExcludedPath>,
714}
715
716#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
718pub struct CodeRepositoryTotals {
719 pub repository_count: usize,
720 pub indexed_file_count: usize,
721 pub symbol_count: usize,
722 pub reference_count: usize,
723 pub chunk_count: usize,
724 pub degraded_file_count: usize,
725 pub parse_status_counts: CodeParseStatusCounts,
726}
727
728#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
730pub struct CodeRepositoryLatencySample {
731 pub query: String,
732 pub kind: CodeQueryKind,
733 pub result_count: usize,
734 pub duration_ms: u64,
735}
736
737#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
739pub struct CodeRepositoryReport {
740 pub repository_id: String,
741 pub alias: String,
742 pub root_path: String,
743 pub path_filters: Vec<String>,
744 pub language_filters: Vec<String>,
745 pub resolved_commit_sha: Option<String>,
746 pub tree_hash: Option<String>,
747 pub indexed_file_count: usize,
748 pub symbol_count: usize,
749 pub reference_count: usize,
750 pub chunk_count: usize,
751 pub degraded_file_count: usize,
752 pub resolved_edge_count: usize,
753 pub ambiguous_edge_count: usize,
754 pub unresolved_edge_count: usize,
755 pub degradation_summary: Vec<String>,
756 pub representative_queries: Vec<String>,
757 pub latency_samples: Vec<CodeRepositoryLatencySample>,
758 pub freshness_state: String,
759}
760
761#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
763pub struct CodeImpactPathGroups {
764 pub in_scope_changed_paths: Vec<String>,
765 pub out_of_scope_changed_paths: Vec<String>,
766}
767
768#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
770pub struct CodeRetrievalHit {
771 pub repository_id: String,
772 pub scope_id: String,
773 pub resolved_commit_sha: String,
774 pub tree_hash: String,
775 pub path: String,
776 pub language_id: String,
777 pub byte_range: RepositoryCodeRange,
778 pub line_range: RepositoryCodeRange,
779 pub symbol_snapshot_id: Option<String>,
780 #[serde(skip_serializing_if = "Option::is_none")]
781 pub canonical_symbol_id: Option<String>,
782 pub file_id: Option<String>,
783 pub retrieval_layers: Vec<CodeRetrievalLayer>,
784 pub index_versions: Vec<String>,
785 pub stale: bool,
786 #[serde(skip_serializing_if = "Option::is_none")]
787 pub degraded_reason: Option<String>,
788 #[serde(skip_serializing_if = "Option::is_none")]
789 pub edge_kind: Option<String>,
790 #[serde(skip_serializing_if = "Option::is_none")]
791 pub edge_resolution_state: Option<String>,
792 #[serde(skip_serializing_if = "Option::is_none")]
793 pub edge_target_hint: Option<String>,
794 #[serde(skip_serializing_if = "Option::is_none")]
795 pub edge_confidence_basis_points: Option<u16>,
796 #[serde(skip_serializing_if = "Option::is_none")]
797 pub edge_confidence_tier: Option<String>,
798 pub score: f64,
799 pub excerpt: String,
800}
801
802fn normalize_filter_list(
803 field: &'static str,
804 values: Vec<String>,
805) -> Result<Vec<String>, DomainError> {
806 let mut normalized = Vec::new();
807 for value in values {
808 let value = required_text(field, value)?;
809 if !normalized.contains(&value) {
810 normalized.push(value);
811 }
812 }
813
814 Ok(normalized)
815}
816
817fn checked_u32(field: &'static str, value: usize) -> Result<u32, DomainError> {
818 u32::try_from(value).map_err(|_| DomainError::invalid(field, "must fit in u32"))
819}
820
821fn append_hash_list(input: &mut Vec<u8>, values: &[String]) {
822 input.extend_from_slice(&(values.len() as u64).to_le_bytes());
823 for value in values {
824 append_hash_part(input, value);
825 }
826}
827
828fn append_hash_part(input: &mut Vec<u8>, value: &str) {
829 input.extend_from_slice(&(value.len() as u64).to_le_bytes());
830 input.extend_from_slice(value.as_bytes());
831}
832
833fn stable_hash64(bytes: &[u8]) -> u64 {
834 const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
835 const FNV_PRIME: u64 = 0x100000001b3;
836
837 let mut hash = FNV_OFFSET_BASIS;
838 for byte in bytes {
839 hash ^= u64::from(*byte);
840 hash = hash.wrapping_mul(FNV_PRIME);
841 }
842
843 hash
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
849
850 #[test]
851 fn selector_trims_and_deduplicates_filters() {
852 let selector = CodeRepositorySelector::new(
853 " repo ",
854 " HEAD ",
855 vec!["src".to_owned(), " src ".to_owned()],
856 vec!["rust".to_owned(), "rust".to_owned()],
857 )
858 .expect("selector should validate");
859
860 assert_eq!(selector.repository, "repo");
861 assert_eq!(selector.ref_selector, "HEAD");
862 assert_eq!(selector.path_filters, ["src"]);
863 assert_eq!(selector.language_filters, ["rust"]);
864 }
865
866 #[test]
867 fn snapshot_scope_id_tracks_tree_and_filters() {
868 let scope = code_snapshot_scope_id(
869 "repo-1",
870 "tree-a",
871 &["src".to_owned()],
872 &["rust".to_owned()],
873 );
874 let same = code_snapshot_scope_id(
875 "repo-1",
876 "tree-a",
877 &["src".to_owned()],
878 &["rust".to_owned()],
879 );
880 let different_tree = code_snapshot_scope_id(
881 "repo-1",
882 "tree-b",
883 &["src".to_owned()],
884 &["rust".to_owned()],
885 );
886
887 assert_eq!(scope, same);
888 assert_ne!(scope, different_tree);
889 assert!(scope.starts_with("git_snapshot:"));
890 }
891
892 #[test]
893 fn retrieval_request_rejects_unbounded_limits() {
894 let selector = CodeRepositorySelector::new("repo", "HEAD", Vec::new(), Vec::new())
895 .expect("selector should validate");
896 let error = CodeRetrievalRequest::new(
897 "symbol",
898 selector,
899 CodeQueryKind::Hybrid,
900 51,
901 FreshnessPolicy::AllowStale,
902 )
903 .expect_err("large limit should fail");
904
905 assert_eq!(error.field, "limit");
906 }
907
908 #[test]
909 fn code_ranges_must_be_ordered() {
910 let error = RepositoryCodeRange::new("line_range", 3, 2).expect_err("range should fail");
911
912 assert_eq!(error.field, "line_range");
913 }
914
915 #[test]
916 fn default_code_index_budget_batches_more_small_files_without_raising_row_or_byte_caps() {
917 let budget = CodeIndexResourceBudget::default();
918
919 assert_eq!(budget.max_files_per_batch, 256);
920 assert_eq!(
921 budget.max_bytes_per_batch,
922 CodeIndexResourceBudget::DEFAULT_MAX_BYTES_PER_BATCH
923 );
924 assert_eq!(
925 budget.max_rows_per_batch,
926 CodeIndexResourceBudget::DEFAULT_MAX_ROWS_PER_BATCH
927 );
928 }
929}