Skip to main content

lean_ctx/core/providers/
mod.rs

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