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/// Builds the stable source scope id for a Git snapshot partition.
15pub 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/// Inclusive byte or line range for repository code index rows.
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct RepositoryCodeRange {
59    pub start: u32,
60    pub end: u32,
61}
62
63impl RepositoryCodeRange {
64    /// Creates an ordered range using one-based lines or zero-based bytes.
65    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/// Code repository identity persisted after registration.
81#[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    /// Validates a repository registration before storage persists it.
92    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/// Repository selector accepted by code index and retrieval APIs.
110#[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    /// Validates a code repository selector with an explicit ref.
120    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/// Code index mode tied to Git snapshots or diffs.
136#[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    /// Validates incremental refs and preserves the mode contract.
146    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/// Code repository indexing request shared by interfaces.
158#[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/// Retrieval query kind for code graph and lexical search.
168#[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/// Code repository retrieval request.
183#[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    /// Validates query text and result limits before storage is consulted.
196    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/// Feature-flag graph query over an indexed repository scope.
221#[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    /// Validates optional filter text and bounds the number of returned flags.
232    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/// Code impact analysis request over a Git diff.
257#[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    /// Validates diff refs and bounds the impact result count.
267    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/// Retrieval layer that contributed to a code hit.
289#[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    /// Stable storage and API representation.
305    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/// Repository index status and diagnostics summary.
321#[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/// Counts and aliases removed when a registered repository is deleted.
343#[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/// File-level code index row.
354#[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/// Previously indexed file hash used to skip unchanged incremental parses.
372#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
373pub struct CodeFileFingerprint {
374    pub path: String,
375    pub blob_hash: String,
376}
377
378/// Symbol definition extracted from tree-sitter syntax.
379#[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/// Reference extracted from tree-sitter syntax and optionally resolved.
401#[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/// Import relationship extracted from code.
422#[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/// Call relationship extracted from code.
439#[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/// Web framework route mapping extracted from source code.
460#[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    /// Lowercase HTTP verb, or `any` when a framework route accepts all methods.
470    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/// Feature flag or runtime configuration relationship extracted from code.
479#[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/// Searchable code chunk.
500#[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/// File-level diagnostic produced by indexing.
515#[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/// Rename/delete lineage marker retained after incremental updates.
525#[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/// Language bucket in a repository scope preview.
536#[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/// Large file surfaced before a full repository index starts.
544#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
545pub struct CodeRepositoryLargestFile {
546    pub path: String,
547    pub byte_count: usize,
548}
549
550/// Path excluded from indexing by preset, ignore file, or request scope.
551#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
552pub struct CodeRepositoryExcludedPath {
553    pub path: String,
554    pub reason: String,
555}
556
557/// Non-mutating preview of the effective repository indexing scope.
558#[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/// Aggregated totals for repository indexes separate from graph-evidence code rows.
576#[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/// Generated/handwritten split for symbols in one code index scope.
592#[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/// Representative query latency captured for an operations report.
601#[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/// Reusable repository operations report.
610#[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/// Diff paths split by the effective repository selector.
638#[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/// Code retrieval hit with source location, layers, and freshness metadata.
647#[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/// One code location where a feature flag is defined, read, or guards code.
683#[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/// Feature flag graph grouped by stable configuration source.
702#[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}