1use serde::{Deserialize, Serialize};
2
3use crate::domain::{
4 CodeIndexCheckpoint, CodeIndexTaskQueueStatus, CodeIndexTaskRecord, FreshnessPolicy,
5};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum CodeRepositoryFreshnessState {
11 Fresh,
12 Pending,
13 Stale,
14 Degraded,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct CodeRepositoryFreshnessCursor {
20 pub source_scope: String,
21 pub checkpoint_state: String,
22 pub total_path_count: usize,
23 pub parsed_file_count: usize,
24 pub committed_file_count: usize,
25 pub committed_symbol_count: usize,
26 pub committed_reference_count: usize,
27 pub committed_chunk_count: usize,
28 pub batch_count: usize,
29 pub pending_file_count: usize,
30 pub updated_at_ms: u64,
31}
32
33impl CodeRepositoryFreshnessCursor {
34 pub fn from_checkpoint(checkpoint: &CodeIndexCheckpoint) -> Self {
35 Self {
36 source_scope: checkpoint.source_scope.clone(),
37 checkpoint_state: checkpoint.state.clone(),
38 total_path_count: checkpoint.total_path_count,
39 parsed_file_count: checkpoint.parsed_file_count,
40 committed_file_count: checkpoint.committed_file_count,
41 committed_symbol_count: checkpoint.committed_symbol_count,
42 committed_reference_count: checkpoint.committed_reference_count,
43 committed_chunk_count: checkpoint.committed_chunk_count,
44 batch_count: checkpoint.batch_count,
45 pending_file_count: checkpoint
46 .total_path_count
47 .saturating_sub(checkpoint.committed_file_count),
48 updated_at_ms: checkpoint.updated_at_ms,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
55pub struct CodeRepositoryPendingIndexWork {
56 pub active_for_repository: bool,
57 pub active_matches_request: bool,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub active_task_id: Option<String>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub active_task_state: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub active_task_source_scope: Option<String>,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub active_task_ref_selector: Option<String>,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub active_task_resolved_commit_sha: Option<String>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub active_task_lease_expires_at_ms: Option<u64>,
70 pub queue_depth: usize,
71 pub queued_task_count: usize,
72 pub running_task_count: usize,
73 pub retrying_task_count: usize,
74 pub dead_letter_task_count: usize,
75 pub running_lease_count: usize,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub last_error: Option<String>,
78}
79
80impl CodeRepositoryPendingIndexWork {
81 pub fn from_task_and_queue(
82 task: Option<&CodeIndexTaskRecord>,
83 active_matches_request: bool,
84 queue: CodeIndexTaskQueueStatus,
85 ) -> Self {
86 let queue_depth = queue
87 .queued_task_count
88 .saturating_add(queue.running_task_count)
89 .saturating_add(queue.retrying_task_count);
90
91 Self {
92 active_for_repository: task.is_some(),
93 active_matches_request,
94 active_task_id: task.map(|task| task.task_id.clone()),
95 active_task_state: task.map(|task| task.state.as_str().to_owned()),
96 active_task_source_scope: task.map(|task| task.source_scope.clone()),
97 active_task_ref_selector: task.map(|task| task.ref_selector.clone()),
98 active_task_resolved_commit_sha: task.map(|task| task.resolved_commit_sha.clone()),
99 active_task_lease_expires_at_ms: task.and_then(|task| task.lease_expires_at_ms),
100 queue_depth,
101 queued_task_count: queue.queued_task_count,
102 running_task_count: queue.running_task_count,
103 retrying_task_count: queue.retrying_task_count,
104 dead_letter_task_count: queue.dead_letter_task_count,
105 running_lease_count: queue.running_lease_count,
106 last_error: queue.last_error,
107 }
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct CodeRepositoryIndexLag {
114 pub requested_ref: String,
115 pub requested_resolved_ref: String,
116 pub served_ref: String,
117 pub requested_ref_indexed: bool,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub pending_file_count: Option<usize>,
120 pub pending_task_count: usize,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct CodeRepositoryFreshnessDiagnostics {
126 pub state: CodeRepositoryFreshnessState,
127 pub freshness_policy: FreshnessPolicy,
128 pub graph_version: u64,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub source_scope: Option<String>,
131 pub scope_stale: bool,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub stale_reason: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub degraded_reason: Option<String>,
136 pub index_lag: CodeRepositoryIndexLag,
137 pub pending: CodeRepositoryPendingIndexWork,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub cursor: Option<CodeRepositoryFreshnessCursor>,
140 pub direct_source_read_required: bool,
141 #[serde(default, skip_serializing_if = "Vec::is_empty")]
142 pub direct_source_read_paths: Vec<String>,
143 #[serde(default, skip_serializing_if = "Vec::is_empty")]
144 pub agent_instructions: Vec<String>,
145}
146
147impl CodeRepositoryFreshnessDiagnostics {
148 pub fn legacy_unknown() -> Self {
149 Self {
150 state: CodeRepositoryFreshnessState::Degraded,
151 freshness_policy: FreshnessPolicy::AllowStale,
152 graph_version: 0,
153 source_scope: None,
154 scope_stale: false,
155 stale_reason: None,
156 degraded_reason: Some(
157 "remote response did not include freshness diagnostics".to_owned(),
158 ),
159 index_lag: CodeRepositoryIndexLag {
160 requested_ref: String::new(),
161 requested_resolved_ref: String::new(),
162 served_ref: String::new(),
163 requested_ref_indexed: false,
164 pending_file_count: None,
165 pending_task_count: 0,
166 },
167 pending: CodeRepositoryPendingIndexWork::default(),
168 cursor: None,
169 direct_source_read_required: false,
170 direct_source_read_paths: Vec::new(),
171 agent_instructions: Vec::new(),
172 }
173 }
174
175 pub(crate) fn code_query(input: CodeRepositoryFreshnessInput) -> Self {
176 let requested_ref_indexed =
177 !input.scope_stale && input.requested_resolved_ref == input.served_ref;
178 let pending_file_count = input
179 .cursor
180 .as_ref()
181 .map(|cursor| cursor.pending_file_count);
182 let pending_task_count = input.pending.queue_depth;
183 let direct_source_read_required = !requested_ref_indexed || input.scope_stale;
184 let state = freshness_state(
185 direct_source_read_required,
186 input.pending.active_matches_request,
187 input.scope_stale,
188 input.degraded_reason.as_ref(),
189 );
190 let agent_instructions = source_read_instructions(
191 direct_source_read_required,
192 &input.requested_ref,
193 &input.served_ref,
194 &input.direct_source_read_paths,
195 );
196
197 Self {
198 state,
199 freshness_policy: input.freshness_policy,
200 graph_version: input.graph_version,
201 source_scope: input.source_scope,
202 scope_stale: input.scope_stale,
203 stale_reason: input.stale_reason,
204 degraded_reason: input.degraded_reason,
205 index_lag: CodeRepositoryIndexLag {
206 requested_ref: input.requested_ref,
207 requested_resolved_ref: input.requested_resolved_ref,
208 served_ref: input.served_ref,
209 requested_ref_indexed,
210 pending_file_count,
211 pending_task_count,
212 },
213 pending: input.pending,
214 cursor: input.cursor,
215 direct_source_read_required,
216 direct_source_read_paths: input.direct_source_read_paths,
217 agent_instructions,
218 }
219 }
220
221 pub(crate) fn graph_only(
222 graph_version: u64,
223 freshness_policy: FreshnessPolicy,
224 source_scope: Option<String>,
225 requested_ref: String,
226 degraded_reason: String,
227 ) -> Self {
228 let input = CodeRepositoryFreshnessInput {
229 graph_version,
230 freshness_policy,
231 source_scope,
232 requested_ref: requested_ref.clone(),
233 requested_resolved_ref: requested_ref.clone(),
234 served_ref: requested_ref,
235 scope_stale: false,
236 stale_reason: None,
237 degraded_reason: Some(degraded_reason),
238 pending: CodeRepositoryPendingIndexWork::default(),
239 cursor: None,
240 direct_source_read_paths: Vec::new(),
241 };
242
243 Self::code_query(input)
244 }
245}
246
247pub(crate) struct CodeRepositoryFreshnessInput {
248 pub graph_version: u64,
249 pub freshness_policy: FreshnessPolicy,
250 pub source_scope: Option<String>,
251 pub requested_ref: String,
252 pub requested_resolved_ref: String,
253 pub served_ref: String,
254 pub scope_stale: bool,
255 pub stale_reason: Option<String>,
256 pub degraded_reason: Option<String>,
257 pub pending: CodeRepositoryPendingIndexWork,
258 pub cursor: Option<CodeRepositoryFreshnessCursor>,
259 pub direct_source_read_paths: Vec<String>,
260}
261
262fn freshness_state(
263 direct_source_read_required: bool,
264 active_matches_request: bool,
265 scope_stale: bool,
266 degraded_reason: Option<&String>,
267) -> CodeRepositoryFreshnessState {
268 if direct_source_read_required && active_matches_request {
269 CodeRepositoryFreshnessState::Pending
270 } else if scope_stale || direct_source_read_required {
271 CodeRepositoryFreshnessState::Stale
272 } else if degraded_reason.is_some() {
273 CodeRepositoryFreshnessState::Degraded
274 } else {
275 CodeRepositoryFreshnessState::Fresh
276 }
277}
278
279fn source_read_instructions(
280 required: bool,
281 requested_ref: &str,
282 served_ref: &str,
283 paths: &[String],
284) -> Vec<String> {
285 if !required {
286 return Vec::new();
287 }
288 let mut instructions = vec![format!(
289 "Code graph results were served from indexed ref {served_ref}; read direct source before relying on files changed at requested ref {requested_ref}."
290 )];
291 if !paths.is_empty() {
292 instructions.push(format!(
293 "Verify returned paths from direct source before editing or citing them: {}.",
294 paths.join(", ")
295 ));
296 }
297
298 instructions
299}