Skip to main content

lean_ctx/core/patterns/
mod.rs

1/// Pattern engine version. Bump when output format changes to maintain
2/// shell-output determinism guarantees. Consumers (proofs, benchmarks)
3/// can embed this version to detect pattern-breaking updates.
4pub const PATTERN_ENGINE_VERSION: u32 = 1;
5
6pub mod ansible;
7pub mod artisan;
8pub mod aws;
9pub mod bazel;
10pub mod bun;
11pub mod cargo;
12pub mod clang;
13pub mod cmake;
14pub mod composer;
15pub mod curl;
16pub mod deno;
17pub mod deps_cmd;
18pub mod docker;
19pub mod dotnet;
20pub mod env_filter;
21pub mod eslint;
22pub mod fd;
23pub mod find;
24pub mod flutter;
25pub mod gh;
26pub mod git;
27pub mod glab;
28pub mod golang;
29pub mod grep;
30pub mod helm;
31pub mod json_schema;
32pub mod just;
33pub mod kubectl;
34pub mod log_dedup;
35pub mod ls;
36pub mod make;
37pub mod maven;
38pub mod mix;
39pub mod mypy;
40pub mod mysql;
41pub mod next_build;
42pub mod ninja;
43pub mod npm;
44pub mod php;
45pub mod pip;
46pub mod playwright;
47pub mod pnpm;
48pub mod poetry;
49pub mod prettier;
50pub mod prisma;
51pub mod psql;
52pub mod ruby;
53pub mod ruff;
54pub mod swift;
55pub mod sysinfo;
56pub mod systemd;
57pub mod terraform;
58pub mod test;
59pub mod typescript;
60pub mod wget;
61pub mod zig;
62
63pub fn compress_output(command: &str, output: &str) -> Option<String> {
64    let cleaned = crate::core::compressor::strip_ansi(output);
65    let output = if cleaned.len() < output.len() {
66        &cleaned
67    } else {
68        output
69    };
70
71    if let Some(engine) = crate::core::filters::FilterEngine::load() {
72        if let Some(filtered) = engine.apply(command, output) {
73            return shorter_only(filtered, output);
74        }
75    }
76
77    if let Some(compressed) = try_specific_pattern(command, output) {
78        if let Some(r) = shorter_only(compressed, output) {
79            return Some(r);
80        }
81    }
82
83    if let Some(r) = json_schema::compress(output) {
84        if let Some(r) = shorter_only(r, output) {
85            return Some(r);
86        }
87    }
88
89    if let Some(r) = log_dedup::compress(output) {
90        if let Some(r) = shorter_only(r, output) {
91            return Some(r);
92        }
93    }
94
95    if let Some(r) = test::compress(output) {
96        if let Some(r) = shorter_only(r, output) {
97            return Some(r);
98        }
99    }
100
101    if output.len() > 8000 {
102        return Some(truncate_large_output(command, output));
103    }
104
105    None
106}
107
108fn shorter_only(compressed: String, original: &str) -> Option<String> {
109    if compressed.len() < original.len() {
110        Some(compressed)
111    } else {
112        None
113    }
114}
115
116fn truncate_large_output(command: &str, output: &str) -> String {
117    let lines: Vec<&str> = output.lines().collect();
118    let total = lines.len();
119    let size = output.len();
120    let head = 30.min(total);
121    let tail = 15.min(total.saturating_sub(head));
122
123    let mut result = String::with_capacity(4096);
124    let cmd_short = if command.len() > 60 {
125        &command[..60]
126    } else {
127        command
128    };
129    result.push_str(&format!("{cmd_short} ({size} bytes, {total} lines):\n"));
130    for line in lines.iter().take(head) {
131        if line.len() > 300 {
132            result.push_str(&line[..300]);
133            result.push_str("…\n");
134        } else {
135            result.push_str(line);
136            result.push('\n');
137        }
138    }
139    if total > head + tail {
140        result.push_str(&format!(
141            "\n[… {} lines omitted …]\n\n",
142            total - head - tail
143        ));
144        for line in lines.iter().skip(total - tail) {
145            if line.len() > 300 {
146                result.push_str(&line[..300]);
147                result.push_str("…\n");
148            } else {
149                result.push_str(line);
150                result.push('\n');
151            }
152        }
153    }
154    result
155}
156
157fn try_specific_pattern(cmd: &str, output: &str) -> Option<String> {
158    let cl = cmd.to_ascii_lowercase();
159    let c = cl.as_str();
160
161    if c.starts_with("git ") {
162        return git::compress(c, output);
163    }
164    if c.starts_with("gh ") {
165        return gh::compress(c, output);
166    }
167    if c.starts_with("glab ") {
168        return glab::try_glab_pattern(c, output);
169    }
170    if c == "terraform" || c.starts_with("terraform ") {
171        return terraform::compress(c, output);
172    }
173    if c == "make" || c.starts_with("make ") {
174        return make::compress(c, output);
175    }
176    if c == "just" || c.starts_with("just ") {
177        return just::compress(c, output);
178    }
179    if c.starts_with("mvn ")
180        || c.starts_with("./mvnw ")
181        || c.starts_with("mvnw ")
182        || c.starts_with("gradle ")
183        || c.starts_with("./gradlew ")
184        || c.starts_with("gradlew ")
185    {
186        return maven::compress(c, output);
187    }
188    if c.starts_with("kubectl ") || c.starts_with("k ") {
189        return kubectl::compress(c, output);
190    }
191    if c.starts_with("helm ") {
192        return helm::compress(c, output);
193    }
194    if c.starts_with("pnpm ") {
195        return pnpm::compress(c, output);
196    }
197    if c.starts_with("bun ") || c.starts_with("bunx ") {
198        return bun::compress(c, output);
199    }
200    if c.starts_with("deno ") {
201        return deno::compress(c, output);
202    }
203    if c.starts_with("npm ") || c.starts_with("yarn ") {
204        return npm::compress(c, output);
205    }
206    if c.starts_with("cargo ") {
207        return cargo::compress(c, output);
208    }
209    if c.starts_with("docker ") || c.starts_with("docker-compose ") {
210        return docker::compress(c, output);
211    }
212    if c.starts_with("pip ") || c.starts_with("pip3 ") || c.starts_with("python -m pip") {
213        return pip::compress(c, output);
214    }
215    if c.starts_with("mypy") || c.starts_with("python -m mypy") || c.starts_with("dmypy ") {
216        return mypy::compress(c, output);
217    }
218    if c.starts_with("pytest") || c.starts_with("python -m pytest") {
219        return test::compress(output);
220    }
221    if c.starts_with("ruff ") {
222        return ruff::compress(c, output);
223    }
224    if c.starts_with("eslint")
225        || c.starts_with("npx eslint")
226        || c.starts_with("biome ")
227        || c.starts_with("stylelint")
228    {
229        return eslint::compress(c, output);
230    }
231    if c.starts_with("prettier") || c.starts_with("npx prettier") {
232        return prettier::compress(output);
233    }
234    if c.starts_with("go ") || c.starts_with("golangci-lint") || c.starts_with("golint") {
235        return golang::compress(c, output);
236    }
237    if c.starts_with("playwright")
238        || c.starts_with("npx playwright")
239        || c.starts_with("cypress")
240        || c.starts_with("npx cypress")
241    {
242        return playwright::compress(c, output);
243    }
244    if c.starts_with("vitest") || c.starts_with("npx vitest") || c.starts_with("pnpm vitest") {
245        return test::compress(output);
246    }
247    if c.starts_with("next ")
248        || c.starts_with("npx next")
249        || c.starts_with("vite ")
250        || c.starts_with("npx vite")
251        || c.starts_with("vp ")
252        || c.starts_with("vite-plus ")
253    {
254        return next_build::compress(c, output);
255    }
256    if c.starts_with("tsc") || c.contains("typescript") {
257        return typescript::compress(output);
258    }
259    if c.starts_with("rubocop")
260        || c.starts_with("bundle ")
261        || c.starts_with("rake ")
262        || c.starts_with("rails test")
263        || c.starts_with("rspec")
264    {
265        return ruby::compress(c, output);
266    }
267    if c.starts_with("grep ") || c.starts_with("rg ") {
268        return grep::compress(output);
269    }
270    if c.starts_with("find ") {
271        return find::compress(output);
272    }
273    if c.starts_with("fd ") || c.starts_with("fdfind ") {
274        return fd::compress(output);
275    }
276    if c.starts_with("ls ") || c == "ls" {
277        return ls::compress(output);
278    }
279    if c.starts_with("curl ") {
280        return curl::compress_with_cmd(c, output);
281    }
282    if c.starts_with("wget ") {
283        return wget::compress(output);
284    }
285    if c == "env" || c.starts_with("env ") || c.starts_with("printenv") {
286        return env_filter::compress(output);
287    }
288    if c.starts_with("dotnet ") {
289        return dotnet::compress(c, output);
290    }
291    if c.starts_with("flutter ")
292        || (c.starts_with("dart ") && (c.contains(" analyze") || c.ends_with(" analyze")))
293    {
294        return flutter::compress(c, output);
295    }
296    if c.starts_with("poetry ")
297        || c.starts_with("uv sync")
298        || (c.starts_with("uv ") && c.contains("pip install"))
299        || c.starts_with("conda ")
300        || c.starts_with("mamba ")
301        || c.starts_with("pipx ")
302    {
303        return poetry::compress(c, output);
304    }
305    if c.starts_with("aws ") {
306        return aws::compress(c, output);
307    }
308    if c.starts_with("psql ") || c.starts_with("pg_") {
309        return psql::compress(c, output);
310    }
311    if c.starts_with("mysql ") || c.starts_with("mariadb ") {
312        return mysql::compress(c, output);
313    }
314    if c.starts_with("prisma ") || c.starts_with("npx prisma") {
315        return prisma::compress(c, output);
316    }
317    if c.starts_with("swift ") {
318        return swift::compress(c, output);
319    }
320    if c.starts_with("zig ") {
321        return zig::compress(c, output);
322    }
323    if c.starts_with("cmake ") || c.starts_with("ctest") {
324        return cmake::compress(c, output);
325    }
326    if c.starts_with("ninja") {
327        return ninja::compress(c, output);
328    }
329    if c.starts_with("ansible") || c.starts_with("ansible-playbook") {
330        return ansible::compress(c, output);
331    }
332    if c.starts_with("composer ") {
333        return composer::compress(c, output);
334    }
335    if c.starts_with("php artisan") || c.starts_with("artisan ") {
336        return artisan::compress(c, output);
337    }
338    if c.starts_with("./vendor/bin/pest") || c.starts_with("pest ") {
339        return artisan::compress("php artisan test", output);
340    }
341    if c.starts_with("mix ") || c.starts_with("iex ") {
342        return mix::compress(c, output);
343    }
344    if c.starts_with("bazel ") || c.starts_with("blaze ") {
345        return bazel::compress(c, output);
346    }
347    if c.starts_with("systemctl ") || c.starts_with("journalctl") {
348        return systemd::compress(c, output);
349    }
350    if c.starts_with("jest") || c.starts_with("npx jest") || c.starts_with("pnpm jest") {
351        return test::compress(output);
352    }
353    if c.starts_with("mocha") || c.starts_with("npx mocha") {
354        return test::compress(output);
355    }
356    if c.starts_with("tofu ") {
357        return terraform::compress(c, output);
358    }
359    if c.starts_with("ps ") || c == "ps" {
360        return sysinfo::compress_ps(output);
361    }
362    if c.starts_with("df ") || c == "df" {
363        return sysinfo::compress_df(output);
364    }
365    if c.starts_with("du ") || c == "du" {
366        return sysinfo::compress_du(output);
367    }
368    if c.starts_with("ping ") {
369        return sysinfo::compress_ping(output);
370    }
371    if c.starts_with("jq ") || c == "jq" {
372        return json_schema::compress(output);
373    }
374    if c.starts_with("hadolint") {
375        return eslint::compress(c, output);
376    }
377    if c.starts_with("yamllint") || c.starts_with("npx yamllint") {
378        return eslint::compress(c, output);
379    }
380    if c.starts_with("markdownlint") || c.starts_with("npx markdownlint") {
381        return eslint::compress(c, output);
382    }
383    if c.starts_with("oxlint") || c.starts_with("npx oxlint") {
384        return eslint::compress(c, output);
385    }
386    if c.starts_with("pyright") || c.starts_with("basedpyright") {
387        return mypy::compress(c, output);
388    }
389    if c.starts_with("turbo ") || c.starts_with("npx turbo") {
390        return npm::compress(c, output);
391    }
392    if c.starts_with("nx ") || c.starts_with("npx nx") {
393        return npm::compress(c, output);
394    }
395    if c.starts_with("clang++ ") || c.starts_with("clang ") {
396        return clang::compress(c, output);
397    }
398    if c.starts_with("gcc ")
399        || c.starts_with("g++ ")
400        || c.starts_with("cc ")
401        || c.starts_with("c++ ")
402    {
403        return cmake::compress(c, output);
404    }
405
406    None
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    #[test]
414    fn routes_git_commands() {
415        let output = "On branch main\nnothing to commit";
416        assert!(compress_output("git status", output).is_some());
417    }
418
419    #[test]
420    fn routes_cargo_commands() {
421        let output = "   Compiling lean-ctx v2.1.1\n    Finished `release` profile [optimized] target(s) in 30.5s";
422        assert!(compress_output("cargo build --release", output).is_some());
423    }
424
425    #[test]
426    fn routes_npm_commands() {
427        let output = "added 150 packages, and audited 151 packages in 5s\n\n25 packages are looking for funding\n  run `npm fund` for details\n\nfound 0 vulnerabilities";
428        assert!(compress_output("npm install", output).is_some());
429    }
430
431    #[test]
432    fn routes_docker_commands() {
433        let output = "CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES";
434        assert!(compress_output("docker ps", output).is_some());
435    }
436
437    #[test]
438    fn routes_mypy_commands() {
439        let output = "src/main.py:10: error: Missing return  [return]\nFound 1 error in 1 file (checked 3 source files)";
440        assert!(compress_output("mypy .", output).is_some());
441        assert!(compress_output("python -m mypy src/", output).is_some());
442    }
443
444    #[test]
445    fn routes_pytest_commands() {
446        let output = "===== test session starts =====\ncollected 5 items\ntest_main.py ..... [100%]\n===== 5 passed in 0.5s =====";
447        assert!(compress_output("pytest", output).is_some());
448        assert!(compress_output("python -m pytest tests/", output).is_some());
449    }
450
451    #[test]
452    fn unknown_command_returns_none() {
453        assert!(compress_output("some-unknown-tool --version", "v1.0").is_none());
454    }
455
456    #[test]
457    fn case_insensitive_routing() {
458        let output = "On branch main\nnothing to commit";
459        assert!(compress_output("Git Status", output).is_some());
460        assert!(compress_output("GIT STATUS", output).is_some());
461    }
462
463    #[test]
464    fn routes_vp_and_vite_plus() {
465        let output = "  VITE v5.0.0  ready in 200 ms\n\n  -> Local:   http://localhost:5173/\n  -> Network: http://192.168.1.2:5173/";
466        assert!(compress_output("vp build", output).is_some());
467        assert!(compress_output("vite-plus build", output).is_some());
468    }
469
470    #[test]
471    fn routes_bunx_commands() {
472        let output = "1 pass tests\n0 fail tests\n3 skip tests\nDone 12ms\nsome extra line\nmore output here";
473        let result = compress_output("bunx test", output);
474        assert!(
475            result.is_some(),
476            "bunx should compress when output is large enough"
477        );
478        assert!(result.unwrap().contains("bun test: 1 passed"));
479    }
480
481    #[test]
482    fn routes_deno_task() {
483        let output = "Task dev deno run --allow-net server.ts\nListening on http://localhost:8000";
484        assert!(try_specific_pattern("deno task dev", output).is_some());
485    }
486
487    #[test]
488    fn routes_jest_commands() {
489        let output = "PASS  tests/main.test.js\nTest Suites: 1 passed, 1 total\nTests:       5 passed, 5 total\nTime:        2.5 s";
490        assert!(try_specific_pattern("jest", output).is_some());
491        assert!(try_specific_pattern("npx jest --coverage", output).is_some());
492    }
493
494    #[test]
495    fn routes_mocha_commands() {
496        let output = "  3 passing (50ms)\n  1 failing\n\n  1) Array #indexOf():\n     Error: expected -1 to equal 0";
497        assert!(try_specific_pattern("mocha", output).is_some());
498        assert!(try_specific_pattern("npx mocha tests/", output).is_some());
499    }
500
501    #[test]
502    fn routes_tofu_commands() {
503        let output = "Initializing the backend...\nInitializing provider plugins...\nTerraform has been successfully initialized!";
504        assert!(try_specific_pattern("tofu init", output).is_some());
505    }
506
507    #[test]
508    fn routes_ps_commands() {
509        let mut lines = vec!["USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND".to_string()];
510        for i in 0..20 {
511            lines.push(format!("user {i} 0.0 0.1 1234 123 ? S 10:00 0:00 proc_{i}"));
512        }
513        let output = lines.join("\n");
514        assert!(try_specific_pattern("ps aux", &output).is_some());
515    }
516
517    #[test]
518    fn routes_ping_commands() {
519        let output = "PING google.com (1.2.3.4): 56 data bytes\n64 bytes from 1.2.3.4: icmp_seq=0 ttl=116 time=12ms\n3 packets transmitted, 3 packets received, 0.0% packet loss\nrtt min/avg/max/stddev = 11/12/13/1 ms";
520        assert!(try_specific_pattern("ping -c 3 google.com", output).is_some());
521    }
522
523    #[test]
524    fn routes_jq_to_json_schema() {
525        let output = "{\"name\": \"test\", \"version\": \"1.0\", \"items\": [{\"id\": 1}, {\"id\": 2}, {\"id\": 3}, {\"id\": 4}, {\"id\": 5}, {\"id\": 6}, {\"id\": 7}, {\"id\": 8}, {\"id\": 9}, {\"id\": 10}]}";
526        assert!(try_specific_pattern("jq '.items' data.json", output).is_some());
527    }
528
529    #[test]
530    fn routes_linting_tools() {
531        let lint_output = "src/main.py:10: error: Missing return\nsrc/main.py:20: error: Unused var\nFound 2 errors";
532        assert!(try_specific_pattern("hadolint Dockerfile", lint_output).is_some());
533        assert!(try_specific_pattern("oxlint src/", lint_output).is_some());
534        assert!(try_specific_pattern("pyright src/", lint_output).is_some());
535        assert!(try_specific_pattern("basedpyright src/", lint_output).is_some());
536    }
537
538    #[test]
539    fn routes_fd_commands() {
540        let output = "src/main.rs\nsrc/lib.rs\nsrc/util/helpers.rs\nsrc/util/math.rs\ntests/integration.rs\n";
541        assert!(try_specific_pattern("fd --extension rs", output).is_some());
542        assert!(try_specific_pattern("fdfind .rs", output).is_some());
543    }
544
545    #[test]
546    fn routes_just_commands() {
547        let output = "Available recipes:\n    build\n    test\n    lint\n";
548        assert!(try_specific_pattern("just --list", output).is_some());
549        assert!(try_specific_pattern("just build", output).is_some());
550    }
551
552    #[test]
553    fn routes_ninja_commands() {
554        let output = "[1/10] Compiling foo.c\n[10/10] Linking app\n";
555        assert!(try_specific_pattern("ninja", output).is_some());
556        assert!(try_specific_pattern("ninja -j4", output).is_some());
557    }
558
559    #[test]
560    fn routes_clang_commands() {
561        let output =
562            "src/main.c:10:5: error: use of undeclared identifier 'foo'\n1 error generated.\n";
563        assert!(try_specific_pattern("clang src/main.c", output).is_some());
564        assert!(try_specific_pattern("clang++ -std=c++17 main.cpp", output).is_some());
565    }
566
567    #[test]
568    fn routes_cargo_run() {
569        let output = "   Compiling foo v0.1.0\n    Finished `dev` profile\nHello, world!";
570        assert!(try_specific_pattern("cargo run", output).is_some());
571    }
572
573    #[test]
574    fn routes_cargo_bench() {
575        let output = "   Compiling foo v0.1.0\ntest bench_parse ... bench: 1234 ns/iter";
576        assert!(try_specific_pattern("cargo bench", output).is_some());
577    }
578
579    #[test]
580    fn routes_build_tools() {
581        let build_output = "   Compiling foo v0.1.0\n    Finished release [optimized]";
582        assert!(try_specific_pattern("gcc -o main main.c", build_output).is_some());
583        assert!(try_specific_pattern("g++ -o main main.cpp", build_output).is_some());
584    }
585
586    #[test]
587    fn routes_monorepo_tools() {
588        let output = "npm warn deprecated inflight@1.0.6\nnpm warn deprecated rimraf@3.0.2\nadded 150 packages, and audited 151 packages in 5s\n\n25 packages are looking for funding\n  run `npm fund` for details\n\nfound 0 vulnerabilities";
589        assert!(try_specific_pattern("turbo install", output).is_some());
590        assert!(try_specific_pattern("nx install", output).is_some());
591    }
592}