Skip to main content

lean_ctx/core/context_kernel/
dashboard_report.rs

1//! Structured dashboard reporting across Context Kernel subsystems.
2
3use serde::Serialize;
4
5/// Activity status and human-readable detail for one kernel subsystem.
6#[derive(Debug, Clone, Serialize)]
7pub struct SubsystemStatus {
8    /// Display name of the subsystem.
9    pub name: String,
10    /// Whether the subsystem has recorded activity.
11    pub active: bool,
12    /// One-line summary of the subsystem's current metrics.
13    pub detail: String,
14}
15
16/// Token and evidence savings summarized across kernel subsystems.
17#[derive(Debug, Clone, Default, Serialize)]
18pub struct TokenSavingsSummary {
19    /// Number of content-deduplication cache hits.
20    pub dedup_hits: usize,
21    /// Estimated tokens saved by schema optimization.
22    pub schema_tokens_saved: usize,
23    /// Number of evidence dispatches recorded.
24    pub evidence_dispatches: usize,
25    /// Number of responses served from cache.
26    pub response_cached: usize,
27}
28
29/// Aggregated report for the Context Kernel dashboard.
30#[derive(Debug, Clone, Serialize)]
31pub struct DashboardReport {
32    /// Version of lean-ctx that generated the report.
33    pub version: &'static str,
34    /// Whether the Context Kernel master switch is enabled.
35    pub kernel_enabled: bool,
36    /// Overall status: `healthy`, `degraded`, or `disabled`.
37    pub health_status: String,
38    /// Activity summaries for each represented subsystem.
39    pub subsystems: Vec<SubsystemStatus>,
40    /// Combined token and evidence savings.
41    pub savings: TokenSavingsSummary,
42    /// Per-provider request distribution.
43    pub provider_distribution: Vec<super::envelope_bridge::ProviderStat>,
44}
45
46/// Generates a point-in-time report from all dashboard subsystems.
47#[must_use]
48pub fn generate_report() -> DashboardReport {
49    let kernel_enabled = super::kernel_config::is_enabled();
50    let dedup = super::ctx_read_dedup::dedup_summary();
51    let schema = super::schema_wiring::schema_savings();
52    let evidence = super::evidence_wiring::dispatch_summary();
53    let adaptive = super::adaptive_bridge::adaptive_summary();
54    let search = crate::tools::search_kernel::search_summary();
55    let response = super::response_evidence::response_summary();
56    let dispatches = evidence.tool_dispatches + evidence.proxy_dispatches;
57    let health_status = if !kernel_enabled {
58        "disabled"
59    } else if super::health::is_healthy() {
60        "healthy"
61    } else {
62        "degraded"
63    };
64
65    DashboardReport {
66        version: env!("CARGO_PKG_VERSION"),
67        kernel_enabled,
68        health_status: health_status.to_owned(),
69        subsystems: vec![
70            status(
71                "Content Dedup",
72                dedup.total_reads > 0,
73                format!(
74                    "{} hits, {} tokens saved",
75                    dedup.dedup_hits, dedup.tokens_saved
76                ),
77            ),
78            status(
79                "Schema Opt",
80                schema.optimizations_applied > 0,
81                format!(
82                    "{} optimizations, {} tokens saved",
83                    schema.optimizations_applied, schema.total_tokens_saved
84                ),
85            ),
86            status(
87                "Evidence",
88                dispatches > 0,
89                format!("{dispatches} dispatches recorded"),
90            ),
91            status(
92                "Adaptive",
93                adaptive.signals_received > 0,
94                format!(
95                    "bounce rate {:.2}, advice: {:?}",
96                    adaptive.current_bounce_rate, adaptive.advice
97                ),
98            ),
99            status(
100                "Search",
101                search.total_searches > 0,
102                format!(
103                    "{} searches, {} repeats detected",
104                    search.total_searches, search.repeated_queries
105                ),
106            ),
107            status(
108                "Response",
109                response.total_responses > 0,
110                format!(
111                    "{} responses, {:.0}% cache hit",
112                    response.total_responses,
113                    response.cache_hit_rate * 100.0
114                ),
115            ),
116        ],
117        savings: TokenSavingsSummary {
118            dedup_hits: dedup.dedup_hits,
119            schema_tokens_saved: schema.total_tokens_saved,
120            evidence_dispatches: dispatches,
121            response_cached: response.cached_responses,
122        },
123        provider_distribution: super::envelope_bridge::provider_stats(),
124    }
125}
126
127fn status(name: &str, active: bool, detail: String) -> SubsystemStatus {
128    SubsystemStatus {
129        name: name.to_owned(),
130        active,
131        detail,
132    }
133}
134
135/// Formats a dashboard report for terminal display.
136#[must_use]
137pub fn format_report(report: &DashboardReport) -> String {
138    let kernel = if report.kernel_enabled { "ON" } else { "OFF" };
139    let subsystems = report
140        .subsystems
141        .iter()
142        .map(|subsystem| {
143            let marker = if subsystem.active { '✓' } else { '○' };
144            format!("  {marker} {:<15} — {}", subsystem.name, subsystem.detail)
145        })
146        .collect::<Vec<_>>()
147        .join("\n");
148    format!(
149        "═══ lean-ctx Kernel Dashboard ═══\n\
150         Version: {} | Status: {} | Kernel: {kernel}\n\n\
151         Subsystems:\n{subsystems}\n\n\
152         Savings: dedup={} schema={}tok evidence={} cached={}",
153        report.version,
154        report.health_status,
155        report.savings.dedup_hits,
156        report.savings.schema_tokens_saved,
157        report.savings.evidence_dispatches,
158        report.savings.response_cached,
159    )
160}
161
162/// Serializes a dashboard report as pretty-printed JSON.
163#[must_use]
164pub fn report_json(report: &DashboardReport) -> String {
165    serde_json::to_string_pretty(report)
166        .unwrap_or_else(|_| r#"{"error":"dashboard serialization failed"}"#.to_owned())
167}
168
169#[cfg(test)]
170mod tests {
171    use super::{format_report, generate_report, report_json};
172
173    #[test]
174    fn report_populated() {
175        assert!(!generate_report().subsystems.is_empty());
176    }
177
178    #[test]
179    fn format_contains_sections() {
180        let formatted = format_report(&generate_report());
181        assert!(formatted.contains("Dashboard"));
182        assert!(formatted.contains("Subsystems"));
183        assert!(formatted.contains("Savings"));
184    }
185
186    #[test]
187    fn json_valid() {
188        let json = report_json(&generate_report());
189        assert!(serde_json::from_str::<serde_json::Value>(&json).is_ok());
190    }
191}