Skip to main content

lean_ctx/core/context_kernel/
coverage_class.rs

1//! Coverage classification and capability detection for client integrations.
2
3/// Describes how completely lean-ctx can manage a client integration.
4#[derive(
5    Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
6)]
7pub enum CoverageClass {
8    /// All client traffic passes through the inline proxy.
9    FullInline,
10    /// Context is managed through MCP, without inline traffic control.
11    #[default]
12    ContextControlled,
13    /// Hooks expose client activity for observation only.
14    ObserveOnly,
15    /// The client has no supported integration surface.
16    Unmanaged,
17}
18
19/// Operations available for a coverage class.
20#[derive(Debug, Clone, Copy)]
21pub struct CoverageCapabilities {
22    /// Whether input context can be compressed.
23    pub can_compress: bool,
24    /// Whether requests can be routed.
25    pub can_route: bool,
26    /// Whether context results can be cached.
27    pub can_cache: bool,
28    /// Whether client output can be measured.
29    pub can_measure_output: bool,
30}
31
32/// Detects the strongest coverage class supported by the available integrations.
33pub fn detect_coverage(has_proxy: bool, has_mcp: bool, has_hooks: bool) -> CoverageClass {
34    if has_proxy {
35        CoverageClass::FullInline
36    } else if has_mcp {
37        CoverageClass::ContextControlled
38    } else if has_hooks {
39        CoverageClass::ObserveOnly
40    } else {
41        CoverageClass::Unmanaged
42    }
43}
44
45/// Returns the stable machine-readable label for a coverage class.
46pub fn coverage_label(class: CoverageClass) -> &'static str {
47    match class {
48        CoverageClass::FullInline => "full_inline",
49        CoverageClass::ContextControlled => "context_controlled",
50        CoverageClass::ObserveOnly => "observe_only",
51        CoverageClass::Unmanaged => "unmanaged",
52    }
53}
54
55/// Returns whether lean-ctx can directly modify context for the client.
56pub fn is_addressable(class: CoverageClass) -> bool {
57    matches!(
58        class,
59        CoverageClass::FullInline | CoverageClass::ContextControlled
60    )
61}
62
63/// Returns the operations available for a coverage class.
64pub fn capabilities(class: CoverageClass) -> CoverageCapabilities {
65    match class {
66        CoverageClass::FullInline => CoverageCapabilities {
67            can_compress: true,
68            can_route: true,
69            can_cache: true,
70            can_measure_output: true,
71        },
72        CoverageClass::ContextControlled => CoverageCapabilities {
73            can_compress: true,
74            can_route: false,
75            can_cache: true,
76            can_measure_output: false,
77        },
78        CoverageClass::ObserveOnly | CoverageClass::Unmanaged => CoverageCapabilities {
79            can_compress: false,
80            can_route: false,
81            can_cache: false,
82            can_measure_output: false,
83        },
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::{CoverageClass, capabilities, detect_coverage, is_addressable};
90
91    #[test]
92    fn detect_proxy_is_full_inline() {
93        assert_eq!(
94            detect_coverage(true, false, false),
95            CoverageClass::FullInline
96        );
97    }
98
99    #[test]
100    fn detect_mcp_only_is_context_controlled() {
101        assert_eq!(
102            detect_coverage(false, true, false),
103            CoverageClass::ContextControlled
104        );
105    }
106
107    #[test]
108    fn detect_hooks_only_is_observe_only() {
109        assert_eq!(
110            detect_coverage(false, false, true),
111            CoverageClass::ObserveOnly
112        );
113    }
114
115    #[test]
116    fn detect_nothing_is_unmanaged() {
117        assert_eq!(
118            detect_coverage(false, false, false),
119            CoverageClass::Unmanaged
120        );
121    }
122
123    #[test]
124    fn full_inline_is_addressable() {
125        assert!(is_addressable(CoverageClass::FullInline));
126    }
127
128    #[test]
129    fn unmanaged_not_addressable() {
130        assert!(!is_addressable(CoverageClass::Unmanaged));
131    }
132
133    #[test]
134    fn capabilities_full_inline_all_true() {
135        let capabilities = capabilities(CoverageClass::FullInline);
136
137        assert!(capabilities.can_compress);
138        assert!(capabilities.can_route);
139        assert!(capabilities.can_cache);
140        assert!(capabilities.can_measure_output);
141    }
142
143    #[test]
144    fn serde_roundtrip() {
145        let class = CoverageClass::ObserveOnly;
146        let serialized = serde_json::to_string(&class).unwrap();
147        let deserialized = serde_json::from_str(&serialized).unwrap();
148
149        assert_eq!(class, deserialized);
150    }
151}