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    pub graph: GraphInspection,
575    pub repository_code_totals: CodeRepositoryTotals,
576    pub indexes: Vec<IndexStatus>,
577    pub index_cursors: Vec<IndexCursor>,
578    pub index_refresh: IndexRefreshDiagnostics,
579    pub file_index: crate::storage::FileIndexDiagnostics,
580    pub runtime: RuntimeStatus,
581}
582
583/// Remote embedding provider probe response with secret-free diagnostics.
584#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
585pub struct EmbeddingProviderProbeResponse {
586    pub metadata: ApiMetadata,
587    pub ok: bool,
588    pub provider: Option<String>,
589    pub model: String,
590    pub dimension: u32,
591    #[serde(skip_serializing_if = "Option::is_none")]
592    pub latency_ms: Option<u64>,
593    #[serde(skip_serializing_if = "Option::is_none")]
594    pub error_code: Option<String>,
595    #[serde(skip_serializing_if = "Option::is_none")]
596    pub error_message: Option<String>,
597    #[serde(skip_serializing_if = "Option::is_none")]
598    pub retryable: Option<bool>,
599}
600
601/// Code repository registration request.
602#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
603pub struct CodeRepositoryRegisterRequest {
604    pub root_path: String,
605    pub alias: String,
606    #[serde(default)]
607    pub path_filters: Vec<String>,
608    #[serde(default)]
609    pub language_filters: Vec<String>,
610}
611
612/// Code repository registration response.
613#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
614pub struct CodeRepositoryRegisterResponse {
615    pub metadata: ApiMetadata,
616    pub registration: CodeRepositoryRegistration,
617    pub status: CodeRepositoryStatus,
618}
619
620/// Code repository index response.
621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
622pub struct CodeRepositoryIndexResponse {
623    pub metadata: ApiMetadata,
624    pub scope: CodeRepositoryScopeMetadata,
625    pub summary: CodeIndexSummary,
626    pub status: CodeRepositoryStatus,
627}
628
629/// Code repository index start response for queued or no-op index requests.
630#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
631pub struct CodeRepositoryIndexStartResponse {
632    pub metadata: ApiMetadata,
633    pub scope: CodeRepositoryScopeMetadata,
634    #[serde(skip_serializing_if = "Option::is_none")]
635    pub summary: Option<CodeIndexSummary>,
636    pub status: CodeRepositoryStatus,
637    #[serde(skip_serializing_if = "Option::is_none")]
638    pub task: Option<CodeIndexTaskRecord>,
639    #[serde(skip_serializing_if = "Option::is_none")]
640    pub checkpoint: Option<CodeIndexCheckpoint>,
641}
642
643/// Code repository scope preview response.
644#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
645pub struct CodeRepositoryScopePreviewResponse {
646    pub metadata: ApiMetadata,
647    pub scope: CodeRepositoryScopeMetadata,
648    pub preview: CodeRepositoryScopePreview,
649}
650
651/// Code repository scope and index metadata attached to code responses.
652#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
653pub struct CodeRepositoryScopeMetadata {
654    pub scope_id: String,
655    pub repository_id: String,
656    pub alias: String,
657    pub requested_ref: String,
658    pub resolved_commit_sha: String,
659    pub tree_hash: String,
660    pub path_filters: Vec<String>,
661    pub language_filters: Vec<String>,
662    pub index_versions: Vec<String>,
663    pub stale: bool,
664}
665
666impl CodeRepositoryScopeMetadata {
667    /// Builds stable scope metadata from the selected repository snapshot.
668    pub fn from_status(
669        status: &CodeRepositoryStatus,
670        selector: &CodeRepositorySelector,
671        requested_ref: impl Into<String>,
672    ) -> Self {
673        Self {
674            scope_id: status.last_indexed_scope_id.clone().unwrap_or_default(),
675            repository_id: status.repository_id.clone(),
676            alias: status.alias.clone(),
677            requested_ref: requested_ref.into(),
678            resolved_commit_sha: status.last_indexed_commit.clone().unwrap_or_default(),
679            tree_hash: status.tree_hash.clone().unwrap_or_default(),
680            path_filters: merged_filters(&status.path_filters, &selector.path_filters),
681            language_filters: merged_filters(&status.language_filters, &selector.language_filters),
682            index_versions: vec![format!(
683                "code:{}:{}",
684                status
685                    .last_indexed_scope_id
686                    .as_deref()
687                    .unwrap_or("unscoped"),
688                status.tree_hash.as_deref().unwrap_or("unindexed")
689            )],
690            stale: status.stale,
691        }
692    }
693
694    /// Builds scope metadata for a queued or running index task.
695    pub fn from_index_task(task: &CodeIndexTaskRecord, requested_ref: impl Into<String>) -> Self {
696        Self {
697            scope_id: task.source_scope.clone(),
698            repository_id: task.repository_id.clone(),
699            alias: task.alias.clone(),
700            requested_ref: requested_ref.into(),
701            resolved_commit_sha: task.resolved_commit_sha.clone(),
702            tree_hash: task.tree_hash.clone(),
703            path_filters: task.path_filters.clone(),
704            language_filters: task.language_filters.clone(),
705            index_versions: vec![format!("code:{}:{}", task.source_scope, task.tree_hash)],
706            stale: true,
707        }
708    }
709}
710
711/// Code repository retrieval response.
712#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
713pub struct CodeRepositoryQueryResponse {
714    pub metadata: ApiMetadata,
715    pub scope: CodeRepositoryScopeMetadata,
716    pub request: CodeRetrievalRequest,
717    pub results: Vec<CodeRetrievalHit>,
718    #[serde(skip_serializing_if = "Option::is_none")]
719    pub degraded_reason: Option<String>,
720}
721
722/// Code repository feature-flag graph response.
723#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
724pub struct CodeRepositoryFeatureFlagsResponse {
725    pub metadata: ApiMetadata,
726    pub scope: CodeRepositoryScopeMetadata,
727    pub request: CodeFeatureFlagRequest,
728    pub flags: Vec<CodeFeatureFlagGraph>,
729    #[serde(skip_serializing_if = "Option::is_none")]
730    pub degraded_reason: Option<String>,
731}
732
733/// Code repository impact response.
734#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
735pub struct CodeRepositoryImpactResponse {
736    pub metadata: ApiMetadata,
737    pub scope: CodeRepositoryScopeMetadata,
738    pub request: CodeImpactRequest,
739    pub path_groups: CodeImpactPathGroups,
740    pub results: Vec<CodeRetrievalHit>,
741}
742
743/// Code repository status response.
744#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
745pub struct CodeRepositoryStatusResponse {
746    pub metadata: ApiMetadata,
747    pub status: CodeRepositoryStatus,
748    #[serde(skip_serializing_if = "Option::is_none")]
749    pub active_task: Option<CodeIndexTaskRecord>,
750    #[serde(skip_serializing_if = "Option::is_none")]
751    pub checkpoint: Option<CodeIndexCheckpoint>,
752    pub retention: CodeScopeRetentionSummary,
753}
754
755/// Code repository operations report response.
756#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
757pub struct CodeRepositoryReportResponse {
758    pub metadata: ApiMetadata,
759    pub scope: CodeRepositoryScopeMetadata,
760    pub report: CodeRepositoryReport,
761}
762
763/// Repository-set creation response.
764#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
765pub struct CodeRepositorySetCreateResponse {
766    pub metadata: ApiMetadata,
767    pub request: CodeRepositorySetCreateRequest,
768    pub repository_set: CodeRepositorySet,
769}
770
771/// Repository-set member addition response.
772#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
773pub struct CodeRepositorySetAddResponse {
774    pub metadata: ApiMetadata,
775    pub request: CodeRepositorySetAddMemberRequest,
776    pub member: CodeRepositorySetMember,
777    pub status: CodeRepositorySetStatus,
778}
779
780/// Repository-set member removal response.
781#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
782pub struct CodeRepositorySetRemoveResponse {
783    pub metadata: ApiMetadata,
784    pub request: CodeRepositorySetRemoveMemberRequest,
785    pub member: CodeRepositorySetMember,
786    pub status: CodeRepositorySetStatus,
787}
788
789/// Repository-set query response.
790#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
791pub struct CodeRepositorySetQueryResponse {
792    pub metadata: ApiMetadata,
793    pub request: CodeRepositorySetQueryRequest,
794    pub status: CodeRepositorySetStatus,
795    pub results: Vec<CodeRepositorySetQueryHit>,
796    pub truncated: bool,
797    #[serde(skip_serializing_if = "Option::is_none")]
798    pub degraded_reason: Option<String>,
799}
800
801/// Repository-set status response.
802#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
803pub struct CodeRepositorySetStatusResponse {
804    pub metadata: ApiMetadata,
805    pub status: CodeRepositorySetStatus,
806}
807
808/// Repository-set overlay refresh response.
809#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
810pub struct CodeRepositorySetRefreshResponse {
811    pub metadata: ApiMetadata,
812    pub status: CodeRepositorySetStatus,
813    #[serde(skip_serializing_if = "Option::is_none")]
814    pub summary: Option<CodeRepositorySetRefreshSummary>,
815    #[serde(skip_serializing_if = "Option::is_none")]
816    pub task: Option<CodeRepositorySetRefreshTaskRecord>,
817}
818
819fn merged_filters(base: &[String], request: &[String]) -> Vec<String> {
820    let mut merged = Vec::new();
821    for value in base.iter().chain(request.iter()) {
822        if !merged.contains(value) {
823            merged.push(value.clone());
824        }
825    }
826
827    merged
828}