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