Skip to main content

relay_knowledge/domain/code/
repository.rs

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
96/// Builds the stable source scope id for a Git snapshot partition.
97pub 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/// Inclusive byte or line range for repository code index rows.
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct RepositoryCodeRange {
141    pub start: u32,
142    pub end: u32,
143}
144
145impl RepositoryCodeRange {
146    /// Creates an ordered range using one-based lines or zero-based bytes.
147    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/// Code repository identity persisted after registration.
163#[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    /// Validates a repository registration before storage persists it.
174    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/// Repository selector accepted by code index and retrieval APIs.
192#[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    /// Validates a code repository selector with an explicit ref.
202    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/// Code index mode tied to Git snapshots or diffs.
218#[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    /// Validates incremental refs and preserves the mode contract.
228    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/// Code repository indexing request shared by interfaces.
240#[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/// Retrieval query kind for code graph and lexical search.
250#[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/// Code repository retrieval request.
265#[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    /// Validates query text and result limits before storage is consulted.
286    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/// Feature-flag graph query over an indexed repository scope.
317#[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    /// Validates optional filter text and bounds the number of returned flags.
328    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/// Code impact analysis request over a Git diff.
353#[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    /// Validates diff refs and bounds the impact result count.
363    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/// Retrieval layer that contributed to a code hit.
385#[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    /// Stable storage and API representation.
401    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/// Repository index status and diagnostics summary.
417#[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/// Counts and aliases removed when a registered repository is deleted.
439#[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/// File-level code index row.
450#[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/// Previously indexed file hash used to skip unchanged incremental parses.
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469pub struct CodeFileFingerprint {
470    pub path: String,
471    pub blob_hash: String,
472}
473
474/// Symbol definition extracted from tree-sitter syntax.
475#[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/// Reference extracted from tree-sitter syntax and optionally resolved.
497#[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/// Import relationship extracted from code.
518#[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/// Call relationship extracted from code.
535#[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/// Web framework route mapping extracted from source code.
556#[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    /// Lowercase HTTP verb, or `any` when a framework route accepts all methods.
566    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/// Feature flag or runtime configuration relationship extracted from code.
575#[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/// Searchable code chunk.
596#[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/// File-level diagnostic produced by indexing.
611#[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/// Rename/delete lineage marker retained after incremental updates.
621#[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/// Language bucket in a repository scope preview.
632#[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/// Large file surfaced before a full repository index starts.
640#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
641pub struct CodeRepositoryLargestFile {
642    pub path: String,
643    pub byte_count: usize,
644}
645
646/// Path excluded from indexing by preset, ignore file, or request scope.
647#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
648pub struct CodeRepositoryExcludedPath {
649    pub path: String,
650    pub reason: String,
651}
652
653/// Non-mutating preview of the effective repository indexing scope.
654#[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/// Aggregated totals for repository indexes separate from graph-evidence code rows.
672#[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/// Generated/handwritten split for symbols in one code index scope.
688#[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/// Representative query latency captured for an operations report.
697#[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/// Reusable repository operations report.
706#[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/// Diff paths split by the effective repository selector.
734#[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/// Code retrieval hit with source location, layers, and freshness metadata.
743#[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/// One code location where a feature flag is defined, read, or guards code.
779#[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/// Feature flag graph grouped by stable configuration source.
798#[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}