Skip to main content

relay_knowledge/api/contracts/
file_index.rs

1use serde::{Deserialize, Serialize};
2
3use crate::domain::FreshnessPolicy;
4
5use super::ApiMetadata;
6
7use crate::storage::FileContentReadModelCursor;
8
9/// Freshness state for local file-index answers.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum FileIndexFreshnessState {
13    Fresh,
14    Pending,
15    Paused,
16    Stale,
17    Degraded,
18    Overflow,
19}
20
21/// Bounded-scan cursor for one local file-index root.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct FileIndexFreshnessCursor {
24    pub source_scope: String,
25    pub root_id: String,
26    pub root_path: String,
27    pub backend: String,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub scan_watermark_ms: Option<u64>,
30    pub indexed_file_count: usize,
31    pub missing_file_count: usize,
32    pub scan_error_count: usize,
33    pub overflow: bool,
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub last_error: Option<String>,
36}
37
38/// Root and file-count lag visible before a caller trusts file-index hits.
39#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
40pub struct FileIndexLag {
41    pub configured_root_count: usize,
42    pub indexed_root_count: usize,
43    pub pending_root_count: usize,
44    pub stale_root_count: usize,
45    pub overflow_root_count: usize,
46    pub missing_file_count: usize,
47    pub pending_task_count: usize,
48}
49
50/// Freshness governance fields returned with local file-index responses.
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct FileIndexFreshnessDiagnostics {
53    pub state: FileIndexFreshnessState,
54    pub freshness_policy: FreshnessPolicy,
55    pub graph_version: u64,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub source_scope: Option<String>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub root_id: Option<String>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub stale_reason: Option<String>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub degraded_reason: Option<String>,
64    pub index_lag: FileIndexLag,
65    pub cursors: Vec<FileIndexFreshnessCursor>,
66    pub direct_source_read_required: bool,
67    pub bounded_rescan_required: bool,
68    #[serde(default, skip_serializing_if = "Vec::is_empty")]
69    pub direct_source_read_paths: Vec<String>,
70    #[serde(default, skip_serializing_if = "Vec::is_empty")]
71    pub agent_instructions: Vec<String>,
72    #[serde(default, skip_serializing_if = "Vec::is_empty")]
73    pub content_read_model_cursors: Vec<FileContentReadModelCursor>,
74}
75
76impl FileIndexFreshnessDiagnostics {
77    pub fn legacy_unknown() -> Self {
78        Self {
79            state: FileIndexFreshnessState::Degraded,
80            freshness_policy: FreshnessPolicy::AllowStale,
81            graph_version: 0,
82            source_scope: None,
83            root_id: None,
84            stale_reason: None,
85            degraded_reason: Some("response did not include file freshness diagnostics".to_owned()),
86            index_lag: FileIndexLag::default(),
87            cursors: Vec::new(),
88            direct_source_read_required: false,
89            bounded_rescan_required: false,
90            direct_source_read_paths: Vec::new(),
91            agent_instructions: Vec::new(),
92            content_read_model_cursors: Vec::new(),
93        }
94    }
95}
96
97/// Bounded local file indexing request.
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct FileIndexRequest {
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub source_scope: Option<String>,
102    #[serde(default)]
103    pub roots: Vec<String>,
104}
105
106/// Local file indexing response.
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108pub struct FileIndexResponse {
109    pub metadata: ApiMetadata,
110    pub summary: crate::storage::FileIndexScanSummary,
111}
112
113/// Bounded local file-location query request.
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115pub struct FileQueryRequest {
116    pub query: String,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub source_scope: Option<String>,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub root_id: Option<String>,
121    pub limit: usize,
122    #[serde(default)]
123    pub freshness_policy: FreshnessPolicy,
124}
125
126/// Local file-location query response.
127#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub struct FileQueryResponse {
129    pub metadata: ApiMetadata,
130    pub query: String,
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub source_scope: Option<String>,
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub root_id: Option<String>,
135    #[serde(default = "FileIndexFreshnessDiagnostics::legacy_unknown")]
136    pub freshness: FileIndexFreshnessDiagnostics,
137    pub results: Vec<crate::storage::FileSearchHit>,
138    pub truncated: bool,
139    pub duration_ms: u64,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub degraded_reason: Option<String>,
142}
143
144/// Bounded local file-content query request.
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146pub struct FileContentQueryRequest {
147    pub query: String,
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub source_scope: Option<String>,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub root_id: Option<String>,
152    pub limit: usize,
153    #[serde(default)]
154    pub freshness_policy: FreshnessPolicy,
155}
156
157/// Local file-content query response with untrusted-source role isolation.
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub struct FileContentQueryResponse {
160    pub metadata: ApiMetadata,
161    pub query: String,
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub source_scope: Option<String>,
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub root_id: Option<String>,
166    #[serde(default = "FileIndexFreshnessDiagnostics::legacy_unknown")]
167    pub freshness: FileIndexFreshnessDiagnostics,
168    pub results: Vec<crate::storage::FileContentSearchHit>,
169    pub truncated: bool,
170    pub duration_ms: u64,
171    #[serde(skip_serializing_if = "Option::is_none")]
172    pub degraded_reason: Option<String>,
173}