Skip to main content

code_system_graph_core/
provider.rs

1use std::path::PathBuf;
2use std::time::Duration;
3
4use async_trait::async_trait;
5use code_system_graph_model::RepoId;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9use tokio_util::sync::CancellationToken;
10
11/// Availability state reported by a local intelligence provider.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum ProviderStatus {
15    /// Provider and repository index are ready.
16    Available,
17    /// Provider is installed but the repository has no existing index.
18    IndexMissing,
19    /// Existing index does not match the repository state.
20    Stale,
21    /// Provider is unavailable.
22    Unavailable,
23    /// Public capabilities are incompatible with the adapter.
24    Incompatible,
25    /// Provider returned an invalid public protocol response.
26    InvalidResponse,
27    /// Provider probing exceeded its configured deadline.
28    TimedOut,
29}
30
31/// Public transport used for one local-intelligence operation.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
33#[serde(rename_all = "snake_case")]
34pub enum ProviderTransport {
35    /// Model Context Protocol over child-process standard I/O.
36    Mcp,
37    /// Direct public command-line invocation without shell interpolation.
38    Cli,
39}
40
41/// Repository-local operation understood by the provider boundary.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum ProviderOperation {
45    /// Resolve a textual anchor to exact local symbols.
46    ResolveSymbols,
47    /// Load bounded callers or callees for one symbol.
48    LocalNeighbors,
49    /// Compute bounded local impact.
50    LocalImpact,
51    /// Build ephemeral local source and flow context.
52    LocalContext,
53    /// Recommend tests affected by changed files.
54    AffectedTests,
55}
56
57/// One discovered operation and the public interface that provides it.
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct ProviderOperationCapability {
60    /// `Code System Graph` operation.
61    pub operation: ProviderOperation,
62    /// Preferred transport for this operation.
63    pub transport: ProviderTransport,
64    /// Public MCP tool or CLI command name.
65    pub public_name: String,
66    /// Supported public input parameter names.
67    pub parameters: Vec<String>,
68}
69
70/// Non-fatal provider limitation observed while probing.
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
72pub struct ProviderDegradation {
73    /// Stable machine-readable degradation kind.
74    pub kind: String,
75    /// Bounded explanation that never includes returned source.
76    pub message: String,
77    /// Process-local diagnostic identifier when a failed request produced one.
78    pub diagnostics_id: Option<String>,
79}
80
81/// Capability report discovered from a provider's public interface.
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct ProviderCapability {
84    /// Provider name.
85    pub provider: String,
86    /// Reported provider version when available.
87    pub version: Option<String>,
88    /// Availability state for the requested repository.
89    pub status: ProviderStatus,
90    /// Public tool names discovered from the provider.
91    pub tools: Vec<String>,
92    /// MCP protocol version negotiated during initialization.
93    pub protocol_version: Option<String>,
94    /// Operations available through compatible public contracts.
95    pub operations: Vec<ProviderOperationCapability>,
96    /// Non-fatal limitations and fallbacks observed during probing.
97    pub degradations: Vec<ProviderDegradation>,
98    /// Bounded remediation guidance.
99    pub remediation: Option<String>,
100}
101
102/// Bounds applied independently to one provider request.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub struct ProviderBudget {
105    /// End-to-end request timeout.
106    pub timeout: Duration,
107    /// Maximum provider output retained in memory.
108    pub max_output_bytes: usize,
109    /// Maximum typed items returned to the caller.
110    pub max_items: usize,
111}
112
113impl Default for ProviderBudget {
114    fn default() -> Self {
115        Self {
116            timeout: Duration::from_secs(5),
117            max_output_bytes: 1024 * 1024,
118            max_items: 50,
119        }
120    }
121}
122
123/// Repository scope and controls shared by all provider requests.
124#[derive(Debug, Clone)]
125pub struct ProviderRequest {
126    /// Stable repository identity.
127    pub repo_id: RepoId,
128    /// Native absolute checkout path passed directly to the child process.
129    pub project_path: PathBuf,
130    /// Explicit execution limits.
131    pub budget: ProviderBudget,
132    /// Cooperative cancellation signal.
133    pub cancellation: CancellationToken,
134}
135
136/// Request to resolve one textual symbol anchor.
137#[derive(Debug, Clone)]
138pub struct ResolveSymbolsRequest {
139    /// Shared repository scope and controls.
140    pub request: ProviderRequest,
141    /// Symbol name, qualified name, or exact local anchor.
142    pub query: String,
143}
144
145/// Direction used for local neighbor traversal.
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum LocalNeighborDirection {
149    /// Functions or methods that call the anchor.
150    Callers,
151    /// Functions, methods, or declarations called by the anchor.
152    Callees,
153}
154
155/// Request for bounded local callers or callees.
156#[derive(Debug, Clone)]
157pub struct LocalNeighborsRequest {
158    /// Shared repository scope and controls.
159    pub request: ProviderRequest,
160    /// Exact or qualified symbol anchor.
161    pub symbol: String,
162    /// Requested traversal direction.
163    pub direction: LocalNeighborDirection,
164}
165
166/// Request for bounded local impact.
167#[derive(Debug, Clone)]
168pub struct LocalImpactRequest {
169    /// Shared repository scope and controls.
170    pub request: ProviderRequest,
171    /// Exact or qualified symbol anchor.
172    pub symbol: String,
173    /// Maximum local dependency depth.
174    pub max_depth: usize,
175}
176
177/// Request for ephemeral source and local-flow context.
178#[derive(Debug, Clone)]
179pub struct LocalContextRequest {
180    /// Shared repository scope and controls.
181    pub request: ProviderRequest,
182    /// Focused context query.
183    pub query: String,
184    /// Maximum source files requested from the provider.
185    pub max_files: usize,
186}
187
188/// Request for tests affected by changed local files.
189#[derive(Debug, Clone)]
190pub struct AffectedTestsRequest {
191    /// Shared repository scope and controls.
192    pub request: ProviderRequest,
193    /// Repository-relative changed file paths.
194    pub changed_files: Vec<String>,
195    /// Maximum dependency traversal depth.
196    pub max_depth: usize,
197}
198
199/// Metadata describing one bounded provider execution.
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
201pub struct ProviderExecution {
202    /// Public transport that produced the result.
203    pub transport: ProviderTransport,
204    /// Bytes retained from provider output.
205    pub output_bytes: usize,
206    /// Whether configured item or byte limits truncated the result.
207    pub truncated: bool,
208    /// Non-fatal failures that caused fallback or partial execution.
209    pub degradations: Vec<ProviderDegradation>,
210}
211
212/// Exact local symbol returned by a compatible provider contract.
213#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
214pub struct ResolvedSymbol {
215    /// Local provider symbol identifier; never treated as a global identity.
216    pub local_id: Option<String>,
217    /// Unqualified symbol name.
218    pub name: String,
219    /// Provider-qualified symbol name when available.
220    pub qualified_name: Option<String>,
221    /// Provider node kind.
222    pub kind: String,
223    /// Repository-relative source path.
224    pub file_path: String,
225    /// One-based source start line.
226    pub start_line: usize,
227    /// Provider ranking score when available.
228    pub score: Option<f64>,
229}
230
231/// Result of one symbol-resolution request.
232#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct ResolveSymbolsResult {
234    /// Bounded matching symbols.
235    pub symbols: Vec<ResolvedSymbol>,
236    /// Execution metadata.
237    pub execution: ProviderExecution,
238}
239
240/// One bounded caller or callee.
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub struct LocalNeighbor {
243    /// Local symbol name.
244    pub name: String,
245    /// Provider node kind.
246    pub kind: String,
247    /// Repository-relative source path.
248    pub file_path: String,
249    /// One-based source start line.
250    pub start_line: usize,
251}
252
253/// Result of one local-neighbor request.
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255pub struct LocalNeighborResult {
256    /// Requested anchor.
257    pub symbol: String,
258    /// Requested direction.
259    pub direction: LocalNeighborDirection,
260    /// Bounded local neighbors.
261    pub neighbors: Vec<LocalNeighbor>,
262    /// Execution metadata.
263    pub execution: ProviderExecution,
264}
265
266/// Result of one bounded local-impact request.
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268pub struct LocalImpactResult {
269    /// Requested anchor.
270    pub symbol: String,
271    /// Effective traversal depth.
272    pub depth: usize,
273    /// Total local nodes reported before `Code System Graph` item truncation.
274    pub provider_node_count: usize,
275    /// Bounded affected local symbols.
276    pub affected: Vec<LocalNeighbor>,
277    /// Execution metadata.
278    pub execution: ProviderExecution,
279}
280
281/// Ephemeral local source and flow context.
282#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
283pub struct LocalContextResult {
284    /// Opaque provider text; callers must not persist this field.
285    pub content: String,
286    /// Execution metadata.
287    pub execution: ProviderExecution,
288}
289
290/// Result of one affected-tests request.
291#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
292pub struct AffectedTestsResult {
293    /// Changed files accepted by the provider.
294    pub changed_files: Vec<String>,
295    /// Bounded repository-relative test paths.
296    pub affected_tests: Vec<String>,
297    /// Total local dependents traversed by the provider.
298    pub total_dependents_traversed: usize,
299    /// Execution metadata.
300    pub execution: ProviderExecution,
301}
302
303/// Error returned by a local intelligence provider adapter.
304#[derive(Debug, Error, PartialEq, Eq)]
305pub enum ProviderError {
306    /// Operation was cancelled.
307    #[error("local intelligence request was cancelled")]
308    Cancelled,
309    /// Operation exceeded its configured deadline.
310    #[error("local intelligence provider timed out (diagnostics: {diagnostics_id})")]
311    Timeout {
312        /// Process-local diagnostic identifier.
313        diagnostics_id: String,
314    },
315    /// Operation exceeded its configured output limit.
316    #[error(
317        "local intelligence provider exceeded the {limit_bytes}-byte output limit (diagnostics: {diagnostics_id})"
318    )]
319    OutputLimit {
320        /// Configured output limit.
321        limit_bytes: usize,
322        /// Process-local diagnostic identifier.
323        diagnostics_id: String,
324    },
325    /// A temporary circuit breaker is open after a provider timeout.
326    #[error("local intelligence provider circuit is temporarily open")]
327    CircuitOpen,
328    /// Request controls or repository scope were invalid.
329    #[error("invalid local intelligence request: {0}")]
330    InvalidRequest(String),
331    /// Provider returned an invalid public response.
332    #[error("invalid local intelligence response: {message} (diagnostics: {diagnostics_id})")]
333    InvalidResponse {
334        /// Bounded response validation failure.
335        message: String,
336        /// Process-local diagnostic identifier.
337        diagnostics_id: String,
338    },
339    /// Provider process or transport failed.
340    #[error("local intelligence transport failed: {message} (diagnostics: {diagnostics_id})")]
341    Transport {
342        /// Bounded transport failure.
343        message: String,
344        /// Process-local diagnostic identifier.
345        diagnostics_id: String,
346    },
347}
348
349/// Optional boundary for repository-local code intelligence.
350#[async_trait]
351pub trait LocalCodeIntelligenceProvider: Send + Sync {
352    /// Returns the stable provider name.
353    fn provider_name(&self) -> &'static str;
354
355    /// Discovers public capabilities for one repository without creating or modifying indexes.
356    ///
357    /// # Errors
358    ///
359    /// Returns [`ProviderError`] for cancellation, timeout, invalid output, or transport failure.
360    async fn probe(&self, request: ProviderRequest) -> Result<ProviderCapability, ProviderError>;
361
362    /// Resolves a textual anchor to bounded exact local symbols.
363    ///
364    /// # Errors
365    ///
366    /// Returns [`ProviderError`] when no compatible public contract can complete the request.
367    async fn resolve_symbols(
368        &self,
369        input: ResolveSymbolsRequest,
370    ) -> Result<ResolveSymbolsResult, ProviderError>;
371
372    /// Returns bounded callers or callees for one local symbol.
373    ///
374    /// # Errors
375    ///
376    /// Returns [`ProviderError`] when no compatible public contract can complete the request.
377    async fn get_local_neighbors(
378        &self,
379        input: LocalNeighborsRequest,
380    ) -> Result<LocalNeighborResult, ProviderError>;
381
382    /// Returns bounded local impact for one symbol.
383    ///
384    /// # Errors
385    ///
386    /// Returns [`ProviderError`] when no compatible public contract can complete the request.
387    async fn get_local_impact(
388        &self,
389        input: LocalImpactRequest,
390    ) -> Result<LocalImpactResult, ProviderError>;
391
392    /// Builds ephemeral local source and flow context.
393    ///
394    /// # Errors
395    ///
396    /// Returns [`ProviderError`] when no compatible public contract can complete the request.
397    async fn build_local_context(
398        &self,
399        input: LocalContextRequest,
400    ) -> Result<LocalContextResult, ProviderError>;
401
402    /// Returns bounded tests affected by changed files when supported.
403    ///
404    /// `Ok(None)` means the optional capability is unavailable.
405    ///
406    /// # Errors
407    ///
408    /// Returns [`ProviderError`] when a declared compatible contract fails.
409    async fn get_affected_tests(
410        &self,
411        input: AffectedTestsRequest,
412    ) -> Result<Option<AffectedTestsResult>, ProviderError>;
413
414    /// Releases provider resources and terminates owned child processes.
415    ///
416    /// # Errors
417    ///
418    /// Returns [`ProviderError`] if orderly shutdown fails.
419    async fn shutdown(&self) -> Result<(), ProviderError>;
420}
421
422#[cfg(test)]
423mod tests {
424    use std::path::PathBuf;
425
426    use async_trait::async_trait;
427    use code_system_graph_model::RepoId;
428    use tokio_util::sync::CancellationToken;
429
430    use super::{
431        AffectedTestsRequest, AffectedTestsResult, LocalCodeIntelligenceProvider, LocalContextRequest, LocalContextResult, LocalImpactRequest, LocalImpactResult, LocalNeighborResult, LocalNeighborsRequest, ProviderBudget, ProviderCapability, ProviderError, ProviderRequest, ProviderStatus, ResolveSymbolsRequest, ResolveSymbolsResult
432    };
433
434    struct FakeProvider;
435
436    #[async_trait]
437    impl LocalCodeIntelligenceProvider for FakeProvider {
438        fn provider_name(&self) -> &'static str {
439            "fake"
440        }
441
442        async fn probe(
443            &self,
444            request: ProviderRequest,
445        ) -> Result<ProviderCapability, ProviderError> {
446            if request.cancellation.is_cancelled() {
447                return Err(ProviderError::Cancelled);
448            }
449            Ok(ProviderCapability {
450                provider: self.provider_name().to_owned(),
451                version: None,
452                status: ProviderStatus::IndexMissing,
453                tools: Vec::new(),
454                protocol_version: None,
455                operations: Vec::new(),
456                degradations: Vec::new(),
457                remediation: Some("Create the index explicitly with CodeGraph.".to_owned()),
458            })
459        }
460
461        async fn resolve_symbols(
462            &self,
463            _input: ResolveSymbolsRequest,
464        ) -> Result<ResolveSymbolsResult, ProviderError> {
465            Err(ProviderError::CircuitOpen)
466        }
467
468        async fn get_local_neighbors(
469            &self,
470            _input: LocalNeighborsRequest,
471        ) -> Result<LocalNeighborResult, ProviderError> {
472            Err(ProviderError::CircuitOpen)
473        }
474
475        async fn get_local_impact(
476            &self,
477            _input: LocalImpactRequest,
478        ) -> Result<LocalImpactResult, ProviderError> {
479            Err(ProviderError::CircuitOpen)
480        }
481
482        async fn build_local_context(
483            &self,
484            _input: LocalContextRequest,
485        ) -> Result<LocalContextResult, ProviderError> {
486            Err(ProviderError::CircuitOpen)
487        }
488
489        async fn get_affected_tests(
490            &self,
491            _input: AffectedTestsRequest,
492        ) -> Result<Option<AffectedTestsResult>, ProviderError> {
493            Ok(None)
494        }
495
496        async fn shutdown(&self) -> Result<(), ProviderError> {
497            Ok(())
498        }
499    }
500
501    #[tokio::test]
502    async fn probe_should_preserve_missing_index_degradation() {
503        let result = FakeProvider
504            .probe(ProviderRequest {
505                repo_id: RepoId::new("repo:web"),
506                project_path: PathBuf::from("/workspace/web"),
507                budget: ProviderBudget::default(),
508                cancellation: CancellationToken::new(),
509            })
510            .await;
511        let status = result.map(|capability| capability.status);
512
513        assert_eq!(status, Ok(ProviderStatus::IndexMissing));
514    }
515}