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