Skip to main content

lean_ctx/shell/
output_policy.rs

1/// Central command output classification.
2///
3/// Every shell command flows through `classify` before any compression
4/// decision is made. This is the SINGLE source of truth — no other
5/// code path may bypass it.
6///
7/// Priority (first match wins):
8///   1. User `excluded_commands` config      → Passthrough
9///   2. `BUILTIN_PASSTHROUGH` + dev scripts  → Passthrough
10///   3. Verbatim data commands               → Verbatim
11///   4. Everything else                      → Compressible
12///      (pattern engine decides specific vs generic later)
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum OutputPolicy {
16    /// Auth flows, dev servers, interactive, streaming, installs.
17    /// Output is passed through with ZERO modification, even when
18    /// `LEAN_CTX_COMPRESS=1` (force_compress) is set.
19    Passthrough,
20
21    /// API data, file content, structured queries, HTTP responses.
22    /// Output is preserved as-is. Only a hard size-cap is applied
23    /// when the output exceeds the context window limit.
24    Verbatim,
25
26    /// Build, test, lint, package manager, git action output.
27    /// Domain-specific pattern compression is applied, then
28    /// generic fallback if no pattern matches.
29    Compressible,
30}
31
32impl OutputPolicy {
33    /// Returns true if the output MUST NOT be compressed under any
34    /// circumstances (not even truncated, except for catastrophic size).
35    pub fn is_protected(&self) -> bool {
36        matches!(self, Self::Passthrough | Self::Verbatim)
37    }
38}
39
40/// Classify a command into an `OutputPolicy`.
41///
42/// `user_excluded` comes from `Config::excluded_commands`.
43/// This function consolidates:
44///   - `is_excluded_command` (compress.rs BUILTIN_PASSTHROUGH)
45///   - `is_verbatim_output` (compress.rs 22+ is_* helpers)
46///   - `is_passthrough_command` (patterns/mod.rs)
47pub fn classify(command: &str, user_excluded: &[String]) -> OutputPolicy {
48    if is_passthrough(command, user_excluded) {
49        return OutputPolicy::Passthrough;
50    }
51    if super::compress::is_verbatim_output(command) {
52        return OutputPolicy::Verbatim;
53    }
54    OutputPolicy::Compressible
55}
56
57fn is_passthrough(command: &str, user_excluded: &[String]) -> bool {
58    super::compress::is_excluded_command(command, user_excluded)
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn passthrough_beats_verbatim() {
67        // `gh` is in BUILTIN_PASSTHROUGH so it must be Passthrough,
68        // even though `gh api` would also match verbatim.
69        assert_eq!(
70            classify("gh api repos/owner/repo/issues", &[]),
71            OutputPolicy::Passthrough
72        );
73    }
74
75    #[test]
76    fn user_excluded_is_passthrough() {
77        let excl = vec!["mycommand".to_string()];
78        assert_eq!(
79            classify("mycommand --flag", &excl),
80            OutputPolicy::Passthrough
81        );
82    }
83
84    #[test]
85    fn curl_is_verbatim() {
86        assert_eq!(
87            classify("curl https://api.example.com", &[]),
88            OutputPolicy::Verbatim
89        );
90    }
91
92    #[test]
93    fn cat_is_verbatim() {
94        assert_eq!(classify("cat package.json", &[]), OutputPolicy::Verbatim);
95    }
96
97    #[test]
98    fn cargo_build_is_compressible() {
99        assert_eq!(classify("cargo build", &[]), OutputPolicy::Compressible);
100    }
101
102    #[test]
103    fn npm_test_is_compressible() {
104        assert_eq!(classify("npm test", &[]), OutputPolicy::Compressible);
105    }
106
107    #[test]
108    fn dev_server_is_passthrough() {
109        assert_eq!(classify("npm run dev", &[]), OutputPolicy::Passthrough);
110        assert_eq!(classify("cargo watch", &[]), OutputPolicy::Passthrough);
111        assert_eq!(classify("cargo run", &[]), OutputPolicy::Passthrough);
112    }
113
114    #[test]
115    fn git_diff_is_verbatim() {
116        // git diff is structural -> verbatim path in compress_if_beneficial,
117        // but the is_verbatim_output check via is_git_data_command should
118        // also catch structural git commands. If not, it's at least
119        // Compressible (structural pattern). Let's verify:
120        let policy = classify("git diff", &[]);
121        // git diff is not in BUILTIN_PASSTHROUGH, not in is_verbatim_output
122        // (it's in is_structural_git_command which feeds has_structural_output
123        // but NOT is_verbatim_output). So it's Compressible, but compress.rs
124        // handles it specially via has_structural_output.
125        assert_eq!(policy, OutputPolicy::Compressible);
126    }
127
128    #[test]
129    fn auth_commands_are_passthrough() {
130        assert_eq!(classify("az login", &[]), OutputPolicy::Passthrough);
131        assert_eq!(
132            classify("gcloud auth login", &[]),
133            OutputPolicy::Passthrough
134        );
135        assert_eq!(classify("firebase login", &[]), OutputPolicy::Passthrough);
136    }
137
138    #[test]
139    fn jq_is_verbatim() {
140        assert_eq!(
141            classify("jq '.items' data.json", &[]),
142            OutputPolicy::Verbatim
143        );
144    }
145
146    #[test]
147    fn unknown_command_is_compressible() {
148        assert_eq!(
149            classify("some-random-tool --flag", &[]),
150            OutputPolicy::Compressible
151        );
152    }
153
154    #[test]
155    fn piped_jq_is_verbatim() {
156        assert_eq!(
157            classify("kubectl get pods -o json | jq '.items[]'", &[]),
158            OutputPolicy::Verbatim
159        );
160    }
161
162    #[test]
163    fn policy_is_protected() {
164        assert!(OutputPolicy::Passthrough.is_protected());
165        assert!(OutputPolicy::Verbatim.is_protected());
166        assert!(!OutputPolicy::Compressible.is_protected());
167    }
168
169    // --- Regression tests for GitHub Issues ---
170
171    #[test]
172    fn issue_198_gh_api_jq() {
173        // gh api is in BUILTIN_PASSTHROUGH via "gh" prefix
174        assert_eq!(
175            classify("gh api repos/yvgude/lean-ctx/issues/198 --jq '.body'", &[]),
176            OutputPolicy::Passthrough
177        );
178    }
179
180    #[test]
181    fn issue_159_cat_pubspec() {
182        assert_eq!(classify("cat pubspec.yaml", &[]), OutputPolicy::Verbatim);
183    }
184
185    #[test]
186    fn issue_114_git_stash() {
187        // git stash list/show should not be over-compressed
188        // "git stash" without subcommand is not in passthrough,
189        // and is_verbatim_output doesn't match plain "git stash".
190        // But "git stash show" is structural.
191        let p = classify("git stash show", &[]);
192        assert_eq!(p, OutputPolicy::Compressible);
193    }
194
195    #[test]
196    fn issue_194_git_diff_raw() {
197        // git diff/show output should be preserved
198        let p = classify("git diff --cached", &[]);
199        assert_eq!(p, OutputPolicy::Compressible);
200    }
201
202    #[test]
203    fn npm_install_is_compressible() {
204        // npm install output is build-like; compressed via npm pattern
205        assert_eq!(
206            classify("npm install -g deepseek-tui", &[]),
207            OutputPolicy::Compressible
208        );
209    }
210
211    #[test]
212    fn pip_install_is_compressible() {
213        assert_eq!(
214            classify("pip install flask", &[]),
215            OutputPolicy::Compressible
216        );
217    }
218
219    #[test]
220    fn kubectl_get_yaml_is_verbatim() {
221        assert_eq!(
222            classify("kubectl get pods -o yaml", &[]),
223            OutputPolicy::Verbatim
224        );
225    }
226
227    #[test]
228    fn docker_inspect_is_verbatim() {
229        assert_eq!(
230            classify("docker inspect my-container", &[]),
231            OutputPolicy::Verbatim
232        );
233    }
234
235    #[test]
236    fn terraform_output_is_verbatim() {
237        assert_eq!(classify("terraform output", &[]), OutputPolicy::Verbatim);
238    }
239
240    #[test]
241    fn heroku_logs_is_verbatim() {
242        assert_eq!(classify("heroku logs --tail", &[]), OutputPolicy::Verbatim);
243    }
244
245    #[test]
246    fn gh_pr_list_is_passthrough() {
247        // "gh" prefix is in BUILTIN_PASSTHROUGH
248        assert_eq!(classify("gh pr list", &[]), OutputPolicy::Passthrough);
249    }
250
251    #[test]
252    fn lean_ctx_is_passthrough() {
253        assert_eq!(
254            classify("lean-ctx init powershell", &[]),
255            OutputPolicy::Passthrough
256        );
257        assert_eq!(
258            classify("lean-ctx overview", &[]),
259            OutputPolicy::Passthrough
260        );
261    }
262
263    #[test]
264    fn stripe_list_is_verbatim() {
265        assert_eq!(classify("stripe charges list", &[]), OutputPolicy::Verbatim);
266    }
267}