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 alembic;
7pub mod ansible;
8pub mod argocd;
9pub mod artisan;
10pub mod aws;
11pub mod bazel;
12pub mod buf;
13pub mod bun;
14pub mod cargo;
15pub mod clang;
16pub mod cmake;
17pub mod composer;
18pub mod cosign;
19pub mod curl;
20pub mod dbt;
21pub mod deno;
22pub mod deploy;
23pub mod deps_cmd;
24pub mod docker;
25pub mod dotnet;
26pub mod env_filter;
27pub mod eslint;
28pub mod fd;
29pub mod find;
30pub mod flutter;
31pub mod flyway;
32pub mod gem;
33pub mod gh;
34pub mod git;
35pub mod glab;
36pub mod golang;
37pub mod grep;
38pub mod grype;
39pub mod helm;
40pub mod jj;
41pub mod json_schema;
42pub mod just;
43pub mod kubectl;
44pub mod linkerd;
45pub mod log_dedup;
46pub mod ls;
47pub mod make;
48pub mod maven;
49pub mod mise;
50pub mod mix;
51pub mod mlflow;
52pub mod mypy;
53pub mod mysql;
54pub mod next_build;
55pub mod ninja;
56pub mod npm;
57pub mod ollama;
58pub mod php;
59pub mod pip;
60pub mod playwright;
61pub mod pnpm;
62pub mod poetry;
63pub mod prettier;
64pub mod prisma;
65pub mod psql;
66pub mod pulumi;
67pub mod pytest;
68pub mod ruby;
69pub mod ruff;
70pub mod semgrep;
71pub mod spark;
72pub mod swift;
73pub mod swiftlint;
74pub mod syft;
75pub mod sysinfo;
76pub mod systemd;
77pub mod terraform;
78pub mod test;
79pub mod trivy;
80pub mod typescript;
81pub mod wget;
82pub mod zig;
83
84use crate::core::tokens::count_tokens;
85
86pub fn compress_output(command: &str, output: &str) -> Option<String> {
87    // Policy gate: protected commands (Passthrough + Verbatim) must
88    // never be pattern-compressed. The caller (compress_if_beneficial
89    // or ctx_shell::handle) should have already checked, but we
90    // defend in depth here.
91    let policy = crate::shell::output_policy::classify(command, &[]);
92    if policy.is_protected() {
93        return None;
94    }
95
96    let stripped;
97    let clean_output = {
98        let s = crate::core::compressor::strip_ansi(output);
99        if s.len() < output.len() {
100            stripped = s;
101            &stripped
102        } else {
103            output
104        }
105    };
106
107    if let Some(engine) = crate::core::filters::FilterEngine::load()
108        && let Some(filtered) = engine.apply(command, clean_output)
109    {
110        return shorter_only(filtered, output);
111    }
112
113    // VCS history (git/jj/gh/glab/hg) is owned by its dedicated compressor. Its
114    // lines look log-ish (one per commit) but are NOT application logs, so the
115    // generic json/log/test fallbacks would mis-summarize them — e.g. truncating
116    // an explicit `git log --oneline -40` to "last 15" or reading a commit
117    // subject like "fix: pending_errors" as an error line. Return the dedicated
118    // compressor's result directly — even when it is not shorter, so an already
119    // compact oneline log is preserved verbatim (full history intact) instead of
120    // being reshaped by a generic heuristic.
121    if has_vcs_owner(command) {
122        // The VCS compressor is authoritative and is kept even when it is not
123        // strictly *shorter*, so a compact `git log --oneline` is preserved
124        // verbatim (full history intact). It must still never return *more*
125        // tokens than its input, though — a tiny adversarial `git status` body
126        // could otherwise reshape into a one-token-larger summary and break the
127        // never-inflate invariant. Allow equal (verbatim), reject only growth.
128        return try_specific_pattern(command, clean_output)
129            .filter(|c| !c.trim().is_empty())
130            .filter(|c| !inflates_tokens(c, output));
131    }
132
133    if let Some(compressed) = try_specific_pattern(command, clean_output)
134        && let Some(r) = shorter_only(compressed, output)
135    {
136        return Some(r);
137    }
138
139    if let Some(r) = json_schema::compress(clean_output)
140        && let Some(r) = shorter_only(r, output)
141    {
142        return Some(r);
143    }
144
145    if let Some(r) = log_dedup::compress(clean_output)
146        && let Some(r) = shorter_only(r, output)
147    {
148        return Some(r);
149    }
150
151    if let Some(r) = test::compress(clean_output)
152        && let Some(r) = shorter_only(r, output)
153    {
154        return Some(r);
155    }
156
157    None
158}
159
160/// True for version-control commands whose output is authoritative under their
161/// own compressor and must not be reinterpreted by the generic log/test
162/// fallbacks (commit history is not application log output).
163pub(crate) fn has_vcs_owner(command: &str) -> bool {
164    let c = command.trim_start().to_ascii_lowercase();
165    c.starts_with("git ")
166        || c.starts_with("jj ")
167        || c.starts_with("gh ")
168        || c.starts_with("glab ")
169        || c.starts_with("hg ")
170}
171
172/// Collapse whitespace into single spaces so comparisons align with logical word tokens.
173fn normalize_shell_tokens(text: &str) -> String {
174    text.split_whitespace().collect::<Vec<_>>().join(" ")
175}
176
177/// True when `compressed` carries *more* tokens than the raw `original` — i.e.
178/// the transform inflated rather than compressed. Compared against the raw
179/// (pre-ANSI-strip) output so the guard matches the public `compress_output`
180/// invariant exactly: the returned text never tokenizes larger than its input.
181fn inflates_tokens(compressed: &str, original: &str) -> bool {
182    count_tokens(compressed) > count_tokens(original)
183}
184
185fn shorter_only(compressed: String, original: &str) -> Option<String> {
186    let orig_n = normalize_shell_tokens(original);
187    let comp_n = normalize_shell_tokens(&compressed);
188    let ct_c = count_tokens(&comp_n);
189    let ct_o = count_tokens(&orig_n);
190    if ct_c < ct_o || (ct_c == ct_o && comp_n.len() < orig_n.len()) {
191        Some(compressed)
192    } else {
193        None
194    }
195}
196
197pub fn try_specific_pattern(cmd: &str, output: &str) -> Option<String> {
198    let cl = cmd.to_ascii_lowercase();
199    let c = cl.as_str();
200
201    if c.starts_with("git ") {
202        return git::compress(c, output);
203    }
204    if c.starts_with("gh ") {
205        return gh::compress(c, output);
206    }
207    if c.starts_with("glab ") {
208        return glab::try_glab_pattern(c, output);
209    }
210    if c == "terraform" || c.starts_with("terraform ") {
211        return terraform::compress(c, output);
212    }
213    if c == "make" || c.starts_with("make ") {
214        return make::compress(c, output);
215    }
216    if c == "just" || c.starts_with("just ") {
217        return just::compress(c, output);
218    }
219    if c.starts_with("mvn ")
220        || c.starts_with("./mvnw ")
221        || c.starts_with("mvnw ")
222        || c.starts_with("gradle ")
223        || c.starts_with("./gradlew ")
224        || c.starts_with("gradlew ")
225    {
226        return maven::compress(c, output);
227    }
228    if c.starts_with("kubectl ") || c.starts_with("k ") {
229        return kubectl::compress(c, output);
230    }
231    if c.starts_with("helm ") {
232        return helm::compress(c, output);
233    }
234    if c.starts_with("pnpm ") {
235        return pnpm::compress(c, output);
236    }
237    if c.starts_with("bun ") || c.starts_with("bunx ") {
238        return bun::compress(c, output);
239    }
240    if c.starts_with("deno ") {
241        return deno::compress(c, output);
242    }
243    if c.starts_with("npm ") || c.starts_with("yarn ") {
244        return npm::compress(c, output);
245    }
246    if c.starts_with("cargo ") {
247        return cargo::compress(c, output);
248    }
249    if c.starts_with("docker ") || c.starts_with("docker-compose ") {
250        return docker::compress(c, output);
251    }
252    if c.starts_with("pip ") || c.starts_with("pip3 ") || c.starts_with("python -m pip") {
253        return pip::compress(c, output);
254    }
255    if c.starts_with("mypy") || c.starts_with("python -m mypy") || c.starts_with("dmypy ") {
256        return mypy::compress(c, output);
257    }
258    if c.starts_with("pytest") || c.starts_with("python -m pytest") {
259        return pytest::compress(c, output).or_else(|| test::compress(output));
260    }
261    if c.starts_with("ruff ") {
262        return ruff::compress(c, output);
263    }
264    if c.starts_with("eslint")
265        || c.starts_with("npx eslint")
266        || c.starts_with("biome ")
267        || c.starts_with("stylelint")
268    {
269        return eslint::compress(c, output);
270    }
271    if c.starts_with("prettier") || c.starts_with("npx prettier") {
272        return prettier::compress(output);
273    }
274    if c.starts_with("go ") || c.starts_with("golangci-lint") || c.starts_with("golint") {
275        return golang::compress(c, output);
276    }
277    if c.starts_with("playwright")
278        || c.starts_with("npx playwright")
279        || c.starts_with("cypress")
280        || c.starts_with("npx cypress")
281    {
282        return playwright::compress(c, output);
283    }
284    if c.starts_with("vitest") || c.starts_with("npx vitest") || c.starts_with("pnpm vitest") {
285        return test::compress(output);
286    }
287    if c.starts_with("next ")
288        || c.starts_with("npx next")
289        || c.starts_with("vite ")
290        || c.starts_with("npx vite")
291        || c.starts_with("vp ")
292        || c.starts_with("vite-plus ")
293    {
294        return next_build::compress(c, output);
295    }
296    if c.starts_with("tsc") || c.contains("typescript") {
297        return typescript::compress(output);
298    }
299    if c.starts_with("rubocop")
300        || c.starts_with("bundle ")
301        || c.starts_with("rake ")
302        || c.starts_with("rails test")
303        || c.starts_with("rspec")
304    {
305        return ruby::compress(c, output);
306    }
307    if c.starts_with("grep ") || c.starts_with("rg ") {
308        return grep::compress(output);
309    }
310    if c.starts_with("find ") {
311        return find::compress(output);
312    }
313    if c.starts_with("fd ") || c.starts_with("fdfind ") {
314        return fd::compress(output);
315    }
316    if c.starts_with("ls ") || c == "ls" {
317        return ls::compress(output);
318    }
319    if c.starts_with("curl ") {
320        return curl::compress_with_cmd(c, output);
321    }
322    if c.starts_with("wget ") {
323        return wget::compress(output);
324    }
325    if c == "env" || c.starts_with("env ") || c.starts_with("printenv") {
326        return env_filter::compress(output);
327    }
328    if c.starts_with("dotnet ") {
329        return dotnet::compress(c, output);
330    }
331    if c.starts_with("flutter ")
332        || (c.starts_with("dart ") && (c.contains(" analyze") || c.ends_with(" analyze")))
333    {
334        return flutter::compress(c, output);
335    }
336    if c.starts_with("poetry ")
337        || c.starts_with("uv ")
338        || c.starts_with("conda ")
339        || c.starts_with("mamba ")
340        || c.starts_with("pipx ")
341    {
342        return poetry::compress(c, output);
343    }
344    if c.starts_with("aws ") {
345        return aws::compress(c, output);
346    }
347    if c.starts_with("psql ") || c.starts_with("pg_") {
348        return psql::compress(c, output);
349    }
350    if c.starts_with("mysql ") || c.starts_with("mariadb ") {
351        return mysql::compress(c, output);
352    }
353    if c.starts_with("prisma ") || c.starts_with("npx prisma") {
354        return prisma::compress(c, output);
355    }
356    if c.starts_with("swift ") {
357        return swift::compress(c, output);
358    }
359    if c.starts_with("zig ") {
360        return zig::compress(c, output);
361    }
362    if c.starts_with("cmake ") || c.starts_with("ctest") {
363        return cmake::compress(c, output);
364    }
365    if c.starts_with("ninja") {
366        return ninja::compress(c, output);
367    }
368    if c.starts_with("ansible") || c.starts_with("ansible-playbook") {
369        return ansible::compress(c, output);
370    }
371    if c.starts_with("composer ") {
372        return composer::compress(c, output);
373    }
374    if c.starts_with("php artisan") || c.starts_with("artisan ") {
375        return artisan::compress(c, output);
376    }
377    if c.starts_with("./vendor/bin/pest") || c.starts_with("pest ") {
378        return artisan::compress("php artisan test", output);
379    }
380    if c.starts_with("mix ") || c.starts_with("iex ") {
381        return mix::compress(c, output);
382    }
383    if c.starts_with("bazel ") || c.starts_with("blaze ") {
384        return bazel::compress(c, output);
385    }
386    if c.starts_with("systemctl ") || c.starts_with("journalctl") {
387        return systemd::compress(c, output);
388    }
389    if c.starts_with("jest") || c.starts_with("npx jest") || c.starts_with("pnpm jest") {
390        return test::compress(output);
391    }
392    if c.starts_with("mocha") || c.starts_with("npx mocha") {
393        return test::compress(output);
394    }
395    if c.starts_with("tofu ") {
396        return terraform::compress(c, output);
397    }
398    if c.starts_with("ps ") || c == "ps" {
399        return sysinfo::compress_ps(output);
400    }
401    if c.starts_with("df ") || c == "df" {
402        return sysinfo::compress_df(output);
403    }
404    if c.starts_with("du ") || c == "du" {
405        return sysinfo::compress_du(output);
406    }
407    if c.starts_with("ping ") {
408        return sysinfo::compress_ping(output);
409    }
410    if c.starts_with("jq ") || c == "jq" {
411        return json_schema::compress(output);
412    }
413    if c.starts_with("hadolint") {
414        return eslint::compress(c, output);
415    }
416    if c.starts_with("yamllint") || c.starts_with("npx yamllint") {
417        return eslint::compress(c, output);
418    }
419    if c.starts_with("markdownlint") || c.starts_with("npx markdownlint") {
420        return eslint::compress(c, output);
421    }
422    if c.starts_with("oxlint") || c.starts_with("npx oxlint") {
423        return eslint::compress(c, output);
424    }
425    if c.starts_with("pyright") || c.starts_with("basedpyright") {
426        return mypy::compress(c, output);
427    }
428    if c.starts_with("turbo ") || c.starts_with("npx turbo") {
429        return npm::compress(c, output);
430    }
431    if c.starts_with("nx ") || c.starts_with("npx nx") {
432        return npm::compress(c, output);
433    }
434    if c.starts_with("clang++ ") || c.starts_with("clang ") {
435        return clang::compress(c, output);
436    }
437    if c.starts_with("gcc ")
438        || c.starts_with("g++ ")
439        || c.starts_with("cc ")
440        || c.starts_with("c++ ")
441    {
442        return cmake::compress(c, output);
443    }
444
445    // --- data domain (#657) ---
446    if c == "dbt" || c.starts_with("dbt ") {
447        return dbt::compress(c, output);
448    }
449    if c == "alembic" || c.starts_with("alembic ") {
450        return alembic::compress(c, output);
451    }
452    if c == "flyway" || c.starts_with("flyway ") {
453        return flyway::compress(c, output);
454    }
455    if c.starts_with("spark-submit") || c.starts_with("spark-sql") || c.starts_with("pyspark") {
456        return spark::compress(c, output);
457    }
458
459    // --- ai domain (#658) ---
460    if c == "ollama" || c.starts_with("ollama ") {
461        return ollama::compress(c, output);
462    }
463    if c.starts_with("mlflow ") {
464        return mlflow::compress(c, output);
465    }
466
467    // --- security / supply-chain (#659) ---
468    if c.starts_with("semgrep ") {
469        return semgrep::compress(c, output);
470    }
471    if c.starts_with("trivy ") {
472        return trivy::compress(c, output);
473    }
474    if c.starts_with("grype ") {
475        return grype::compress(c, output);
476    }
477    if c.starts_with("syft ") {
478        return syft::compress(c, output);
479    }
480    if c.starts_with("cosign ") {
481        return cosign::compress(c, output);
482    }
483    if c.starts_with("swiftlint") {
484        return swiftlint::compress(c, output);
485    }
486
487    // --- vcs / toolchain (#660) ---
488    if c == "jj" || c.starts_with("jj ") {
489        return jj::compress(c, output);
490    }
491    if c == "mise" || c.starts_with("mise ") {
492        return mise::compress(c, output);
493    }
494    if c == "buf" || c.starts_with("buf ") {
495        return buf::compress(c, output);
496    }
497    if c.starts_with("gem ") {
498        return gem::compress(c, output);
499    }
500
501    // --- edge / infra (#661) ---
502    if c == "pulumi" || c.starts_with("pulumi ") {
503        return pulumi::compress(c, output);
504    }
505    if c.starts_with("linkerd ") {
506        return linkerd::compress(c, output);
507    }
508    if c.starts_with("argocd ") {
509        return argocd::compress(c, output);
510    }
511    if c == "vercel"
512        || c.starts_with("vercel ")
513        || c == "fly"
514        || c.starts_with("fly ")
515        || c.starts_with("flyctl ")
516        || c.starts_with("wrangler ")
517        || c.starts_with("skaffold ")
518        || c.starts_with("supabase ")
519    {
520        return deploy::compress(c, output);
521    }
522
523    None
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529
530    #[test]
531    fn routes_git_commands() {
532        let output = "On branch main\nnothing to commit";
533        assert!(compress_output("git status", output).is_some());
534    }
535
536    #[test]
537    fn vcs_path_never_inflates_tokens() {
538        // Regression for the `compress_output_never_inflates_tokens` property
539        // (Windows CI): the VCS branch returned the git compressor's reshaped
540        // output without the `shorter_only` guard the other paths use, so this
541        // tiny adversarial `git status` body grew 10 -> 11 tokens. The result
542        // must now never tokenize larger than its input (None == use original).
543        let output = " Abu a\nAa aa_A00A\n\n-";
544        if let Some(compressed) = compress_output("git status", output) {
545            assert!(
546                count_tokens(&compressed) <= count_tokens(output),
547                "VCS compress inflated: {} > {}",
548                count_tokens(&compressed),
549                count_tokens(output),
550            );
551        }
552    }
553
554    #[test]
555    fn routes_cargo_commands() {
556        let output = "   Compiling lean-ctx v2.1.1\n    Finished `release` profile [optimized] target(s) in 30.5s";
557        assert!(compress_output("cargo build --release", output).is_some());
558    }
559
560    #[test]
561    fn routes_npm_commands() {
562        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";
563        assert!(compress_output("npm install", output).is_some());
564    }
565
566    #[test]
567    fn routes_docker_commands() {
568        let output = "CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES";
569        // docker ps is Verbatim (via is_container_listing), so compress_output
570        // correctly returns None (policy gate). docker build should still compress.
571        assert!(compress_output("docker ps", output).is_none());
572        let build_output =
573            "Step 1/5 : FROM node:18\n ---> abc123\nStep 2/5 : COPY . .\nSuccessfully built def456";
574        assert!(compress_output("docker build .", build_output).is_some());
575    }
576
577    #[test]
578    fn routes_mypy_commands() {
579        let output = "src/main.py:10: error: Missing return  [return]\nFound 1 error in 1 file (checked 3 source files)";
580        assert!(compress_output("mypy .", output).is_some());
581        assert!(compress_output("python -m mypy src/", output).is_some());
582    }
583
584    #[test]
585    fn routes_pytest_commands() {
586        let output = "===== test session starts =====\ncollected 5 items\ntest_main.py ..... [100%]\n===== 5 passed in 0.5s =====";
587        assert!(compress_output("pytest", output).is_some());
588        assert!(compress_output("python -m pytest tests/", output).is_some());
589    }
590
591    #[test]
592    fn routes_data_domain() {
593        let dbt =
594            "20:14:02  Found 12 models\n20:14:20  Done. PASS=11 WARN=0 ERROR=1 SKIP=0 TOTAL=12";
595        assert!(
596            compress_output("dbt run", dbt).is_some(),
597            "dbt routed+compressible"
598        );
599        let alembic = "INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.\nINFO  [alembic.runtime.migration] Running upgrade  -> a1b2c3, create users table\nINFO  [alembic.runtime.migration] Running upgrade a1b2c3 -> d4e5f6, add email index";
600        assert!(compress_output("alembic upgrade head", alembic).is_some());
601        let flyway = "Flyway Community Edition 9.22.0 by Redgate\nDatabase: jdbc:postgresql://localhost/db\nMigrating schema \"public\" to version \"5 - add orders\"\nSuccessfully applied 1 migration to schema \"public\", now at version v5";
602        assert!(compress_output("flyway migrate", flyway).is_some());
603        let spark = "23/01/01 12:00:00 INFO SparkContext: Running Spark version 3.4.0\n23/01/01 12:00:01 INFO ResourceUtils: none configured\n23/01/01 12:00:10 INFO DAGScheduler: Job 0 finished: collect, took 5.1 s\n23/01/01 12:00:15 ERROR Executor: Exception in task 0.0";
604        assert!(compress_output("spark-submit app.py", spark).is_some());
605    }
606
607    #[test]
608    fn routes_ai_domain() {
609        let ollama = "NAME              ID              SIZE      MODIFIED\nllama3.2:latest   a80c4f17acd5    2.0 GB    3 days ago\nqwen2.5-coder:7b  2b0496514337    4.7 GB    2 weeks ago";
610        assert!(compress_output("ollama list", ollama).is_some());
611        let mlflow = "2024/01/01 12:00:01 INFO mlflow.projects.backend.local: === Running command 'python train.py' ===\nCollecting numpy==1.26.0\nDownloading numpy-1.26.0.whl (18.2 MB)\n2024/01/01 12:00:30 INFO mlflow.projects: === Run (ID 'abc123def456') succeeded ===";
612        assert!(compress_output("mlflow run .", mlflow).is_some());
613    }
614
615    #[test]
616    fn routes_security_domain() {
617        let trivy = "2024-01-01T12:00:00.000Z\tINFO\tscanning\nnginx:latest (debian 12.1)\n=====\nTotal: 45 (LOW: 20, HIGH: 8, CRITICAL: 2)";
618        assert!(compress_output("trivy image nginx", trivy).is_some());
619        let grype = "NAME       INSTALLED  FIXED-IN  TYPE  VULNERABILITY   SEVERITY\nlibssl1.1  1.1.1n     1.1.1w    deb   CVE-2023-1234   Critical\nzlib1g     1.2.11     1.2.13    deb   CVE-2022-5678   High";
620        assert!(compress_output("grype nginx", grype).is_some());
621        let syft = "NAME       VERSION    TYPE\nadduser    3.118      deb\napt        2.6.1      deb\nlodash     4.17.21    npm";
622        assert!(compress_output("syft nginx", syft).is_some());
623        let semgrep = "Scanning 120 files.\n  src/app.py\n     python.security.dangerous-subprocess-use\n        Detected subprocess.\n         42┆ subprocess.call(x)\nRan 450 rules on 120 files: 1 findings.";
624        assert!(compress_output("semgrep scan", semgrep).is_some());
625        let swiftlint = "Linting Swift files in current working directory\nLinting 'A.swift' (1/3)\nLinting 'B.swift' (2/3)\nLinting 'C.swift' (3/3)\n/path/A.swift:10:5: warning: Line Length Violation: Line should be 120 chars or less (line_length)\n/path/A.swift:22:1: warning: Trailing Whitespace Violation: no trailing whitespace (trailing_whitespace)\n/path/B.swift:5:1: error: Force Cast Violation: avoid force casts (force_cast)\n/path/C.swift:8:3: warning: Todo Violation: resolve TODOs (todo)\nDone linting! Found 4 violations, 1 serious in 3 files.";
626        assert!(compress_output("swiftlint", swiftlint).is_some());
627    }
628
629    #[test]
630    fn routes_vcs_toolchain_domain() {
631        let jj = "@  qpvuntsm user@host.com 2024-01-01 12:00:00 1234abcd\n│  add feature x\n○  zzzzmmmm user@host.com 2024-01-01 11:00:00 main 5678efab\n│  initial commit\n~";
632        assert!(compress_output("jj log", jj).is_some());
633        let mise = "node    20.10.0  ~/.config/mise/config.toml\npython  3.12.0   ~/.tool-versions\nrust    1.75.0   ~/.config/mise/config.toml";
634        assert!(compress_output("mise ls", mise).is_some());
635        let buf_lines: Vec<String> = (0..30)
636            .map(|i| format!("proto/f{i}.proto:{i}:1:Field name should be lower_snake_case here."))
637            .collect();
638        let buf = buf_lines.join("\n");
639        assert!(compress_output("buf lint", &buf).is_some());
640        let gem = "Fetching rails-7.1.0.gem\nSuccessfully installed activesupport-7.1.0\nSuccessfully installed rails-7.1.0\nParsing documentation for rails-7.1.0\nInstalling ri documentation for rails-7.1.0\nDone installing documentation for rails after 3 seconds\n2 gems installed";
641        assert!(compress_output("gem install rails", gem).is_some());
642        let uv = "Resolved 42 packages in 120ms\nDownloading numpy (18.2MiB)\n 100%|████████| 18.2M/18.2M [00:01<00:00, 15.3MiB/s]\nDownloading pandas (12.1MiB)\n 100%|████████| 12.1M/12.1M [00:00<00:00, 14.1MiB/s]\nPrepared 5 packages in 1.2s\nInstalled 5 packages in 30ms\n + numpy==1.26.0\n + pandas==2.1.0";
643        assert!(compress_output("uv add pandas", uv).is_some());
644    }
645
646    #[test]
647    fn routes_edge_infra_domain() {
648        let pulumi = "Updating (dev):\n     Type   Name   Status\n +   pulumi:pulumi:Stack proj created\n +   aws:s3:Bucket b1 created\n +   aws:s3:Bucket b2 created\n +   aws:s3:Bucket b3 created\n +   aws:lambda:Function fn created\n\nOutputs:\n    url: \"https://x.example.com\"\n\nResources:\n    + 5 created\n    10 unchanged\n\nDuration: 35s";
649        assert!(compress_output("pulumi up", pulumi).is_some());
650        let linkerd = "kubernetes-api\n--------------\n√ can initialize the client\n√ can query the Kubernetes API\n√ is running the minimum kubectl version\n\nlinkerd-existence\n-----------------\n√ 'linkerd-config' config map exists\n× control plane pods are ready\n    some pods are not ready\n\nStatus check results are ×";
651        assert!(compress_output("linkerd check", linkerd).is_some());
652        let argocd = "Name:               argocd/myapp\nProject:            default\nSync Status:        Synced\nHealth Status:      Healthy\n\nGROUP  KIND  NAMESPACE  NAME  STATUS  HEALTH  HOOK  MESSAGE\n  Service ns s1 Synced Healthy\n  Service ns s2 Synced Healthy\n  Service ns s3 Synced Healthy\napps Deployment ns d1 OutOfSync Progressing";
653        assert!(compress_output("argocd app get myapp", argocd).is_some());
654        let vercel = "Vercel CLI 33.0.0\nInstalling dependencies...\nadded 420 packages in 12s\nBuilding...\nCompiling pages\nCollecting page data\nGenerating static pages\nProduction: https://my-app.vercel.app [45s]";
655        assert!(compress_output("vercel deploy --prod", vercel).is_some());
656        let wrangler = "wrangler 3.0.0\n-------------------\nyour worker has access to the following bindings:\n- KV Namespaces:\n  - CACHE: abc123\nTotal Upload: 1.2 MiB / gzip: 0.4 MiB\nUploaded my-worker (3.5 sec)\nPublished my-worker (1.2 sec)\n  https://my-worker.example.workers.dev";
657        assert!(compress_output("wrangler deploy", wrangler).is_some());
658    }
659
660    #[test]
661    fn unknown_command_returns_none() {
662        assert!(compress_output("some-unknown-tool --version", "v1.0").is_none());
663    }
664
665    #[test]
666    fn case_insensitive_routing() {
667        let output = "On branch main\nnothing to commit";
668        assert!(compress_output("Git Status", output).is_some());
669        assert!(compress_output("GIT STATUS", output).is_some());
670    }
671
672    #[test]
673    fn routes_vp_and_vite_plus() {
674        let output = "  VITE v5.0.0  ready in 200 ms\n\n  -> Local:   http://localhost:5173/\n  -> Network: http://192.168.1.2:5173/";
675        assert!(compress_output("vp build", output).is_some());
676        assert!(compress_output("vite-plus build", output).is_some());
677    }
678
679    #[test]
680    fn routes_bunx_commands() {
681        let output = "1 pass tests\n0 fail tests\n3 skip tests\nDone 12ms\nsome extra line\nmore output here";
682        let result = compress_output("bunx test", output);
683        assert!(
684            result.is_some(),
685            "bunx should compress when output is large enough"
686        );
687        assert!(result.unwrap().contains("bun test: 1 passed"));
688    }
689
690    #[test]
691    fn routes_deno_task() {
692        let output = "Task dev deno run --allow-net server.ts\nListening on http://localhost:8000";
693        assert!(try_specific_pattern("deno task dev", output).is_some());
694    }
695
696    #[test]
697    fn routes_jest_commands() {
698        let output = "PASS  tests/main.test.js\nTest Suites: 1 passed, 1 total\nTests:       5 passed, 5 total\nTime:        2.5 s";
699        assert!(try_specific_pattern("jest", output).is_some());
700        assert!(try_specific_pattern("npx jest --coverage", output).is_some());
701    }
702
703    #[test]
704    fn routes_mocha_commands() {
705        let output = "  3 passing (50ms)\n  1 failing\n\n  1) Array #indexOf():\n     Error: expected -1 to equal 0";
706        assert!(try_specific_pattern("mocha", output).is_some());
707        assert!(try_specific_pattern("npx mocha tests/", output).is_some());
708    }
709
710    #[test]
711    fn routes_tofu_commands() {
712        let output = "Initializing the backend...\nInitializing provider plugins...\nTerraform has been successfully initialized!";
713        assert!(try_specific_pattern("tofu init", output).is_some());
714    }
715
716    #[test]
717    fn routes_ps_commands() {
718        let mut lines = vec!["USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND".to_string()];
719        for i in 0..20 {
720            lines.push(format!("user {i} 0.0 0.1 1234 123 ? S 10:00 0:00 proc_{i}"));
721        }
722        let output = lines.join("\n");
723        assert!(try_specific_pattern("ps aux", &output).is_some());
724    }
725
726    #[test]
727    fn routes_ping_commands() {
728        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";
729        assert!(try_specific_pattern("ping -c 3 google.com", output).is_some());
730    }
731
732    #[test]
733    fn routes_jq_to_json_schema() {
734        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}]}";
735        assert!(try_specific_pattern("jq '.items' data.json", output).is_some());
736    }
737
738    #[test]
739    fn routes_linting_tools() {
740        let lint_output = "src/main.py:10: error: Missing return\nsrc/main.py:20: error: Unused var\nFound 2 errors";
741        assert!(try_specific_pattern("hadolint Dockerfile", lint_output).is_some());
742        assert!(try_specific_pattern("oxlint src/", lint_output).is_some());
743        assert!(try_specific_pattern("pyright src/", lint_output).is_some());
744        assert!(try_specific_pattern("basedpyright src/", lint_output).is_some());
745    }
746
747    #[test]
748    fn routes_fd_commands() {
749        let output = "src/main.rs\nsrc/lib.rs\nsrc/util/helpers.rs\nsrc/util/math.rs\ntests/integration.rs\n";
750        assert!(try_specific_pattern("fd --extension rs", output).is_some());
751        assert!(try_specific_pattern("fdfind .rs", output).is_some());
752    }
753
754    #[test]
755    fn routes_just_commands() {
756        let output = "Available recipes:\n    build\n    test\n    lint\n";
757        assert!(try_specific_pattern("just --list", output).is_some());
758        assert!(try_specific_pattern("just build", output).is_some());
759    }
760
761    #[test]
762    fn routes_ninja_commands() {
763        let output = "[1/10] Compiling foo.c\n[10/10] Linking app\n";
764        assert!(try_specific_pattern("ninja", output).is_some());
765        assert!(try_specific_pattern("ninja -j4", output).is_some());
766    }
767
768    #[test]
769    fn routes_clang_commands() {
770        let output =
771            "src/main.c:10:5: error: use of undeclared identifier 'foo'\n1 error generated.\n";
772        assert!(try_specific_pattern("clang src/main.c", output).is_some());
773        assert!(try_specific_pattern("clang++ -std=c++17 main.cpp", output).is_some());
774    }
775
776    #[test]
777    fn routes_cargo_run() {
778        let output = "   Compiling foo v0.1.0\n    Finished `dev` profile\nHello, world!";
779        assert!(try_specific_pattern("cargo run", output).is_some());
780    }
781
782    #[test]
783    fn routes_cargo_bench() {
784        let output = "   Compiling foo v0.1.0\ntest bench_parse ... bench: 1234 ns/iter";
785        assert!(try_specific_pattern("cargo bench", output).is_some());
786    }
787
788    #[test]
789    fn routes_build_tools() {
790        let build_output = "   Compiling foo v0.1.0\n    Finished release [optimized]";
791        assert!(try_specific_pattern("gcc -o main main.c", build_output).is_some());
792        assert!(try_specific_pattern("g++ -o main main.cpp", build_output).is_some());
793    }
794
795    #[test]
796    fn routes_monorepo_tools() {
797        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";
798        assert!(try_specific_pattern("turbo install", output).is_some());
799        assert!(try_specific_pattern("nx install", output).is_some());
800    }
801
802    #[test]
803    fn gh_api_passthrough_never_compresses() {
804        let huge = "line\n".repeat(5000);
805        assert!(
806            compress_output("gh api repos/owner/repo/actions/jobs/123/logs", &huge).is_none(),
807            "gh api must never be compressed, even for large output"
808        );
809        assert!(compress_output("gh api repos/owner/repo/actions/runs/123/logs", &huge).is_none());
810    }
811
812    #[test]
813    fn gh_log_flags_passthrough() {
814        let huge = "line\n".repeat(5000);
815        assert!(compress_output("gh run view 123 --log-failed", &huge).is_none());
816        assert!(compress_output("gh run view 123 --log", &huge).is_none());
817    }
818
819    #[test]
820    fn gh_structured_commands_still_compress() {
821        let output = "On branch main\nnothing to commit";
822        assert!(try_specific_pattern("gh pr list", output).is_some());
823        assert!(try_specific_pattern("gh run list", output).is_some());
824    }
825}