1use serde::{Deserialize, Serialize};
2
3use super::code::SymbolRole;
4use super::code_repository_helpers::{
5 append_hash_list, append_hash_part, checked_u32, normalize_filter_list, stable_hash64,
6};
7use super::{
8 CodeParseStatus, CodeParseStatusCounts, CodeWorkspaceDetectionConfig, DomainError,
9 FreshnessPolicy, error::required_text,
10};
11
12const CODE_SNAPSHOT_FACT_VERSION: &str = "code-facts-js-ts-import-edges-v1-sbom-dependencies-v2-python-type-refs-v1-scope-compat-v1-workspace-imports-v1-generated-files-v1-web-routes-v1";
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15struct FieldQualifiers {
16 search_text: String,
17 kind_filters: Vec<String>,
18 language_filters: Vec<String>,
19 path_substrings: Vec<String>,
20 name_substrings: Vec<String>,
21}
22
23fn parse_field_qualifiers(query: &str) -> FieldQualifiers {
24 let mut plain_terms = Vec::new();
25 let mut qualifiers = FieldQualifiers {
26 search_text: String::new(),
27 kind_filters: Vec::new(),
28 language_filters: Vec::new(),
29 path_substrings: Vec::new(),
30 name_substrings: Vec::new(),
31 };
32
33 for token in query.split_whitespace() {
34 if !push_field_qualifier(token, &mut qualifiers) {
35 plain_terms.push(token);
36 }
37 }
38
39 qualifiers.search_text = plain_terms.join(" ");
40 if qualifiers.search_text.is_empty() && !query.trim().is_empty() {
41 qualifiers.search_text = query.trim().to_owned();
42 }
43
44 qualifiers
45}
46
47fn push_field_qualifier(token: &str, qualifiers: &mut FieldQualifiers) -> bool {
48 let Some((prefix, value)) = token.split_once(':') else {
49 return false;
50 };
51 if value.trim().is_empty() {
52 return false;
53 }
54 if value.starts_with(':') {
55 return false;
56 }
57
58 match prefix.to_ascii_lowercase().as_str() {
59 "kind" => {
60 extend_qualifier_values(&mut qualifiers.kind_filters, value, true);
61 true
62 }
63 "lang" | "language" => {
64 extend_qualifier_values(&mut qualifiers.language_filters, value, true);
65 true
66 }
67 "path" => {
68 extend_qualifier_values(&mut qualifiers.path_substrings, value, false);
69 true
70 }
71 "name" => {
72 extend_qualifier_values(&mut qualifiers.name_substrings, value, false);
73 true
74 }
75 _ => false,
76 }
77}
78
79fn extend_qualifier_values(values: &mut Vec<String>, raw_value: &str, ascii_lowercase: bool) {
80 for value in raw_value
81 .split(',')
82 .map(str::trim)
83 .filter(|value| !value.is_empty())
84 {
85 let value = if ascii_lowercase {
86 value.to_ascii_lowercase()
87 } else {
88 value.to_owned()
89 };
90 if !values.contains(&value) {
91 values.push(value);
92 }
93 }
94}
95
96pub fn code_snapshot_scope_id(
98 repository_id: &str,
99 tree_hash: &str,
100 path_filters: &[String],
101 language_filters: &[String],
102) -> String {
103 let mut input = Vec::new();
104 append_hash_part(&mut input, "git_snapshot");
105 append_hash_part(&mut input, repository_id);
106 append_hash_part(&mut input, tree_hash);
107 append_hash_list(&mut input, path_filters);
108 append_hash_list(&mut input, language_filters);
109 append_hash_part(&mut input, CODE_SNAPSHOT_FACT_VERSION);
110
111 format!("git_snapshot:{:016x}", stable_hash64(&input))
112}
113
114pub fn code_snapshot_expected_scope_id(
115 repository_id: &str,
116 tree_hash: &str,
117 path_filters: &[String],
118 language_filters: &[String],
119) -> Option<String> {
120 Some(code_snapshot_scope_id(
121 repository_id,
122 tree_hash,
123 path_filters,
124 language_filters,
125 ))
126}
127
128pub fn code_snapshot_scope_is_fact_versioned(source_scope: &str) -> bool {
129 let Some(scope_hash) = source_scope.strip_prefix("git_snapshot:") else {
130 return false;
131 };
132 scope_hash.len() == 16
133 && scope_hash
134 .chars()
135 .all(|character| character.is_ascii_hexdigit())
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct RepositoryCodeRange {
141 pub start: u32,
142 pub end: u32,
143}
144
145impl RepositoryCodeRange {
146 pub fn new(field: &'static str, start: usize, end: usize) -> Result<Self, DomainError> {
148 if end < start {
149 return Err(DomainError::invalid(
150 field,
151 "end must be greater than or equal to start",
152 ));
153 }
154
155 Ok(Self {
156 start: checked_u32(field, start)?,
157 end: checked_u32(field, end)?,
158 })
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct CodeRepositoryRegistration {
165 pub repository_id: String,
166 pub alias: String,
167 pub root_path: String,
168 pub path_filters: Vec<String>,
169 pub language_filters: Vec<String>,
170}
171
172impl CodeRepositoryRegistration {
173 pub fn new(
175 repository_id: impl Into<String>,
176 alias: impl Into<String>,
177 root_path: impl Into<String>,
178 path_filters: Vec<String>,
179 language_filters: Vec<String>,
180 ) -> Result<Self, DomainError> {
181 Ok(Self {
182 repository_id: required_text("repository_id", repository_id)?,
183 alias: required_text("alias", alias)?,
184 root_path: required_text("root_path", root_path)?,
185 path_filters: normalize_filter_list("path_filter", path_filters)?,
186 language_filters: normalize_filter_list("language_filter", language_filters)?,
187 })
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193pub struct CodeRepositorySelector {
194 pub repository: String,
195 pub ref_selector: String,
196 pub path_filters: Vec<String>,
197 pub language_filters: Vec<String>,
198}
199
200impl CodeRepositorySelector {
201 pub fn new(
203 repository: impl Into<String>,
204 ref_selector: impl Into<String>,
205 path_filters: Vec<String>,
206 language_filters: Vec<String>,
207 ) -> Result<Self, DomainError> {
208 Ok(Self {
209 repository: required_text("repository", repository)?,
210 ref_selector: required_text("ref_selector", ref_selector)?,
211 path_filters: normalize_filter_list("path_filter", path_filters)?,
212 language_filters: normalize_filter_list("language_filter", language_filters)?,
213 })
214 }
215}
216
217#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(rename_all = "snake_case")]
220pub enum CodeIndexMode {
221 Full,
222 Incremental { base_ref: String, head_ref: String },
223 WorktreeOverlay,
224}
225
226impl CodeIndexMode {
227 pub fn incremental(
229 base_ref: impl Into<String>,
230 head_ref: impl Into<String>,
231 ) -> Result<Self, DomainError> {
232 Ok(Self::Incremental {
233 base_ref: required_text("base_ref", base_ref)?,
234 head_ref: required_text("head_ref", head_ref)?,
235 })
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
241pub struct CodeIndexRequest {
242 pub repository: CodeRepositorySelector,
243 pub mode: CodeIndexMode,
244 #[serde(default)]
245 pub workspace_detection: CodeWorkspaceDetectionConfig,
246 pub freshness_policy: FreshnessPolicy,
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(rename_all = "snake_case")]
252pub enum CodeQueryKind {
253 Hybrid,
254 Symbol,
255 Definition,
256 References,
257 Callers,
258 Callees,
259 Imports,
260 Sbom,
261 Impact,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266pub struct CodeRetrievalRequest {
267 pub query: String,
268 pub repository: CodeRepositorySelector,
269 pub code_query_kind: CodeQueryKind,
270 pub limit: usize,
271 pub freshness_policy: FreshnessPolicy,
272 #[serde(default)]
273 pub exclude_generated: bool,
274 #[serde(default, skip_serializing_if = "Vec::is_empty")]
275 pub query_kind_filters: Vec<String>,
276 #[serde(default, skip_serializing_if = "Vec::is_empty")]
277 pub query_language_filters: Vec<String>,
278 #[serde(default, skip_serializing_if = "Vec::is_empty")]
279 pub query_path_substrings: Vec<String>,
280 #[serde(default, skip_serializing_if = "Vec::is_empty")]
281 pub query_name_substrings: Vec<String>,
282}
283
284impl CodeRetrievalRequest {
285 pub fn new(
287 query: impl Into<String>,
288 repository: CodeRepositorySelector,
289 code_query_kind: CodeQueryKind,
290 limit: usize,
291 freshness_policy: FreshnessPolicy,
292 ) -> Result<Self, DomainError> {
293 let limit = match limit {
294 1..=50 => limit,
295 0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
296 _ => return Err(DomainError::invalid("limit", "must be 50 or less")),
297 };
298
299 let qualifiers = parse_field_qualifiers(&required_text("query", query)?);
300
301 Ok(Self {
302 query: qualifiers.search_text,
303 repository,
304 code_query_kind,
305 limit,
306 freshness_policy,
307 exclude_generated: false,
308 query_kind_filters: qualifiers.kind_filters,
309 query_language_filters: qualifiers.language_filters,
310 query_path_substrings: qualifiers.path_substrings,
311 query_name_substrings: qualifiers.name_substrings,
312 })
313 }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
318pub struct CodeFeatureFlagRequest {
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub query: Option<String>,
321 pub repository: CodeRepositorySelector,
322 pub limit: usize,
323 pub freshness_policy: FreshnessPolicy,
324}
325
326impl CodeFeatureFlagRequest {
327 pub fn new(
329 query: Option<String>,
330 repository: CodeRepositorySelector,
331 limit: usize,
332 freshness_policy: FreshnessPolicy,
333 ) -> Result<Self, DomainError> {
334 let limit = match limit {
335 1..=100 => limit,
336 0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
337 _ => return Err(DomainError::invalid("limit", "must be 100 or less")),
338 };
339 let query = query
340 .map(|value| required_text("query", value))
341 .transpose()?;
342
343 Ok(Self {
344 query,
345 repository,
346 limit,
347 freshness_policy,
348 })
349 }
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
354pub struct CodeImpactRequest {
355 pub repository: CodeRepositorySelector,
356 pub base_ref: String,
357 pub head_ref: String,
358 pub limit: usize,
359}
360
361impl CodeImpactRequest {
362 pub fn new(
364 repository: CodeRepositorySelector,
365 base_ref: impl Into<String>,
366 head_ref: impl Into<String>,
367 limit: usize,
368 ) -> Result<Self, DomainError> {
369 let limit = match limit {
370 1..=100 => limit,
371 0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
372 _ => return Err(DomainError::invalid("limit", "must be 100 or less")),
373 };
374
375 Ok(Self {
376 repository,
377 base_ref: required_text("base_ref", base_ref)?,
378 head_ref: required_text("head_ref", head_ref)?,
379 limit,
380 })
381 }
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
386#[serde(rename_all = "snake_case")]
387pub enum CodeRetrievalLayer {
388 Lexical,
389 Symbol,
390 Definition,
391 Reference,
392 CallGraph,
393 ImportGraph,
394 Sbom,
395 Impact,
396 TextFallback,
397}
398
399impl CodeRetrievalLayer {
400 pub const fn as_str(self) -> &'static str {
402 match self {
403 Self::Lexical => "lexical",
404 Self::Symbol => "symbol",
405 Self::Definition => "definition",
406 Self::Reference => "reference",
407 Self::CallGraph => "call_graph",
408 Self::ImportGraph => "import_graph",
409 Self::Sbom => "sbom",
410 Self::Impact => "impact",
411 Self::TextFallback => "text_fallback",
412 }
413 }
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
418pub struct CodeRepositoryStatus {
419 pub repository_id: String,
420 pub alias: String,
421 pub root_path: String,
422 pub path_filters: Vec<String>,
423 pub language_filters: Vec<String>,
424 #[serde(skip_serializing_if = "Option::is_none")]
425 pub last_indexed_scope_id: Option<String>,
426 pub last_indexed_commit: Option<String>,
427 pub tree_hash: Option<String>,
428 pub state: String,
429 pub indexed_file_count: usize,
430 pub symbol_count: usize,
431 pub reference_count: usize,
432 pub chunk_count: usize,
433 pub stale: bool,
434 #[serde(skip_serializing_if = "Option::is_none")]
435 pub degraded_reason: Option<String>,
436}
437
438#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
440pub struct CodeRepositoryRemovalSummary {
441 pub repository_id: String,
442 pub aliases_removed: Vec<String>,
443 pub removed_scope_count: usize,
444 pub removed_index_task_count: usize,
445 pub removed_repository_set_member_count: usize,
446 pub invalidated_repository_set_count: usize,
447}
448
449#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
451pub struct RepositoryCodeFileRecord {
452 pub repository_id: String,
453 pub source_scope: String,
454 pub file_id: String,
455 pub path: String,
456 pub language_id: String,
457 pub blob_hash: String,
458 pub byte_len: usize,
459 pub line_count: usize,
460 pub parse_status: CodeParseStatus,
461 #[serde(default)]
462 pub is_generated: bool,
463 #[serde(skip_serializing_if = "Option::is_none")]
464 pub degraded_reason: Option<String>,
465}
466
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469pub struct CodeFileFingerprint {
470 pub path: String,
471 pub blob_hash: String,
472}
473
474#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
476pub struct RepositoryCodeSymbolRecord {
477 pub repository_id: String,
478 pub source_scope: String,
479 pub symbol_snapshot_id: String,
480 pub canonical_symbol_id: String,
481 pub file_id: String,
482 pub path: String,
483 pub language_id: String,
484 pub name: String,
485 pub qualified_name: String,
486 pub kind: String,
487 pub signature: String,
488 #[serde(skip_serializing_if = "Option::is_none")]
489 pub doc_comment: Option<String>,
490 pub byte_range: RepositoryCodeRange,
491 pub line_range: RepositoryCodeRange,
492 #[serde(default, skip_serializing_if = "Option::is_none")]
493 pub symbol_role: Option<SymbolRole>,
494}
495
496#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
498pub struct RepositoryCodeReferenceRecord {
499 pub repository_id: String,
500 pub source_scope: String,
501 pub reference_id: String,
502 pub file_id: String,
503 pub path: String,
504 pub name: String,
505 pub kind: String,
506 #[serde(skip_serializing_if = "Option::is_none")]
507 pub target_symbol_snapshot_id: Option<String>,
508 #[serde(skip_serializing_if = "Option::is_none")]
509 pub target_hint: Option<String>,
510 pub resolution_state: String,
511 pub confidence_basis_points: u16,
512 pub confidence_tier: String,
513 pub byte_range: RepositoryCodeRange,
514 pub line_range: RepositoryCodeRange,
515}
516
517#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
519pub struct CodeImportRecord {
520 pub repository_id: String,
521 pub source_scope: String,
522 pub import_id: String,
523 pub file_id: String,
524 pub path: String,
525 pub module: String,
526 #[serde(skip_serializing_if = "Option::is_none")]
527 pub target_hint: Option<String>,
528 pub resolution_state: String,
529 pub confidence_basis_points: u16,
530 pub confidence_tier: String,
531 pub line_range: RepositoryCodeRange,
532}
533
534#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
536pub struct CodeCallRecord {
537 pub repository_id: String,
538 pub source_scope: String,
539 pub call_id: String,
540 pub file_id: String,
541 pub path: String,
542 pub caller_symbol_snapshot_id: Option<String>,
543 pub caller_name: Option<String>,
544 #[serde(skip_serializing_if = "Option::is_none")]
545 pub callee_symbol_snapshot_id: Option<String>,
546 pub callee_name: String,
547 #[serde(skip_serializing_if = "Option::is_none")]
548 pub target_hint: Option<String>,
549 pub resolution_state: String,
550 pub confidence_basis_points: u16,
551 pub confidence_tier: String,
552 pub line_range: RepositoryCodeRange,
553}
554
555#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
557pub struct CodeRouteRecord {
558 pub repository_id: String,
559 pub source_scope: String,
560 pub route_id: String,
561 pub file_id: String,
562 pub path: String,
563 pub language_id: String,
564 pub url: String,
565 pub http_method: String,
567 pub handler_name: String,
568 #[serde(skip_serializing_if = "Option::is_none")]
569 pub handler_symbol_snapshot_id: Option<String>,
570 pub framework: String,
571 pub line_range: RepositoryCodeRange,
572}
573
574#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
576pub struct CodeFeatureFlagRecord {
577 pub repository_id: String,
578 pub source_scope: String,
579 pub feature_flag_id: String,
580 pub usage_id: String,
581 pub file_id: String,
582 pub path: String,
583 pub language_id: String,
584 pub name: String,
585 pub source_kind: String,
586 pub source_key: String,
587 pub edge_kind: String,
588 pub confidence_basis_points: u16,
589 pub confidence_tier: String,
590 pub byte_range: RepositoryCodeRange,
591 pub line_range: RepositoryCodeRange,
592 pub excerpt: String,
593}
594
595#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
597pub struct RepositoryCodeChunkRecord {
598 pub repository_id: String,
599 pub source_scope: String,
600 pub chunk_id: String,
601 pub file_id: String,
602 pub path: String,
603 pub language_id: String,
604 pub content: String,
605 pub byte_range: RepositoryCodeRange,
606 pub line_range: RepositoryCodeRange,
607 pub symbol_snapshot_id: Option<String>,
608}
609
610#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
612pub struct CodeFileDiagnostic {
613 pub repository_id: String,
614 pub source_scope: String,
615 pub path: String,
616 pub parse_status: CodeParseStatus,
617 pub message: String,
618}
619
620#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
622pub struct CodePathTombstone {
623 pub repository_id: String,
624 pub source_scope: String,
625 pub old_path: String,
626 pub new_path: Option<String>,
627 pub base_ref: String,
628 pub head_ref: String,
629}
630
631#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
633pub struct CodeRepositoryLanguagePreview {
634 pub language_id: String,
635 pub file_count: usize,
636 pub byte_count: usize,
637}
638
639#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
641pub struct CodeRepositoryLargestFile {
642 pub path: String,
643 pub byte_count: usize,
644}
645
646#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
648pub struct CodeRepositoryExcludedPath {
649 pub path: String,
650 pub reason: String,
651}
652
653#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
655pub struct CodeRepositoryScopePreview {
656 pub repository_id: String,
657 pub alias: String,
658 pub requested_ref: String,
659 pub resolved_commit_sha: String,
660 pub tree_hash: String,
661 pub selected_file_count: usize,
662 pub selected_byte_count: usize,
663 pub unsupported_file_count: usize,
664 pub generated_or_heavy_file_count: usize,
665 pub expected_degraded_file_count: usize,
666 pub language_distribution: Vec<CodeRepositoryLanguagePreview>,
667 pub largest_files: Vec<CodeRepositoryLargestFile>,
668 pub excluded_paths: Vec<CodeRepositoryExcludedPath>,
669}
670
671#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
673pub struct CodeRepositoryTotals {
674 pub repository_count: usize,
675 pub indexed_file_count: usize,
676 pub symbol_count: usize,
677 #[serde(default)]
678 pub handwritten_symbol_count: usize,
679 #[serde(default)]
680 pub generated_symbol_count: usize,
681 pub reference_count: usize,
682 pub chunk_count: usize,
683 pub degraded_file_count: usize,
684 pub parse_status_counts: CodeParseStatusCounts,
685}
686
687#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
689pub struct CodeSymbolGenerationCounts {
690 #[serde(default)]
691 pub handwritten_symbol_count: usize,
692 #[serde(default)]
693 pub generated_symbol_count: usize,
694}
695
696#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
698pub struct CodeRepositoryLatencySample {
699 pub query: String,
700 pub kind: CodeQueryKind,
701 pub result_count: usize,
702 pub duration_ms: u64,
703}
704
705#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
707pub struct CodeRepositoryReport {
708 pub repository_id: String,
709 pub alias: String,
710 pub root_path: String,
711 pub path_filters: Vec<String>,
712 pub language_filters: Vec<String>,
713 pub resolved_commit_sha: Option<String>,
714 pub tree_hash: Option<String>,
715 pub indexed_file_count: usize,
716 pub symbol_count: usize,
717 #[serde(default)]
718 pub handwritten_symbol_count: usize,
719 #[serde(default)]
720 pub generated_symbol_count: usize,
721 pub reference_count: usize,
722 pub chunk_count: usize,
723 pub degraded_file_count: usize,
724 pub resolved_edge_count: usize,
725 pub ambiguous_edge_count: usize,
726 pub unresolved_edge_count: usize,
727 pub degradation_summary: Vec<String>,
728 pub representative_queries: Vec<String>,
729 pub latency_samples: Vec<CodeRepositoryLatencySample>,
730 pub freshness_state: String,
731}
732
733#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
735pub struct CodeImpactPathGroups {
736 pub in_scope_changed_paths: Vec<String>,
737 pub out_of_scope_changed_paths: Vec<String>,
738}
739
740pub use super::code_staleness::StalenessHint;
741
742#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
744pub struct CodeRetrievalHit {
745 pub repository_id: String,
746 pub scope_id: String,
747 pub resolved_commit_sha: String,
748 pub tree_hash: String,
749 pub path: String,
750 pub language_id: String,
751 pub byte_range: RepositoryCodeRange,
752 pub line_range: RepositoryCodeRange,
753 pub symbol_snapshot_id: Option<String>,
754 #[serde(skip_serializing_if = "Option::is_none")]
755 pub canonical_symbol_id: Option<String>,
756 pub file_id: Option<String>,
757 pub retrieval_layers: Vec<CodeRetrievalLayer>,
758 pub index_versions: Vec<String>,
759 pub stale: bool,
760 #[serde(skip_serializing_if = "Option::is_none")]
761 pub staleness_hint: Option<StalenessHint>,
762 #[serde(skip_serializing_if = "Option::is_none")]
763 pub degraded_reason: Option<String>,
764 #[serde(skip_serializing_if = "Option::is_none")]
765 pub edge_kind: Option<String>,
766 #[serde(skip_serializing_if = "Option::is_none")]
767 pub edge_resolution_state: Option<String>,
768 #[serde(skip_serializing_if = "Option::is_none")]
769 pub edge_target_hint: Option<String>,
770 #[serde(skip_serializing_if = "Option::is_none")]
771 pub edge_confidence_basis_points: Option<u16>,
772 #[serde(skip_serializing_if = "Option::is_none")]
773 pub edge_confidence_tier: Option<String>,
774 pub score: f64,
775 pub excerpt: String,
776}
777
778#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
780pub struct CodeFeatureFlagUsage {
781 pub usage_id: String,
782 pub path: String,
783 pub language_id: String,
784 pub file_id: String,
785 pub byte_range: RepositoryCodeRange,
786 pub line_range: RepositoryCodeRange,
787 pub edge_kind: String,
788 #[serde(skip_serializing_if = "Option::is_none")]
789 pub related_symbol_snapshot_id: Option<String>,
790 #[serde(skip_serializing_if = "Option::is_none")]
791 pub related_symbol_name: Option<String>,
792 pub confidence_basis_points: u16,
793 pub confidence_tier: String,
794 pub excerpt: String,
795}
796
797#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
799pub struct CodeFeatureFlagGraph {
800 pub feature_flag_id: String,
801 pub name: String,
802 pub source_kind: String,
803 pub source_key: String,
804 pub score: f64,
805 pub usages: Vec<CodeFeatureFlagUsage>,
806}
807
808#[cfg(test)]
809mod fact_version_tests {
810 use super::{
811 CODE_SNAPSHOT_FACT_VERSION, CodeQueryKind, CodeRepositorySelector, CodeRetrievalRequest,
812 FreshnessPolicy, parse_field_qualifiers,
813 };
814
815 #[test]
816 fn code_snapshot_fact_version_includes_generated_and_web_route_facts() {
817 assert!(CODE_SNAPSHOT_FACT_VERSION.contains("generated-files-v1"));
818 assert!(CODE_SNAPSHOT_FACT_VERSION.contains("web-routes-v1"));
819 }
820
821 #[test]
822 fn field_qualifiers_strip_known_tags_and_keep_search_text() {
823 let parsed = parse_field_qualifiers(
824 "kind:function,method lang:rust path:storage name:query search_code",
825 );
826
827 assert_eq!(parsed.search_text, "search_code");
828 assert_eq!(parsed.kind_filters, ["function", "method"]);
829 assert_eq!(parsed.language_filters, ["rust"]);
830 assert_eq!(parsed.path_substrings, ["storage"]);
831 assert_eq!(parsed.name_substrings, ["query"]);
832 }
833
834 #[test]
835 fn field_qualifiers_keep_unknown_tags_as_plain_text() {
836 let parsed = parse_field_qualifiers("owner:runtime lang:rust refresh");
837
838 assert_eq!(parsed.search_text, "owner:runtime refresh");
839 assert_eq!(parsed.language_filters, ["rust"]);
840 }
841
842 #[test]
843 fn field_qualifiers_keep_double_colon_paths_as_plain_text() {
844 let parsed = parse_field_qualifiers("path:storage path::normalize_filter name::Worker");
845
846 assert_eq!(parsed.search_text, "path::normalize_filter name::Worker");
847 assert_eq!(parsed.path_substrings, ["storage"]);
848 assert!(parsed.name_substrings.is_empty());
849 }
850
851 #[test]
852 fn retrieval_request_carries_inline_filters_from_query_text() {
853 let request = CodeRetrievalRequest::new(
854 "language:Rust kind:Function path:storage name:query search_code",
855 CodeRepositorySelector::new("repo", "HEAD", Vec::new(), Vec::new())
856 .expect("selector validates"),
857 CodeQueryKind::Hybrid,
858 10,
859 FreshnessPolicy::AllowStale,
860 )
861 .expect("request validates");
862
863 assert_eq!(request.query, "search_code");
864 assert_eq!(request.query_language_filters, ["rust"]);
865 assert_eq!(request.query_kind_filters, ["function"]);
866 assert_eq!(request.query_path_substrings, ["storage"]);
867 assert_eq!(request.query_name_substrings, ["query"]);
868 }
869}