Skip to main content

lean_ctx/core/ocla/builtin/
compression_provider.rs

1//! BuiltinCompressionProvider — fail-closed compression via ContentPort + core::compressor.
2//!
3//! Uses Config::find_project_root() for bounded root resolution. Reports
4//! capability Unavailable when no valid project root exists. Rejects non-file refs
5//! and propagates all errors (fail-closed, no fabricated fallbacks).
6
7use std::collections::BTreeMap;
8use std::path::{Path, PathBuf};
9use std::sync::OnceLock;
10
11use crate::core::compressor;
12use crate::core::config::Config;
13use crate::core::ocla::OclaError;
14use crate::core::ocla::content_port::CompressionContentPort;
15use crate::core::ocla::traits::{CompressionProvider, OclaService};
16use crate::core::ocla::types::{
17    CompressionRequest, CompressionResult, OCLA_API_VERSION, OclaCapability, OclaCapabilityKind,
18    OclaCapabilityStatus, OclaResult,
19};
20use crate::core::ocla_bus::{self, OclaEvent};
21use crate::core::tokens;
22
23static DEFAULT_PORT: OnceLock<Option<CompressionContentPort>> = OnceLock::new();
24
25fn port_from_project_root(root_str: Option<String>) -> Option<CompressionContentPort> {
26    let root_str = root_str?;
27    if root_str.trim().is_empty() || root_str == "." {
28        return None;
29    }
30    let root = PathBuf::from(root_str);
31    let mut component_path = PathBuf::new();
32    for component in root.components() {
33        component_path.push(component.as_os_str());
34        if component_path
35            .symlink_metadata()
36            .ok()?
37            .file_type()
38            .is_symlink()
39        {
40            return None;
41        }
42    }
43    if !root.symlink_metadata().ok()?.is_dir() {
44        return None;
45    }
46    let canonical = root.canonicalize().ok()?;
47    CompressionContentPort::new(canonical)
48}
49
50fn try_default_port() -> Option<&'static CompressionContentPort> {
51    DEFAULT_PORT
52        .get_or_init(|| port_from_project_root(Config::find_project_root()))
53        .as_ref()
54}
55
56pub struct BuiltinCompressionProvider;
57
58impl BuiltinCompressionProvider {
59    pub fn new() -> Self {
60        Self
61    }
62
63    pub fn compress_with_port(
64        &self,
65        request: CompressionRequest,
66        port: &CompressionContentPort,
67    ) -> OclaResult<CompressionResult> {
68        if request.source_tokens == 0 {
69            return Err(OclaError::InvalidRequest(
70                "source_tokens must be > 0".into(),
71            ));
72        }
73        if request.target_tokens == 0 {
74            return Err(OclaError::InvalidRequest(
75                "target_tokens must be > 0".into(),
76            ));
77        }
78
79        if !request.source_ref.starts_with("file:") {
80            return Err(OclaError::InvalidRequest(format!(
81                "only file: refs supported, got: {}",
82                request.source_ref.split(':').next().unwrap_or("unknown")
83            )));
84        }
85
86        let bytes = port.resolve(&request.source_ref)?;
87
88        let source_text = std::str::from_utf8(&bytes)
89            .map_err(|_| OclaError::InvalidRequest("source is not valid UTF-8".into()))?;
90
91        let ext = request
92            .source_ref
93            .strip_prefix("file:")
94            .and_then(|p| Path::new(p).extension())
95            .and_then(|e| e.to_str());
96
97        let compressed = compressor::aggressive_compress(source_text, ext);
98        let delivered_tokens = tokens::count_tokens(&compressed) as u64;
99
100        if delivered_tokens >= request.source_tokens {
101            return Err(OclaError::InvalidRequest(
102                "compression produced no gain (output >= source)".into(),
103            ));
104        }
105
106        if delivered_tokens > request.target_tokens {
107            return Err(OclaError::InvalidRequest(format!(
108                "compressed output ({delivered_tokens}) exceeds target ({})",
109                request.target_tokens
110            )));
111        }
112
113        let ref_key = port.persist(compressed.as_bytes())?;
114
115        ocla_bus::emit(OclaEvent::CompressionApplied {
116            path: Some(request.source_ref.clone()),
117            before_tokens: request.source_tokens,
118            after_tokens: delivered_tokens,
119            strategy: "aggressive_compress".to_string(),
120        });
121
122        Ok(CompressionResult {
123            delivered_ref: ref_key,
124            delivered_tokens,
125            recovery_ref: Some(request.source_ref),
126        })
127    }
128}
129
130impl Default for BuiltinCompressionProvider {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136impl OclaService for BuiltinCompressionProvider {
137    fn capability(&self) -> OclaCapability {
138        if try_default_port().is_some() {
139            OclaCapability::available(OclaCapabilityKind::CompressionProvider)
140        } else {
141            OclaCapability {
142                kind: OclaCapabilityKind::CompressionProvider,
143                api_version: OCLA_API_VERSION.to_string(),
144                status: OclaCapabilityStatus::Unavailable,
145                limits: BTreeMap::new(),
146            }
147        }
148    }
149}
150
151impl CompressionProvider for BuiltinCompressionProvider {
152    fn compress(&self, request: CompressionRequest) -> OclaResult<CompressionResult> {
153        let port = try_default_port().ok_or_else(|| {
154            OclaError::InvalidRequest("compression unavailable: no valid project root".into())
155        })?;
156        self.compress_with_port(request, port)
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::core::ocla::types::OclaRequestContext;
164    use std::fs;
165
166    fn ctx() -> OclaRequestContext {
167        OclaRequestContext {
168            request_id: "r1".into(),
169            session_id: "s1".into(),
170            agent_id: "agent-test".into(),
171            content_ref: "blake3:test".into(),
172            tenant_id: None,
173            trace_id: "tr-unit".into(),
174        }
175    }
176
177    #[test]
178    fn compress_rejects_non_file_ref() {
179        let dir = tempfile::tempdir().unwrap();
180        let root = dir.path().canonicalize().unwrap();
181        let port = CompressionContentPort::new(&root).unwrap();
182        let provider = BuiltinCompressionProvider::new();
183        let err = provider
184            .compress_with_port(
185                CompressionRequest {
186                    context: ctx(),
187                    source_ref: "mem:buffer-123".into(),
188                    source_tokens: 1000,
189                    target_tokens: 300,
190                    quality_policy_ref: None,
191                },
192                &port,
193            )
194            .unwrap_err();
195        assert!(err.to_string().contains("only file: refs"));
196    }
197
198    #[test]
199    fn compress_rejects_zero_source_tokens() {
200        let dir = tempfile::tempdir().unwrap();
201        let root = dir.path().canonicalize().unwrap();
202        let port = CompressionContentPort::new(&root).unwrap();
203        let provider = BuiltinCompressionProvider::new();
204        let err = provider
205            .compress_with_port(
206                CompressionRequest {
207                    context: ctx(),
208                    source_ref: "file:test.rs".into(),
209                    source_tokens: 0,
210                    target_tokens: 300,
211                    quality_policy_ref: None,
212                },
213                &port,
214            )
215            .unwrap_err();
216        assert!(err.to_string().contains("source_tokens"));
217    }
218
219    #[test]
220    fn compress_rejects_zero_target_tokens() {
221        let dir = tempfile::tempdir().unwrap();
222        let root = dir.path().canonicalize().unwrap();
223        let port = CompressionContentPort::new(&root).unwrap();
224        let provider = BuiltinCompressionProvider::new();
225        let err = provider
226            .compress_with_port(
227                CompressionRequest {
228                    context: ctx(),
229                    source_ref: "file:test.rs".into(),
230                    source_tokens: 100,
231                    target_tokens: 0,
232                    quality_policy_ref: None,
233                },
234                &port,
235            )
236            .unwrap_err();
237        assert!(err.to_string().contains("target_tokens"));
238    }
239
240    #[test]
241    fn compress_with_real_file_returns_blake3_ref() {
242        let dir = tempfile::tempdir().unwrap();
243        let root = dir.path().canonicalize().unwrap();
244        let content = "use std::collections::HashMap;\n\
245            use std::io::{self, Read, Write, BufReader, BufWriter};\n\
246            use std::fs::File;\n\n\
247            /// Verbose doc that should compress well.\n\
248            /// Another line of documentation.\n\
249            /// Even more verbose documentation here.\n\
250            fn main() -> io::Result<()> {\n    \
251            // Initialize the hashmap for storing key-value pairs\n    \
252            let mut hash_map_instance: HashMap<String, String> = HashMap::new();\n    \
253            // Insert the first key-value pair into the hashmap\n    \
254            hash_map_instance.insert(String::from(\"key_one\"), String::from(\"value_one\"));\n    \
255            // Insert the second key-value pair into the hashmap\n    \
256            hash_map_instance.insert(String::from(\"key_two\"), String::from(\"value_two\"));\n    \
257            // Insert the third key-value pair into the hashmap\n    \
258            hash_map_instance.insert(String::from(\"key_three\"), String::from(\"value_three\"));\n    \
259            // Iterate over all key-value pairs and print them\n    \
260            for (key_variable, value_variable) in hash_map_instance.iter() {\n        \
261            // Print the current key and value\n        \
262            println!(\"Key: {}, Value: {}\", key_variable, value_variable);\n    \
263            }\n    \
264            // Open a file for reading\n    \
265            let input_file_handle: File = File::open(\"input.txt\")?;\n    \
266            // Create a buffered reader\n    \
267            let mut buffered_reader_instance: BufReader<File> = BufReader::new(input_file_handle);\n    \
268            // Read all contents into a string\n    \
269            let mut file_contents_buffer: String = String::new();\n    \
270            buffered_reader_instance.read_to_string(&mut file_contents_buffer)?;\n    \
271            // Print the length\n    \
272            println!(\"Read {} bytes from input file\", file_contents_buffer.len());\n    \
273            Ok(())\n\
274            }\n";
275        fs::write(root.join("test.rs"), content).unwrap();
276
277        let port = CompressionContentPort::new(&root).unwrap();
278        let provider = BuiltinCompressionProvider::new();
279        let source_tokens = tokens::count_tokens(content) as u64;
280
281        let result = provider.compress_with_port(
282            CompressionRequest {
283                context: ctx(),
284                source_ref: "file:test.rs".into(),
285                source_tokens,
286                target_tokens: source_tokens,
287                quality_policy_ref: None,
288            },
289            &port,
290        );
291
292        let r = result.unwrap();
293        assert!(r.delivered_ref.starts_with("blake3:"));
294        assert!(r.delivered_tokens < source_tokens);
295        assert_eq!(r.recovery_ref, Some("file:test.rs".into()));
296        let delivered = port.retrieve(&r.delivered_ref).unwrap();
297        assert!(delivered.len() < content.len());
298    }
299
300    #[test]
301    fn compress_propagates_resolve_error() {
302        let dir = tempfile::tempdir().unwrap();
303        let root = dir.path().canonicalize().unwrap();
304        let port = CompressionContentPort::new(&root).unwrap();
305        let provider = BuiltinCompressionProvider::new();
306        let err = provider
307            .compress_with_port(
308                CompressionRequest {
309                    context: ctx(),
310                    source_ref: "file:nonexistent.rs".into(),
311                    source_tokens: 100,
312                    target_tokens: 50,
313                    quality_policy_ref: None,
314                },
315                &port,
316            )
317            .unwrap_err();
318        let msg = err.to_string();
319        assert!(
320            msg.contains("resolve")
321                || msg.contains("No such file")
322                || msg.contains("not found")
323                || msg.contains("cannot find"),
324            "unexpected error: {msg}"
325        );
326    }
327
328    #[test]
329    fn compress_empty_file_persists_empty_output() {
330        let dir = tempfile::tempdir().unwrap();
331        let root = dir.path().canonicalize().unwrap();
332        fs::write(root.join("empty.rs"), b"").unwrap();
333        let port = CompressionContentPort::new(&root).unwrap();
334
335        let result = BuiltinCompressionProvider::new()
336            .compress_with_port(
337                CompressionRequest {
338                    context: ctx(),
339                    source_ref: "file:empty.rs".into(),
340                    source_tokens: 1,
341                    target_tokens: 1,
342                    quality_policy_ref: None,
343                },
344                &port,
345            )
346            .unwrap();
347
348        assert_eq!(result.delivered_tokens, 0);
349        assert_eq!(result.recovery_ref, Some("file:empty.rs".into()));
350        assert!(port.retrieve(&result.delivered_ref).unwrap().is_empty());
351    }
352
353    #[test]
354    fn no_gain_rejects_before_persisting() {
355        let dir = tempfile::tempdir().unwrap();
356        let root = dir.path().canonicalize().unwrap();
357        let content = "let value = 1;\n";
358        fs::write(root.join("small.rs"), content).unwrap();
359        let compressed = compressor::aggressive_compress(content, Some("rs"));
360        let source_tokens = tokens::count_tokens(&compressed) as u64;
361        assert!(source_tokens > 0);
362        let ref_key = format!("blake3:{}", blake3::hash(compressed.as_bytes()).to_hex());
363        let port = CompressionContentPort::new(&root).unwrap();
364
365        let err = BuiltinCompressionProvider::new()
366            .compress_with_port(
367                CompressionRequest {
368                    context: ctx(),
369                    source_ref: "file:small.rs".into(),
370                    source_tokens,
371                    target_tokens: 100,
372                    quality_policy_ref: None,
373                },
374                &port,
375            )
376            .unwrap_err();
377
378        assert!(err.to_string().contains("no gain"));
379        assert!(port.retrieve(&ref_key).is_err());
380    }
381
382    #[test]
383    fn target_cap_rejects_before_persisting() {
384        let dir = tempfile::tempdir().unwrap();
385        let root = dir.path().canonicalize().unwrap();
386        let content = "// repeated documentation\nfn main() {\n    let value = 1;\n}\n";
387        fs::write(root.join("target.rs"), content).unwrap();
388        let compressed = compressor::aggressive_compress(content, Some("rs"));
389        let delivered_tokens = tokens::count_tokens(&compressed) as u64;
390        assert!(delivered_tokens > 1);
391        let ref_key = format!("blake3:{}", blake3::hash(compressed.as_bytes()).to_hex());
392        let port = CompressionContentPort::new(&root).unwrap();
393
394        let err = BuiltinCompressionProvider::new()
395            .compress_with_port(
396                CompressionRequest {
397                    context: ctx(),
398                    source_ref: "file:target.rs".into(),
399                    source_tokens: tokens::count_tokens(content) as u64,
400                    target_tokens: delivered_tokens - 1,
401                    quality_policy_ref: None,
402                },
403                &port,
404            )
405            .unwrap_err();
406
407        assert!(err.to_string().contains("exceeds target"));
408        assert!(port.retrieve(&ref_key).is_err());
409    }
410
411    #[cfg(unix)]
412    #[test]
413    fn project_root_symlink_is_unavailable() {
414        let dir = tempfile::tempdir().unwrap();
415        let link = dir.path().join("root-link");
416        std::os::unix::fs::symlink(dir.path(), &link).unwrap();
417        assert!(port_from_project_root(Some(link.to_string_lossy().into_owned())).is_none());
418    }
419
420    #[test]
421    fn project_root_empty_or_dot_is_unavailable() {
422        assert!(port_from_project_root(Some(String::new())).is_none());
423        assert!(port_from_project_root(Some(".".into())).is_none());
424    }
425
426    #[test]
427    fn capability_reflects_root_availability() {
428        let provider = BuiltinCompressionProvider::new();
429        let cap = provider.capability();
430        assert_eq!(cap.kind, OclaCapabilityKind::CompressionProvider);
431    }
432}