Skip to main content

relay_knowledge/api/contracts/
file_index.rs

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