Skip to main content

relay_knowledge/api/
operations.rs

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