Skip to main content

relay_knowledge/domain/code/
repository.rs

1use serde::{Deserialize, Serialize};
2
3use super::{
4    CodeDependencyRecord, CodeParseStatus, CodeParseStatusCounts, DomainError, FreshnessPolicy,
5    error::required_text,
6};
7
8const CODE_SNAPSHOT_FACT_VERSION: &str = "code-facts-js-ts-import-edges-v1-sbom-dependencies-v2";
9
10/// Builds the stable source scope id for a Git snapshot partition.
11pub fn code_snapshot_scope_id(
12    repository_id: &str,
13    tree_hash: &str,
14    path_filters: &[String],
15    language_filters: &[String],
16) -> String {
17    let mut input = Vec::new();
18    append_hash_part(&mut input, "git_snapshot");
19    append_hash_part(&mut input, repository_id);
20    append_hash_part(&mut input, tree_hash);
21    append_hash_list(&mut input, path_filters);
22    append_hash_list(&mut input, language_filters);
23    append_hash_part(&mut input, CODE_SNAPSHOT_FACT_VERSION);
24
25    format!("git_snapshot:{:016x}", stable_hash64(&input))
26}
27
28pub fn code_snapshot_expected_scope_id(
29    repository_id: &str,
30    tree_hash: &str,
31    path_filters: &[String],
32    language_filters: &[String],
33) -> Option<String> {
34    Some(code_snapshot_scope_id(
35        repository_id,
36        tree_hash,
37        path_filters,
38        language_filters,
39    ))
40}
41
42/// Inclusive byte or line range for repository code index rows.
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct RepositoryCodeRange {
45    pub start: u32,
46    pub end: u32,
47}
48
49impl RepositoryCodeRange {
50    /// Creates an ordered range using one-based lines or zero-based bytes.
51    pub fn new(field: &'static str, start: usize, end: usize) -> Result<Self, DomainError> {
52        if end < start {
53            return Err(DomainError::invalid(
54                field,
55                "end must be greater than or equal to start",
56            ));
57        }
58
59        Ok(Self {
60            start: checked_u32(field, start)?,
61            end: checked_u32(field, end)?,
62        })
63    }
64}
65
66/// Code repository identity persisted after registration.
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68pub struct CodeRepositoryRegistration {
69    pub repository_id: String,
70    pub alias: String,
71    pub root_path: String,
72    pub path_filters: Vec<String>,
73    pub language_filters: Vec<String>,
74}
75
76impl CodeRepositoryRegistration {
77    /// Validates a repository registration before storage persists it.
78    pub fn new(
79        repository_id: impl Into<String>,
80        alias: impl Into<String>,
81        root_path: impl Into<String>,
82        path_filters: Vec<String>,
83        language_filters: Vec<String>,
84    ) -> Result<Self, DomainError> {
85        Ok(Self {
86            repository_id: required_text("repository_id", repository_id)?,
87            alias: required_text("alias", alias)?,
88            root_path: required_text("root_path", root_path)?,
89            path_filters: normalize_filter_list("path_filter", path_filters)?,
90            language_filters: normalize_filter_list("language_filter", language_filters)?,
91        })
92    }
93}
94
95/// Repository selector accepted by code index and retrieval APIs.
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct CodeRepositorySelector {
98    pub repository: String,
99    pub ref_selector: String,
100    pub path_filters: Vec<String>,
101    pub language_filters: Vec<String>,
102}
103
104impl CodeRepositorySelector {
105    /// Validates a code repository selector with an explicit ref.
106    pub fn new(
107        repository: impl Into<String>,
108        ref_selector: impl Into<String>,
109        path_filters: Vec<String>,
110        language_filters: Vec<String>,
111    ) -> Result<Self, DomainError> {
112        Ok(Self {
113            repository: required_text("repository", repository)?,
114            ref_selector: required_text("ref_selector", ref_selector)?,
115            path_filters: normalize_filter_list("path_filter", path_filters)?,
116            language_filters: normalize_filter_list("language_filter", language_filters)?,
117        })
118    }
119}
120
121/// Code index mode tied to Git snapshots or diffs.
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum CodeIndexMode {
125    Full,
126    Incremental { base_ref: String, head_ref: String },
127    WorktreeOverlay,
128}
129
130impl CodeIndexMode {
131    /// Validates incremental refs and preserves the mode contract.
132    pub fn incremental(
133        base_ref: impl Into<String>,
134        head_ref: impl Into<String>,
135    ) -> Result<Self, DomainError> {
136        Ok(Self::Incremental {
137            base_ref: required_text("base_ref", base_ref)?,
138            head_ref: required_text("head_ref", head_ref)?,
139        })
140    }
141}
142
143/// Code repository indexing request shared by interfaces.
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145pub struct CodeIndexRequest {
146    pub repository: CodeRepositorySelector,
147    pub mode: CodeIndexMode,
148    pub freshness_policy: FreshnessPolicy,
149}
150
151/// Retrieval query kind for code graph and lexical search.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
153#[serde(rename_all = "snake_case")]
154pub enum CodeQueryKind {
155    Hybrid,
156    Symbol,
157    Definition,
158    References,
159    Callers,
160    Callees,
161    Imports,
162    Sbom,
163    Impact,
164}
165
166/// Code repository retrieval request.
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168pub struct CodeRetrievalRequest {
169    pub query: String,
170    pub repository: CodeRepositorySelector,
171    pub code_query_kind: CodeQueryKind,
172    pub limit: usize,
173    pub freshness_policy: FreshnessPolicy,
174}
175
176impl CodeRetrievalRequest {
177    /// Validates query text and result limits before storage is consulted.
178    pub fn new(
179        query: impl Into<String>,
180        repository: CodeRepositorySelector,
181        code_query_kind: CodeQueryKind,
182        limit: usize,
183        freshness_policy: FreshnessPolicy,
184    ) -> Result<Self, DomainError> {
185        let limit = match limit {
186            1..=50 => limit,
187            0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
188            _ => return Err(DomainError::invalid("limit", "must be 50 or less")),
189        };
190
191        Ok(Self {
192            query: required_text("query", query)?,
193            repository,
194            code_query_kind,
195            limit,
196            freshness_policy,
197        })
198    }
199}
200
201/// Feature-flag graph query over an indexed repository scope.
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
203pub struct CodeFeatureFlagRequest {
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub query: Option<String>,
206    pub repository: CodeRepositorySelector,
207    pub limit: usize,
208    pub freshness_policy: FreshnessPolicy,
209}
210
211impl CodeFeatureFlagRequest {
212    /// Validates optional filter text and bounds the number of returned flags.
213    pub fn new(
214        query: Option<String>,
215        repository: CodeRepositorySelector,
216        limit: usize,
217        freshness_policy: FreshnessPolicy,
218    ) -> Result<Self, DomainError> {
219        let limit = match limit {
220            1..=100 => limit,
221            0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
222            _ => return Err(DomainError::invalid("limit", "must be 100 or less")),
223        };
224        let query = query
225            .map(|value| required_text("query", value))
226            .transpose()?;
227
228        Ok(Self {
229            query,
230            repository,
231            limit,
232            freshness_policy,
233        })
234    }
235}
236
237/// Code impact analysis request over a Git diff.
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
239pub struct CodeImpactRequest {
240    pub repository: CodeRepositorySelector,
241    pub base_ref: String,
242    pub head_ref: String,
243    pub limit: usize,
244}
245
246impl CodeImpactRequest {
247    /// Validates diff refs and bounds the impact result count.
248    pub fn new(
249        repository: CodeRepositorySelector,
250        base_ref: impl Into<String>,
251        head_ref: impl Into<String>,
252        limit: usize,
253    ) -> Result<Self, DomainError> {
254        let limit = match limit {
255            1..=100 => limit,
256            0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
257            _ => return Err(DomainError::invalid("limit", "must be 100 or less")),
258        };
259
260        Ok(Self {
261            repository,
262            base_ref: required_text("base_ref", base_ref)?,
263            head_ref: required_text("head_ref", head_ref)?,
264            limit,
265        })
266    }
267}
268
269/// Retrieval layer that contributed to a code hit.
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
271#[serde(rename_all = "snake_case")]
272pub enum CodeRetrievalLayer {
273    Lexical,
274    Symbol,
275    Definition,
276    Reference,
277    CallGraph,
278    ImportGraph,
279    Sbom,
280    Impact,
281    TextFallback,
282}
283
284impl CodeRetrievalLayer {
285    /// Stable storage and API representation.
286    pub const fn as_str(self) -> &'static str {
287        match self {
288            Self::Lexical => "lexical",
289            Self::Symbol => "symbol",
290            Self::Definition => "definition",
291            Self::Reference => "reference",
292            Self::CallGraph => "call_graph",
293            Self::ImportGraph => "import_graph",
294            Self::Sbom => "sbom",
295            Self::Impact => "impact",
296            Self::TextFallback => "text_fallback",
297        }
298    }
299}
300
301/// Repository index status and diagnostics summary.
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
303pub struct CodeRepositoryStatus {
304    pub repository_id: String,
305    pub alias: String,
306    pub root_path: String,
307    pub path_filters: Vec<String>,
308    pub language_filters: Vec<String>,
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub last_indexed_scope_id: Option<String>,
311    pub last_indexed_commit: Option<String>,
312    pub tree_hash: Option<String>,
313    pub state: String,
314    pub indexed_file_count: usize,
315    pub symbol_count: usize,
316    pub reference_count: usize,
317    pub chunk_count: usize,
318    pub stale: bool,
319    #[serde(skip_serializing_if = "Option::is_none")]
320    pub degraded_reason: Option<String>,
321}
322
323/// File-level code index row.
324#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
325pub struct RepositoryCodeFileRecord {
326    pub repository_id: String,
327    pub source_scope: String,
328    pub file_id: String,
329    pub path: String,
330    pub language_id: String,
331    pub blob_hash: String,
332    pub byte_len: usize,
333    pub line_count: usize,
334    pub parse_status: CodeParseStatus,
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub degraded_reason: Option<String>,
337}
338
339/// Previously indexed file hash used to skip unchanged incremental parses.
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341pub struct CodeFileFingerprint {
342    pub path: String,
343    pub blob_hash: String,
344}
345
346/// Symbol definition extracted from tree-sitter syntax.
347#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
348pub struct RepositoryCodeSymbolRecord {
349    pub repository_id: String,
350    pub source_scope: String,
351    pub symbol_snapshot_id: String,
352    pub canonical_symbol_id: String,
353    pub file_id: String,
354    pub path: String,
355    pub language_id: String,
356    pub name: String,
357    pub qualified_name: String,
358    pub kind: String,
359    pub signature: String,
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub doc_comment: Option<String>,
362    pub byte_range: RepositoryCodeRange,
363    pub line_range: RepositoryCodeRange,
364}
365
366/// Reference extracted from tree-sitter syntax and optionally resolved.
367#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
368pub struct RepositoryCodeReferenceRecord {
369    pub repository_id: String,
370    pub source_scope: String,
371    pub reference_id: String,
372    pub file_id: String,
373    pub path: String,
374    pub name: String,
375    pub kind: String,
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub target_symbol_snapshot_id: Option<String>,
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub target_hint: Option<String>,
380    pub resolution_state: String,
381    pub confidence_basis_points: u16,
382    pub confidence_tier: String,
383    pub byte_range: RepositoryCodeRange,
384    pub line_range: RepositoryCodeRange,
385}
386
387/// Import relationship extracted from code.
388#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
389pub struct CodeImportRecord {
390    pub repository_id: String,
391    pub source_scope: String,
392    pub import_id: String,
393    pub file_id: String,
394    pub path: String,
395    pub module: String,
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub target_hint: Option<String>,
398    pub resolution_state: String,
399    pub confidence_basis_points: u16,
400    pub confidence_tier: String,
401    pub line_range: RepositoryCodeRange,
402}
403
404/// Call relationship extracted from code.
405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
406pub struct CodeCallRecord {
407    pub repository_id: String,
408    pub source_scope: String,
409    pub call_id: String,
410    pub file_id: String,
411    pub path: String,
412    pub caller_symbol_snapshot_id: Option<String>,
413    pub caller_name: Option<String>,
414    #[serde(skip_serializing_if = "Option::is_none")]
415    pub callee_symbol_snapshot_id: Option<String>,
416    pub callee_name: String,
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub target_hint: Option<String>,
419    pub resolution_state: String,
420    pub confidence_basis_points: u16,
421    pub confidence_tier: String,
422    pub line_range: RepositoryCodeRange,
423}
424
425/// Feature flag or runtime configuration relationship extracted from code.
426#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
427pub struct CodeFeatureFlagRecord {
428    pub repository_id: String,
429    pub source_scope: String,
430    pub feature_flag_id: String,
431    pub usage_id: String,
432    pub file_id: String,
433    pub path: String,
434    pub language_id: String,
435    pub name: String,
436    pub source_kind: String,
437    pub source_key: String,
438    pub edge_kind: String,
439    pub confidence_basis_points: u16,
440    pub confidence_tier: String,
441    pub byte_range: RepositoryCodeRange,
442    pub line_range: RepositoryCodeRange,
443    pub excerpt: String,
444}
445
446/// Searchable code chunk.
447#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
448pub struct RepositoryCodeChunkRecord {
449    pub repository_id: String,
450    pub source_scope: String,
451    pub chunk_id: String,
452    pub file_id: String,
453    pub path: String,
454    pub language_id: String,
455    pub content: String,
456    pub byte_range: RepositoryCodeRange,
457    pub line_range: RepositoryCodeRange,
458    pub symbol_snapshot_id: Option<String>,
459}
460
461/// File-level diagnostic produced by indexing.
462#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
463pub struct CodeFileDiagnostic {
464    pub repository_id: String,
465    pub source_scope: String,
466    pub path: String,
467    pub parse_status: CodeParseStatus,
468    pub message: String,
469}
470
471/// Rename/delete lineage marker retained after incremental updates.
472#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
473pub struct CodePathTombstone {
474    pub repository_id: String,
475    pub source_scope: String,
476    pub old_path: String,
477    pub new_path: Option<String>,
478    pub base_ref: String,
479    pub head_ref: String,
480}
481
482/// Parsed index changes ready to commit into storage.
483#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
484pub struct CodeIndexSnapshot {
485    pub repository_id: String,
486    pub source_scope: String,
487    #[serde(skip_serializing_if = "Option::is_none")]
488    pub base_resolved_commit_sha: Option<String>,
489    pub resolved_commit_sha: String,
490    pub tree_hash: String,
491    pub path_filters: Vec<String>,
492    pub language_filters: Vec<String>,
493    pub full_replace: bool,
494    pub changed_path_count: usize,
495    pub skipped_unchanged_count: usize,
496    pub deleted_paths: Vec<String>,
497    pub tombstones: Vec<CodePathTombstone>,
498    pub files: Vec<RepositoryCodeFileRecord>,
499    pub symbols: Vec<RepositoryCodeSymbolRecord>,
500    pub references: Vec<RepositoryCodeReferenceRecord>,
501    pub imports: Vec<CodeImportRecord>,
502    pub calls: Vec<CodeCallRecord>,
503    pub dependencies: Vec<CodeDependencyRecord>,
504    pub feature_flags: Vec<CodeFeatureFlagRecord>,
505    pub chunks: Vec<RepositoryCodeChunkRecord>,
506    pub diagnostics: Vec<CodeFileDiagnostic>,
507}
508
509/// Resource budget used to partition repository indexing into bounded batches.
510#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
511pub struct CodeIndexResourceBudget {
512    pub max_files_per_batch: usize,
513    pub max_bytes_per_batch: usize,
514    pub max_rows_per_batch: usize,
515}
516
517impl CodeIndexResourceBudget {
518    pub const DEFAULT_MAX_FILES_PER_BATCH: usize = 512;
519    pub const DEFAULT_MAX_BYTES_PER_BATCH: usize = 16 * 1024 * 1024;
520    pub const DEFAULT_MAX_ROWS_PER_BATCH: usize = 150_000;
521
522    /// Creates a non-zero resource budget for batch parsing and SQLite writes.
523    pub fn new(
524        max_files_per_batch: usize,
525        max_bytes_per_batch: usize,
526        max_rows_per_batch: usize,
527    ) -> Result<Self, DomainError> {
528        if max_files_per_batch == 0 {
529            return Err(DomainError::invalid(
530                "max_files_per_batch",
531                "must be greater than zero",
532            ));
533        }
534        if max_bytes_per_batch == 0 {
535            return Err(DomainError::invalid(
536                "max_bytes_per_batch",
537                "must be greater than zero",
538            ));
539        }
540        if max_rows_per_batch == 0 {
541            return Err(DomainError::invalid(
542                "max_rows_per_batch",
543                "must be greater than zero",
544            ));
545        }
546
547        Ok(Self {
548            max_files_per_batch,
549            max_bytes_per_batch,
550            max_rows_per_batch,
551        })
552    }
553}
554
555impl Default for CodeIndexResourceBudget {
556    fn default() -> Self {
557        Self {
558            max_files_per_batch: Self::DEFAULT_MAX_FILES_PER_BATCH,
559            max_bytes_per_batch: Self::DEFAULT_MAX_BYTES_PER_BATCH,
560            max_rows_per_batch: Self::DEFAULT_MAX_ROWS_PER_BATCH,
561        }
562    }
563}
564
565/// Stable metadata for one resumable repository indexing session.
566#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
567pub struct CodeIndexSession {
568    pub repository_id: String,
569    pub source_scope: String,
570    #[serde(skip_serializing_if = "Option::is_none")]
571    pub base_resolved_commit_sha: Option<String>,
572    pub resolved_commit_sha: String,
573    pub tree_hash: String,
574    pub path_filters: Vec<String>,
575    pub language_filters: Vec<String>,
576    pub full_replace: bool,
577    pub total_path_count: usize,
578    pub changed_path_count: usize,
579    pub skipped_unchanged_count: usize,
580    pub deleted_paths: Vec<String>,
581    pub tombstones: Vec<CodePathTombstone>,
582    pub resource_budget: CodeIndexResourceBudget,
583}
584
585/// One bounded parse result committed under a checkpointed index session.
586#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
587pub struct CodeIndexBatch {
588    pub repository_id: String,
589    pub source_scope: String,
590    pub batch_index: usize,
591    pub parsed_byte_count: usize,
592    pub files: Vec<RepositoryCodeFileRecord>,
593    pub symbols: Vec<RepositoryCodeSymbolRecord>,
594    pub references: Vec<RepositoryCodeReferenceRecord>,
595    pub imports: Vec<CodeImportRecord>,
596    pub dependencies: Vec<CodeDependencyRecord>,
597    pub feature_flags: Vec<CodeFeatureFlagRecord>,
598    pub chunks: Vec<RepositoryCodeChunkRecord>,
599    pub diagnostics: Vec<CodeFileDiagnostic>,
600}
601
602impl CodeIndexBatch {
603    /// Counts mutable SQLite rows written by this batch.
604    pub fn row_count(&self) -> usize {
605        self.files
606            .len()
607            .saturating_add(self.symbols.len())
608            .saturating_add(self.references.len())
609            .saturating_add(self.imports.len())
610            .saturating_add(self.dependencies.len())
611            .saturating_add(self.feature_flags.len())
612            .saturating_add(self.chunks.len())
613            .saturating_add(self.diagnostics.len())
614    }
615}
616
617/// Durable progress checkpoint for a repository indexing session.
618#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
619pub struct CodeIndexCheckpoint {
620    pub repository_id: String,
621    pub source_scope: String,
622    pub state: String,
623    pub total_path_count: usize,
624    pub parsed_file_count: usize,
625    pub committed_file_count: usize,
626    pub committed_symbol_count: usize,
627    pub committed_reference_count: usize,
628    pub committed_chunk_count: usize,
629    pub batch_count: usize,
630    #[serde(skip_serializing_if = "Option::is_none")]
631    pub last_path: Option<String>,
632    pub resource_budget: CodeIndexResourceBudget,
633    pub updated_at_ms: u64,
634}
635
636/// Persistent lifecycle for background code repository index tasks.
637#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
638#[serde(rename_all = "snake_case")]
639pub enum CodeIndexTaskState {
640    Queued,
641    Running,
642    Succeeded,
643    Retrying,
644    Failed,
645    DeadLetter,
646    Cancelled,
647}
648
649impl CodeIndexTaskState {
650    /// Stable storage and API representation.
651    pub const fn as_str(self) -> &'static str {
652        match self {
653            Self::Queued => "queued",
654            Self::Running => "running",
655            Self::Succeeded => "succeeded",
656            Self::Retrying => "retrying",
657            Self::Failed => "failed",
658            Self::DeadLetter => "dead_letter",
659            Self::Cancelled => "cancelled",
660        }
661    }
662
663    /// Parses the stable storage and API representation.
664    pub fn parse(value: &str) -> Result<Self, DomainError> {
665        match value {
666            "queued" => Ok(Self::Queued),
667            "running" => Ok(Self::Running),
668            "succeeded" => Ok(Self::Succeeded),
669            "retrying" => Ok(Self::Retrying),
670            "failed" => Ok(Self::Failed),
671            "dead_letter" => Ok(Self::DeadLetter),
672            "cancelled" => Ok(Self::Cancelled),
673            _ => Err(DomainError::invalid(
674                "code_index_task_state",
675                "unknown code index task state",
676            )),
677        }
678    }
679
680    /// Returns whether the task can still consume executor capacity.
681    pub const fn is_unfinished(self) -> bool {
682        matches!(self, Self::Queued | Self::Running | Self::Retrying)
683    }
684}
685
686/// Durable background task for one code repository index request.
687#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
688pub struct CodeIndexTaskRecord {
689    pub task_id: String,
690    pub repository_id: String,
691    pub alias: String,
692    pub ref_selector: String,
693    pub resolved_commit_sha: String,
694    pub tree_hash: String,
695    pub source_scope: String,
696    pub path_filters: Vec<String>,
697    pub language_filters: Vec<String>,
698    pub mode: CodeIndexMode,
699    pub state: CodeIndexTaskState,
700    #[serde(skip_serializing_if = "Option::is_none")]
701    pub lease_owner: Option<String>,
702    #[serde(skip_serializing_if = "Option::is_none")]
703    pub lease_expires_at_ms: Option<u64>,
704    pub attempt_count: u32,
705    pub next_retry_at_ms: u64,
706    pub input_fingerprint: String,
707    pub resource_budget: CodeIndexResourceBudget,
708    pub payload_json: String,
709    #[serde(skip_serializing_if = "Option::is_none")]
710    pub last_error_kind: Option<String>,
711    #[serde(skip_serializing_if = "Option::is_none")]
712    pub last_error_message: Option<String>,
713    pub created_at_ms: u64,
714    pub updated_at_ms: u64,
715}
716
717/// Scope retention result after pruning old repository snapshots.
718#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
719pub struct CodeScopeRetentionSummary {
720    pub repository_id: String,
721    pub retained_scope_count: usize,
722    pub prunable_scope_count: usize,
723    pub pruned_scope_count: usize,
724    pub retained_scopes: Vec<String>,
725    pub prunable_scopes: Vec<String>,
726    pub pruned_scopes: Vec<String>,
727}
728
729/// Coarse phase timing and counts reported by repository indexing.
730#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
731pub struct CodeIndexProgressSummary {
732    pub git_file_count: usize,
733    pub blob_read_count: usize,
734    pub parsed_file_count: usize,
735    pub sqlite_write_count: usize,
736    pub skipped_file_count: usize,
737    pub degraded_file_count: usize,
738    pub batch_count: usize,
739    pub checkpoint_file_count: usize,
740    pub resource_budget: CodeIndexResourceBudget,
741}
742
743/// Result of applying a code index snapshot.
744#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
745pub struct CodeIndexSummary {
746    pub repository_id: String,
747    pub source_scope: String,
748    pub resolved_commit_sha: String,
749    pub tree_hash: String,
750    pub indexed_file_count: usize,
751    pub changed_path_count: usize,
752    pub skipped_unchanged_count: usize,
753    pub deleted_path_count: usize,
754    pub symbol_count: usize,
755    pub reference_count: usize,
756    pub chunk_count: usize,
757    pub degraded_file_count: usize,
758    pub progress: CodeIndexProgressSummary,
759}
760
761/// Language bucket in a repository scope preview.
762#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
763pub struct CodeRepositoryLanguagePreview {
764    pub language_id: String,
765    pub file_count: usize,
766    pub byte_count: usize,
767}
768
769/// Large file surfaced before a full repository index starts.
770#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
771pub struct CodeRepositoryLargestFile {
772    pub path: String,
773    pub byte_count: usize,
774}
775
776/// Path excluded from indexing by preset, ignore file, or request scope.
777#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
778pub struct CodeRepositoryExcludedPath {
779    pub path: String,
780    pub reason: String,
781}
782
783/// Non-mutating preview of the effective repository indexing scope.
784#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
785pub struct CodeRepositoryScopePreview {
786    pub repository_id: String,
787    pub alias: String,
788    pub requested_ref: String,
789    pub resolved_commit_sha: String,
790    pub tree_hash: String,
791    pub selected_file_count: usize,
792    pub selected_byte_count: usize,
793    pub unsupported_file_count: usize,
794    pub generated_or_heavy_file_count: usize,
795    pub expected_degraded_file_count: usize,
796    pub language_distribution: Vec<CodeRepositoryLanguagePreview>,
797    pub largest_files: Vec<CodeRepositoryLargestFile>,
798    pub excluded_paths: Vec<CodeRepositoryExcludedPath>,
799}
800
801/// Aggregated totals for repository indexes separate from graph-evidence code rows.
802#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
803pub struct CodeRepositoryTotals {
804    pub repository_count: usize,
805    pub indexed_file_count: usize,
806    pub symbol_count: usize,
807    pub reference_count: usize,
808    pub chunk_count: usize,
809    pub degraded_file_count: usize,
810    pub parse_status_counts: CodeParseStatusCounts,
811}
812
813/// Representative query latency captured for an operations report.
814#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
815pub struct CodeRepositoryLatencySample {
816    pub query: String,
817    pub kind: CodeQueryKind,
818    pub result_count: usize,
819    pub duration_ms: u64,
820}
821
822/// Reusable repository operations report.
823#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
824pub struct CodeRepositoryReport {
825    pub repository_id: String,
826    pub alias: String,
827    pub root_path: String,
828    pub path_filters: Vec<String>,
829    pub language_filters: Vec<String>,
830    pub resolved_commit_sha: Option<String>,
831    pub tree_hash: Option<String>,
832    pub indexed_file_count: usize,
833    pub symbol_count: usize,
834    pub reference_count: usize,
835    pub chunk_count: usize,
836    pub degraded_file_count: usize,
837    pub resolved_edge_count: usize,
838    pub ambiguous_edge_count: usize,
839    pub unresolved_edge_count: usize,
840    pub degradation_summary: Vec<String>,
841    pub representative_queries: Vec<String>,
842    pub latency_samples: Vec<CodeRepositoryLatencySample>,
843    pub freshness_state: String,
844}
845
846/// Diff paths split by the effective repository selector.
847#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
848pub struct CodeImpactPathGroups {
849    pub in_scope_changed_paths: Vec<String>,
850    pub out_of_scope_changed_paths: Vec<String>,
851}
852
853/// Code retrieval hit with source location, layers, and freshness metadata.
854#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
855pub struct CodeRetrievalHit {
856    pub repository_id: String,
857    pub scope_id: String,
858    pub resolved_commit_sha: String,
859    pub tree_hash: String,
860    pub path: String,
861    pub language_id: String,
862    pub byte_range: RepositoryCodeRange,
863    pub line_range: RepositoryCodeRange,
864    pub symbol_snapshot_id: Option<String>,
865    #[serde(skip_serializing_if = "Option::is_none")]
866    pub canonical_symbol_id: Option<String>,
867    pub file_id: Option<String>,
868    pub retrieval_layers: Vec<CodeRetrievalLayer>,
869    pub index_versions: Vec<String>,
870    pub stale: bool,
871    #[serde(skip_serializing_if = "Option::is_none")]
872    pub degraded_reason: Option<String>,
873    #[serde(skip_serializing_if = "Option::is_none")]
874    pub edge_kind: Option<String>,
875    #[serde(skip_serializing_if = "Option::is_none")]
876    pub edge_resolution_state: Option<String>,
877    #[serde(skip_serializing_if = "Option::is_none")]
878    pub edge_target_hint: Option<String>,
879    #[serde(skip_serializing_if = "Option::is_none")]
880    pub edge_confidence_basis_points: Option<u16>,
881    #[serde(skip_serializing_if = "Option::is_none")]
882    pub edge_confidence_tier: Option<String>,
883    pub score: f64,
884    pub excerpt: String,
885}
886
887/// One code location where a feature flag is defined, read, or guards code.
888#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
889pub struct CodeFeatureFlagUsage {
890    pub usage_id: String,
891    pub path: String,
892    pub language_id: String,
893    pub file_id: String,
894    pub byte_range: RepositoryCodeRange,
895    pub line_range: RepositoryCodeRange,
896    pub edge_kind: String,
897    #[serde(skip_serializing_if = "Option::is_none")]
898    pub related_symbol_snapshot_id: Option<String>,
899    #[serde(skip_serializing_if = "Option::is_none")]
900    pub related_symbol_name: Option<String>,
901    pub confidence_basis_points: u16,
902    pub confidence_tier: String,
903    pub excerpt: String,
904}
905
906/// Feature flag graph grouped by stable configuration source.
907#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
908pub struct CodeFeatureFlagGraph {
909    pub feature_flag_id: String,
910    pub name: String,
911    pub source_kind: String,
912    pub source_key: String,
913    pub score: f64,
914    pub usages: Vec<CodeFeatureFlagUsage>,
915}
916
917fn normalize_filter_list(
918    field: &'static str,
919    values: Vec<String>,
920) -> Result<Vec<String>, DomainError> {
921    let mut normalized = Vec::new();
922    for value in values {
923        let value = required_text(field, value)?;
924        if !normalized.contains(&value) {
925            normalized.push(value);
926        }
927    }
928
929    Ok(normalized)
930}
931
932fn checked_u32(field: &'static str, value: usize) -> Result<u32, DomainError> {
933    u32::try_from(value).map_err(|_| DomainError::invalid(field, "must fit in u32"))
934}
935
936fn append_hash_list(input: &mut Vec<u8>, values: &[String]) {
937    input.extend_from_slice(&(values.len() as u64).to_le_bytes());
938    for value in values {
939        append_hash_part(input, value);
940    }
941}
942
943fn append_hash_part(input: &mut Vec<u8>, value: &str) {
944    input.extend_from_slice(&(value.len() as u64).to_le_bytes());
945    input.extend_from_slice(value.as_bytes());
946}
947
948fn stable_hash64(bytes: &[u8]) -> u64 {
949    const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
950    const FNV_PRIME: u64 = 0x100000001b3;
951
952    let mut hash = FNV_OFFSET_BASIS;
953    for byte in bytes {
954        hash ^= u64::from(*byte);
955        hash = hash.wrapping_mul(FNV_PRIME);
956    }
957
958    hash
959}