Skip to main content

relay_knowledge/api/
file_index.rs

1use serde::{Deserialize, Serialize};
2
3use crate::domain::FreshnessPolicy;
4
5/// Freshness state for local file-index answers.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum FileIndexFreshnessState {
9    Fresh,
10    Pending,
11    Paused,
12    Stale,
13    Degraded,
14    Overflow,
15}
16
17/// Bounded-scan cursor for one local file-index root.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct FileIndexFreshnessCursor {
20    pub source_scope: String,
21    pub root_id: String,
22    pub root_path: String,
23    pub backend: String,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub scan_watermark_ms: Option<u64>,
26    pub indexed_file_count: usize,
27    pub missing_file_count: usize,
28    pub scan_error_count: usize,
29    pub overflow: bool,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub last_error: Option<String>,
32}
33
34/// Root and file-count lag visible before a caller trusts file-index hits.
35#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
36pub struct FileIndexLag {
37    pub configured_root_count: usize,
38    pub indexed_root_count: usize,
39    pub pending_root_count: usize,
40    pub stale_root_count: usize,
41    pub overflow_root_count: usize,
42    pub missing_file_count: usize,
43    pub pending_task_count: usize,
44}
45
46/// Freshness governance fields returned with local file-index responses.
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct FileIndexFreshnessDiagnostics {
49    pub state: FileIndexFreshnessState,
50    pub freshness_policy: FreshnessPolicy,
51    pub graph_version: u64,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub source_scope: Option<String>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub root_id: Option<String>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub stale_reason: Option<String>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub degraded_reason: Option<String>,
60    pub index_lag: FileIndexLag,
61    pub cursors: Vec<FileIndexFreshnessCursor>,
62    pub direct_source_read_required: bool,
63    pub bounded_rescan_required: bool,
64    #[serde(default, skip_serializing_if = "Vec::is_empty")]
65    pub direct_source_read_paths: Vec<String>,
66    #[serde(default, skip_serializing_if = "Vec::is_empty")]
67    pub agent_instructions: Vec<String>,
68}
69
70impl FileIndexFreshnessDiagnostics {
71    pub fn legacy_unknown() -> Self {
72        Self {
73            state: FileIndexFreshnessState::Degraded,
74            freshness_policy: FreshnessPolicy::AllowStale,
75            graph_version: 0,
76            source_scope: None,
77            root_id: None,
78            stale_reason: None,
79            degraded_reason: Some("response did not include file freshness diagnostics".to_owned()),
80            index_lag: FileIndexLag::default(),
81            cursors: Vec::new(),
82            direct_source_read_required: false,
83            bounded_rescan_required: false,
84            direct_source_read_paths: Vec::new(),
85            agent_instructions: Vec::new(),
86        }
87    }
88}