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