Skip to main content

relay_knowledge/api/
operations.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    domain::{
7        CodeImpactPathGroups, CodeImpactRequest, CodeIndexSummary, CodeRepositoryRegistration,
8        CodeRepositoryReport, CodeRepositoryScopePreview, CodeRepositorySelector,
9        CodeRepositoryStatus, CodeRepositoryTotals, CodeRetrievalHit, CodeRetrievalRequest,
10        CommitReceipt, ConfidenceScore, EvidenceExtractionMetadata, EvidenceModality, EvidenceSpan,
11        ExtractionDiagnostic, FactStatus, FreshnessPolicy, FusionDiagnostics, GraphVersionRange,
12        IndexKind, IndexStatus, LayoutRegion, ProposalConflictRecord, ProposalRecord,
13        ProposalState, RerankDiagnostics, RetrievalBackendStatus, RetrievalBudgetUsed,
14        RetrievalHit, RetrievalMode, RetrievedContextPack, ServiceDefinitionPlan,
15        ServiceManagerAction, ServiceOperatorStatus, WorkerKind, WorkerStatus, WorkerTaskRecord,
16    },
17    storage::{GraphInspection, IndexCursor, IndexRefreshDiagnostics},
18};
19
20use super::{AgentProtocolStatus, ApiMetadata, RuntimeStatus};
21
22pub const GRAPH_CANVAS_DEFAULT_LIMIT: usize = 250;
23pub const GRAPH_CANVAS_MAX_LIMIT: usize = 1000;
24
25/// Graph canvas view selected by the Web workspace.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum GraphCanvasKind {
29    Knowledge,
30    Code,
31    Mixed,
32}
33
34impl GraphCanvasKind {
35    /// Parses the stable Web query representation.
36    pub fn parse(value: &str) -> Result<Self, String> {
37        match value {
38            "knowledge" => Ok(Self::Knowledge),
39            "code" => Ok(Self::Code),
40            "mixed" => Ok(Self::Mixed),
41            _ => Err(format!("unsupported graph canvas kind '{value}'")),
42        }
43    }
44
45    /// Returns the stable Web query representation.
46    pub const fn as_str(self) -> &'static str {
47        match self {
48            Self::Knowledge => "knowledge",
49            Self::Code => "code",
50            Self::Mixed => "mixed",
51        }
52    }
53}
54
55/// Bounded graph canvas request for same-origin Web exploration.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct GraphCanvasRequest {
58    pub kind: GraphCanvasKind,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub source_scope: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub query: Option<String>,
63    pub limit: usize,
64}
65
66/// Node rendered in the Web graph canvas.
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68pub struct GraphCanvasNode {
69    pub id: String,
70    pub kind: String,
71    pub label: String,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub subtitle: Option<String>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub source_scope: Option<String>,
76    pub graph_version: u64,
77    pub weight: u32,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub status: Option<String>,
80    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
81    pub details: BTreeMap<String, String>,
82}
83
84/// Edge rendered in the Web graph canvas.
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct GraphCanvasEdge {
87    pub id: String,
88    pub kind: String,
89    pub source: String,
90    pub target: String,
91    pub label: String,
92    pub graph_version: u64,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub confidence_basis_points: Option<u16>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub evidence_count: Option<usize>,
97    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
98    pub details: BTreeMap<String, String>,
99}
100
101/// Bounded graph canvas summary for truncation and legend hints.
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct GraphCanvasSummary {
104    pub kind: GraphCanvasKind,
105    pub node_count: usize,
106    pub edge_count: usize,
107    pub truncated: bool,
108    pub available_kinds: Vec<String>,
109}
110
111/// Same-origin graph canvas response.
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct GraphCanvasResponse {
114    pub metadata: ApiMetadata,
115    pub nodes: Vec<GraphCanvasNode>,
116    pub edges: Vec<GraphCanvasEdge>,
117    pub summary: GraphCanvasSummary,
118}
119
120/// Evidence item supplied to the ingest API.
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct IngestEvidence {
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub id: Option<String>,
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub source_path: Option<String>,
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub span: Option<EvidenceSpan>,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub confidence: Option<ConfidenceScore>,
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub status: Option<FactStatus>,
133    pub content: String,
134    #[serde(default)]
135    pub entity_labels: Vec<String>,
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub extraction: Option<IngestEvidenceExtraction>,
138}
139
140/// Optional multimodal extraction metadata supplied with an evidence item.
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142pub struct IngestEvidenceExtraction {
143    pub modality: EvidenceModality,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub source_uri: Option<String>,
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub source_hash: Option<String>,
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub media_hash: Option<String>,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub extractor: Option<String>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub extractor_version: Option<String>,
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub observed_at: Option<String>,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub parent_evidence_id: Option<String>,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub layout_region: Option<LayoutRegion>,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub embedding_model: Option<String>,
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub embedding_dimension: Option<u16>,
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub diagnostic: Option<ExtractionDiagnostic>,
166}
167
168impl IngestEvidenceExtraction {
169    /// Converts API metadata into the domain extraction contract.
170    pub fn into_domain_metadata(self) -> EvidenceExtractionMetadata {
171        EvidenceExtractionMetadata {
172            modality: self.modality,
173            source_uri: self.source_uri,
174            source_hash: self.source_hash,
175            media_hash: self.media_hash,
176            extractor: self.extractor,
177            extractor_version: self.extractor_version,
178            observed_at: self.observed_at,
179            parent_evidence_id: self.parent_evidence_id,
180            layout_region: self.layout_region,
181            embedding_model: self.embedding_model,
182            embedding_dimension: self.embedding_dimension,
183            diagnostic: self
184                .diagnostic
185                .unwrap_or_else(|| EvidenceExtractionMetadata::text_span().diagnostic),
186        }
187    }
188}
189
190/// Structured relation supplied to the ingest API.
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub struct IngestRelation {
193    pub id: String,
194    pub source_entity_label: String,
195    pub relation_type: String,
196    pub target_entity_label: String,
197    #[serde(default)]
198    pub evidence_ids: Vec<String>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub confidence: Option<ConfidenceScore>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub status: Option<FactStatus>,
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub version_range: Option<GraphVersionRange>,
205}
206
207/// Structured claim supplied to the ingest API.
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
209pub struct IngestClaim {
210    pub id: String,
211    pub subject_entity_label: String,
212    pub predicate: String,
213    pub object: String,
214    #[serde(default)]
215    pub evidence_ids: Vec<String>,
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub confidence: Option<ConfidenceScore>,
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub status: Option<FactStatus>,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub version_range: Option<GraphVersionRange>,
222}
223
224/// Structured event supplied to the ingest API.
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226pub struct IngestEvent {
227    pub id: String,
228    pub event_type: String,
229    #[serde(default)]
230    pub entity_labels: Vec<String>,
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub occurred_at: Option<String>,
233    #[serde(default)]
234    pub evidence_ids: Vec<String>,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub confidence: Option<ConfidenceScore>,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub status: Option<FactStatus>,
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub version_range: Option<GraphVersionRange>,
241}
242
243/// Ingest request shared by CLI, Web, HTTP, and future agent adapters.
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
245pub struct IngestRequest {
246    pub source_scope: String,
247    #[serde(default)]
248    pub evidence: Vec<IngestEvidence>,
249    #[serde(default)]
250    pub relations: Vec<IngestRelation>,
251    #[serde(default)]
252    pub claims: Vec<IngestClaim>,
253    #[serde(default)]
254    pub events: Vec<IngestEvent>,
255}
256
257/// Ingest response with committed graph and refreshed index versions.
258#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259pub struct IngestResponse {
260    pub metadata: ApiMetadata,
261    pub receipt: CommitReceipt,
262    pub indexes: Vec<IndexStatus>,
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub index_refresh_error: Option<String>,
265}
266
267/// Maintenance-worker output for derived OCR, caption, table, layout, or image embeddings.
268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
269pub struct MultimodalExtractionRequest {
270    pub source_scope: String,
271    pub parent_evidence_id: String,
272    pub derived_evidence: Vec<IngestEvidence>,
273}
274
275/// Commit result for a bounded multimodal maintenance batch.
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277pub struct MultimodalExtractionResponse {
278    pub metadata: ApiMetadata,
279    pub parent_evidence_id: String,
280    pub derived_evidence_count: usize,
281    pub receipt: CommitReceipt,
282    pub indexes: Vec<IndexStatus>,
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub index_refresh_error: Option<String>,
285}
286
287/// Hybrid retrieval request over graph facts and derived indexes.
288#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
289pub struct HybridRetrievalRequest {
290    pub query: String,
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub source_scope: Option<String>,
293    pub limit: usize,
294    pub freshness: FreshnessPolicy,
295}
296
297impl HybridRetrievalRequest {
298    /// Creates a bounded default retrieval request for human-facing interfaces.
299    pub fn new(query: impl Into<String>) -> Self {
300        Self {
301            query: query.into(),
302            source_scope: None,
303            limit: 10,
304            freshness: FreshnessPolicy::default(),
305        }
306    }
307}
308
309/// Retrieval response with freshness and degradation information.
310#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
311pub struct HybridRetrievalResponse {
312    pub metadata: ApiMetadata,
313    pub context_pack: RetrievedContextPack,
314    pub retrieval_mode: RetrievalMode,
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub source_scope: Option<String>,
317    pub freshness: FreshnessPolicy,
318    pub results: Vec<RetrievalHit>,
319    pub fusion: FusionDiagnostics,
320    pub rerank: RerankDiagnostics,
321    #[serde(default, skip_serializing_if = "Vec::is_empty")]
322    pub backend_statuses: Vec<RetrievalBackendStatus>,
323    pub truncated: bool,
324    pub budget_used: RetrievalBudgetUsed,
325    #[serde(skip_serializing_if = "Option::is_none")]
326    pub degraded_reason: Option<String>,
327    pub indexes: Vec<IndexStatus>,
328}
329
330/// Graph inspection request with optional scope filtering reserved for adapters.
331#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
332pub struct GraphInspectionRequest {
333    #[serde(skip_serializing_if = "Option::is_none")]
334    pub source_scope: Option<String>,
335}
336
337/// Graph inspection response for diagnostics and agent adapters.
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339pub struct GraphInspectionResponse {
340    pub metadata: ApiMetadata,
341    pub graph: GraphInspection,
342    pub repository_code_totals: CodeRepositoryTotals,
343}
344
345/// Index refresh request. Empty `kinds` means all v1 index families.
346#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
347pub struct IndexRefreshRequest {
348    #[serde(default)]
349    pub kinds: Vec<IndexKind>,
350}
351
352/// Index refresh response after metadata is updated.
353#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
354pub struct IndexRefreshResponse {
355    pub metadata: ApiMetadata,
356    pub indexes: Vec<IndexStatus>,
357    pub index_cursors: Vec<IndexCursor>,
358    pub diagnostics: IndexRefreshDiagnostics,
359}
360
361/// Bounded local file indexing request.
362#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363pub struct FileIndexRequest {
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub source_scope: Option<String>,
366    #[serde(default)]
367    pub roots: Vec<String>,
368}
369
370/// Local file indexing response.
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
372pub struct FileIndexResponse {
373    pub metadata: ApiMetadata,
374    pub summary: crate::storage::FileIndexScanSummary,
375}
376
377/// Bounded local file-location query request.
378#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
379pub struct FileQueryRequest {
380    pub query: String,
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub source_scope: Option<String>,
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub root_id: Option<String>,
385    pub limit: usize,
386}
387
388/// Local file-location query response.
389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
390pub struct FileQueryResponse {
391    pub metadata: ApiMetadata,
392    pub query: String,
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub source_scope: Option<String>,
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub root_id: Option<String>,
397    pub results: Vec<crate::storage::FileSearchHit>,
398    pub truncated: bool,
399    pub duration_ms: u64,
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub degraded_reason: Option<String>,
402}
403
404/// Service manager status surfaced without exposing platform-specific handles.
405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
406pub struct ServiceStatusResponse {
407    pub metadata: ApiMetadata,
408    pub service_name: String,
409    pub mode: String,
410    pub background_enabled: bool,
411    pub silent_updates_enabled: bool,
412    pub service_definition_path: String,
413    pub index_refresh: IndexRefreshDiagnostics,
414    pub file_index: crate::storage::FileIndexDiagnostics,
415    pub agent_protocols: AgentProtocolStatus,
416    pub operator: ServiceOperatorStatus,
417    pub workers: Vec<WorkerStatus>,
418    pub proposal_backlog: usize,
419    pub audit_sink: AuditSinkStatus,
420}
421
422/// Durable audit sink health surfaced in service diagnostics.
423#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
424pub struct AuditSinkStatus {
425    pub durable: bool,
426    pub event_count: usize,
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub last_error: Option<String>,
429}
430
431/// Worker status filter. Missing kind means all worker families.
432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
433pub struct WorkerStatusRequest {
434    #[serde(skip_serializing_if = "Option::is_none")]
435    pub kind: Option<WorkerKind>,
436}
437
438/// Worker status response.
439#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
440pub struct WorkerStatusResponse {
441    pub metadata: ApiMetadata,
442    pub workers: Vec<WorkerStatus>,
443}
444
445/// Bounded foreground worker run request.
446#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
447pub struct WorkerRunRequest {
448    #[serde(skip_serializing_if = "Option::is_none")]
449    pub kind: Option<WorkerKind>,
450}
451
452/// Bounded foreground worker run response.
453#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
454pub struct WorkerRunResponse {
455    pub metadata: ApiMetadata,
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub task: Option<WorkerTaskRecord>,
458    #[serde(default)]
459    pub proposals: Vec<ProposalRecord>,
460    pub workers: Vec<WorkerStatus>,
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub degraded_reason: Option<String>,
463}
464
465/// Proposal list filter.
466#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
467pub struct ProposalListApiRequest {
468    #[serde(skip_serializing_if = "Option::is_none")]
469    pub state: Option<ProposalState>,
470    pub limit: usize,
471}
472
473/// Proposal list response.
474#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
475pub struct ProposalListResponse {
476    pub metadata: ApiMetadata,
477    pub proposals: Vec<ProposalRecord>,
478}
479
480/// Proposal detail response with conflict lifecycle.
481#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
482pub struct ProposalShowResponse {
483    pub metadata: ApiMetadata,
484    pub proposal: ProposalRecord,
485    pub conflicts: Vec<ProposalConflictRecord>,
486    pub payload: serde_json::Value,
487}
488
489/// Manual proposal decision request.
490#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
491pub struct ProposalDecisionApiRequest {
492    pub actor: String,
493    #[serde(skip_serializing_if = "Option::is_none")]
494    pub reason: Option<String>,
495}
496
497/// Manual proposal decision response.
498#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
499pub struct ProposalDecisionResponse {
500    pub metadata: ApiMetadata,
501    pub proposal: ProposalRecord,
502    #[serde(skip_serializing_if = "Option::is_none")]
503    pub receipt: Option<CommitReceipt>,
504    #[serde(skip_serializing_if = "Option::is_none")]
505    pub index_refresh_error: Option<String>,
506}
507
508/// Durable audit query request.
509#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
510pub struct AuditQueryApiRequest {
511    #[serde(skip_serializing_if = "Option::is_none")]
512    pub operation: Option<String>,
513    pub limit: usize,
514}
515
516/// Durable audit query response.
517#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
518pub struct AuditQueryResponse {
519    pub metadata: ApiMetadata,
520    pub events: Vec<crate::domain::AuditEventRecord>,
521}
522
523/// Service manager plan request.
524#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
525pub struct ServicePlanRequest {
526    pub action: ServiceManagerAction,
527}
528
529/// Service manager plan response.
530#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
531pub struct ServicePlanResponse {
532    pub metadata: ApiMetadata,
533    pub plan: ServiceDefinitionPlan,
534}
535
536/// Service definition write response.
537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
538pub struct ServiceDefinitionWriteResponse {
539    pub metadata: ApiMetadata,
540    pub plan: ServiceDefinitionPlan,
541    pub written: bool,
542}
543
544/// Service silent-update operator response.
545#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
546pub struct ServiceOperatorResponse {
547    pub metadata: ApiMetadata,
548    pub operator: ServiceOperatorStatus,
549}
550
551/// Startup recovery report for resident service mode.
552#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
553pub struct ServiceRecoveryReport {
554    pub metadata: ApiMetadata,
555    pub graph_version: u64,
556    pub stale_index_kinds: Vec<IndexKind>,
557    pub refreshed_index_kinds: Vec<IndexKind>,
558    pub index_lag_max: u64,
559    pub task_queue_depth: usize,
560    pub dead_letter_count: usize,
561    pub heartbeat_state: String,
562}
563
564/// Aggregated health response for CLI/Web/service diagnostics.
565#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
566pub struct HealthResponse {
567    pub metadata: ApiMetadata,
568    pub healthy: bool,
569    pub graph: GraphInspection,
570    pub repository_code_totals: CodeRepositoryTotals,
571    pub indexes: Vec<IndexStatus>,
572    pub index_cursors: Vec<IndexCursor>,
573    pub index_refresh: IndexRefreshDiagnostics,
574    pub file_index: crate::storage::FileIndexDiagnostics,
575    pub runtime: RuntimeStatus,
576}
577
578/// Remote embedding provider probe response with secret-free diagnostics.
579#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
580pub struct EmbeddingProviderProbeResponse {
581    pub metadata: ApiMetadata,
582    pub ok: bool,
583    pub provider: Option<String>,
584    pub model: String,
585    pub dimension: u32,
586    #[serde(skip_serializing_if = "Option::is_none")]
587    pub latency_ms: Option<u64>,
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub error_code: Option<String>,
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub error_message: Option<String>,
592    #[serde(skip_serializing_if = "Option::is_none")]
593    pub retryable: Option<bool>,
594}
595
596/// Code repository registration request.
597#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
598pub struct CodeRepositoryRegisterRequest {
599    pub root_path: String,
600    pub alias: String,
601    #[serde(default)]
602    pub path_filters: Vec<String>,
603    #[serde(default)]
604    pub language_filters: Vec<String>,
605}
606
607/// Code repository registration response.
608#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
609pub struct CodeRepositoryRegisterResponse {
610    pub metadata: ApiMetadata,
611    pub registration: CodeRepositoryRegistration,
612    pub status: CodeRepositoryStatus,
613}
614
615/// Code repository index response.
616#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
617pub struct CodeRepositoryIndexResponse {
618    pub metadata: ApiMetadata,
619    pub scope: CodeRepositoryScopeMetadata,
620    pub summary: CodeIndexSummary,
621    pub status: CodeRepositoryStatus,
622}
623
624/// Code repository scope preview response.
625#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
626pub struct CodeRepositoryScopePreviewResponse {
627    pub metadata: ApiMetadata,
628    pub scope: CodeRepositoryScopeMetadata,
629    pub preview: CodeRepositoryScopePreview,
630}
631
632/// Code repository scope and index metadata attached to code responses.
633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
634pub struct CodeRepositoryScopeMetadata {
635    pub scope_id: String,
636    pub repository_id: String,
637    pub alias: String,
638    pub requested_ref: String,
639    pub resolved_commit_sha: String,
640    pub tree_hash: String,
641    pub path_filters: Vec<String>,
642    pub language_filters: Vec<String>,
643    pub index_versions: Vec<String>,
644    pub stale: bool,
645}
646
647impl CodeRepositoryScopeMetadata {
648    /// Builds stable scope metadata from the selected repository snapshot.
649    pub fn from_status(
650        status: &CodeRepositoryStatus,
651        selector: &CodeRepositorySelector,
652        requested_ref: impl Into<String>,
653    ) -> Self {
654        Self {
655            scope_id: status.last_indexed_scope_id.clone().unwrap_or_default(),
656            repository_id: status.repository_id.clone(),
657            alias: status.alias.clone(),
658            requested_ref: requested_ref.into(),
659            resolved_commit_sha: status.last_indexed_commit.clone().unwrap_or_default(),
660            tree_hash: status.tree_hash.clone().unwrap_or_default(),
661            path_filters: merged_filters(&status.path_filters, &selector.path_filters),
662            language_filters: merged_filters(&status.language_filters, &selector.language_filters),
663            index_versions: vec![format!(
664                "code:{}:{}",
665                status
666                    .last_indexed_scope_id
667                    .as_deref()
668                    .unwrap_or("unscoped"),
669                status.tree_hash.as_deref().unwrap_or("unindexed")
670            )],
671            stale: status.stale,
672        }
673    }
674}
675
676/// Code repository retrieval response.
677#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
678pub struct CodeRepositoryQueryResponse {
679    pub metadata: ApiMetadata,
680    pub scope: CodeRepositoryScopeMetadata,
681    pub request: CodeRetrievalRequest,
682    pub results: Vec<CodeRetrievalHit>,
683    #[serde(skip_serializing_if = "Option::is_none")]
684    pub degraded_reason: Option<String>,
685}
686
687/// Code repository impact response.
688#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
689pub struct CodeRepositoryImpactResponse {
690    pub metadata: ApiMetadata,
691    pub scope: CodeRepositoryScopeMetadata,
692    pub request: CodeImpactRequest,
693    pub path_groups: CodeImpactPathGroups,
694    pub results: Vec<CodeRetrievalHit>,
695}
696
697/// Code repository status response.
698#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
699pub struct CodeRepositoryStatusResponse {
700    pub metadata: ApiMetadata,
701    pub status: CodeRepositoryStatus,
702}
703
704/// Code repository operations report response.
705#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
706pub struct CodeRepositoryReportResponse {
707    pub metadata: ApiMetadata,
708    pub scope: CodeRepositoryScopeMetadata,
709    pub report: CodeRepositoryReport,
710}
711
712fn merged_filters(base: &[String], request: &[String]) -> Vec<String> {
713    let mut merged = Vec::new();
714    for value in base.iter().chain(request.iter()) {
715        if !merged.contains(value) {
716            merged.push(value.clone());
717        }
718    }
719
720    merged
721}