lean_ctx/shell/compress/
classification.rs1use super::passthrough::{BUILTIN_PASSTHROUGH, DEV_SCRIPT_KEYWORDS, SCRIPT_RUNNER_PREFIXES};
2
3fn is_dev_script_runner(cmd: &str) -> bool {
4 for prefix in SCRIPT_RUNNER_PREFIXES {
5 if let Some(rest) = cmd.strip_prefix(prefix) {
6 let script_name = rest.split_whitespace().next().unwrap_or("");
7 for kw in DEV_SCRIPT_KEYWORDS {
8 if script_name.contains(kw) {
9 return true;
10 }
11 }
12 }
13 }
14 false
15}
16
17pub(in crate::shell) fn is_excluded_command(command: &str, excluded: &[String]) -> bool {
18 let cmd = command.trim().to_lowercase();
19 for pattern in BUILTIN_PASSTHROUGH {
20 if pattern.starts_with("--") {
21 if cmd.contains(pattern) {
22 return true;
23 }
24 } else if pattern.ends_with(' ') || pattern.ends_with('\t') {
25 if cmd == pattern.trim() || cmd.starts_with(pattern) {
26 return true;
27 }
28 } else if cmd == *pattern
29 || cmd.starts_with(&format!("{pattern} "))
30 || cmd.starts_with(&format!("{pattern}\t"))
31 || cmd.contains(&format!(" {pattern} "))
32 || cmd.contains(&format!(" {pattern}\t"))
33 || cmd.contains(&format!("|{pattern} "))
34 || cmd.contains(&format!("|{pattern}\t"))
35 || cmd.ends_with(&format!(" {pattern}"))
36 || cmd.ends_with(&format!("|{pattern}"))
37 {
38 return true;
39 }
40 }
41
42 if is_dev_script_runner(&cmd) {
43 return true;
44 }
45
46 if excluded.is_empty() {
47 return false;
48 }
49 excluded.iter().any(|excl| {
50 let excl_lower = excl.trim().to_lowercase();
51 cmd == excl_lower || cmd.starts_with(&format!("{excl_lower} "))
52 })
53}
54
55pub(super) fn is_search_output(command: &str) -> bool {
56 let c = command.trim_start();
57 c.starts_with("grep ")
58 || c.starts_with("rg ")
59 || c.starts_with("find ")
60 || c.starts_with("fd ")
61 || c.starts_with("ag ")
62 || c.starts_with("ack ")
63}
64
65pub fn has_structural_output(command: &str) -> bool {
71 if is_verbatim_output(command) {
72 return true;
73 }
74 if is_standalone_diff_command(command) {
75 return true;
76 }
77 is_structural_git_command(command)
78}
79
80pub fn is_verbatim_output(command: &str) -> bool {
84 is_verbatim_single(command) || is_verbatim_pipe_tail(command)
85}
86
87fn is_verbatim_single(command: &str) -> bool {
88 is_http_client(command)
89 || is_file_viewer(command)
90 || is_data_format_tool(command)
91 || is_binary_viewer(command)
92 || is_infra_inspection(command)
93 || is_crypto_command(command)
94 || is_database_query(command)
95 || is_dns_network_inspection(command)
96 || is_language_one_liner(command)
97 || is_container_listing(command)
98 || is_file_listing(command)
99 || is_system_query(command)
100 || is_cloud_cli_query(command)
101 || is_cli_api_data_command(command)
102 || is_package_manager_info(command)
103 || is_version_or_help(command)
104 || is_config_viewer(command)
105 || is_log_viewer(command)
106 || is_archive_listing(command)
107 || is_clipboard_tool(command)
108 || is_git_data_command(command)
109 || is_git_write_command(command)
110 || is_task_dry_run(command)
111 || is_env_dump(command)
112}
113
114fn is_cli_api_data_command(command: &str) -> bool {
117 let cl = command.trim().to_ascii_lowercase();
118
119 if cl.starts_with("gh ")
121 && (cl.starts_with("gh api ")
122 || cl.starts_with("gh api\t")
123 || cl.contains(" --json")
124 || cl.contains(" --jq ")
125 || cl.contains(" --template ")
126 || (cl.contains("run view") && (cl.contains("--log") || cl.contains("log-failed")))
127 || cl.starts_with("gh search ")
128 || cl.starts_with("gh release view")
129 || cl.starts_with("gh gist view")
130 || cl.starts_with("gh gist list"))
131 {
132 return true;
133 }
134
135 if cl.starts_with("glab ") && cl.starts_with("glab api ") {
137 return true;
138 }
139
140 if cl.starts_with("jira ") && (cl.contains(" view") || cl.contains(" list")) {
142 return true;
143 }
144
145 if cl.starts_with("linear ") {
147 return true;
148 }
149
150 let first = first_binary(command);
152 if matches!(
153 first,
154 "stripe" | "twilio" | "vercel" | "netlify" | "flyctl" | "fly" | "railway" | "supabase"
155 ) && (cl.contains(" list")
156 || cl.contains(" get")
157 || cl.contains(" show")
158 || cl.contains(" status")
159 || cl.contains(" info")
160 || cl.contains(" logs")
161 || cl.contains(" inspect")
162 || cl.contains(" export")
163 || cl.contains(" describe"))
164 {
165 return true;
166 }
167
168 if cl.starts_with("wrangler ")
170 && !cl.starts_with("wrangler dev")
171 && (cl.contains(" tail") || cl.contains(" secret list") || cl.contains(" kv "))
172 {
173 return true;
174 }
175
176 if cl.starts_with("heroku ")
178 && (cl.contains(" config")
179 || cl.contains(" logs")
180 || cl.contains(" ps")
181 || cl.contains(" info"))
182 {
183 return true;
184 }
185
186 false
187}
188
189fn is_verbatim_pipe_tail(command: &str) -> bool {
192 if !command.contains('|') {
193 return false;
194 }
195 let last_segment = command.rsplit('|').next().unwrap_or("").trim();
196 if last_segment.is_empty() {
197 return false;
198 }
199 is_verbatim_single(last_segment)
200}
201
202fn is_http_client(command: &str) -> bool {
203 let first = first_binary(command);
204 matches!(
205 first,
206 "curl" | "wget" | "http" | "https" | "xh" | "curlie" | "grpcurl" | "grpc_cli"
207 )
208}
209
210fn is_file_viewer(command: &str) -> bool {
211 let first = first_binary(command);
212 match first {
213 "cat" | "bat" | "batcat" | "pygmentize" | "highlight" => true,
214 "head" | "tail" => !command.contains("-f") && !command.contains("--follow"),
215 "sed" | "awk" | "gawk" | "mawk" | "nawk" => !has_in_place_flag(command),
222 _ => false,
223 }
224}
225
226fn has_in_place_flag(command: &str) -> bool {
233 command.split_whitespace().any(|tok| {
234 if let Some(long) = tok.strip_prefix("--") {
235 return long.starts_with("in-place");
236 }
237 match tok.strip_prefix('-') {
238 Some(rest) if !rest.is_empty() => rest
241 .split('.')
242 .next()
243 .is_some_and(|cluster| cluster.contains('i')),
244 _ => false,
245 }
246 })
247}
248
249fn is_data_format_tool(command: &str) -> bool {
250 let first = first_binary(command);
251 matches!(
252 first,
253 "jq" | "yq"
254 | "xq"
255 | "fx"
256 | "gron"
257 | "mlr"
258 | "miller"
259 | "dasel"
260 | "csvlook"
261 | "csvcut"
262 | "csvgrep"
263 | "csvjson"
264 | "in2csv"
265 | "sql2csv"
266 )
267}
268
269fn is_binary_viewer(command: &str) -> bool {
270 let first = first_binary(command);
271 matches!(first, "xxd" | "hexdump" | "od" | "strings" | "file")
272}
273
274fn is_infra_inspection(command: &str) -> bool {
275 let cl = command.trim().to_ascii_lowercase();
276 if cl.starts_with("terraform output")
277 || cl.starts_with("terraform show")
278 || cl.starts_with("terraform state show")
279 || cl.starts_with("terraform state list")
280 || cl.starts_with("terraform state pull")
281 || cl.starts_with("tofu output")
282 || cl.starts_with("tofu show")
283 || cl.starts_with("tofu state show")
284 || cl.starts_with("tofu state list")
285 || cl.starts_with("tofu state pull")
286 || cl.starts_with("pulumi stack output")
287 || cl.starts_with("pulumi stack export")
288 {
289 return true;
290 }
291 if cl.starts_with("docker inspect") || cl.starts_with("podman inspect") {
292 return true;
293 }
294 if (cl.starts_with("kubectl get") || cl.starts_with("k get"))
295 && (cl.contains("-o yaml")
296 || cl.contains("-o json")
297 || cl.contains("-oyaml")
298 || cl.contains("-ojson")
299 || cl.contains("--output yaml")
300 || cl.contains("--output json")
301 || cl.contains("--output=yaml")
302 || cl.contains("--output=json"))
303 {
304 return true;
305 }
306 if cl.starts_with("kubectl describe") || cl.starts_with("k describe") {
307 return true;
308 }
309 if cl.starts_with("helm get") || cl.starts_with("helm template") {
310 return true;
311 }
312 false
313}
314
315fn is_crypto_command(command: &str) -> bool {
316 let first = first_binary(command);
317 if first == "openssl" {
318 return true;
319 }
320 matches!(first, "gpg" | "age" | "ssh-keygen" | "certutil")
321}
322
323fn is_database_query(command: &str) -> bool {
324 let cl = command.to_ascii_lowercase();
325 if cl.starts_with("psql ") && (cl.contains(" -c ") || cl.contains("--command")) {
326 return true;
327 }
328 if cl.starts_with("mysql ") && (cl.contains(" -e ") || cl.contains("--execute")) {
329 return true;
330 }
331 if cl.starts_with("mariadb ") && (cl.contains(" -e ") || cl.contains("--execute")) {
332 return true;
333 }
334 if cl.starts_with("sqlite3 ") && cl.contains('"') {
335 return true;
336 }
337 if cl.starts_with("mongosh ") && cl.contains("--eval") {
338 return true;
339 }
340 false
341}
342
343fn is_dns_network_inspection(command: &str) -> bool {
344 let first = first_binary(command);
345 matches!(
346 first,
347 "dig" | "nslookup" | "host" | "whois" | "drill" | "resolvectl"
348 )
349}
350
351fn is_language_one_liner(command: &str) -> bool {
352 let cl = command.to_ascii_lowercase();
353 (cl.starts_with("python ") || cl.starts_with("python3 "))
354 && (cl.contains(" -c ") || cl.contains(" -c\"") || cl.contains(" -c'"))
355 || (cl.starts_with("node ") && (cl.contains(" -e ") || cl.contains(" --eval")))
356 || (cl.starts_with("ruby ") && cl.contains(" -e "))
357 || (cl.starts_with("perl ") && cl.contains(" -e "))
358 || (cl.starts_with("php ") && cl.contains(" -r "))
359}
360
361fn is_container_listing(command: &str) -> bool {
362 let cl = command.trim().to_ascii_lowercase();
363 if cl.starts_with("docker ps") || cl.starts_with("docker images") {
364 return true;
365 }
366 if cl.starts_with("podman ps") || cl.starts_with("podman images") {
367 return true;
368 }
369 if cl.starts_with("helm list") || cl.starts_with("helm ls") {
371 return true;
372 }
373 if cl.starts_with("docker compose ps") || cl.starts_with("docker-compose ps") {
374 return true;
375 }
376 false
377}
378
379fn is_file_listing(command: &str) -> bool {
380 let first = first_binary(command);
381 matches!(
382 first,
383 "find" | "fd" | "fdfind" | "ls" | "exa" | "eza" | "lsd"
384 )
385}
386
387fn is_system_query(command: &str) -> bool {
388 let first = first_binary(command);
389 matches!(
390 first,
391 "stat"
392 | "wc"
393 | "du"
394 | "df"
395 | "free"
396 | "uname"
397 | "id"
398 | "whoami"
399 | "hostname"
400 | "uptime"
401 | "lscpu"
402 | "lsblk"
403 | "ip"
404 | "ifconfig"
405 | "route"
406 | "ss"
407 | "netstat"
408 | "base64"
409 | "sha256sum"
410 | "sha1sum"
411 | "md5sum"
412 | "cksum"
413 | "readlink"
414 | "realpath"
415 | "which"
416 | "type"
417 | "command"
418 )
419}
420
421fn is_cloud_cli_query(command: &str) -> bool {
422 let cl = command.trim().to_ascii_lowercase();
423 let cloud_query_verbs = [
424 "describe",
425 "get",
426 "list",
427 "show",
428 "export",
429 "inspect",
430 "info",
431 "status",
432 "whoami",
433 "caller-identity",
434 "account",
435 ];
436
437 let is_aws = cl.starts_with("aws ") && !cl.starts_with("aws configure");
438 let is_gcloud =
439 cl.starts_with("gcloud ") && !cl.starts_with("gcloud auth") && !cl.contains(" deploy");
440 let is_az = cl.starts_with("az ") && !cl.starts_with("az login");
441
442 if !(is_aws || is_gcloud || is_az) {
443 return false;
444 }
445
446 cloud_query_verbs
447 .iter()
448 .any(|verb| cl.contains(&format!(" {verb}")))
449}
450
451fn is_package_manager_info(command: &str) -> bool {
452 let cl = command.trim().to_ascii_lowercase();
453
454 if cl.starts_with("npm ") {
455 return cl.starts_with("npm list")
456 || cl.starts_with("npm ls")
457 || cl.starts_with("npm info")
458 || cl.starts_with("npm view")
459 || cl.starts_with("npm show")
460 || cl.starts_with("npm outdated")
461 || cl.starts_with("npm audit");
462 }
463 if cl.starts_with("yarn ") {
464 return cl.starts_with("yarn list")
465 || cl.starts_with("yarn info")
466 || cl.starts_with("yarn why")
467 || cl.starts_with("yarn outdated")
468 || cl.starts_with("yarn audit");
469 }
470 if cl.starts_with("pnpm ") {
471 return cl.starts_with("pnpm list")
472 || cl.starts_with("pnpm ls")
473 || cl.starts_with("pnpm why")
474 || cl.starts_with("pnpm outdated")
475 || cl.starts_with("pnpm audit");
476 }
477 if cl.starts_with("pip ") || cl.starts_with("pip3 ") {
478 return cl.contains(" list") || cl.contains(" show") || cl.contains(" freeze");
479 }
480 if cl.starts_with("gem ") {
481 return cl.starts_with("gem list")
482 || cl.starts_with("gem info")
483 || cl.starts_with("gem specification");
484 }
485 if cl.starts_with("cargo ") {
486 return cl.starts_with("cargo metadata")
487 || cl.starts_with("cargo tree")
488 || cl.starts_with("cargo pkgid");
489 }
490 if cl.starts_with("go ") {
491 return cl.starts_with("go list") || cl.starts_with("go version");
492 }
493 if cl.starts_with("composer ") {
494 return cl.starts_with("composer show")
495 || cl.starts_with("composer info")
496 || cl.starts_with("composer outdated");
497 }
498 if cl.starts_with("brew ") {
499 return cl.starts_with("brew list")
500 || cl.starts_with("brew info")
501 || cl.starts_with("brew deps")
502 || cl.starts_with("brew outdated");
503 }
504 if cl.starts_with("apt ") || cl.starts_with("dpkg ") {
505 return cl.starts_with("apt list")
506 || cl.starts_with("apt show")
507 || cl.starts_with("dpkg -l")
508 || cl.starts_with("dpkg --list")
509 || cl.starts_with("dpkg -s");
510 }
511 false
512}
513
514fn is_version_or_help(command: &str) -> bool {
515 let parts: Vec<&str> = command.split_whitespace().collect();
516 if parts.len() < 2 || parts.len() > 3 {
517 return false;
518 }
519 parts.iter().any(|p| {
520 *p == "--version"
521 || *p == "-V"
522 || p.eq_ignore_ascii_case("version")
523 || *p == "--help"
524 || *p == "-h"
525 || p.eq_ignore_ascii_case("help")
526 })
527}
528
529fn is_config_viewer(command: &str) -> bool {
530 let cl = command.trim().to_ascii_lowercase();
531 if cl.starts_with("git config") && !cl.contains("--set") && !cl.contains("--unset") {
532 return true;
533 }
534 if cl.starts_with("npm config list") || cl.starts_with("npm config get") {
535 return true;
536 }
537 if cl.starts_with("yarn config") && !cl.contains(" set") {
538 return true;
539 }
540 if cl.starts_with("pip config list") || cl.starts_with("pip3 config list") {
541 return true;
542 }
543 if cl.starts_with("rustup show") || cl.starts_with("rustup target list") {
544 return true;
545 }
546 if cl.starts_with("docker context ls") || cl.starts_with("docker context list") {
547 return true;
548 }
549 if cl.starts_with("kubectl config")
550 && (cl.contains("view") || cl.contains("get-contexts") || cl.contains("current-context"))
551 {
552 return true;
553 }
554 false
555}
556
557fn is_log_viewer(command: &str) -> bool {
558 let cl = command.trim().to_ascii_lowercase();
559 if cl.starts_with("journalctl") && !cl.contains("-f") && !cl.contains("--follow") {
560 return true;
561 }
562 if cl.starts_with("dmesg") && !cl.contains("-w") && !cl.contains("--follow") {
563 return true;
564 }
565 if cl.starts_with("docker logs") && !cl.contains("-f") && !cl.contains("--follow") {
566 return true;
567 }
568 if cl.starts_with("kubectl logs") && !cl.contains("-f") && !cl.contains("--follow") {
569 return true;
570 }
571 if cl.starts_with("docker compose logs") && !cl.contains("-f") && !cl.contains("--follow") {
572 return true;
573 }
574 false
575}
576
577fn is_archive_listing(command: &str) -> bool {
578 let cl = command.trim().to_ascii_lowercase();
579 if cl.starts_with("tar ") && (cl.contains(" -tf") || cl.contains(" -t") || cl.contains(" tf")) {
580 return true;
581 }
582 if cl.starts_with("unzip -l") || cl.starts_with("unzip -Z") {
583 return true;
584 }
585 let first = first_binary(command);
586 matches!(first, "zipinfo" | "lsar" | "7z" if cl.contains(" l ") || cl.contains(" l\t"))
587 || first == "zipinfo"
588 || first == "lsar"
589}
590
591fn is_clipboard_tool(command: &str) -> bool {
592 let first = first_binary(command);
593 if matches!(first, "pbpaste" | "wl-paste") {
594 return true;
595 }
596 let cl = command.trim().to_ascii_lowercase();
597 if cl.starts_with("xclip") && cl.contains("-o") {
598 return true;
599 }
600 if cl.starts_with("xsel") && (cl.contains("-o") || cl.contains("--output")) {
601 return true;
602 }
603 false
604}
605
606fn is_git_write_command(command: &str) -> bool {
610 let cl = command.trim().to_ascii_lowercase();
611 if !cl.starts_with("git ") {
612 return false;
613 }
614 let git_write_subs = [
615 "commit",
616 "push",
617 "pull",
618 "merge",
619 "rebase",
620 "cherry-pick",
621 "tag",
622 "reset",
623 ];
624 let mut skip_next = false;
625 for arg in cl.split_whitespace().skip(1) {
626 if skip_next {
627 skip_next = false;
628 continue;
629 }
630 if arg == "-c" || arg == "-C" || arg == "--git-dir" || arg == "--work-tree" {
631 skip_next = true;
632 continue;
633 }
634 if arg.starts_with('-') {
635 continue;
636 }
637 return git_write_subs.contains(&arg);
638 }
639 false
640}
641
642pub(super) fn is_git_data_command(command: &str) -> bool {
643 let cl = command.trim().to_ascii_lowercase();
644 if !cl.contains("git") {
645 return false;
646 }
647 let exact_data_subs = [
648 "remote",
649 "rev-parse",
650 "rev-list",
651 "ls-files",
652 "ls-tree",
653 "ls-remote",
654 "shortlog",
655 "for-each-ref",
656 "cat-file",
657 "name-rev",
658 "describe",
659 "merge-base",
660 ];
661
662 let mut tokens = cl.split_whitespace();
663 while let Some(tok) = tokens.next() {
664 let base = tok.rsplit('/').next().unwrap_or(tok);
665 if base != "git" {
666 continue;
667 }
668 let mut skip_next = false;
669 for arg in tokens.by_ref() {
670 if skip_next {
671 skip_next = false;
672 continue;
673 }
674 if arg == "-c" || arg == "-C" || arg == "--git-dir" || arg == "--work-tree" {
675 skip_next = true;
676 continue;
677 }
678 if arg.starts_with('-') {
679 continue;
680 }
681 return exact_data_subs.contains(&arg);
682 }
683 return false;
684 }
685 false
686}
687
688fn is_task_dry_run(command: &str) -> bool {
689 let cl = command.trim().to_ascii_lowercase();
690 if cl.starts_with("make ") && (cl.contains(" -n") || cl.contains(" --dry-run")) {
691 return true;
692 }
693 if cl.starts_with("ansible") && (cl.contains("--check") || cl.contains("--diff")) {
694 return true;
695 }
696 false
697}
698
699fn is_env_dump(command: &str) -> bool {
700 let first = first_binary(command);
701 matches!(first, "env" | "printenv" | "set" | "export" | "locale")
702}
703
704fn first_binary(command: &str) -> &str {
706 let first = command.split_whitespace().next().unwrap_or("");
707 first.rsplit('/').next().unwrap_or(first)
708}
709
710fn is_standalone_diff_command(command: &str) -> bool {
712 let first = command.split_whitespace().next().unwrap_or("");
713 let base = first.rsplit('/').next().unwrap_or(first);
714 base.eq_ignore_ascii_case("diff")
715 || base.eq_ignore_ascii_case("colordiff")
716 || base.eq_ignore_ascii_case("icdiff")
717 || base.eq_ignore_ascii_case("delta")
718}
719
720fn is_structural_git_command(command: &str) -> bool {
722 let mut tokens = command.split_whitespace();
723 while let Some(tok) = tokens.next() {
724 let base = tok.rsplit('/').next().unwrap_or(tok);
725 if !base.eq_ignore_ascii_case("git") {
726 continue;
727 }
728 let mut skip_next = false;
729 let remaining: Vec<&str> = tokens.collect();
730 for arg in &remaining {
731 if skip_next {
732 skip_next = false;
733 continue;
734 }
735 if *arg == "-C" || *arg == "-c" || *arg == "--git-dir" || *arg == "--work-tree" {
736 skip_next = true;
737 continue;
738 }
739 if arg.starts_with('-') {
740 continue;
741 }
742 let sub = arg.to_ascii_lowercase();
743 return match sub.as_str() {
744 "diff" | "show" | "blame" => true,
745 "log" => has_patch_flag(&remaining) || has_stat_flag(&remaining),
746 "stash" => remaining.iter().any(|a| a.eq_ignore_ascii_case("show")),
747 _ => false,
748 };
749 }
750 return false;
751 }
752 false
753}
754
755fn has_patch_flag(args: &[&str]) -> bool {
757 args.iter()
758 .any(|a| *a == "-p" || *a == "--patch" || a.starts_with("-p"))
759}
760
761fn has_stat_flag(args: &[&str]) -> bool {
763 args.iter()
764 .any(|a| *a == "--stat" || a.starts_with("--stat="))
765}
766
767enum ToonHeader {
768 Tabular,
770 LengthArray,
772}
773
774fn toon_header_kind(line: &str) -> Option<ToonHeader> {
780 let open = line.find('[')?;
781 if open == 0 || line[..open].contains(char::is_whitespace) {
782 return None;
783 }
784 let after = &line[open + 1..];
785 let close = after.find(']')?;
786 let len_part = &after[..close];
787 if len_part.is_empty() || !len_part.bytes().all(|b| b.is_ascii_digit()) {
788 return None;
789 }
790 let rest = after[close + 1..].trim_start();
791 if rest.starts_with('{') && rest.contains('}') && rest.trim_end().ends_with(':') {
792 return Some(ToonHeader::Tabular);
793 }
794 if rest.starts_with(':') {
795 return Some(ToonHeader::LengthArray);
796 }
797 None
798}
799
800pub(super) fn looks_like_toon(output: &str) -> bool {
810 let mut tabular_headers = 0usize;
811 let mut length_arrays = 0usize;
812 let mut indented = 0usize;
813 let mut non_empty = 0usize;
814
815 for raw in output.lines() {
816 let trimmed_end = raw.trim_end();
817 let body = trimmed_end.trim_start();
818 if body.is_empty() {
819 continue;
820 }
821 non_empty += 1;
822 if body.len() != trimmed_end.len() {
823 indented += 1;
824 }
825 match toon_header_kind(body) {
826 Some(ToonHeader::Tabular) => tabular_headers += 1,
827 Some(ToonHeader::LengthArray) => length_arrays += 1,
828 None => {}
829 }
830 }
831
832 if non_empty < 2 {
833 return false;
834 }
835 if tabular_headers > 0 {
837 return true;
838 }
839 length_arrays > 0 && indented * 2 >= non_empty
843}
844
845#[cfg(test)]
846mod toon_tests {
847 use super::looks_like_toon;
848
849 #[test]
850 fn detects_tabular_array_header() {
851 let toon = "users[2]{id,name,role}:\n 1,alice,admin\n 2,bob,user";
852 assert!(looks_like_toon(toon));
853 }
854
855 #[test]
856 fn detects_nested_tabular_header() {
857 let toon = "result:\n tasks[3]{id,status,title}:\n 1,open,First\n 2,done,Second\n 3,open,Third";
858 assert!(looks_like_toon(toon));
859 }
860
861 #[test]
862 fn detects_length_prefixed_array_with_indent() {
863 let toon = "config:\n tags[3]: alpha,beta,gamma\n ports[2]: 80,443";
864 assert!(looks_like_toon(toon));
865 }
866
867 #[test]
868 fn rejects_plain_yaml_without_toon_markers() {
869 let yaml = "name: lean-ctx\nversion: 3.7.2\nfeatures:\n - compress\n - read";
870 assert!(!looks_like_toon(yaml));
871 }
872
873 #[test]
874 fn rejects_json_payload() {
875 let json = "{\n \"id\": 1,\n \"items\": [1, 2, 3],\n \"ok\": true\n}";
876 assert!(!looks_like_toon(json));
877 }
878
879 #[test]
880 fn rejects_prose_with_bracketed_reference() {
881 let prose = "See note [1] above for details.\nAnother line of plain log output here.";
882 assert!(!looks_like_toon(prose));
883 }
884
885 #[test]
886 fn rejects_lean_ctx_footer_line() {
887 let line = "[lean-ctx: 120->40 tok, compressed]\nsome other content line";
888 assert!(!looks_like_toon(line));
889 }
890
891 #[test]
892 fn rejects_single_line() {
893 assert!(!looks_like_toon("users[2]{id,name}:"));
894 }
895}