Skip to main content

lean_ctx/core/context_kernel/
live_dashboard.rs

1//! Aggregated live metrics for dashboard consumers.
2
3use std::panic::{AssertUnwindSafe, catch_unwind};
4
5use super::{coverage_class, mcp_bridge, proxy_bridge, receipt_chain, usage_normalizer};
6
7/// Complete kernel metrics snapshot for dashboard rendering.
8#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
9pub struct DashboardSnapshot {
10    /// Current ETPAO (proxy).
11    pub proxy_etpao: f64,
12    /// Current ETPAO (MCP).
13    pub mcp_etpao: f64,
14    /// Identity summary.
15    pub identity: IdentityView,
16    /// Usage overview.
17    pub usage: UsageView,
18    /// Receipt chain summary.
19    pub chain: ChainView,
20    /// Coverage breakdown.
21    pub coverage: CoverageView,
22}
23
24/// Identity attribution totals visible to the dashboard.
25#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
26pub struct IdentityView {
27    /// Number of distinct users observed.
28    pub total_users: usize,
29    /// Tokens attributed to users.
30    pub total_tokens: usize,
31    /// Tokens saved for users.
32    pub total_saved: usize,
33    /// Fraction of attributed requests that were accepted.
34    pub acceptance_rate: f64,
35}
36
37/// Provider-normalized usage totals visible to the dashboard.
38#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
39pub struct UsageView {
40    /// Number of normalized provider requests.
41    pub total_requests: usize,
42    /// Total normalized tokens consumed.
43    pub total_tokens: usize,
44    /// Total tokens saved by context optimization.
45    pub total_saved: usize,
46    /// Ratio of delivered tokens to original tokens.
47    pub compression_ratio: f64,
48    /// Most frequently observed model, when any usage exists.
49    pub top_model: Option<String>,
50}
51
52/// Request-to-outcome evidence-chain totals.
53#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
54pub struct ChainView {
55    /// Number of complete or partial chain entries.
56    pub total_entries: usize,
57    /// Entries with accepted outcomes.
58    pub accepted: usize,
59    /// Entries with rejected outcomes.
60    pub rejected: usize,
61    /// Fraction of entries whose plans used the kernel.
62    pub kernel_hit_rate: f64,
63    /// Average phantom savings percentage across receipts.
64    pub phantom_savings_pct: f64,
65}
66
67/// Coverage labels for the proxy and MCP integration paths.
68#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
69pub struct CoverageView {
70    /// Stable proxy coverage label.
71    pub proxy_coverage: String,
72    /// Stable MCP coverage label.
73    pub mcp_coverage: String,
74    /// Whether both paths permit direct context management.
75    pub is_fully_addressable: bool,
76}
77
78/// Returns a point-in-time view of all live kernel metrics.
79#[must_use]
80pub fn snapshot() -> DashboardSnapshot {
81    catch_unwind(AssertUnwindSafe(build_snapshot)).unwrap_or_default()
82}
83
84/// Returns the current dashboard snapshot as JSON.
85#[must_use]
86pub fn snapshot_json() -> String {
87    serde_json::to_string(&snapshot()).unwrap_or_else(|_| "{}".to_owned())
88}
89
90/// Returns a compact human-readable dashboard summary.
91#[must_use]
92pub fn format_summary() -> String {
93    let value = snapshot();
94    format!(
95        "ETPAO proxy={:.2} mcp={:.2}; requests={}; compression={:.2}; chain={}/{} accepted",
96        value.proxy_etpao,
97        value.mcp_etpao,
98        value.usage.total_requests,
99        value.usage.compression_ratio,
100        value.chain.accepted,
101        value.chain.total_entries,
102    )
103}
104
105fn build_snapshot() -> DashboardSnapshot {
106    let identity = proxy_bridge::identity_summary();
107    let proxy_etpao = proxy_bridge::etpao_summary();
108    let usage = usage_normalizer::session_usage();
109    let compression = usage_normalizer::compression_overview();
110    let chain = receipt_chain::chain_summary();
111    let proxy_coverage = coverage_class::CoverageClass::FullInline;
112    let mcp_coverage = coverage_class::CoverageClass::ContextControlled;
113
114    DashboardSnapshot {
115        proxy_etpao: proxy_etpao.etpao,
116        mcp_etpao: mcp_bridge::mcp_etpao(),
117        identity: IdentityView {
118            total_users: identity.total_users,
119            total_tokens: identity.total_tokens,
120            total_saved: identity.total_savings,
121            acceptance_rate: proxy_etpao.first_pass_rate,
122        },
123        usage: UsageView {
124            total_requests: usage.total_requests,
125            total_tokens: usage.total_tokens,
126            total_saved: usage.total_saved,
127            compression_ratio: compression.avg_compression_ratio,
128            top_model: compression.best_model,
129        },
130        chain: ChainView {
131            total_entries: chain.total_entries,
132            accepted: chain.accepted,
133            rejected: chain.rejected,
134            kernel_hit_rate: receipt_chain::kernel_hit_rate(),
135            phantom_savings_pct: average(chain.total_phantom_savings_pct, chain.total_entries),
136        },
137        coverage: CoverageView {
138            proxy_coverage: coverage_class::coverage_label(proxy_coverage).to_owned(),
139            mcp_coverage: coverage_class::coverage_label(mcp_coverage).to_owned(),
140            is_fully_addressable: coverage_class::is_addressable(proxy_coverage)
141                && coverage_class::is_addressable(mcp_coverage),
142        },
143    }
144}
145
146fn average(total: f64, count: usize) -> f64 {
147    if count == 0 {
148        0.0
149    } else {
150        total / count as f64
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use std::sync::{Mutex, MutexGuard};
157
158    use super::{DashboardSnapshot, format_summary, snapshot, snapshot_json};
159    use crate::core::context_kernel::mcp_bridge::{self, McpCallData};
160    use crate::core::context_kernel::proxy_bridge::{self, ProxyRequestData};
161
162    static TEST_LOCK: Mutex<()> = Mutex::new(());
163
164    fn isolated_test() -> MutexGuard<'static, ()> {
165        let guard = TEST_LOCK
166            .lock()
167            .unwrap_or_else(std::sync::PoisonError::into_inner);
168        proxy_bridge::reset_state();
169        mcp_bridge::reset_mcp_state();
170        crate::core::context_kernel::usage_normalizer::reset_usage();
171        crate::core::context_kernel::receipt_chain::reset_chain();
172        guard
173    }
174
175    #[test]
176    fn empty_snapshot() {
177        let _guard = isolated_test();
178        let value = snapshot();
179
180        assert_eq!(value.proxy_etpao, 0.0);
181        assert_eq!(value.mcp_etpao, 0.0);
182        assert_eq!(value.identity.total_tokens, 0);
183        assert_eq!(value.usage.total_requests, 0);
184        assert_eq!(value.chain.total_entries, 0);
185    }
186
187    #[test]
188    fn snapshot_serializes() {
189        let _guard = isolated_test();
190        let decoded: DashboardSnapshot = serde_json::from_str(&snapshot_json()).unwrap();
191
192        assert_eq!(decoded.proxy_etpao, 0.0);
193    }
194
195    #[test]
196    fn format_summary_non_empty() {
197        let _guard = isolated_test();
198        let summary = format_summary();
199
200        assert!(summary.contains("ETPAO"));
201        assert!(summary.contains("compression"));
202    }
203
204    #[test]
205    fn snapshot_includes_coverage() {
206        let _guard = isolated_test();
207        let coverage = snapshot().coverage;
208
209        assert_eq!(coverage.proxy_coverage, "full_inline");
210        assert_eq!(coverage.mcp_coverage, "context_controlled");
211        assert!(coverage.is_fully_addressable);
212    }
213
214    #[test]
215    fn snapshot_after_activity() {
216        let _guard = isolated_test();
217        let _ = proxy_bridge::process_proxy_request(&ProxyRequestData {
218            input_tokens: 80,
219            output_tokens: 20,
220            request_count: 1,
221            ..ProxyRequestData::default()
222        });
223        mcp_bridge::record_mcp_call(&McpCallData {
224            input_tokens: 40,
225            output_tokens: 10,
226            call_number: 1,
227            ..McpCallData::default()
228        });
229
230        let value = snapshot();
231        assert!(value.proxy_etpao > 0.0);
232        assert!(value.mcp_etpao > 0.0);
233        assert!(value.identity.total_tokens > 0);
234    }
235}