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, CodeIndexTaskQueueStatus, CodeIndexTaskRecord,
9        CodeRepositoryRegistration, CodeRepositoryRemovalSummary, CodeRepositoryReport,
10        CodeRepositoryScopePreview, CodeRepositorySelector, CodeRepositorySet,
11        CodeRepositorySetAddMemberRequest, CodeRepositorySetCreateRequest, CodeRepositorySetMember,
12        CodeRepositorySetQueryHit, CodeRepositorySetQueryRequest, CodeRepositorySetRefreshSummary,
13        CodeRepositorySetRefreshTaskRecord, CodeRepositorySetRemoveMemberRequest,
14        CodeRepositorySetStatus, CodeRepositoryStatus, CodeRepositoryTotals, CodeRetrievalHit,
15        CodeRetrievalRequest, CodeScopeRetentionSummary, CommitReceipt, ConfidenceScore,
16        EvidenceExtractionMetadata, EvidenceModality, EvidenceSpan, ExtractionDiagnostic,
17        FactStatus, FreshnessPolicy, FusionDiagnostics, GraphVersionRange, IndexKind, IndexStatus,
18        LayoutRegion, ProposalConflictRecord, ProposalRecord, ProposalState, RerankDiagnostics,
19        RetrievalBackendStatus, RetrievalBudgetUsed, RetrievalHit, RetrievalMode,
20        RetrievedContextPack, ServiceDefinitionPlan, ServiceOperatorStatus, SoftwareBuildTarget,
21        SoftwareComponent, SoftwareDependencyUsage, SoftwareDesignElement, SoftwareFile,
22        SoftwareGlobalRequest, SoftwareGlobalStatus, SoftwareIacResource, SoftwareRelationship,
23        SoftwareSdkUsage, SoftwareTopic, WorkerKind, WorkerStatus, WorkerTaskRecord,
24    },
25    storage::{GraphInspection, IndexCursor, IndexRefreshDiagnostics},
26};
27
28use super::{AgentProtocolStatus, ApiMetadata, CodeRepositoryFreshnessDiagnostics, 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    #[serde(default)]
337    pub index_cursors: Vec<IndexCursor>,
338    #[serde(default)]
339    pub index_refresh: IndexRefreshDiagnostics,
340}
341
342/// Graph inspection request with optional scope filtering reserved for adapters.
343#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
344pub struct GraphInspectionRequest {
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub source_scope: Option<String>,
347}
348
349/// Graph inspection response for diagnostics and agent adapters.
350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
351pub struct GraphInspectionResponse {
352    pub metadata: ApiMetadata,
353    pub graph: GraphInspection,
354    pub repository_code_totals: CodeRepositoryTotals,
355}
356
357/// Index refresh request. Empty `kinds` means all v1 index families.
358#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
359pub struct IndexRefreshRequest {
360    #[serde(default)]
361    pub kinds: Vec<IndexKind>,
362}
363
364/// Index refresh response after metadata is updated.
365#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
366pub struct IndexRefreshResponse {
367    pub metadata: ApiMetadata,
368    pub indexes: Vec<IndexStatus>,
369    pub index_cursors: Vec<IndexCursor>,
370    pub diagnostics: IndexRefreshDiagnostics,
371}
372
373/// Service manager status surfaced without exposing platform-specific handles.
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
375pub struct ServiceStatusResponse {
376    pub metadata: ApiMetadata,
377    pub service_name: String,
378    pub mode: String,
379    pub background_enabled: bool,
380    pub silent_updates_enabled: bool,
381    pub service_definition_path: String,
382    pub storage: StorageTopologyDiagnostics,
383    pub index_refresh: IndexRefreshDiagnostics,
384    pub file_index: crate::storage::FileIndexDiagnostics,
385    pub agent_protocols: AgentProtocolStatus,
386    pub operator: ServiceOperatorStatus,
387    pub workers: Vec<WorkerStatus>,
388    pub code_index_workers: CodeIndexWorkerStatus,
389    pub proposal_backlog: usize,
390    pub audit_sink: AuditSinkStatus,
391    #[serde(default = "super::watcher_diagnostics::WatcherDiagnostics::default_disabled")]
392    pub watcher: super::watcher_diagnostics::WatcherDiagnostics,
393}
394
395/// Storage topology and shard-catalog diagnostics surfaced by the control plane.
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
397pub struct StorageTopologyDiagnostics {
398    pub topology: String,
399    pub control_database_path: String,
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub repository_shards_dir: Option<String>,
402    pub shard_catalog_active: bool,
403    pub active_shard_count: usize,
404    pub staged_shard_count: usize,
405    pub missing_shard_count: usize,
406    pub runtime_state_paths: Vec<String>,
407    pub shards: Vec<StorageShardDiagnostics>,
408    #[serde(skip_serializing_if = "Option::is_none")]
409    pub degraded_reason: Option<String>,
410}
411
412/// One partitioned SQLite shard reported through storage diagnostics.
413#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
414pub struct StorageShardDiagnostics {
415    pub repository_id: String,
416    pub state: String,
417    pub shard_locator: String,
418    pub resolved_path: String,
419    pub source_scope_count: usize,
420    pub exists: bool,
421    pub updated_at_ms: u64,
422    #[serde(skip_serializing_if = "Option::is_none")]
423    pub degraded_reason: Option<String>,
424}
425
426/// Control-plane storage topology response.
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
428pub struct StorageTopologyResponse {
429    pub metadata: ApiMetadata,
430    pub storage: StorageTopologyDiagnostics,
431}
432
433/// Master-worker diagnostics for repository code indexing.
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
435pub struct CodeIndexWorkerStatus {
436    pub configured_worker_count: usize,
437    pub active_worker_slots: usize,
438    pub queue_depth: usize,
439    pub queued_task_count: usize,
440    pub running_task_count: usize,
441    pub retrying_task_count: usize,
442    pub dead_letter_task_count: usize,
443    pub running_lease_count: usize,
444    #[serde(skip_serializing_if = "Option::is_none")]
445    pub last_error: Option<String>,
446}
447
448impl CodeIndexWorkerStatus {
449    /// Overlays resident master runtime configuration onto durable task queue state.
450    pub fn from_queue(configured_worker_count: usize, queue: CodeIndexTaskQueueStatus) -> Self {
451        let queue_depth = queue
452            .queued_task_count
453            .saturating_add(queue.retrying_task_count);
454
455        Self {
456            configured_worker_count,
457            active_worker_slots: configured_worker_count.saturating_sub(queue.running_task_count),
458            queue_depth,
459            queued_task_count: queue.queued_task_count,
460            running_task_count: queue.running_task_count,
461            retrying_task_count: queue.retrying_task_count,
462            dead_letter_task_count: queue.dead_letter_task_count,
463            running_lease_count: queue.running_lease_count,
464            last_error: queue.last_error,
465        }
466    }
467}
468
469/// Durable audit sink health surfaced in service diagnostics.
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
471pub struct AuditSinkStatus {
472    pub durable: bool,
473    pub event_count: usize,
474    #[serde(skip_serializing_if = "Option::is_none")]
475    pub last_error: Option<String>,
476}
477
478/// Worker status filter. Missing kind means all worker families.
479#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
480pub struct WorkerStatusRequest {
481    #[serde(skip_serializing_if = "Option::is_none")]
482    pub kind: Option<WorkerKind>,
483}
484
485/// Worker status response.
486#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
487pub struct WorkerStatusResponse {
488    pub metadata: ApiMetadata,
489    pub workers: Vec<WorkerStatus>,
490}
491
492/// Bounded foreground worker run request.
493#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
494pub struct WorkerRunRequest {
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub kind: Option<WorkerKind>,
497}
498
499/// Bounded foreground worker run response.
500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
501pub struct WorkerRunResponse {
502    pub metadata: ApiMetadata,
503    #[serde(skip_serializing_if = "Option::is_none")]
504    pub task: Option<WorkerTaskRecord>,
505    #[serde(default)]
506    pub proposals: Vec<ProposalRecord>,
507    pub workers: Vec<WorkerStatus>,
508    #[serde(skip_serializing_if = "Option::is_none")]
509    pub degraded_reason: Option<String>,
510}
511
512/// Preview split-worker request for one durable code-index task.
513#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
514pub struct CodeIndexWorkerRunRequest {
515    #[serde(skip_serializing_if = "Option::is_none")]
516    pub task_id: Option<String>,
517}
518
519/// Preview split-worker result for one durable code-index task.
520#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
521pub struct CodeIndexWorkerRunResponse {
522    pub metadata: ApiMetadata,
523    pub worker_kind: String,
524    pub claimed: bool,
525    #[serde(skip_serializing_if = "Option::is_none")]
526    pub task: Option<CodeIndexTaskRecord>,
527}
528
529/// Proposal list filter.
530#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
531pub struct ProposalListApiRequest {
532    #[serde(skip_serializing_if = "Option::is_none")]
533    pub state: Option<ProposalState>,
534    pub limit: usize,
535}
536
537/// Proposal list response.
538#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
539pub struct ProposalListResponse {
540    pub metadata: ApiMetadata,
541    pub proposals: Vec<ProposalRecord>,
542}
543
544/// Proposal detail response with conflict lifecycle.
545#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
546pub struct ProposalShowResponse {
547    pub metadata: ApiMetadata,
548    pub proposal: ProposalRecord,
549    pub conflicts: Vec<ProposalConflictRecord>,
550    pub payload: serde_json::Value,
551}
552
553/// Manual proposal decision request.
554#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
555pub struct ProposalDecisionApiRequest {
556    pub actor: String,
557    #[serde(skip_serializing_if = "Option::is_none")]
558    pub reason: Option<String>,
559}
560
561/// Manual proposal decision response.
562#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
563pub struct ProposalDecisionResponse {
564    pub metadata: ApiMetadata,
565    pub proposal: ProposalRecord,
566    #[serde(skip_serializing_if = "Option::is_none")]
567    pub receipt: Option<CommitReceipt>,
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub index_refresh_error: Option<String>,
570}
571
572/// Durable audit query request.
573#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
574pub struct AuditQueryApiRequest {
575    #[serde(skip_serializing_if = "Option::is_none")]
576    pub operation: Option<String>,
577    pub limit: usize,
578}
579
580/// Durable audit query response.
581#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
582pub struct AuditQueryResponse {
583    pub metadata: ApiMetadata,
584    pub events: Vec<crate::domain::AuditEventRecord>,
585}
586
587/// Service definition write response.
588#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
589pub struct ServiceDefinitionWriteResponse {
590    pub metadata: ApiMetadata,
591    pub plan: ServiceDefinitionPlan,
592    pub written: bool,
593}
594
595/// Service silent-update operator response.
596#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
597pub struct ServiceOperatorResponse {
598    pub metadata: ApiMetadata,
599    pub operator: ServiceOperatorStatus,
600}
601
602/// Startup recovery report for resident service mode.
603#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
604pub struct ServiceRecoveryReport {
605    pub metadata: ApiMetadata,
606    pub graph_version: u64,
607    pub stale_index_kinds: Vec<IndexKind>,
608    pub refreshed_index_kinds: Vec<IndexKind>,
609    pub index_lag_max: u64,
610    pub task_queue_depth: usize,
611    pub dead_letter_count: usize,
612    pub heartbeat_state: String,
613}
614
615/// Aggregated health response for CLI/Web/service diagnostics.
616#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
617pub struct HealthResponse {
618    pub metadata: ApiMetadata,
619    pub healthy: bool,
620    #[serde(skip_serializing_if = "Option::is_none")]
621    pub degraded_reason: Option<String>,
622    pub storage: StorageTopologyDiagnostics,
623    pub graph: GraphInspection,
624    pub repository_code_totals: CodeRepositoryTotals,
625    pub indexes: Vec<IndexStatus>,
626    pub index_cursors: Vec<IndexCursor>,
627    pub index_refresh: IndexRefreshDiagnostics,
628    pub file_index: crate::storage::FileIndexDiagnostics,
629    pub runtime: RuntimeStatus,
630}
631
632/// Remote embedding provider probe response with secret-free diagnostics.
633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
634pub struct EmbeddingProviderProbeResponse {
635    pub metadata: ApiMetadata,
636    pub ok: bool,
637    pub provider: Option<String>,
638    pub model: String,
639    pub dimension: u32,
640    #[serde(skip_serializing_if = "Option::is_none")]
641    pub latency_ms: Option<u64>,
642    #[serde(skip_serializing_if = "Option::is_none")]
643    pub error_code: Option<String>,
644    #[serde(skip_serializing_if = "Option::is_none")]
645    pub error_message: Option<String>,
646    #[serde(skip_serializing_if = "Option::is_none")]
647    pub retryable: Option<bool>,
648}
649
650/// Code repository registration request.
651#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
652pub struct CodeRepositoryRegisterRequest {
653    pub root_path: String,
654    #[serde(default)]
655    pub alias: String,
656    #[serde(default)]
657    pub path_filters: Vec<String>,
658    #[serde(default)]
659    pub language_filters: Vec<String>,
660}
661
662/// Code repository registration response.
663#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
664pub struct CodeRepositoryRegisterResponse {
665    pub metadata: ApiMetadata,
666    pub registration: CodeRepositoryRegistration,
667    pub status: CodeRepositoryStatus,
668}
669
670/// Code repository removal response.
671#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
672pub struct CodeRepositoryRemoveResponse {
673    pub metadata: ApiMetadata,
674    pub removed_status: CodeRepositoryStatus,
675    pub summary: CodeRepositoryRemovalSummary,
676}
677
678/// Code repository index response.
679#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
680pub struct CodeRepositoryIndexResponse {
681    pub metadata: ApiMetadata,
682    pub scope: CodeRepositoryScopeMetadata,
683    pub summary: CodeIndexSummary,
684    pub status: CodeRepositoryStatus,
685}
686
687/// Code repository index start response for queued or no-op index requests.
688#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
689pub struct CodeRepositoryIndexStartResponse {
690    pub metadata: ApiMetadata,
691    pub scope: CodeRepositoryScopeMetadata,
692    #[serde(skip_serializing_if = "Option::is_none")]
693    pub summary: Option<CodeIndexSummary>,
694    pub status: CodeRepositoryStatus,
695    #[serde(skip_serializing_if = "Option::is_none")]
696    pub task: Option<CodeIndexTaskRecord>,
697    #[serde(skip_serializing_if = "Option::is_none")]
698    pub checkpoint: Option<CodeIndexCheckpoint>,
699}
700
701/// Code repository index task reset response.
702#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
703pub struct CodeRepositoryIndexResetResponse {
704    pub metadata: ApiMetadata,
705    pub status: CodeRepositoryStatus,
706    pub reset_task_count: usize,
707    pub reset_tasks: Vec<CodeIndexTaskRecord>,
708    #[serde(skip_serializing_if = "Option::is_none")]
709    pub active_task: Option<CodeIndexTaskRecord>,
710    #[serde(skip_serializing_if = "Option::is_none")]
711    pub checkpoint: Option<CodeIndexCheckpoint>,
712    pub retention: CodeScopeRetentionSummary,
713}
714
715/// Code repository scope preview response.
716#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
717pub struct CodeRepositoryScopePreviewResponse {
718    pub metadata: ApiMetadata,
719    pub scope: CodeRepositoryScopeMetadata,
720    pub preview: CodeRepositoryScopePreview,
721}
722
723/// Code repository scope and index metadata attached to code responses.
724#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
725pub struct CodeRepositoryScopeMetadata {
726    pub scope_id: String,
727    pub repository_id: String,
728    pub alias: String,
729    pub requested_ref: String,
730    pub resolved_commit_sha: String,
731    pub tree_hash: String,
732    pub path_filters: Vec<String>,
733    pub language_filters: Vec<String>,
734    #[serde(default)]
735    pub indexed_file_count: usize,
736    pub index_versions: Vec<String>,
737    pub stale: bool,
738}
739
740impl CodeRepositoryScopeMetadata {
741    /// Builds stable scope metadata from the selected repository snapshot.
742    pub fn from_status(
743        status: &CodeRepositoryStatus,
744        selector: &CodeRepositorySelector,
745        requested_ref: impl Into<String>,
746    ) -> Self {
747        Self {
748            scope_id: status.last_indexed_scope_id.clone().unwrap_or_default(),
749            repository_id: status.repository_id.clone(),
750            alias: status.alias.clone(),
751            requested_ref: requested_ref.into(),
752            resolved_commit_sha: status.last_indexed_commit.clone().unwrap_or_default(),
753            tree_hash: status.tree_hash.clone().unwrap_or_default(),
754            path_filters: merged_filters(&status.path_filters, &selector.path_filters),
755            language_filters: merged_filters(&status.language_filters, &selector.language_filters),
756            indexed_file_count: status.indexed_file_count,
757            index_versions: vec![format!(
758                "code:{}:{}",
759                status
760                    .last_indexed_scope_id
761                    .as_deref()
762                    .unwrap_or("unscoped"),
763                status.tree_hash.as_deref().unwrap_or("unindexed")
764            )],
765            stale: status.stale,
766        }
767    }
768
769    /// Builds scope metadata for a queued or running index task.
770    pub fn from_index_task(task: &CodeIndexTaskRecord, requested_ref: impl Into<String>) -> Self {
771        Self {
772            scope_id: task.source_scope.clone(),
773            repository_id: task.repository_id.clone(),
774            alias: task.alias.clone(),
775            requested_ref: requested_ref.into(),
776            resolved_commit_sha: task.resolved_commit_sha.clone(),
777            tree_hash: task.tree_hash.clone(),
778            path_filters: task.path_filters.clone(),
779            language_filters: task.language_filters.clone(),
780            indexed_file_count: 0,
781            index_versions: vec![format!("code:{}:{}", task.source_scope, task.tree_hash)],
782            stale: true,
783        }
784    }
785}
786
787/// Code repository retrieval response.
788#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
789pub struct CodeRepositoryQueryResponse {
790    pub metadata: ApiMetadata,
791    pub scope: CodeRepositoryScopeMetadata,
792    #[serde(default = "CodeRepositoryFreshnessDiagnostics::legacy_unknown")]
793    pub freshness: CodeRepositoryFreshnessDiagnostics,
794    pub request: CodeRetrievalRequest,
795    pub results: Vec<CodeRetrievalHit>,
796    #[serde(skip_serializing_if = "Option::is_none")]
797    pub degraded_reason: Option<String>,
798}
799
800/// Code repository feature-flag graph response.
801#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
802pub struct CodeRepositoryFeatureFlagsResponse {
803    pub metadata: ApiMetadata,
804    pub scope: CodeRepositoryScopeMetadata,
805    #[serde(default = "CodeRepositoryFreshnessDiagnostics::legacy_unknown")]
806    pub freshness: CodeRepositoryFreshnessDiagnostics,
807    pub request: CodeFeatureFlagRequest,
808    pub flags: Vec<CodeFeatureFlagGraph>,
809    #[serde(skip_serializing_if = "Option::is_none")]
810    pub degraded_reason: Option<String>,
811}
812
813/// Code repository impact response.
814#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
815pub struct CodeRepositoryImpactResponse {
816    pub metadata: ApiMetadata,
817    pub scope: CodeRepositoryScopeMetadata,
818    pub request: CodeImpactRequest,
819    pub path_groups: CodeImpactPathGroups,
820    pub results: Vec<CodeRetrievalHit>,
821}
822
823/// Code repository status response.
824#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
825pub struct CodeRepositoryStatusResponse {
826    pub metadata: ApiMetadata,
827    pub status: CodeRepositoryStatus,
828    #[serde(skip_serializing_if = "Option::is_none")]
829    pub active_task: Option<CodeIndexTaskRecord>,
830    #[serde(skip_serializing_if = "Option::is_none")]
831    pub checkpoint: Option<CodeIndexCheckpoint>,
832    pub retention: CodeScopeRetentionSummary,
833}
834
835/// Code repository operations report response.
836#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
837pub struct CodeRepositoryReportResponse {
838    pub metadata: ApiMetadata,
839    pub scope: CodeRepositoryScopeMetadata,
840    pub report: CodeRepositoryReport,
841}
842
843/// Repository-scoped software global model projection response.
844#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
845pub struct SoftwareGlobalResponse {
846    pub metadata: ApiMetadata,
847    pub scope: CodeRepositoryScopeMetadata,
848    pub request: SoftwareGlobalRequest,
849    pub status: SoftwareGlobalStatus,
850    pub components: Vec<SoftwareComponent>,
851    pub dependency_usages: Vec<SoftwareDependencyUsage>,
852    pub sdk_usages: Vec<SoftwareSdkUsage>,
853    pub files: Vec<SoftwareFile>,
854    pub topics: Vec<SoftwareTopic>,
855    pub relationships: Vec<SoftwareRelationship>,
856    pub build_targets: Vec<SoftwareBuildTarget>,
857    pub iac_resources: Vec<SoftwareIacResource>,
858    pub design_elements: Vec<SoftwareDesignElement>,
859}
860
861/// Repository-set creation response.
862#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
863pub struct CodeRepositorySetCreateResponse {
864    pub metadata: ApiMetadata,
865    pub request: CodeRepositorySetCreateRequest,
866    pub repository_set: CodeRepositorySet,
867}
868
869/// Repository-set member addition response.
870#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
871pub struct CodeRepositorySetAddResponse {
872    pub metadata: ApiMetadata,
873    pub request: CodeRepositorySetAddMemberRequest,
874    pub member: CodeRepositorySetMember,
875    pub status: CodeRepositorySetStatus,
876}
877
878/// Repository-set member removal response.
879#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
880pub struct CodeRepositorySetRemoveResponse {
881    pub metadata: ApiMetadata,
882    pub request: CodeRepositorySetRemoveMemberRequest,
883    pub member: CodeRepositorySetMember,
884    pub status: CodeRepositorySetStatus,
885}
886
887/// Repository-set query response.
888#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
889pub struct CodeRepositorySetQueryResponse {
890    pub metadata: ApiMetadata,
891    pub request: CodeRepositorySetQueryRequest,
892    pub status: CodeRepositorySetStatus,
893    pub results: Vec<CodeRepositorySetQueryHit>,
894    pub truncated: bool,
895    #[serde(skip_serializing_if = "Option::is_none")]
896    pub degraded_reason: Option<String>,
897}
898
899/// Repository-set status response.
900#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
901pub struct CodeRepositorySetStatusResponse {
902    pub metadata: ApiMetadata,
903    pub status: CodeRepositorySetStatus,
904}
905
906/// Repository-set overlay refresh response.
907#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
908pub struct CodeRepositorySetRefreshResponse {
909    pub metadata: ApiMetadata,
910    pub status: CodeRepositorySetStatus,
911    #[serde(skip_serializing_if = "Option::is_none")]
912    pub summary: Option<CodeRepositorySetRefreshSummary>,
913    #[serde(skip_serializing_if = "Option::is_none")]
914    pub task: Option<CodeRepositorySetRefreshTaskRecord>,
915}
916
917fn merged_filters(base: &[String], request: &[String]) -> Vec<String> {
918    let mut merged = Vec::new();
919    for value in base.iter().chain(request.iter()) {
920        if !merged.contains(value) {
921            merged.push(value.clone());
922        }
923    }
924
925    merged
926}