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
14pub fn code_snapshot_scope_id(
16 repository_id: &str,
17 tree_hash: &str,
18 path_filters: &[String],
19 language_filters: &[String],
20) -> String {
21 let mut input = Vec::new();
22 append_hash_part(&mut input, "git_snapshot");
23 append_hash_part(&mut input, repository_id);
24 append_hash_part(&mut input, tree_hash);
25 append_hash_list(&mut input, path_filters);
26 append_hash_list(&mut input, language_filters);
27 append_hash_part(&mut input, CODE_SNAPSHOT_FACT_VERSION);
28
29 format!("git_snapshot:{:016x}", stable_hash64(&input))
30}
31
32pub fn code_snapshot_expected_scope_id(
33 repository_id: &str,
34 tree_hash: &str,
35 path_filters: &[String],
36 language_filters: &[String],
37) -> Option<String> {
38 Some(code_snapshot_scope_id(
39 repository_id,
40 tree_hash,
41 path_filters,
42 language_filters,
43 ))
44}
45
46pub fn code_snapshot_scope_is_fact_versioned(source_scope: &str) -> bool {
47 let Some(scope_hash) = source_scope.strip_prefix("git_snapshot:") else {
48 return false;
49 };
50 scope_hash.len() == 16
51 && scope_hash
52 .chars()
53 .all(|character| character.is_ascii_hexdigit())
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct RepositoryCodeRange {
59 pub start: u32,
60 pub end: u32,
61}
62
63impl RepositoryCodeRange {
64 pub fn new(field: &'static str, start: usize, end: usize) -> Result<Self, DomainError> {
66 if end < start {
67 return Err(DomainError::invalid(
68 field,
69 "end must be greater than or equal to start",
70 ));
71 }
72
73 Ok(Self {
74 start: checked_u32(field, start)?,
75 end: checked_u32(field, end)?,
76 })
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82pub struct CodeRepositoryRegistration {
83 pub repository_id: String,
84 pub alias: String,
85 pub root_path: String,
86 pub path_filters: Vec<String>,
87 pub language_filters: Vec<String>,
88}
89
90impl CodeRepositoryRegistration {
91 pub fn new(
93 repository_id: impl Into<String>,
94 alias: impl Into<String>,
95 root_path: impl Into<String>,
96 path_filters: Vec<String>,
97 language_filters: Vec<String>,
98 ) -> Result<Self, DomainError> {
99 Ok(Self {
100 repository_id: required_text("repository_id", repository_id)?,
101 alias: required_text("alias", alias)?,
102 root_path: required_text("root_path", root_path)?,
103 path_filters: normalize_filter_list("path_filter", path_filters)?,
104 language_filters: normalize_filter_list("language_filter", language_filters)?,
105 })
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111pub struct CodeRepositorySelector {
112 pub repository: String,
113 pub ref_selector: String,
114 pub path_filters: Vec<String>,
115 pub language_filters: Vec<String>,
116}
117
118impl CodeRepositorySelector {
119 pub fn new(
121 repository: impl Into<String>,
122 ref_selector: impl Into<String>,
123 path_filters: Vec<String>,
124 language_filters: Vec<String>,
125 ) -> Result<Self, DomainError> {
126 Ok(Self {
127 repository: required_text("repository", repository)?,
128 ref_selector: required_text("ref_selector", ref_selector)?,
129 path_filters: normalize_filter_list("path_filter", path_filters)?,
130 language_filters: normalize_filter_list("language_filter", language_filters)?,
131 })
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub enum CodeIndexMode {
139 Full,
140 Incremental { base_ref: String, head_ref: String },
141 WorktreeOverlay,
142}
143
144impl CodeIndexMode {
145 pub fn incremental(
147 base_ref: impl Into<String>,
148 head_ref: impl Into<String>,
149 ) -> Result<Self, DomainError> {
150 Ok(Self::Incremental {
151 base_ref: required_text("base_ref", base_ref)?,
152 head_ref: required_text("head_ref", head_ref)?,
153 })
154 }
155}
156
157#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159pub struct CodeIndexRequest {
160 pub repository: CodeRepositorySelector,
161 pub mode: CodeIndexMode,
162 #[serde(default)]
163 pub workspace_detection: CodeWorkspaceDetectionConfig,
164 pub freshness_policy: FreshnessPolicy,
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case")]
170pub enum CodeQueryKind {
171 Hybrid,
172 Symbol,
173 Definition,
174 References,
175 Callers,
176 Callees,
177 Imports,
178 Sbom,
179 Impact,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub struct CodeRetrievalRequest {
185 pub query: String,
186 pub repository: CodeRepositorySelector,
187 pub code_query_kind: CodeQueryKind,
188 pub limit: usize,
189 pub freshness_policy: FreshnessPolicy,
190 #[serde(default)]
191 pub exclude_generated: bool,
192}
193
194impl CodeRetrievalRequest {
195 pub fn new(
197 query: impl Into<String>,
198 repository: CodeRepositorySelector,
199 code_query_kind: CodeQueryKind,
200 limit: usize,
201 freshness_policy: FreshnessPolicy,
202 ) -> Result<Self, DomainError> {
203 let limit = match limit {
204 1..=50 => limit,
205 0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
206 _ => return Err(DomainError::invalid("limit", "must be 50 or less")),
207 };
208
209 Ok(Self {
210 query: required_text("query", query)?,
211 repository,
212 code_query_kind,
213 limit,
214 freshness_policy,
215 exclude_generated: false,
216 })
217 }
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
222pub struct CodeFeatureFlagRequest {
223 #[serde(skip_serializing_if = "Option::is_none")]
224 pub query: Option<String>,
225 pub repository: CodeRepositorySelector,
226 pub limit: usize,
227 pub freshness_policy: FreshnessPolicy,
228}
229
230impl CodeFeatureFlagRequest {
231 pub fn new(
233 query: Option<String>,
234 repository: CodeRepositorySelector,
235 limit: usize,
236 freshness_policy: FreshnessPolicy,
237 ) -> Result<Self, DomainError> {
238 let limit = match limit {
239 1..=100 => limit,
240 0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
241 _ => return Err(DomainError::invalid("limit", "must be 100 or less")),
242 };
243 let query = query
244 .map(|value| required_text("query", value))
245 .transpose()?;
246
247 Ok(Self {
248 query,
249 repository,
250 limit,
251 freshness_policy,
252 })
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
258pub struct CodeImpactRequest {
259 pub repository: CodeRepositorySelector,
260 pub base_ref: String,
261 pub head_ref: String,
262 pub limit: usize,
263}
264
265impl CodeImpactRequest {
266 pub fn new(
268 repository: CodeRepositorySelector,
269 base_ref: impl Into<String>,
270 head_ref: impl Into<String>,
271 limit: usize,
272 ) -> Result<Self, DomainError> {
273 let limit = match limit {
274 1..=100 => limit,
275 0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
276 _ => return Err(DomainError::invalid("limit", "must be 100 or less")),
277 };
278
279 Ok(Self {
280 repository,
281 base_ref: required_text("base_ref", base_ref)?,
282 head_ref: required_text("head_ref", head_ref)?,
283 limit,
284 })
285 }
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
290#[serde(rename_all = "snake_case")]
291pub enum CodeRetrievalLayer {
292 Lexical,
293 Symbol,
294 Definition,
295 Reference,
296 CallGraph,
297 ImportGraph,
298 Sbom,
299 Impact,
300 TextFallback,
301}
302
303impl CodeRetrievalLayer {
304 pub const fn as_str(self) -> &'static str {
306 match self {
307 Self::Lexical => "lexical",
308 Self::Symbol => "symbol",
309 Self::Definition => "definition",
310 Self::Reference => "reference",
311 Self::CallGraph => "call_graph",
312 Self::ImportGraph => "import_graph",
313 Self::Sbom => "sbom",
314 Self::Impact => "impact",
315 Self::TextFallback => "text_fallback",
316 }
317 }
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
322pub struct CodeRepositoryStatus {
323 pub repository_id: String,
324 pub alias: String,
325 pub root_path: String,
326 pub path_filters: Vec<String>,
327 pub language_filters: Vec<String>,
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub last_indexed_scope_id: Option<String>,
330 pub last_indexed_commit: Option<String>,
331 pub tree_hash: Option<String>,
332 pub state: String,
333 pub indexed_file_count: usize,
334 pub symbol_count: usize,
335 pub reference_count: usize,
336 pub chunk_count: usize,
337 pub stale: bool,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub degraded_reason: Option<String>,
340}
341
342#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
344pub struct CodeRepositoryRemovalSummary {
345 pub repository_id: String,
346 pub aliases_removed: Vec<String>,
347 pub removed_scope_count: usize,
348 pub removed_index_task_count: usize,
349 pub removed_repository_set_member_count: usize,
350 pub invalidated_repository_set_count: usize,
351}
352
353#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
355pub struct RepositoryCodeFileRecord {
356 pub repository_id: String,
357 pub source_scope: String,
358 pub file_id: String,
359 pub path: String,
360 pub language_id: String,
361 pub blob_hash: String,
362 pub byte_len: usize,
363 pub line_count: usize,
364 pub parse_status: CodeParseStatus,
365 #[serde(default)]
366 pub is_generated: bool,
367 #[serde(skip_serializing_if = "Option::is_none")]
368 pub degraded_reason: Option<String>,
369}
370
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
373pub struct CodeFileFingerprint {
374 pub path: String,
375 pub blob_hash: String,
376}
377
378#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
380pub struct RepositoryCodeSymbolRecord {
381 pub repository_id: String,
382 pub source_scope: String,
383 pub symbol_snapshot_id: String,
384 pub canonical_symbol_id: String,
385 pub file_id: String,
386 pub path: String,
387 pub language_id: String,
388 pub name: String,
389 pub qualified_name: String,
390 pub kind: String,
391 pub signature: String,
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub doc_comment: Option<String>,
394 pub byte_range: RepositoryCodeRange,
395 pub line_range: RepositoryCodeRange,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
397 pub symbol_role: Option<SymbolRole>,
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
402pub struct RepositoryCodeReferenceRecord {
403 pub repository_id: String,
404 pub source_scope: String,
405 pub reference_id: String,
406 pub file_id: String,
407 pub path: String,
408 pub name: String,
409 pub kind: String,
410 #[serde(skip_serializing_if = "Option::is_none")]
411 pub target_symbol_snapshot_id: Option<String>,
412 #[serde(skip_serializing_if = "Option::is_none")]
413 pub target_hint: Option<String>,
414 pub resolution_state: String,
415 pub confidence_basis_points: u16,
416 pub confidence_tier: String,
417 pub byte_range: RepositoryCodeRange,
418 pub line_range: RepositoryCodeRange,
419}
420
421#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
423pub struct CodeImportRecord {
424 pub repository_id: String,
425 pub source_scope: String,
426 pub import_id: String,
427 pub file_id: String,
428 pub path: String,
429 pub module: String,
430 #[serde(skip_serializing_if = "Option::is_none")]
431 pub target_hint: Option<String>,
432 pub resolution_state: String,
433 pub confidence_basis_points: u16,
434 pub confidence_tier: String,
435 pub line_range: RepositoryCodeRange,
436}
437
438#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
440pub struct CodeCallRecord {
441 pub repository_id: String,
442 pub source_scope: String,
443 pub call_id: String,
444 pub file_id: String,
445 pub path: String,
446 pub caller_symbol_snapshot_id: Option<String>,
447 pub caller_name: Option<String>,
448 #[serde(skip_serializing_if = "Option::is_none")]
449 pub callee_symbol_snapshot_id: Option<String>,
450 pub callee_name: String,
451 #[serde(skip_serializing_if = "Option::is_none")]
452 pub target_hint: Option<String>,
453 pub resolution_state: String,
454 pub confidence_basis_points: u16,
455 pub confidence_tier: String,
456 pub line_range: RepositoryCodeRange,
457}
458
459#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
461pub struct CodeRouteRecord {
462 pub repository_id: String,
463 pub source_scope: String,
464 pub route_id: String,
465 pub file_id: String,
466 pub path: String,
467 pub language_id: String,
468 pub url: String,
469 pub http_method: String,
471 pub handler_name: String,
472 #[serde(skip_serializing_if = "Option::is_none")]
473 pub handler_symbol_snapshot_id: Option<String>,
474 pub framework: String,
475 pub line_range: RepositoryCodeRange,
476}
477
478#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
480pub struct CodeFeatureFlagRecord {
481 pub repository_id: String,
482 pub source_scope: String,
483 pub feature_flag_id: String,
484 pub usage_id: String,
485 pub file_id: String,
486 pub path: String,
487 pub language_id: String,
488 pub name: String,
489 pub source_kind: String,
490 pub source_key: String,
491 pub edge_kind: String,
492 pub confidence_basis_points: u16,
493 pub confidence_tier: String,
494 pub byte_range: RepositoryCodeRange,
495 pub line_range: RepositoryCodeRange,
496 pub excerpt: String,
497}
498
499#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
501pub struct RepositoryCodeChunkRecord {
502 pub repository_id: String,
503 pub source_scope: String,
504 pub chunk_id: String,
505 pub file_id: String,
506 pub path: String,
507 pub language_id: String,
508 pub content: String,
509 pub byte_range: RepositoryCodeRange,
510 pub line_range: RepositoryCodeRange,
511 pub symbol_snapshot_id: Option<String>,
512}
513
514#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
516pub struct CodeFileDiagnostic {
517 pub repository_id: String,
518 pub source_scope: String,
519 pub path: String,
520 pub parse_status: CodeParseStatus,
521 pub message: String,
522}
523
524#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
526pub struct CodePathTombstone {
527 pub repository_id: String,
528 pub source_scope: String,
529 pub old_path: String,
530 pub new_path: Option<String>,
531 pub base_ref: String,
532 pub head_ref: String,
533}
534
535#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
537pub struct CodeRepositoryLanguagePreview {
538 pub language_id: String,
539 pub file_count: usize,
540 pub byte_count: usize,
541}
542
543#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
545pub struct CodeRepositoryLargestFile {
546 pub path: String,
547 pub byte_count: usize,
548}
549
550#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
552pub struct CodeRepositoryExcludedPath {
553 pub path: String,
554 pub reason: String,
555}
556
557#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
559pub struct CodeRepositoryScopePreview {
560 pub repository_id: String,
561 pub alias: String,
562 pub requested_ref: String,
563 pub resolved_commit_sha: String,
564 pub tree_hash: String,
565 pub selected_file_count: usize,
566 pub selected_byte_count: usize,
567 pub unsupported_file_count: usize,
568 pub generated_or_heavy_file_count: usize,
569 pub expected_degraded_file_count: usize,
570 pub language_distribution: Vec<CodeRepositoryLanguagePreview>,
571 pub largest_files: Vec<CodeRepositoryLargestFile>,
572 pub excluded_paths: Vec<CodeRepositoryExcludedPath>,
573}
574
575#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
577pub struct CodeRepositoryTotals {
578 pub repository_count: usize,
579 pub indexed_file_count: usize,
580 pub symbol_count: usize,
581 #[serde(default)]
582 pub handwritten_symbol_count: usize,
583 #[serde(default)]
584 pub generated_symbol_count: usize,
585 pub reference_count: usize,
586 pub chunk_count: usize,
587 pub degraded_file_count: usize,
588 pub parse_status_counts: CodeParseStatusCounts,
589}
590
591#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
593pub struct CodeSymbolGenerationCounts {
594 #[serde(default)]
595 pub handwritten_symbol_count: usize,
596 #[serde(default)]
597 pub generated_symbol_count: usize,
598}
599
600#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
602pub struct CodeRepositoryLatencySample {
603 pub query: String,
604 pub kind: CodeQueryKind,
605 pub result_count: usize,
606 pub duration_ms: u64,
607}
608
609#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
611pub struct CodeRepositoryReport {
612 pub repository_id: String,
613 pub alias: String,
614 pub root_path: String,
615 pub path_filters: Vec<String>,
616 pub language_filters: Vec<String>,
617 pub resolved_commit_sha: Option<String>,
618 pub tree_hash: Option<String>,
619 pub indexed_file_count: usize,
620 pub symbol_count: usize,
621 #[serde(default)]
622 pub handwritten_symbol_count: usize,
623 #[serde(default)]
624 pub generated_symbol_count: usize,
625 pub reference_count: usize,
626 pub chunk_count: usize,
627 pub degraded_file_count: usize,
628 pub resolved_edge_count: usize,
629 pub ambiguous_edge_count: usize,
630 pub unresolved_edge_count: usize,
631 pub degradation_summary: Vec<String>,
632 pub representative_queries: Vec<String>,
633 pub latency_samples: Vec<CodeRepositoryLatencySample>,
634 pub freshness_state: String,
635}
636
637#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
639pub struct CodeImpactPathGroups {
640 pub in_scope_changed_paths: Vec<String>,
641 pub out_of_scope_changed_paths: Vec<String>,
642}
643
644pub use super::code_staleness::StalenessHint;
645
646#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
648pub struct CodeRetrievalHit {
649 pub repository_id: String,
650 pub scope_id: String,
651 pub resolved_commit_sha: String,
652 pub tree_hash: String,
653 pub path: String,
654 pub language_id: String,
655 pub byte_range: RepositoryCodeRange,
656 pub line_range: RepositoryCodeRange,
657 pub symbol_snapshot_id: Option<String>,
658 #[serde(skip_serializing_if = "Option::is_none")]
659 pub canonical_symbol_id: Option<String>,
660 pub file_id: Option<String>,
661 pub retrieval_layers: Vec<CodeRetrievalLayer>,
662 pub index_versions: Vec<String>,
663 pub stale: bool,
664 #[serde(skip_serializing_if = "Option::is_none")]
665 pub staleness_hint: Option<StalenessHint>,
666 #[serde(skip_serializing_if = "Option::is_none")]
667 pub degraded_reason: Option<String>,
668 #[serde(skip_serializing_if = "Option::is_none")]
669 pub edge_kind: Option<String>,
670 #[serde(skip_serializing_if = "Option::is_none")]
671 pub edge_resolution_state: Option<String>,
672 #[serde(skip_serializing_if = "Option::is_none")]
673 pub edge_target_hint: Option<String>,
674 #[serde(skip_serializing_if = "Option::is_none")]
675 pub edge_confidence_basis_points: Option<u16>,
676 #[serde(skip_serializing_if = "Option::is_none")]
677 pub edge_confidence_tier: Option<String>,
678 pub score: f64,
679 pub excerpt: String,
680}
681
682#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
684pub struct CodeFeatureFlagUsage {
685 pub usage_id: String,
686 pub path: String,
687 pub language_id: String,
688 pub file_id: String,
689 pub byte_range: RepositoryCodeRange,
690 pub line_range: RepositoryCodeRange,
691 pub edge_kind: String,
692 #[serde(skip_serializing_if = "Option::is_none")]
693 pub related_symbol_snapshot_id: Option<String>,
694 #[serde(skip_serializing_if = "Option::is_none")]
695 pub related_symbol_name: Option<String>,
696 pub confidence_basis_points: u16,
697 pub confidence_tier: String,
698 pub excerpt: String,
699}
700
701#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
703pub struct CodeFeatureFlagGraph {
704 pub feature_flag_id: String,
705 pub name: String,
706 pub source_kind: String,
707 pub source_key: String,
708 pub score: f64,
709 pub usages: Vec<CodeFeatureFlagUsage>,
710}
711
712#[cfg(test)]
713mod fact_version_tests {
714 use super::CODE_SNAPSHOT_FACT_VERSION;
715
716 #[test]
717 fn code_snapshot_fact_version_includes_generated_and_web_route_facts() {
718 assert!(CODE_SNAPSHOT_FACT_VERSION.contains("generated-files-v1"));
719 assert!(CODE_SNAPSHOT_FACT_VERSION.contains("web-routes-v1"));
720 }
721}