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