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 = 128;
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, Default, PartialEq, Eq, Serialize, Deserialize)]
553pub struct CodeIndexProgressSummary {
554 pub git_file_count: usize,
555 pub blob_read_count: usize,
556 pub parsed_file_count: usize,
557 pub sqlite_write_count: usize,
558 pub skipped_file_count: usize,
559 pub degraded_file_count: usize,
560 pub batch_count: usize,
561 pub checkpoint_file_count: usize,
562 pub resource_budget: CodeIndexResourceBudget,
563}
564
565#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
567pub struct CodeIndexSummary {
568 pub repository_id: String,
569 pub source_scope: String,
570 pub resolved_commit_sha: String,
571 pub tree_hash: String,
572 pub indexed_file_count: usize,
573 pub changed_path_count: usize,
574 pub skipped_unchanged_count: usize,
575 pub deleted_path_count: usize,
576 pub symbol_count: usize,
577 pub reference_count: usize,
578 pub chunk_count: usize,
579 pub degraded_file_count: usize,
580 pub progress: CodeIndexProgressSummary,
581}
582
583#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
585pub struct CodeRepositoryLanguagePreview {
586 pub language_id: String,
587 pub file_count: usize,
588 pub byte_count: usize,
589}
590
591#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
593pub struct CodeRepositoryLargestFile {
594 pub path: String,
595 pub byte_count: usize,
596}
597
598#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
600pub struct CodeRepositoryExcludedPath {
601 pub path: String,
602 pub reason: String,
603}
604
605#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
607pub struct CodeRepositoryScopePreview {
608 pub repository_id: String,
609 pub alias: String,
610 pub requested_ref: String,
611 pub resolved_commit_sha: String,
612 pub tree_hash: String,
613 pub selected_file_count: usize,
614 pub selected_byte_count: usize,
615 pub unsupported_file_count: usize,
616 pub generated_or_heavy_file_count: usize,
617 pub expected_degraded_file_count: usize,
618 pub language_distribution: Vec<CodeRepositoryLanguagePreview>,
619 pub largest_files: Vec<CodeRepositoryLargestFile>,
620 pub excluded_paths: Vec<CodeRepositoryExcludedPath>,
621}
622
623#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
625pub struct CodeRepositoryTotals {
626 pub repository_count: usize,
627 pub indexed_file_count: usize,
628 pub symbol_count: usize,
629 pub reference_count: usize,
630 pub chunk_count: usize,
631 pub degraded_file_count: usize,
632 pub parse_status_counts: CodeParseStatusCounts,
633}
634
635#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
637pub struct CodeRepositoryLatencySample {
638 pub query: String,
639 pub kind: CodeQueryKind,
640 pub result_count: usize,
641 pub duration_ms: u64,
642}
643
644#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
646pub struct CodeRepositoryReport {
647 pub repository_id: String,
648 pub alias: String,
649 pub root_path: String,
650 pub path_filters: Vec<String>,
651 pub language_filters: Vec<String>,
652 pub resolved_commit_sha: Option<String>,
653 pub tree_hash: Option<String>,
654 pub indexed_file_count: usize,
655 pub symbol_count: usize,
656 pub reference_count: usize,
657 pub chunk_count: usize,
658 pub degraded_file_count: usize,
659 pub resolved_edge_count: usize,
660 pub ambiguous_edge_count: usize,
661 pub unresolved_edge_count: usize,
662 pub degradation_summary: Vec<String>,
663 pub representative_queries: Vec<String>,
664 pub latency_samples: Vec<CodeRepositoryLatencySample>,
665 pub freshness_state: String,
666}
667
668#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
670pub struct CodeImpactPathGroups {
671 pub in_scope_changed_paths: Vec<String>,
672 pub out_of_scope_changed_paths: Vec<String>,
673}
674
675#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
677pub struct CodeRetrievalHit {
678 pub repository_id: String,
679 pub scope_id: String,
680 pub resolved_commit_sha: String,
681 pub tree_hash: String,
682 pub path: String,
683 pub language_id: String,
684 pub byte_range: RepositoryCodeRange,
685 pub line_range: RepositoryCodeRange,
686 pub symbol_snapshot_id: Option<String>,
687 #[serde(skip_serializing_if = "Option::is_none")]
688 pub canonical_symbol_id: Option<String>,
689 pub file_id: Option<String>,
690 pub retrieval_layers: Vec<CodeRetrievalLayer>,
691 pub index_versions: Vec<String>,
692 pub stale: bool,
693 #[serde(skip_serializing_if = "Option::is_none")]
694 pub degraded_reason: Option<String>,
695 #[serde(skip_serializing_if = "Option::is_none")]
696 pub edge_kind: Option<String>,
697 #[serde(skip_serializing_if = "Option::is_none")]
698 pub edge_resolution_state: Option<String>,
699 #[serde(skip_serializing_if = "Option::is_none")]
700 pub edge_target_hint: Option<String>,
701 #[serde(skip_serializing_if = "Option::is_none")]
702 pub edge_confidence_basis_points: Option<u16>,
703 #[serde(skip_serializing_if = "Option::is_none")]
704 pub edge_confidence_tier: Option<String>,
705 pub score: f64,
706 pub excerpt: String,
707}
708
709fn normalize_filter_list(
710 field: &'static str,
711 values: Vec<String>,
712) -> Result<Vec<String>, DomainError> {
713 let mut normalized = Vec::new();
714 for value in values {
715 let value = required_text(field, value)?;
716 if !normalized.contains(&value) {
717 normalized.push(value);
718 }
719 }
720
721 Ok(normalized)
722}
723
724fn checked_u32(field: &'static str, value: usize) -> Result<u32, DomainError> {
725 u32::try_from(value).map_err(|_| DomainError::invalid(field, "must fit in u32"))
726}
727
728fn append_hash_list(input: &mut Vec<u8>, values: &[String]) {
729 input.extend_from_slice(&(values.len() as u64).to_le_bytes());
730 for value in values {
731 append_hash_part(input, value);
732 }
733}
734
735fn append_hash_part(input: &mut Vec<u8>, value: &str) {
736 input.extend_from_slice(&(value.len() as u64).to_le_bytes());
737 input.extend_from_slice(value.as_bytes());
738}
739
740fn stable_hash64(bytes: &[u8]) -> u64 {
741 const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
742 const FNV_PRIME: u64 = 0x100000001b3;
743
744 let mut hash = FNV_OFFSET_BASIS;
745 for byte in bytes {
746 hash ^= u64::from(*byte);
747 hash = hash.wrapping_mul(FNV_PRIME);
748 }
749
750 hash
751}
752
753#[cfg(test)]
754mod tests {
755 use super::*;
756
757 #[test]
758 fn selector_trims_and_deduplicates_filters() {
759 let selector = CodeRepositorySelector::new(
760 " repo ",
761 " HEAD ",
762 vec!["src".to_owned(), " src ".to_owned()],
763 vec!["rust".to_owned(), "rust".to_owned()],
764 )
765 .expect("selector should validate");
766
767 assert_eq!(selector.repository, "repo");
768 assert_eq!(selector.ref_selector, "HEAD");
769 assert_eq!(selector.path_filters, ["src"]);
770 assert_eq!(selector.language_filters, ["rust"]);
771 }
772
773 #[test]
774 fn snapshot_scope_id_tracks_tree_and_filters() {
775 let scope = code_snapshot_scope_id(
776 "repo-1",
777 "tree-a",
778 &["src".to_owned()],
779 &["rust".to_owned()],
780 );
781 let same = code_snapshot_scope_id(
782 "repo-1",
783 "tree-a",
784 &["src".to_owned()],
785 &["rust".to_owned()],
786 );
787 let different_tree = code_snapshot_scope_id(
788 "repo-1",
789 "tree-b",
790 &["src".to_owned()],
791 &["rust".to_owned()],
792 );
793
794 assert_eq!(scope, same);
795 assert_ne!(scope, different_tree);
796 assert!(scope.starts_with("git_snapshot:"));
797 }
798
799 #[test]
800 fn retrieval_request_rejects_unbounded_limits() {
801 let selector = CodeRepositorySelector::new("repo", "HEAD", Vec::new(), Vec::new())
802 .expect("selector should validate");
803 let error = CodeRetrievalRequest::new(
804 "symbol",
805 selector,
806 CodeQueryKind::Hybrid,
807 51,
808 FreshnessPolicy::AllowStale,
809 )
810 .expect_err("large limit should fail");
811
812 assert_eq!(error.field, "limit");
813 }
814
815 #[test]
816 fn code_ranges_must_be_ordered() {
817 let error = RepositoryCodeRange::new("line_range", 3, 2).expect_err("range should fail");
818
819 assert_eq!(error.field, "line_range");
820 }
821}