Skip to main content

lean_ctx/core/providers/
mod.rs

1pub mod cache;
2pub mod config;
3pub mod config_provider;
4pub mod connection;
5pub mod credentials;
6pub mod emspace;
7pub mod github;
8pub mod gitlab;
9pub mod hardened_http;
10pub mod health;
11pub mod init;
12pub mod jira;
13pub mod jira_oauth;
14pub mod mcp_bridge;
15pub mod postgres;
16pub mod provenance;
17pub mod provider_trait;
18pub mod registry;
19pub mod scaffold;
20pub mod selected_gitlab;
21pub mod snapshot_projection;
22
23pub use provider_trait::{ContextPacket, ContextProvider, ProviderParams};
24pub use registry::{ProviderRegistry, global_registry};
25
26use serde::{Deserialize, Serialize};
27
28use crate::core::evidence::Claim;
29
30/// Intern a string to a process-global `&'static str`, leaking each *unique* value at
31/// most once. Provider constructors run per `ctx_provider`/`ctx_preload` call, so a
32/// naive `Box::leak` per construction leaked unboundedly; interning bounds the leak to
33/// the finite set of distinct provider ids/names/actions.
34pub(crate) fn intern(s: String) -> &'static str {
35    use std::collections::HashSet;
36    use std::sync::{Mutex, OnceLock};
37    static POOL: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
38    let pool = POOL.get_or_init(|| Mutex::new(HashSet::new()));
39    let mut guard = pool
40        .lock()
41        .unwrap_or_else(std::sync::PoisonError::into_inner);
42    if let Some(&existing) = guard.get(s.as_str()) {
43        return existing;
44    }
45    let leaked: &'static str = Box::leak(s.into_boxed_str());
46    guard.insert(leaked);
47    leaked
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct ProviderResult {
52    pub provider: String,
53    pub resource_type: String,
54    pub items: Vec<ProviderItem>,
55    pub total_count: Option<usize>,
56    pub truncated: bool,
57}
58
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60pub struct ProviderItem {
61    pub id: String,
62    pub title: String,
63    pub state: Option<String>,
64    pub author: Option<String>,
65    pub created_at: Option<String>,
66    pub updated_at: Option<String>,
67    pub url: Option<String>,
68    pub labels: Vec<String>,
69    pub body: Option<String>,
70    /// Attributable evidence distilled from this item (confidence + source).
71    /// Empty for plain records; populated by research/extraction providers.
72    #[serde(default, skip_serializing_if = "Vec::is_empty")]
73    pub claims: Vec<Claim>,
74    /// Repository paths (files or directories) the item is about, declared by
75    /// the provider rather than found in its text. Indexed as cross-source
76    /// references beside the paths extracted from the body.
77    #[serde(default, skip_serializing_if = "Vec::is_empty")]
78    pub references: Vec<String>,
79}
80
81impl ProviderResult {
82    pub fn format_compact(&self) -> String {
83        let mut out = self.compact_header(self.items.len());
84        for item in &self.items {
85            out.push_str(&item.format_compact());
86        }
87        out
88    }
89
90    /// The list line of [`Self::format_compact`] for `count` listed items.
91    pub fn compact_header(&self, count: usize) -> String {
92        format!(
93            "{} {} ({}{}):\n",
94            self.provider,
95            self.resource_type,
96            count,
97            if self.truncated { "+" } else { "" }
98        )
99    }
100}
101
102impl ProviderItem {
103    /// The lines of one item in [`ProviderResult::format_compact`]: all it
104    /// shows of the item, so it can be admitted as that item's source.
105    pub fn format_compact(&self) -> String {
106        let state = self.state.as_deref().unwrap_or("");
107        let labels = if self.labels.is_empty() {
108            String::new()
109        } else {
110            format!(" [{}]", self.labels.join(","))
111        };
112        let mut out = format!("  #{} {} ({}){}\n", self.id, self.title, state, labels);
113        for claim in &self.claims {
114            out.push_str("    ▸ ");
115            out.push_str(&claim.render());
116            out.push('\n');
117        }
118        out
119    }
120}