Skip to main content

lean_ctx_server/engine/
operators.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Minimal reusable boundary over LeanCTX Local's existing operators.
4//!
5//! This module deliberately delegates to the established implementations. It
6//! gives another in-process runtime a small typed surface without copying or
7//! moving read, search, shell-compression, cache, or redaction algorithms.
8
9use std::path::Path;
10
11pub use crate::tools::ctx_read::ReadOutput;
12pub use crate::tools::ctx_search::SearchOutcome;
13pub use lean_ctx::core::protocol::CrpMode;
14
15/// Process-local cache reused by shared read operators without exposing its storage internals.
16pub struct OperatorCache {
17    inner: lean_ctx::core::cache::SessionCache,
18}
19
20impl Default for OperatorCache {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl OperatorCache {
27    pub fn new() -> Self {
28        Self {
29            inner: lean_ctx::core::cache::SessionCache::new(),
30        }
31    }
32
33    /// Clear all process-local entries, returning the number removed.
34    pub fn clear(&mut self) -> usize {
35        self.inner.clear()
36    }
37}
38
39/// Inputs for one existing Local file-read operation.
40#[derive(Debug, Clone, Copy)]
41pub struct ReadRequest<'a> {
42    pub path: &'a Path,
43    pub project_root: &'a Path,
44    pub mode: &'a str,
45    pub fresh: bool,
46    pub crp_mode: CrpMode,
47    pub task: Option<&'a str>,
48    pub aggressiveness: Option<f64>,
49    pub protect: &'a [String],
50}
51
52/// Run the established cached Local read path without changing its output.
53pub fn read(
54    cache: &mut OperatorCache,
55    request: ReadRequest<'_>,
56) -> Result<ReadOutput, lean_ctx::core::error::PathJailError> {
57    let path = lean_ctx::core::pathjail::jail_path(request.path, request.project_root)?;
58    let path = path.to_string_lossy();
59    let output = if request.fresh {
60        crate::tools::ctx_read::handle_fresh_with_task_resolved_tuned(
61            &mut cache.inner,
62            &path,
63            request.mode,
64            request.crp_mode,
65            request.task,
66            request.aggressiveness,
67            request.protect,
68        )
69    } else {
70        crate::tools::ctx_read::handle_with_task_resolved_tuned(
71            &mut cache.inner,
72            &path,
73            request.mode,
74            request.crp_mode,
75            request.task,
76            request.aggressiveness,
77            request.protect,
78        )
79    };
80    Ok(output)
81}
82
83/// Inputs for one existing Local regex-search operation.
84#[derive(Debug, Clone, Copy)]
85pub struct SearchRequest<'a> {
86    pub pattern: &'a str,
87    pub directory: &'a Path,
88    pub project_root: &'a Path,
89    pub include: Option<&'a str>,
90    pub max_results: usize,
91    pub crp_mode: CrpMode,
92    pub respect_gitignore: bool,
93    pub allow_secret_paths: bool,
94    pub anchored: bool,
95    pub exclude: Option<&'a str>,
96    pub exclude_pattern: Option<&'a str>,
97}
98
99/// Run the established Local regex-search path without changing its output.
100pub fn search(
101    request: SearchRequest<'_>,
102) -> Result<SearchOutcome, lean_ctx::core::error::PathJailError> {
103    let directory = lean_ctx::core::pathjail::jail_path(request.directory, request.project_root)?;
104    let directory = directory.to_string_lossy();
105    Ok(crate::tools::ctx_search::handle_filtered(
106        request.pattern,
107        &directory,
108        request.include,
109        request.max_results,
110        request.crp_mode,
111        request.respect_gitignore,
112        request.allow_secret_paths,
113        request.anchored,
114        request.exclude,
115        request.exclude_pattern,
116    ))
117}
118
119/// Compress completed shell output through the established Local outcome path.
120pub fn compress_shell_output(command: &str, output: &str, exit_code: i32) -> String {
121    lean_ctx::shell::compress::engine::compress_for_outcome(command, output, exit_code)
122}
123
124/// Apply the established always-on Local secret-redaction rules.
125pub fn redact_output(input: &str) -> String {
126    lean_ctx::core::redaction::redact_text(input)
127}
128
129/// Execute the shared low-level signature operator used by Via Edge.
130pub fn render_shared_signatures(content: &str, file_ext: &str, tdd: bool) -> String {
131    let (signatures, _) =
132        lean_ctx::core::signatures::extract_signatures_with_backend(content, file_ext);
133    signatures
134        .iter()
135        .map(|signature| {
136            if tdd {
137                signature.to_tdd_located()
138            } else {
139                signature.to_compact_located()
140            }
141        })
142        .collect::<Vec<_>>()
143        .join("\n")
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    fn assert_read_eq(actual: &ReadOutput, expected: &ReadOutput) {
151        assert_eq!(actual.content, expected.content);
152        assert_eq!(actual.resolved_mode, expected.resolved_mode);
153        assert_eq!(actual.output_tokens, expected.output_tokens);
154        assert_eq!(actual.is_cache_hit, expected.is_cache_hit);
155    }
156
157    #[test]
158    fn read_boundary_matches_existing_operator() {
159        let dir = tempfile::tempdir().unwrap();
160        let path = dir.path().join("sample.rs");
161        std::fs::write(&path, "pub fn answer() -> u32 { 42 }\n").unwrap();
162        let request = ReadRequest {
163            path: &path,
164            project_root: dir.path(),
165            mode: "signatures",
166            fresh: false,
167            crp_mode: CrpMode::Off,
168            task: None,
169            aggressiveness: None,
170            protect: &[],
171        };
172        let mut boundary_cache = OperatorCache::new();
173        let mut direct_cache = lean_ctx::core::cache::SessionCache::new();
174
175        let actual = read(&mut boundary_cache, request).unwrap();
176        let path = lean_ctx::core::pathjail::jail_path(request.path, request.project_root).unwrap();
177        let path = path.to_string_lossy();
178        let expected = crate::tools::ctx_read::handle_with_task_resolved_tuned(
179            &mut direct_cache,
180            &path,
181            request.mode,
182            request.crp_mode,
183            request.task,
184            request.aggressiveness,
185            request.protect,
186        );
187
188        assert_read_eq(&actual, &expected);
189    }
190
191    #[test]
192    fn fresh_read_boundary_matches_existing_operator() {
193        let dir = tempfile::tempdir().unwrap();
194        let path = dir.path().join("fresh.txt");
195        std::fs::write(&path, "alpha\nbeta\n").unwrap();
196        let request = ReadRequest {
197            path: &path,
198            project_root: dir.path(),
199            mode: "full",
200            fresh: true,
201            crp_mode: CrpMode::Off,
202            task: Some("alpha"),
203            aggressiveness: None,
204            protect: &[],
205        };
206        let mut boundary_cache = OperatorCache::new();
207        let mut direct_cache = lean_ctx::core::cache::SessionCache::new();
208
209        let actual = read(&mut boundary_cache, request).unwrap();
210        let path = lean_ctx::core::pathjail::jail_path(request.path, request.project_root).unwrap();
211        let path = path.to_string_lossy();
212        let expected = crate::tools::ctx_read::handle_fresh_with_task_resolved_tuned(
213            &mut direct_cache,
214            &path,
215            request.mode,
216            request.crp_mode,
217            request.task,
218            request.aggressiveness,
219            request.protect,
220        );
221
222        assert_read_eq(&actual, &expected);
223    }
224
225    #[test]
226    fn search_boundary_matches_existing_operator() {
227        // Search answers "withheld under policy" while any content policy is active, and
228        // policy tests elsewhere in the crate pin one process-wide. Pin "none" here, or a
229        // parallel test decides this result (red in every v4-runtime run on 08.11.).
230        let _no_policy = lean_ctx::core::policy::runtime::TestPolicyOverride::set(None);
231        let dir = tempfile::tempdir().unwrap();
232        std::fs::write(dir.path().join("sample.txt"), "alpha needle omega\n").unwrap();
233        let request = SearchRequest {
234            pattern: "needle",
235            directory: dir.path(),
236            project_root: dir.path(),
237            include: Some("*.txt"),
238            max_results: 10,
239            crp_mode: CrpMode::Off,
240            respect_gitignore: true,
241            allow_secret_paths: false,
242            anchored: false,
243            exclude: None,
244            exclude_pattern: None,
245        };
246
247        let actual = search(request).unwrap();
248        let directory =
249            lean_ctx::core::pathjail::jail_path(request.directory, request.project_root).unwrap();
250        let directory = directory.to_string_lossy();
251        let expected = crate::tools::ctx_search::handle_filtered(
252            request.pattern,
253            &directory,
254            request.include,
255            request.max_results,
256            request.crp_mode,
257            request.respect_gitignore,
258            request.allow_secret_paths,
259            request.anchored,
260            request.exclude,
261            request.exclude_pattern,
262        );
263
264        assert_eq!(actual.text, expected.text);
265        assert_eq!(actual.modeled_baseline, expected.modeled_baseline);
266        assert_eq!(actual.observed_tokens, expected.observed_tokens);
267    }
268
269    #[test]
270    fn shell_boundary_matches_existing_operator() {
271        let output = "running 2 tests\ntest alpha ... ok\ntest beta ... ok\ntest result: ok. 2 passed; 0 failed\n";
272        assert_eq!(
273            compress_shell_output("cargo test", output, 0),
274            lean_ctx::shell::compress::engine::compress_for_outcome("cargo test", output, 0)
275        );
276    }
277
278    #[test]
279    fn failed_shell_boundary_matches_existing_operator() {
280        let output = "step 1 ok\nstep 2 ok\nerror: deployment failed\n";
281        assert_eq!(
282            compress_shell_output("./deploy.sh", output, 13),
283            lean_ctx::shell::compress::engine::compress_for_outcome("./deploy.sh", output, 13)
284        );
285    }
286
287    #[test]
288    fn redaction_boundary_matches_existing_operator() {
289        let input = "Authorization: Bearer abcdefghijklmnopqrstuvwxyz";
290        let actual = redact_output(input);
291        assert_eq!(actual, lean_ctx::core::redaction::redact_text(input));
292        assert!(!actual.contains("abcdefghijklmnopqrstuvwxyz"));
293    }
294
295    #[test]
296    fn cache_boundary_reuses_session_cache_type() {
297        let mut cache = OperatorCache::new();
298        cache
299            .inner
300            .store("virtual.txt", "same cache implementation");
301        assert_eq!(cache.clear(), 1);
302        assert!(cache.inner.get_full_content("virtual.txt").is_none());
303    }
304
305    #[test]
306    fn shared_signature_operator_matches_local_extractor() {
307        let source = "pub fn answer() -> u32 { 42 }\nfn detail() {}\n";
308        let expected = lean_ctx::core::signatures::extract_signatures(source, "rs")
309            .iter()
310            .map(lean_ctx::core::signatures::Signature::to_compact_located)
311            .collect::<Vec<_>>()
312            .join("\n");
313        assert_eq!(render_shared_signatures(source, "rs", false), expected);
314    }
315
316    #[test]
317    #[cfg(not(feature = "no-jail"))]
318    fn read_boundary_rejects_paths_outside_project_root() {
319        let project = tempfile::tempdir().unwrap();
320        let outside = tempfile::NamedTempFile::new().unwrap();
321        let mut cache = OperatorCache::new();
322        let result = read(
323            &mut cache,
324            ReadRequest {
325                path: outside.path(),
326                project_root: project.path(),
327                mode: "full",
328                fresh: false,
329                crp_mode: CrpMode::Off,
330                task: None,
331                aggressiveness: None,
332                protect: &[],
333            },
334        );
335        assert!(result.is_err());
336    }
337
338    #[test]
339    #[cfg(not(feature = "no-jail"))]
340    fn search_boundary_rejects_directories_outside_project_root() {
341        let project = tempfile::tempdir().unwrap();
342        let outside = tempfile::tempdir().unwrap();
343        let result = search(SearchRequest {
344            pattern: "needle",
345            directory: outside.path(),
346            project_root: project.path(),
347            include: None,
348            max_results: 10,
349            crp_mode: CrpMode::Off,
350            respect_gitignore: true,
351            allow_secret_paths: false,
352            anchored: false,
353            exclude: None,
354            exclude_pattern: None,
355        });
356        assert!(result.is_err());
357    }
358}