1use std::collections::BTreeMap;
22use std::path::PathBuf;
23
24use clap::{Args, Parser, Subcommand, ValueEnum};
25use serde::{Deserialize, Serialize};
26pub use tokmd_tool_schema::ToolSchemaFormat;
27pub use tokmd_types::{
28 AnalysisFormat, ChildIncludeMode, ChildrenMode, ConfigMode, ExportFormat, RedactMode,
29 TableFormat,
30};
31
32#[derive(Parser, Debug)]
40#[command(name = "tokmd", version, long_about = None)]
41pub struct Cli {
42 #[command(flatten)]
43 pub global: GlobalArgs,
44
45 #[command(flatten)]
47 pub lang: CliLangArgs,
48
49 #[command(subcommand)]
50 pub command: Option<Commands>,
51
52 #[arg(long, visible_alias = "view", global = true)]
54 pub profile: Option<String>,
55}
56
57#[derive(Args, Debug, Clone, Default)]
58pub struct GlobalArgs {
59 #[arg(
65 long = "exclude",
66 visible_alias = "ignore",
67 value_name = "PATTERN",
68 global = true
69 )]
70 pub excluded: Vec<String>,
71
72 #[arg(long, value_enum, value_name = "MODE", default_value_t = ConfigMode::Auto)]
74 pub config: ConfigMode,
75
76 #[arg(long)]
78 pub hidden: bool,
79
80 #[arg(long)]
84 pub no_ignore: bool,
85
86 #[arg(long)]
88 pub no_ignore_parent: bool,
89
90 #[arg(long)]
92 pub no_ignore_dot: bool,
93
94 #[arg(long, visible_alias = "no-ignore-git")]
96 pub no_ignore_vcs: bool,
97
98 #[arg(long)]
100 pub treat_doc_strings_as_comments: bool,
101
102 #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
104 pub verbose: u8,
105
106 #[arg(long, global = true)]
108 pub no_progress: bool,
109}
110
111#[derive(Subcommand, Debug, Clone)]
112pub enum Commands {
113 Lang(CliLangArgs),
115
116 Module(CliModuleArgs),
118
119 Export(CliExportArgs),
121
122 Analyze(CliAnalyzeArgs),
124
125 Badge(BadgeArgs),
127
128 Init(InitArgs),
130
131 Completions(CompletionsArgs),
133
134 Run(RunArgs),
136
137 Diff(DiffArgs),
139
140 Context(CliContextArgs),
142
143 CheckIgnore(CliCheckIgnoreArgs),
145
146 Tools(ToolsArgs),
148
149 Gate(CliGateArgs),
151
152 Cockpit(CockpitArgs),
154
155 Baseline(BaselineArgs),
157
158 Handoff(HandoffArgs),
160
161 Sensor(SensorArgs),
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, Default)]
166pub struct UserConfig {
167 pub profiles: BTreeMap<String, Profile>,
168 pub repos: BTreeMap<String, String>, }
170
171#[derive(Debug, Clone, Serialize, Deserialize, Default)]
172pub struct Profile {
173 pub format: Option<String>, pub top: Option<usize>,
176
177 pub files: Option<bool>,
179
180 pub module_roots: Option<Vec<String>>,
182 pub module_depth: Option<usize>,
183 pub min_code: Option<usize>,
184 pub max_rows: Option<usize>,
185 pub redact: Option<RedactMode>,
186 pub meta: Option<bool>,
187
188 pub children: Option<String>,
190}
191
192#[derive(Args, Debug, Clone)]
193pub struct RunArgs {
194 #[arg(value_name = "PATH", default_value = ".")]
196 pub paths: Vec<PathBuf>,
197
198 #[arg(long)]
200 pub output_dir: Option<PathBuf>,
201
202 #[arg(long)]
204 pub name: Option<String>,
205
206 #[arg(long, value_enum)]
208 pub analysis: Option<AnalysisPreset>,
209
210 #[arg(long, value_enum)]
212 pub redact: Option<RedactMode>,
213}
214
215#[derive(Args, Debug, Clone)]
216pub struct DiffArgs {
217 #[arg(long)]
219 pub from: Option<String>,
220
221 #[arg(long)]
223 pub to: Option<String>,
224
225 #[arg(value_name = "REF", num_args = 2)]
227 pub refs: Vec<String>,
228
229 #[arg(long, value_enum, default_value_t = DiffFormat::Md)]
231 pub format: DiffFormat,
232
233 #[arg(long)]
235 pub compact: bool,
236
237 #[arg(long, value_enum, default_value_t = ColorMode::Auto)]
239 pub color: ColorMode,
240}
241
242#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
243#[serde(rename_all = "kebab-case")]
244pub enum DiffFormat {
245 #[default]
247 Md,
248 Json,
250}
251
252#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
253#[serde(rename_all = "kebab-case")]
254pub enum ColorMode {
255 #[default]
257 Auto,
258 Always,
260 Never,
262}
263
264#[derive(Args, Debug, Clone)]
265pub struct CompletionsArgs {
266 #[arg(value_enum)]
268 pub shell: Shell,
269}
270
271#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
272#[serde(rename_all = "kebab-case")]
273pub enum Shell {
274 Bash,
275 Elvish,
276 Fish,
277 Powershell,
278 Zsh,
279}
280
281#[derive(Args, Debug, Clone, Default)]
282pub struct CliLangArgs {
283 #[arg(value_name = "PATH")]
285 pub paths: Option<Vec<PathBuf>>,
286
287 #[arg(long, value_enum)]
289 pub format: Option<TableFormat>,
290
291 #[arg(long)]
294 pub top: Option<usize>,
295
296 #[arg(long)]
298 pub files: bool,
299
300 #[arg(long, value_enum)]
302 pub children: Option<ChildrenMode>,
303}
304
305#[derive(Args, Debug, Clone)]
306pub struct CliModuleArgs {
307 #[arg(value_name = "PATH")]
309 pub paths: Option<Vec<PathBuf>>,
310
311 #[arg(long, value_enum)]
313 pub format: Option<TableFormat>,
314
315 #[arg(long)]
318 pub top: Option<usize>,
319
320 #[arg(long, value_delimiter = ',')]
325 pub module_roots: Option<Vec<String>>,
326
327 #[arg(long)]
333 pub module_depth: Option<usize>,
334
335 #[arg(long, value_enum)]
337 pub children: Option<ChildIncludeMode>,
338}
339
340#[derive(Args, Debug, Clone)]
341pub struct CliExportArgs {
342 #[arg(value_name = "PATH")]
344 pub paths: Option<Vec<PathBuf>>,
345
346 #[arg(long, value_enum)]
348 pub format: Option<ExportFormat>,
349
350 #[arg(long, value_name = "PATH", visible_alias = "out")]
352 pub output: Option<PathBuf>,
353
354 #[arg(long, value_delimiter = ',')]
356 pub module_roots: Option<Vec<String>>,
357
358 #[arg(long)]
360 pub module_depth: Option<usize>,
361
362 #[arg(long, value_enum)]
364 pub children: Option<ChildIncludeMode>,
365
366 #[arg(long)]
368 pub min_code: Option<usize>,
369
370 #[arg(long)]
372 pub max_rows: Option<usize>,
373
374 #[arg(long, action = clap::ArgAction::Set)]
376 pub meta: Option<bool>,
377
378 #[arg(long, value_enum)]
380 pub redact: Option<RedactMode>,
381
382 #[arg(long, value_name = "PATH")]
384 pub strip_prefix: Option<PathBuf>,
385}
386
387#[derive(Args, Debug, Clone)]
388pub struct CliAnalyzeArgs {
389 #[arg(value_name = "INPUT", default_value = ".")]
391 pub inputs: Vec<PathBuf>,
392
393 #[arg(long, value_enum)]
395 pub preset: Option<AnalysisPreset>,
396
397 #[arg(long, value_enum)]
399 pub format: Option<AnalysisFormat>,
400
401 #[arg(long)]
403 pub window: Option<usize>,
404
405 #[arg(long, action = clap::ArgAction::SetTrue, conflicts_with = "no_git")]
407 pub git: bool,
408
409 #[arg(long = "no-git", action = clap::ArgAction::SetTrue, conflicts_with = "git")]
411 pub no_git: bool,
412
413 #[arg(long)]
415 pub output_dir: Option<PathBuf>,
416
417 #[arg(long)]
419 pub max_files: Option<usize>,
420
421 #[arg(long)]
423 pub max_bytes: Option<u64>,
424
425 #[arg(long)]
427 pub max_file_bytes: Option<u64>,
428
429 #[arg(long)]
431 pub max_commits: Option<usize>,
432
433 #[arg(long)]
435 pub max_commit_files: Option<usize>,
436
437 #[arg(long, value_enum)]
439 pub granularity: Option<ImportGranularity>,
440
441 #[arg(long)]
443 pub effort_model: Option<EffortModelKind>,
444
445 #[arg(long)]
447 pub effort_layer: Option<EffortLayer>,
448
449 #[arg(long = "effort-base-ref")]
451 pub effort_base_ref: Option<String>,
452
453 #[arg(long = "effort-head-ref")]
455 pub effort_head_ref: Option<String>,
456
457 #[arg(long)]
459 pub monte_carlo: bool,
460
461 #[arg(long = "mc-iterations")]
463 pub mc_iterations: Option<usize>,
464
465 #[arg(long = "mc-seed")]
467 pub mc_seed: Option<u64>,
468
469 #[arg(long)]
471 pub detail_functions: bool,
472
473 #[arg(long)]
475 pub near_dup: bool,
476
477 #[arg(long, default_value = "0.80")]
479 pub near_dup_threshold: f64,
480
481 #[arg(long, default_value = "2000")]
483 pub near_dup_max_files: usize,
484
485 #[arg(long, value_enum)]
487 pub near_dup_scope: Option<NearDupScope>,
488
489 #[arg(long, default_value = "10000")]
491 pub near_dup_max_pairs: usize,
492
493 #[arg(long, value_name = "GLOB")]
495 pub near_dup_exclude: Vec<String>,
496
497 #[arg(long, value_name = "KEY")]
499 pub explain: Option<String>,
500}
501
502#[derive(Args, Debug, Clone)]
503pub struct BadgeArgs {
504 #[arg(value_name = "INPUT", default_value = ".")]
506 pub inputs: Vec<PathBuf>,
507
508 #[arg(long, value_enum)]
510 pub metric: BadgeMetric,
511
512 #[arg(long, value_enum)]
514 pub preset: Option<AnalysisPreset>,
515
516 #[arg(long, action = clap::ArgAction::SetTrue, conflicts_with = "no_git")]
518 pub git: bool,
519
520 #[arg(long = "no-git", action = clap::ArgAction::SetTrue, conflicts_with = "git")]
522 pub no_git: bool,
523
524 #[arg(long)]
526 pub max_commits: Option<usize>,
527
528 #[arg(long)]
530 pub max_commit_files: Option<usize>,
531
532 #[arg(long, visible_alias = "out")]
534 pub output: Option<PathBuf>,
535}
536
537#[derive(Args, Debug, Clone)]
538pub struct InitArgs {
539 #[arg(long, value_name = "DIR", default_value = ".")]
541 pub dir: PathBuf,
542
543 #[arg(long)]
545 pub force: bool,
546
547 #[arg(long)]
549 pub print: bool,
550
551 #[arg(long, value_enum, default_value_t = InitProfile::Default)]
553 pub template: InitProfile,
554
555 #[arg(long)]
557 pub non_interactive: bool,
558}
559
560#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
561#[serde(rename_all = "kebab-case")]
562pub enum AnalysisPreset {
563 Receipt,
564 Estimate,
565 Health,
566 Risk,
567 Supply,
568 Architecture,
569 Topics,
570 Security,
571 Identity,
572 Git,
573 Deep,
574 Fun,
575}
576
577#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
578#[serde(rename_all = "kebab-case")]
579pub enum ImportGranularity {
580 Module,
581 File,
582}
583
584#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
585#[serde(rename_all = "kebab-case")]
586pub enum EffortModelKind {
587 Cocomo81Basic,
588 Cocomo2Early,
589 Ensemble,
590}
591
592#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
593#[serde(rename_all = "kebab-case")]
594pub enum EffortLayer {
595 Headline,
596 Why,
597 Full,
598}
599
600#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
601#[serde(rename_all = "kebab-case")]
602pub enum BadgeMetric {
603 Lines,
604 Tokens,
605 Bytes,
606 Doc,
607 Blank,
608 Hotspot,
609}
610
611#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
612#[serde(rename_all = "kebab-case")]
613pub enum InitProfile {
614 Default,
615 Rust,
616 Node,
617 Mono,
618 Python,
619 Go,
620 Cpp,
621}
622
623#[derive(Args, Debug, Clone)]
624pub struct CliContextArgs {
625 #[arg(value_name = "PATH")]
627 pub paths: Option<Vec<PathBuf>>,
628
629 #[arg(long, default_value = "128k")]
631 pub budget: String,
632
633 #[arg(long, value_enum, default_value_t = ContextStrategy::Greedy)]
635 pub strategy: ContextStrategy,
636
637 #[arg(long, value_enum, default_value_t = ValueMetric::Code)]
639 pub rank_by: ValueMetric,
640
641 #[arg(long = "mode", value_enum, default_value_t = ContextOutput::List)]
643 pub output_mode: ContextOutput,
644
645 #[arg(long)]
647 pub compress: bool,
648
649 #[arg(long)]
651 pub no_smart_exclude: bool,
652
653 #[arg(long, value_delimiter = ',')]
655 pub module_roots: Option<Vec<String>>,
656
657 #[arg(long)]
659 pub module_depth: Option<usize>,
660
661 #[arg(long)]
663 pub git: bool,
664
665 #[arg(long = "no-git")]
667 pub no_git: bool,
668
669 #[arg(long, default_value = "1000")]
671 pub max_commits: usize,
672
673 #[arg(long, default_value = "100")]
675 pub max_commit_files: usize,
676
677 #[arg(long, value_name = "PATH", visible_alias = "out")]
679 pub output: Option<PathBuf>,
680
681 #[arg(long)]
683 pub force: bool,
684
685 #[arg(long, value_name = "DIR", conflicts_with = "output")]
687 pub bundle_dir: Option<PathBuf>,
688
689 #[arg(long, default_value = "10485760")]
691 pub max_output_bytes: u64,
692
693 #[arg(long, value_name = "PATH")]
695 pub log: Option<PathBuf>,
696
697 #[arg(long, default_value = "0.15")]
699 pub max_file_pct: f64,
700
701 #[arg(long)]
703 pub max_file_tokens: Option<usize>,
704
705 #[arg(long)]
707 pub require_git_scores: bool,
708}
709
710#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
711#[serde(rename_all = "kebab-case")]
712pub enum ContextStrategy {
713 #[default]
715 Greedy,
716 Spread,
718}
719
720#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
721#[serde(rename_all = "kebab-case")]
722pub enum ValueMetric {
723 #[default]
725 Code,
726 Tokens,
728 Churn,
730 Hotspot,
732}
733
734#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
735#[serde(rename_all = "kebab-case")]
736pub enum ContextOutput {
737 #[default]
739 List,
740 Bundle,
742 Json,
744}
745
746#[derive(Args, Debug, Clone)]
747pub struct CliCheckIgnoreArgs {
748 #[arg(value_name = "PATH", required = true)]
750 pub paths: Vec<PathBuf>,
751
752 #[arg(long, short = 'v')]
754 pub verbose: bool,
755}
756
757#[derive(Args, Debug, Clone)]
758pub struct ToolsArgs {
759 #[arg(long, value_enum, default_value_t = ToolSchemaFormat::Jsonschema)]
761 pub format: ToolSchemaFormat,
762
763 #[arg(long)]
765 pub pretty: bool,
766}
767
768#[derive(Args, Debug, Clone)]
769pub struct CliGateArgs {
770 #[arg(value_name = "INPUT")]
772 pub input: Option<PathBuf>,
773
774 #[arg(long)]
776 pub policy: Option<PathBuf>,
777
778 #[arg(long, value_name = "PATH")]
783 pub baseline: Option<PathBuf>,
784
785 #[arg(long, value_name = "PATH")]
790 pub ratchet_config: Option<PathBuf>,
791
792 #[arg(long, value_enum)]
794 pub preset: Option<AnalysisPreset>,
795
796 #[arg(long, value_enum, default_value_t = GateFormat::Text)]
798 pub format: GateFormat,
799
800 #[arg(long)]
802 pub fail_fast: bool,
803}
804
805#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
806#[serde(rename_all = "kebab-case")]
807pub enum GateFormat {
808 #[default]
810 Text,
811 Json,
813}
814
815#[derive(Args, Debug, Clone)]
816pub struct CockpitArgs {
817 #[arg(long, default_value = "main")]
819 pub base: String,
820
821 #[arg(long, default_value = "HEAD")]
823 pub head: String,
824
825 #[arg(long, value_enum, default_value_t = CockpitFormat::Json)]
827 pub format: CockpitFormat,
828
829 #[arg(long, value_name = "PATH")]
831 pub output: Option<std::path::PathBuf>,
832
833 #[arg(long, value_name = "DIR")]
835 pub artifacts_dir: Option<std::path::PathBuf>,
836
837 #[arg(long, value_name = "PATH")]
842 pub baseline: Option<std::path::PathBuf>,
843
844 #[arg(long, value_enum, default_value_t = DiffRangeMode::TwoDot)]
846 pub diff_range: DiffRangeMode,
847
848 #[arg(long)]
854 pub sensor_mode: bool,
855}
856
857#[derive(Args, Debug, Clone)]
858pub struct BaselineArgs {
859 #[arg(default_value = ".")]
861 pub path: PathBuf,
862
863 #[arg(long, default_value = ".tokmd/baseline.json")]
865 pub output: PathBuf,
866
867 #[arg(long)]
869 pub determinism: bool,
870
871 #[arg(long, short)]
873 pub force: bool,
874}
875
876#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
877#[serde(rename_all = "kebab-case")]
878pub enum CockpitFormat {
879 #[default]
881 Json,
882 Md,
884 Sections,
886}
887
888#[derive(Args, Debug, Clone)]
889pub struct HandoffArgs {
890 #[arg(value_name = "PATH")]
892 pub paths: Option<Vec<PathBuf>>,
893
894 #[arg(long, default_value = ".handoff")]
896 pub out_dir: PathBuf,
897
898 #[arg(long, default_value = "128k")]
900 pub budget: String,
901
902 #[arg(long, value_enum, default_value_t = ContextStrategy::Greedy)]
904 pub strategy: ContextStrategy,
905
906 #[arg(long, value_enum, default_value_t = ValueMetric::Hotspot)]
908 pub rank_by: ValueMetric,
909
910 #[arg(long, value_enum, default_value_t = HandoffPreset::Risk)]
912 pub preset: HandoffPreset,
913
914 #[arg(long, value_delimiter = ',')]
916 pub module_roots: Option<Vec<String>>,
917
918 #[arg(long)]
920 pub module_depth: Option<usize>,
921
922 #[arg(long)]
924 pub force: bool,
925
926 #[arg(long)]
928 pub compress: bool,
929
930 #[arg(long)]
932 pub no_smart_exclude: bool,
933
934 #[arg(long = "no-git")]
936 pub no_git: bool,
937
938 #[arg(long, default_value = "1000")]
940 pub max_commits: usize,
941
942 #[arg(long, default_value = "100")]
944 pub max_commit_files: usize,
945
946 #[arg(long, default_value = "0.15")]
948 pub max_file_pct: f64,
949
950 #[arg(long)]
952 pub max_file_tokens: Option<usize>,
953}
954
955#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
956#[serde(rename_all = "kebab-case")]
957pub enum HandoffPreset {
958 Minimal,
960 Standard,
962 #[default]
964 Risk,
965 Deep,
967}
968
969#[derive(Args, Debug, Clone, Serialize, Deserialize)]
970pub struct SensorArgs {
971 #[arg(long, default_value = "main")]
973 pub base: String,
974
975 #[arg(long, default_value = "HEAD")]
977 pub head: String,
978
979 #[arg(
981 long,
982 value_name = "PATH",
983 default_value = "artifacts/tokmd/report.json"
984 )]
985 pub output: std::path::PathBuf,
986
987 #[arg(long, value_enum, default_value_t = SensorFormat::Json)]
989 pub format: SensorFormat,
990}
991
992#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
993#[serde(rename_all = "kebab-case")]
994pub enum SensorFormat {
995 #[default]
997 Json,
998 Md,
1000}
1001
1002#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1003#[serde(rename_all = "kebab-case")]
1004pub enum NearDupScope {
1005 #[default]
1007 Module,
1008 Lang,
1010 Global,
1012}
1013
1014#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1015#[serde(rename_all = "kebab-case")]
1016pub enum DiffRangeMode {
1017 #[default]
1019 TwoDot,
1020 ThreeDot,
1022}
1023
1024pub use tokmd_settings::{
1029 AnalyzeConfig, BadgeConfig, ContextConfig, ExportConfig, GateConfig, GateRule, ModuleConfig,
1030 RatchetRuleConfig, ScanConfig, TomlConfig, TomlResult, ViewProfile,
1031};
1032
1033impl From<&GlobalArgs> for tokmd_settings::ScanOptions {
1038 fn from(g: &GlobalArgs) -> Self {
1039 Self {
1040 excluded: g.excluded.clone(),
1041 config: g.config,
1042 hidden: g.hidden,
1043 no_ignore: g.no_ignore,
1044 no_ignore_parent: g.no_ignore_parent,
1045 no_ignore_dot: g.no_ignore_dot,
1046 no_ignore_vcs: g.no_ignore_vcs,
1047 treat_doc_strings_as_comments: g.treat_doc_strings_as_comments,
1048 }
1049 }
1050}
1051
1052impl From<GlobalArgs> for tokmd_settings::ScanOptions {
1053 fn from(g: GlobalArgs) -> Self {
1054 Self::from(&g)
1055 }
1056}
1057
1058#[cfg(test)]
1059mod tests {
1060 use super::*;
1061
1062 #[test]
1064 fn user_config_default_is_empty() {
1065 let c = UserConfig::default();
1066 assert!(c.profiles.is_empty());
1067 assert!(c.repos.is_empty());
1068 }
1069
1070 #[test]
1071 fn profile_default_all_none() {
1072 let p = Profile::default();
1073 assert!(p.format.is_none());
1074 assert!(p.top.is_none());
1075 assert!(p.files.is_none());
1076 assert!(p.module_roots.is_none());
1077 assert!(p.module_depth.is_none());
1078 assert!(p.min_code.is_none());
1079 assert!(p.max_rows.is_none());
1080 assert!(p.redact.is_none());
1081 assert!(p.meta.is_none());
1082 assert!(p.children.is_none());
1083 }
1084
1085 #[test]
1086 fn global_args_default() {
1087 let g = GlobalArgs::default();
1088 assert!(g.excluded.is_empty());
1089 assert_eq!(g.config, ConfigMode::Auto);
1090 assert!(!g.hidden);
1091 assert!(!g.no_ignore);
1092 assert_eq!(g.verbose, 0);
1093 }
1094
1095 #[test]
1096 fn cli_lang_args_default() {
1097 let a = CliLangArgs::default();
1098 assert!(a.paths.is_none());
1099 assert!(a.format.is_none());
1100 assert!(a.top.is_none());
1101 assert!(!a.files);
1102 assert!(a.children.is_none());
1103 }
1104
1105 #[test]
1107 fn analysis_preset_serde_roundtrip() {
1108 for variant in [
1109 AnalysisPreset::Receipt,
1110 AnalysisPreset::Estimate,
1111 AnalysisPreset::Health,
1112 AnalysisPreset::Risk,
1113 AnalysisPreset::Supply,
1114 AnalysisPreset::Architecture,
1115 AnalysisPreset::Topics,
1116 AnalysisPreset::Security,
1117 AnalysisPreset::Identity,
1118 AnalysisPreset::Git,
1119 AnalysisPreset::Deep,
1120 AnalysisPreset::Fun,
1121 ] {
1122 let json = serde_json::to_string(&variant).unwrap();
1123 let back: AnalysisPreset = serde_json::from_str(&json).unwrap();
1124 assert_eq!(back, variant);
1125 }
1126 }
1127
1128 #[test]
1129 fn diff_format_default_is_md() {
1130 assert_eq!(DiffFormat::default(), DiffFormat::Md);
1131 }
1132
1133 #[test]
1134 fn diff_format_serde_roundtrip() {
1135 for variant in [DiffFormat::Md, DiffFormat::Json] {
1136 let json = serde_json::to_string(&variant).unwrap();
1137 let back: DiffFormat = serde_json::from_str(&json).unwrap();
1138 assert_eq!(back, variant);
1139 }
1140 }
1141
1142 #[test]
1143 fn color_mode_default_is_auto() {
1144 assert_eq!(ColorMode::default(), ColorMode::Auto);
1145 }
1146
1147 #[test]
1148 fn context_strategy_default_is_greedy() {
1149 assert_eq!(ContextStrategy::default(), ContextStrategy::Greedy);
1150 }
1151
1152 #[test]
1153 fn value_metric_default_is_code() {
1154 assert_eq!(ValueMetric::default(), ValueMetric::Code);
1155 }
1156
1157 #[test]
1158 fn context_output_default_is_list() {
1159 assert_eq!(ContextOutput::default(), ContextOutput::List);
1160 }
1161
1162 #[test]
1163 fn gate_format_default_is_text() {
1164 assert_eq!(GateFormat::default(), GateFormat::Text);
1165 }
1166
1167 #[test]
1168 fn cockpit_format_default_is_json() {
1169 assert_eq!(CockpitFormat::default(), CockpitFormat::Json);
1170 }
1171
1172 #[test]
1173 fn handoff_preset_default_is_risk() {
1174 assert_eq!(HandoffPreset::default(), HandoffPreset::Risk);
1175 }
1176
1177 #[test]
1178 fn sensor_format_default_is_json() {
1179 assert_eq!(SensorFormat::default(), SensorFormat::Json);
1180 }
1181
1182 #[test]
1183 fn near_dup_scope_default_is_module() {
1184 assert_eq!(NearDupScope::default(), NearDupScope::Module);
1185 }
1186
1187 #[test]
1188 fn diff_range_mode_default_is_two_dot() {
1189 assert_eq!(DiffRangeMode::default(), DiffRangeMode::TwoDot);
1190 }
1191
1192 #[test]
1194 fn analysis_preset_uses_kebab_case() {
1195 assert_eq!(
1196 serde_json::to_string(&AnalysisPreset::Receipt).unwrap(),
1197 "\"receipt\""
1198 );
1199 assert_eq!(
1200 serde_json::to_string(&AnalysisPreset::Deep).unwrap(),
1201 "\"deep\""
1202 );
1203 }
1204
1205 #[test]
1206 fn context_strategy_uses_kebab_case() {
1207 assert_eq!(
1208 serde_json::to_string(&ContextStrategy::Greedy).unwrap(),
1209 "\"greedy\""
1210 );
1211 assert_eq!(
1212 serde_json::to_string(&ContextStrategy::Spread).unwrap(),
1213 "\"spread\""
1214 );
1215 }
1216
1217 #[test]
1218 fn value_metric_uses_kebab_case() {
1219 assert_eq!(
1220 serde_json::to_string(&ValueMetric::Hotspot).unwrap(),
1221 "\"hotspot\""
1222 );
1223 }
1224
1225 #[test]
1227 fn user_config_serde_roundtrip() {
1228 let mut c = UserConfig::default();
1229 c.profiles.insert(
1230 "llm_safe".into(),
1231 Profile {
1232 format: Some("json".into()),
1233 top: Some(10),
1234 redact: Some(RedactMode::All),
1235 ..Profile::default()
1236 },
1237 );
1238 c.repos.insert("owner/repo".into(), "llm_safe".into());
1239
1240 let json = serde_json::to_string(&c).unwrap();
1241 let back: UserConfig = serde_json::from_str(&json).unwrap();
1242 assert_eq!(back.profiles.len(), 1);
1243 assert_eq!(back.repos.len(), 1);
1244 assert_eq!(back.profiles["llm_safe"].top, Some(10));
1245 }
1246
1247 #[test]
1249 fn global_args_to_scan_options() {
1250 let g = GlobalArgs {
1251 excluded: vec!["target".into()],
1252 config: ConfigMode::None,
1253 hidden: true,
1254 no_ignore: true,
1255 no_ignore_parent: false,
1256 no_ignore_dot: false,
1257 no_ignore_vcs: false,
1258 treat_doc_strings_as_comments: true,
1259 verbose: 0,
1260 no_progress: false,
1261 };
1262 let opts: tokmd_settings::ScanOptions = (&g).into();
1263 assert_eq!(opts.excluded, vec!["target"]);
1264 assert_eq!(opts.config, ConfigMode::None);
1265 assert!(opts.hidden);
1266 assert!(opts.no_ignore);
1267 assert!(opts.treat_doc_strings_as_comments);
1268 }
1269
1270 #[test]
1271 fn global_args_owned_to_scan_options() {
1272 let g = GlobalArgs {
1273 excluded: vec!["vendor".into()],
1274 config: ConfigMode::Auto,
1275 hidden: false,
1276 ..GlobalArgs::default()
1277 };
1278 let opts: tokmd_settings::ScanOptions = g.into();
1279 assert_eq!(opts.excluded, vec!["vendor"]);
1280 assert!(!opts.hidden);
1281 }
1282}