1use serde::{Deserialize, Serialize};
2
3use crate::domain::{
4 CodeGraphContextBudget, CodeGraphContextPack, CodeGraphContextRequest, CodeIndexCheckpoint,
5 CodeIndexTaskQueueStatus, CodeIndexTaskRecord, CodeRepositorySelector, CodeRepositoryStatus,
6 CodeRetrievalLayer, FreshnessPolicy,
7};
8
9use super::ApiMetadata;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13pub struct CodeRepositoryScopeMetadata {
14 pub scope_id: String,
15 pub repository_id: String,
16 pub alias: String,
17 pub requested_ref: String,
18 pub resolved_commit_sha: String,
19 pub tree_hash: String,
20 pub path_filters: Vec<String>,
21 pub language_filters: Vec<String>,
22 #[serde(default)]
23 pub indexed_file_count: usize,
24 pub index_versions: Vec<String>,
25 pub stale: bool,
26}
27
28impl CodeRepositoryScopeMetadata {
29 pub fn from_status(
31 status: &CodeRepositoryStatus,
32 selector: &CodeRepositorySelector,
33 requested_ref: impl Into<String>,
34 ) -> Self {
35 Self {
36 scope_id: status.last_indexed_scope_id.clone().unwrap_or_default(),
37 repository_id: status.repository_id.clone(),
38 alias: status.alias.clone(),
39 requested_ref: requested_ref.into(),
40 resolved_commit_sha: status.last_indexed_commit.clone().unwrap_or_default(),
41 tree_hash: status.tree_hash.clone().unwrap_or_default(),
42 path_filters: merged_filters(&status.path_filters, &selector.path_filters),
43 language_filters: merged_filters(&status.language_filters, &selector.language_filters),
44 indexed_file_count: status.indexed_file_count,
45 index_versions: vec![format!(
46 "code:{}:{}",
47 status
48 .last_indexed_scope_id
49 .as_deref()
50 .unwrap_or("unscoped"),
51 status.tree_hash.as_deref().unwrap_or("unindexed")
52 )],
53 stale: status.stale,
54 }
55 }
56
57 pub fn from_index_task(task: &CodeIndexTaskRecord, requested_ref: impl Into<String>) -> Self {
59 Self {
60 scope_id: task.source_scope.clone(),
61 repository_id: task.repository_id.clone(),
62 alias: task.alias.clone(),
63 requested_ref: requested_ref.into(),
64 resolved_commit_sha: task.resolved_commit_sha.clone(),
65 tree_hash: task.tree_hash.clone(),
66 path_filters: task.path_filters.clone(),
67 language_filters: task.language_filters.clone(),
68 indexed_file_count: 0,
69 index_versions: vec![format!("code:{}:{}", task.source_scope, task.tree_hash)],
70 stale: true,
71 }
72 }
73}
74
75fn merged_filters(base: &[String], request: &[String]) -> Vec<String> {
76 let mut merged = Vec::new();
77 for value in base.iter().chain(request.iter()) {
78 if !merged.contains(value) {
79 merged.push(value.clone());
80 }
81 }
82
83 merged
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum CodeRepositoryFreshnessState {
90 Fresh,
91 Pending,
92 Stale,
93 Degraded,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
98pub struct CodeRepositoryFreshnessCursor {
99 pub source_scope: String,
100 pub checkpoint_state: String,
101 pub total_path_count: usize,
102 pub parsed_file_count: usize,
103 pub committed_file_count: usize,
104 pub committed_symbol_count: usize,
105 pub committed_reference_count: usize,
106 pub committed_chunk_count: usize,
107 pub batch_count: usize,
108 pub pending_file_count: usize,
109 pub updated_at_ms: u64,
110}
111
112impl CodeRepositoryFreshnessCursor {
113 pub fn from_checkpoint(checkpoint: &CodeIndexCheckpoint) -> Self {
114 Self {
115 source_scope: checkpoint.source_scope.clone(),
116 checkpoint_state: checkpoint.state.clone(),
117 total_path_count: checkpoint.total_path_count,
118 parsed_file_count: checkpoint.parsed_file_count,
119 committed_file_count: checkpoint.committed_file_count,
120 committed_symbol_count: checkpoint.committed_symbol_count,
121 committed_reference_count: checkpoint.committed_reference_count,
122 committed_chunk_count: checkpoint.committed_chunk_count,
123 batch_count: checkpoint.batch_count,
124 pending_file_count: checkpoint
125 .total_path_count
126 .saturating_sub(checkpoint.committed_file_count),
127 updated_at_ms: checkpoint.updated_at_ms,
128 }
129 }
130}
131
132#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
134pub struct CodeRepositoryPendingIndexWork {
135 pub active_for_repository: bool,
136 pub active_matches_request: bool,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub active_task_id: Option<String>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub active_task_state: Option<String>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub active_task_source_scope: Option<String>,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub active_task_ref_selector: Option<String>,
145 #[serde(skip_serializing_if = "Option::is_none")]
146 pub active_task_resolved_commit_sha: Option<String>,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub active_task_lease_expires_at_ms: Option<u64>,
149 pub queue_depth: usize,
150 pub queued_task_count: usize,
151 pub running_task_count: usize,
152 pub retrying_task_count: usize,
153 pub dead_letter_task_count: usize,
154 pub running_lease_count: usize,
155 #[serde(skip_serializing_if = "Option::is_none")]
156 pub last_error: Option<String>,
157}
158
159impl CodeRepositoryPendingIndexWork {
160 pub fn from_task_and_queue(
161 task: Option<&CodeIndexTaskRecord>,
162 active_matches_request: bool,
163 queue: CodeIndexTaskQueueStatus,
164 ) -> Self {
165 let queue_depth = queue
166 .queued_task_count
167 .saturating_add(queue.running_task_count)
168 .saturating_add(queue.retrying_task_count);
169
170 Self {
171 active_for_repository: task.is_some(),
172 active_matches_request,
173 active_task_id: task.map(|task| task.task_id.clone()),
174 active_task_state: task.map(|task| task.state.as_str().to_owned()),
175 active_task_source_scope: task.map(|task| task.source_scope.clone()),
176 active_task_ref_selector: task.map(|task| task.ref_selector.clone()),
177 active_task_resolved_commit_sha: task.map(|task| task.resolved_commit_sha.clone()),
178 active_task_lease_expires_at_ms: task.and_then(|task| task.lease_expires_at_ms),
179 queue_depth,
180 queued_task_count: queue.queued_task_count,
181 running_task_count: queue.running_task_count,
182 retrying_task_count: queue.retrying_task_count,
183 dead_letter_task_count: queue.dead_letter_task_count,
184 running_lease_count: queue.running_lease_count,
185 last_error: queue.last_error,
186 }
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct CodeRepositoryIndexLag {
193 pub requested_ref: String,
194 pub requested_resolved_ref: String,
195 pub served_ref: String,
196 pub requested_ref_indexed: bool,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub pending_file_count: Option<usize>,
199 pub pending_task_count: usize,
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub struct CodeRepositoryFreshnessDiagnostics {
205 pub state: CodeRepositoryFreshnessState,
206 pub freshness_policy: FreshnessPolicy,
207 pub graph_version: u64,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub source_scope: Option<String>,
210 pub scope_stale: bool,
211 #[serde(skip_serializing_if = "Option::is_none")]
212 pub stale_reason: Option<String>,
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub degraded_reason: Option<String>,
215 pub index_lag: CodeRepositoryIndexLag,
216 pub pending: CodeRepositoryPendingIndexWork,
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub cursor: Option<CodeRepositoryFreshnessCursor>,
219 pub direct_source_read_required: bool,
220 #[serde(default, skip_serializing_if = "Vec::is_empty")]
221 pub direct_source_read_paths: Vec<String>,
222 #[serde(default, skip_serializing_if = "Vec::is_empty")]
223 pub agent_instructions: Vec<String>,
224}
225
226impl CodeRepositoryFreshnessDiagnostics {
227 pub fn legacy_unknown() -> Self {
228 Self {
229 state: CodeRepositoryFreshnessState::Degraded,
230 freshness_policy: FreshnessPolicy::AllowStale,
231 graph_version: 0,
232 source_scope: None,
233 scope_stale: false,
234 stale_reason: None,
235 degraded_reason: Some(
236 "remote response did not include freshness diagnostics".to_owned(),
237 ),
238 index_lag: CodeRepositoryIndexLag {
239 requested_ref: String::new(),
240 requested_resolved_ref: String::new(),
241 served_ref: String::new(),
242 requested_ref_indexed: false,
243 pending_file_count: None,
244 pending_task_count: 0,
245 },
246 pending: CodeRepositoryPendingIndexWork::default(),
247 cursor: None,
248 direct_source_read_required: false,
249 direct_source_read_paths: Vec::new(),
250 agent_instructions: Vec::new(),
251 }
252 }
253
254 pub(crate) fn code_query(input: CodeRepositoryFreshnessInput) -> Self {
255 let requested_ref_indexed =
256 !input.scope_stale && input.requested_resolved_ref == input.served_ref;
257 let pending_file_count = input
258 .cursor
259 .as_ref()
260 .map(|cursor| cursor.pending_file_count);
261 let pending_task_count = input.pending.queue_depth;
262 let direct_source_read_required = !requested_ref_indexed || input.scope_stale;
263 let state = freshness_state(
264 direct_source_read_required,
265 input.pending.active_matches_request,
266 input.scope_stale,
267 input.degraded_reason.as_ref(),
268 );
269 let agent_instructions = source_read_instructions(
270 direct_source_read_required,
271 &input.requested_ref,
272 &input.served_ref,
273 &input.direct_source_read_paths,
274 );
275
276 Self {
277 state,
278 freshness_policy: input.freshness_policy,
279 graph_version: input.graph_version,
280 source_scope: input.source_scope,
281 scope_stale: input.scope_stale,
282 stale_reason: input.stale_reason,
283 degraded_reason: input.degraded_reason,
284 index_lag: CodeRepositoryIndexLag {
285 requested_ref: input.requested_ref,
286 requested_resolved_ref: input.requested_resolved_ref,
287 served_ref: input.served_ref,
288 requested_ref_indexed,
289 pending_file_count,
290 pending_task_count,
291 },
292 pending: input.pending,
293 cursor: input.cursor,
294 direct_source_read_required,
295 direct_source_read_paths: input.direct_source_read_paths,
296 agent_instructions,
297 }
298 }
299
300 pub(crate) fn graph_only(
301 graph_version: u64,
302 freshness_policy: FreshnessPolicy,
303 source_scope: Option<String>,
304 requested_ref: String,
305 degraded_reason: String,
306 ) -> Self {
307 let input = CodeRepositoryFreshnessInput {
308 graph_version,
309 freshness_policy,
310 source_scope,
311 requested_ref: requested_ref.clone(),
312 requested_resolved_ref: requested_ref.clone(),
313 served_ref: requested_ref,
314 scope_stale: false,
315 stale_reason: None,
316 degraded_reason: Some(degraded_reason),
317 pending: CodeRepositoryPendingIndexWork::default(),
318 cursor: None,
319 direct_source_read_paths: Vec::new(),
320 };
321
322 Self::code_query(input)
323 }
324
325 pub(crate) fn merge_direct_source_read_paths(
326 &mut self,
327 paths: impl IntoIterator<Item = String>,
328 ) {
329 let mut merged = self
330 .direct_source_read_paths
331 .iter()
332 .cloned()
333 .collect::<std::collections::BTreeSet<_>>();
334 merged.extend(paths);
335 self.direct_source_read_paths = merged.into_iter().collect();
336 self.agent_instructions = source_read_instructions(
337 self.direct_source_read_required,
338 &self.index_lag.requested_ref,
339 &self.index_lag.served_ref,
340 &self.direct_source_read_paths,
341 );
342 }
343}
344
345pub(crate) struct CodeRepositoryFreshnessInput {
346 pub graph_version: u64,
347 pub freshness_policy: FreshnessPolicy,
348 pub source_scope: Option<String>,
349 pub requested_ref: String,
350 pub requested_resolved_ref: String,
351 pub served_ref: String,
352 pub scope_stale: bool,
353 pub stale_reason: Option<String>,
354 pub degraded_reason: Option<String>,
355 pub pending: CodeRepositoryPendingIndexWork,
356 pub cursor: Option<CodeRepositoryFreshnessCursor>,
357 pub direct_source_read_paths: Vec<String>,
358}
359
360#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
362pub struct CodeGraphContextResponse {
363 pub metadata: ApiMetadata,
364 pub query: String,
365 pub repository_scope: CodeRepositoryScopeMetadata,
366 #[serde(default = "CodeRepositoryFreshnessDiagnostics::legacy_unknown")]
367 pub freshness: CodeRepositoryFreshnessDiagnostics,
368 pub budget: CodeGraphContextBudget,
369 pub truncated: bool,
370 pub retrieval_layers: Vec<CodeRetrievalLayer>,
371 pub request: CodeGraphContextRequest,
372 #[serde(flatten)]
373 pub pack: CodeGraphContextPack,
374 #[serde(default, skip_serializing_if = "Vec::is_empty")]
375 pub diagnostics: Vec<String>,
376}
377
378fn freshness_state(
379 direct_source_read_required: bool,
380 active_matches_request: bool,
381 scope_stale: bool,
382 degraded_reason: Option<&String>,
383) -> CodeRepositoryFreshnessState {
384 if direct_source_read_required && active_matches_request {
385 CodeRepositoryFreshnessState::Pending
386 } else if scope_stale || direct_source_read_required {
387 CodeRepositoryFreshnessState::Stale
388 } else if degraded_reason.is_some() {
389 CodeRepositoryFreshnessState::Degraded
390 } else {
391 CodeRepositoryFreshnessState::Fresh
392 }
393}
394
395fn source_read_instructions(
396 required: bool,
397 requested_ref: &str,
398 served_ref: &str,
399 paths: &[String],
400) -> Vec<String> {
401 if !required {
402 return Vec::new();
403 }
404 let mut instructions = vec![format!(
405 "Code graph results were served from indexed ref {served_ref}; read direct source before relying on files changed at requested ref {requested_ref}."
406 )];
407 if !paths.is_empty() {
408 instructions.push(format!(
409 "Verify returned paths from direct source before editing or citing them: {}.",
410 paths.join(", ")
411 ));
412 }
413
414 instructions
415}
416
417#[cfg(test)]
418#[path = "code_repository_tests.rs"]
419mod tests;