1use std::collections::BTreeMap;
20use std::path::Path;
21
22use serde::{Deserialize, Serialize};
23
24pub use tokmd_types::{ChildIncludeMode, ChildrenMode, ConfigMode, ExportFormat, RedactMode};
26
27#[derive(Debug, Clone, Default, Serialize, Deserialize)]
34pub struct UserConfig {
35 pub profiles: BTreeMap<String, Profile>,
36 pub repos: BTreeMap<String, String>, }
38
39#[derive(Debug, Clone, Default, Serialize, Deserialize)]
41pub struct Profile {
42 pub format: Option<String>, pub top: Option<usize>,
45
46 pub files: Option<bool>,
48
49 pub module_roots: Option<Vec<String>>,
51 pub module_depth: Option<usize>,
52 pub min_code: Option<usize>,
53 pub max_rows: Option<usize>,
54 pub redact: Option<RedactMode>,
55 pub meta: Option<bool>,
56
57 pub children: Option<String>,
59}
60
61#[derive(Debug, Clone, Default, Serialize, Deserialize)]
67pub struct ScanOptions {
68 #[serde(default)]
70 pub excluded: Vec<String>,
71
72 #[serde(default)]
74 pub config: ConfigMode,
75
76 #[serde(default)]
78 pub hidden: bool,
79
80 #[serde(default)]
82 pub no_ignore: bool,
83
84 #[serde(default)]
86 pub no_ignore_parent: bool,
87
88 #[serde(default)]
90 pub no_ignore_dot: bool,
91
92 #[serde(default)]
94 pub no_ignore_vcs: bool,
95
96 #[serde(default)]
98 pub treat_doc_strings_as_comments: bool,
99}
100
101#[derive(Debug, Clone, Default, Serialize, Deserialize)]
103pub struct ScanSettings {
104 #[serde(default)]
106 pub paths: Vec<String>,
107
108 #[serde(flatten)]
110 pub options: ScanOptions,
111}
112
113impl ScanSettings {
114 pub fn current_dir() -> Self {
116 Self {
117 paths: vec![".".to_string()],
118 ..Default::default()
119 }
120 }
121
122 pub fn for_paths(paths: Vec<String>) -> Self {
124 Self {
125 paths,
126 ..Default::default()
127 }
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct LangSettings {
134 #[serde(default)]
136 pub top: usize,
137
138 #[serde(default)]
140 pub files: bool,
141
142 #[serde(default = "default_children_mode")]
144 pub children: ChildrenMode,
145
146 #[serde(default)]
148 pub redact: Option<RedactMode>,
149}
150
151impl Default for LangSettings {
152 fn default() -> Self {
153 Self {
154 top: 0,
155 files: false,
156 children: ChildrenMode::Collapse,
157 redact: None,
158 }
159 }
160}
161
162fn default_children_mode() -> ChildrenMode {
163 ChildrenMode::Collapse
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct ModuleSettings {
169 #[serde(default)]
171 pub top: usize,
172
173 #[serde(default = "default_module_roots")]
175 pub module_roots: Vec<String>,
176
177 #[serde(default = "default_module_depth")]
179 pub module_depth: usize,
180
181 #[serde(default = "default_child_include_mode")]
183 pub children: ChildIncludeMode,
184
185 #[serde(default)]
187 pub redact: Option<RedactMode>,
188}
189
190fn default_module_roots() -> Vec<String> {
191 vec!["crates".to_string(), "packages".to_string()]
192}
193
194fn default_module_depth() -> usize {
195 2
196}
197
198fn default_child_include_mode() -> ChildIncludeMode {
199 ChildIncludeMode::Separate
200}
201
202impl Default for ModuleSettings {
203 fn default() -> Self {
204 Self {
205 top: 0,
206 module_roots: default_module_roots(),
207 module_depth: default_module_depth(),
208 children: default_child_include_mode(),
209 redact: None,
210 }
211 }
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct ExportSettings {
217 #[serde(default = "default_export_format")]
219 pub format: ExportFormat,
220
221 #[serde(default = "default_module_roots")]
223 pub module_roots: Vec<String>,
224
225 #[serde(default = "default_module_depth")]
227 pub module_depth: usize,
228
229 #[serde(default = "default_child_include_mode")]
231 pub children: ChildIncludeMode,
232
233 #[serde(default)]
235 pub min_code: usize,
236
237 #[serde(default)]
239 pub max_rows: usize,
240
241 #[serde(default = "default_redact_mode")]
243 pub redact: RedactMode,
244
245 #[serde(default = "default_meta")]
247 pub meta: bool,
248
249 #[serde(default)]
251 pub strip_prefix: Option<String>,
252}
253
254fn default_redact_mode() -> RedactMode {
255 RedactMode::None
256}
257
258fn default_export_format() -> ExportFormat {
259 ExportFormat::Jsonl
260}
261
262fn default_meta() -> bool {
263 true
264}
265
266impl Default for ExportSettings {
267 fn default() -> Self {
268 Self {
269 format: default_export_format(),
270 module_roots: default_module_roots(),
271 module_depth: default_module_depth(),
272 children: default_child_include_mode(),
273 min_code: 0,
274 max_rows: 0,
275 redact: RedactMode::None,
276 meta: true,
277 strip_prefix: None,
278 }
279 }
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct AnalyzeSettings {
285 #[serde(default = "default_preset")]
287 pub preset: String,
288
289 #[serde(default)]
291 pub window: Option<usize>,
292
293 #[serde(default)]
295 pub git: Option<bool>,
296
297 #[serde(default)]
299 pub max_files: Option<usize>,
300
301 #[serde(default)]
303 pub max_bytes: Option<u64>,
304
305 #[serde(default)]
307 pub max_file_bytes: Option<u64>,
308
309 #[serde(default)]
311 pub max_commits: Option<usize>,
312
313 #[serde(default)]
315 pub max_commit_files: Option<usize>,
316
317 #[serde(default = "default_granularity")]
319 pub granularity: String,
320
321 #[serde(default)]
323 pub effort_model: Option<String>,
324
325 #[serde(default)]
327 pub effort_layer: Option<String>,
328
329 #[serde(default)]
331 pub effort_base_ref: Option<String>,
332
333 #[serde(default)]
335 pub effort_head_ref: Option<String>,
336
337 #[serde(default)]
339 pub effort_monte_carlo: Option<bool>,
340
341 #[serde(default)]
343 pub effort_mc_iterations: Option<usize>,
344
345 #[serde(default)]
347 pub effort_mc_seed: Option<u64>,
348}
349
350fn default_preset() -> String {
351 "receipt".to_string()
352}
353
354fn default_granularity() -> String {
355 "module".to_string()
356}
357
358impl Default for AnalyzeSettings {
359 fn default() -> Self {
360 Self {
361 preset: default_preset(),
362 window: None,
363 git: None,
364 max_files: None,
365 max_bytes: None,
366 max_file_bytes: None,
367 max_commits: None,
368 max_commit_files: None,
369 granularity: default_granularity(),
370 effort_model: None,
371 effort_layer: None,
372 effort_base_ref: None,
373 effort_head_ref: None,
374 effort_monte_carlo: None,
375 effort_mc_iterations: None,
376 effort_mc_seed: None,
377 }
378 }
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct CockpitSettings {
384 #[serde(default = "default_cockpit_base")]
386 pub base: String,
387
388 #[serde(default = "default_cockpit_head")]
390 pub head: String,
391
392 #[serde(default = "default_cockpit_range_mode")]
394 pub range_mode: String,
395
396 #[serde(default)]
398 pub baseline: Option<String>,
399}
400
401fn default_cockpit_base() -> String {
402 "main".to_string()
403}
404
405fn default_cockpit_head() -> String {
406 "HEAD".to_string()
407}
408
409fn default_cockpit_range_mode() -> String {
410 "two-dot".to_string()
411}
412
413impl Default for CockpitSettings {
414 fn default() -> Self {
415 Self {
416 base: default_cockpit_base(),
417 head: default_cockpit_head(),
418 range_mode: default_cockpit_range_mode(),
419 baseline: None,
420 }
421 }
422}
423
424#[derive(Debug, Clone, Default, Serialize, Deserialize)]
426pub struct DiffSettings {
427 pub from: String,
429
430 pub to: String,
432}
433
434#[derive(Debug, Clone, Default, Serialize, Deserialize)]
440#[serde(default)]
441pub struct TomlConfig {
442 pub scan: ScanConfig,
444
445 pub module: ModuleConfig,
447
448 pub export: ExportConfig,
450
451 pub analyze: AnalyzeConfig,
453
454 pub context: ContextConfig,
456
457 pub badge: BadgeConfig,
459
460 pub gate: GateConfig,
462
463 #[serde(default)]
465 pub view: BTreeMap<String, ViewProfile>,
466}
467
468#[derive(Debug, Clone, Default, Serialize, Deserialize)]
470#[serde(default)]
471pub struct ScanConfig {
472 pub paths: Option<Vec<String>>,
474
475 pub exclude: Option<Vec<String>>,
477
478 pub hidden: Option<bool>,
480
481 pub config: Option<String>,
483
484 pub no_ignore: Option<bool>,
486
487 pub no_ignore_parent: Option<bool>,
489
490 pub no_ignore_dot: Option<bool>,
492
493 pub no_ignore_vcs: Option<bool>,
495
496 pub doc_comments: Option<bool>,
498}
499
500#[derive(Debug, Clone, Default, Serialize, Deserialize)]
502#[serde(default)]
503pub struct ModuleConfig {
504 pub roots: Option<Vec<String>>,
506
507 pub depth: Option<usize>,
509
510 pub children: Option<String>,
512}
513
514#[derive(Debug, Clone, Default, Serialize, Deserialize)]
516#[serde(default)]
517pub struct ExportConfig {
518 pub min_code: Option<usize>,
520
521 pub max_rows: Option<usize>,
523
524 pub redact: Option<String>,
526
527 pub format: Option<String>,
529
530 pub children: Option<String>,
532}
533
534#[derive(Debug, Clone, Default, Serialize, Deserialize)]
536#[serde(default)]
537pub struct AnalyzeConfig {
538 pub preset: Option<String>,
540
541 pub window: Option<usize>,
543
544 pub format: Option<String>,
546
547 pub git: Option<bool>,
549
550 pub max_files: Option<usize>,
552
553 pub max_bytes: Option<u64>,
555
556 pub max_file_bytes: Option<u64>,
558
559 pub max_commits: Option<usize>,
561
562 pub max_commit_files: Option<usize>,
564
565 pub granularity: Option<String>,
567
568 pub effort_model: Option<String>,
570
571 pub effort_layer: Option<String>,
573
574 pub effort_base_ref: Option<String>,
576
577 pub effort_head_ref: Option<String>,
579
580 pub effort_monte_carlo: Option<bool>,
582
583 pub effort_mc_iterations: Option<usize>,
585
586 pub effort_mc_seed: Option<u64>,
588}
589
590#[derive(Debug, Clone, Default, Serialize, Deserialize)]
592#[serde(default)]
593pub struct ContextConfig {
594 pub budget: Option<String>,
596
597 pub strategy: Option<String>,
599
600 pub rank_by: Option<String>,
602
603 pub output: Option<String>,
605
606 pub compress: Option<bool>,
608}
609
610#[derive(Debug, Clone, Default, Serialize, Deserialize)]
612#[serde(default)]
613pub struct BadgeConfig {
614 pub metric: Option<String>,
616}
617
618#[derive(Debug, Clone, Default, Serialize, Deserialize)]
620#[serde(default)]
621pub struct GateConfig {
622 pub policy: Option<String>,
624
625 pub baseline: Option<String>,
627
628 pub preset: Option<String>,
630
631 pub fail_fast: Option<bool>,
633
634 pub rules: Option<Vec<GateRule>>,
636
637 pub ratchet: Option<Vec<RatchetRuleConfig>>,
639
640 pub allow_missing_baseline: Option<bool>,
642
643 pub allow_missing_current: Option<bool>,
645}
646
647#[derive(Debug, Clone, Serialize, Deserialize)]
649pub struct RatchetRuleConfig {
650 pub pointer: String,
652
653 #[serde(default)]
655 pub max_increase_pct: Option<f64>,
656
657 #[serde(default)]
659 pub max_value: Option<f64>,
660
661 #[serde(default)]
663 pub level: Option<String>,
664
665 #[serde(default)]
667 pub description: Option<String>,
668}
669
670#[derive(Debug, Clone, Serialize, Deserialize)]
672pub struct GateRule {
673 pub name: String,
675
676 pub pointer: String,
678
679 pub op: String,
681
682 #[serde(default)]
684 pub value: Option<serde_json::Value>,
685
686 #[serde(default)]
688 pub values: Option<Vec<serde_json::Value>>,
689
690 #[serde(default)]
692 pub negate: bool,
693
694 #[serde(default)]
696 pub level: Option<String>,
697
698 #[serde(default)]
700 pub message: Option<String>,
701}
702
703#[derive(Debug, Clone, Default, Serialize, Deserialize)]
705#[serde(default)]
706pub struct ViewProfile {
707 pub format: Option<String>,
710
711 pub top: Option<usize>,
713
714 pub files: Option<bool>,
717
718 pub module_roots: Option<Vec<String>>,
721
722 pub module_depth: Option<usize>,
724
725 pub min_code: Option<usize>,
727
728 pub max_rows: Option<usize>,
730
731 pub redact: Option<String>,
733
734 pub meta: Option<bool>,
736
737 pub children: Option<String>,
739
740 pub preset: Option<String>,
743
744 pub window: Option<usize>,
746
747 pub budget: Option<String>,
750
751 pub strategy: Option<String>,
753
754 pub rank_by: Option<String>,
756
757 pub output: Option<String>,
759
760 pub compress: Option<bool>,
762
763 pub metric: Option<String>,
766}
767
768impl TomlConfig {
769 pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
771 toml::from_str(s)
772 }
773
774 pub fn from_file(path: &Path) -> std::io::Result<Self> {
776 let content = std::fs::read_to_string(path)?;
777 toml::from_str(&content)
778 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
779 }
780}
781
782pub type TomlResult<T> = Result<T, toml::de::Error>;
784
785#[cfg(test)]
786mod tests {
787 use super::*;
788 use std::io::Write;
789 use tempfile::NamedTempFile;
790
791 #[test]
792 fn scan_options_default() {
793 let opts = ScanOptions::default();
794 assert!(opts.excluded.is_empty());
795 assert!(!opts.hidden);
796 assert!(!opts.no_ignore);
797 }
798
799 #[test]
800 fn scan_settings_current_dir() {
801 let s = ScanSettings::current_dir();
802 assert_eq!(s.paths, vec!["."]);
803 }
804
805 #[test]
806 fn scan_settings_for_paths() {
807 let s = ScanSettings::for_paths(vec!["src".into(), "lib".into()]);
808 assert_eq!(s.paths.len(), 2);
809 }
810
811 #[test]
812 fn scan_settings_flatten() {
813 let s = ScanSettings {
815 paths: vec![".".into()],
816 options: ScanOptions {
817 hidden: true,
818 ..Default::default()
819 },
820 };
821 assert!(s.options.hidden);
822 }
823
824 #[test]
825 fn serde_roundtrip_scan_options() {
826 let opts = ScanOptions {
827 excluded: vec!["target".into()],
828 config: ConfigMode::None,
829 hidden: true,
830 no_ignore: false,
831 no_ignore_parent: true,
832 no_ignore_dot: false,
833 no_ignore_vcs: true,
834 treat_doc_strings_as_comments: true,
835 };
836 let json = serde_json::to_string(&opts).unwrap();
837 let back: ScanOptions = serde_json::from_str(&json).unwrap();
838 assert_eq!(back.excluded, opts.excluded);
839 assert!(back.hidden);
840 assert!(back.no_ignore_parent);
841 assert!(back.no_ignore_vcs);
842 assert!(back.treat_doc_strings_as_comments);
843 }
844
845 #[test]
846 fn serde_roundtrip_scan_settings() {
847 let s = ScanSettings {
848 paths: vec!["src".into()],
849 options: ScanOptions {
850 excluded: vec!["*.bak".into()],
851 ..Default::default()
852 },
853 };
854 let json = serde_json::to_string(&s).unwrap();
855 let back: ScanSettings = serde_json::from_str(&json).unwrap();
856 assert_eq!(back.paths, s.paths);
857 assert_eq!(back.options.excluded, s.options.excluded);
858 }
859
860 #[test]
861 fn serde_roundtrip_lang_settings() {
862 let s = LangSettings {
863 top: 10,
864 files: true,
865 children: ChildrenMode::Separate,
866 redact: Some(RedactMode::Paths),
867 };
868 let json = serde_json::to_string(&s).unwrap();
869 let back: LangSettings = serde_json::from_str(&json).unwrap();
870 assert_eq!(back.top, 10);
871 assert!(back.files);
872 }
873
874 #[test]
875 fn serde_roundtrip_export_settings() {
876 let s = ExportSettings::default();
877 let json = serde_json::to_string(&s).unwrap();
878 let back: ExportSettings = serde_json::from_str(&json).unwrap();
879 assert_eq!(back.min_code, 0);
880 assert!(back.meta);
881 }
882
883 #[test]
884 fn serde_roundtrip_analyze_settings() {
885 let s = AnalyzeSettings::default();
886 let json = serde_json::to_string(&s).unwrap();
887 let back: AnalyzeSettings = serde_json::from_str(&json).unwrap();
888 assert_eq!(back.preset, "receipt");
889 assert_eq!(back.granularity, "module");
890 }
891
892 #[test]
893 fn serde_roundtrip_cockpit_settings() {
894 let s = CockpitSettings::default();
895 let json = serde_json::to_string(&s).unwrap();
896 let back: CockpitSettings = serde_json::from_str(&json).unwrap();
897 assert_eq!(back.base, "main");
898 assert_eq!(back.head, "HEAD");
899 assert_eq!(back.range_mode, "two-dot");
900 assert!(back.baseline.is_none());
901 }
902
903 #[test]
904 fn serde_roundtrip_cockpit_settings_with_baseline() {
905 let s = CockpitSettings {
906 base: "v1.0".into(),
907 head: "feature".into(),
908 range_mode: "three-dot".into(),
909 baseline: Some("baseline.json".into()),
910 };
911 let json = serde_json::to_string(&s).unwrap();
912 let back: CockpitSettings = serde_json::from_str(&json).unwrap();
913 assert_eq!(back.base, "v1.0");
914 assert_eq!(back.baseline, Some("baseline.json".to_string()));
915 }
916
917 #[test]
918 fn serde_roundtrip_diff_settings() {
919 let s = DiffSettings {
920 from: "v1.0".into(),
921 to: "v2.0".into(),
922 };
923 let json = serde_json::to_string(&s).unwrap();
924 let back: DiffSettings = serde_json::from_str(&json).unwrap();
925 assert_eq!(back.from, "v1.0");
926 assert_eq!(back.to, "v2.0");
927 }
928
929 #[test]
930 fn toml_parse_and_view_profiles() {
931 let toml_str = r#"
932[scan]
933hidden = true
934
935[view.llm]
936format = "json"
937top = 10
938"#;
939 let config = TomlConfig::parse(toml_str).expect("parse config");
940 assert_eq!(config.scan.hidden, Some(true));
941 let llm = config.view.get("llm").expect("llm profile");
942 assert_eq!(llm.format.as_deref(), Some("json"));
943 assert_eq!(llm.top, Some(10));
944 }
945
946 #[test]
947 fn toml_from_file_roundtrip() {
948 let toml_content = r#"
949[module]
950depth = 3
951roots = ["src", "tests"]
952"#;
953
954 let mut temp_file = NamedTempFile::new().expect("temp file");
955 temp_file
956 .write_all(toml_content.as_bytes())
957 .expect("write config");
958
959 let config = TomlConfig::from_file(temp_file.path()).expect("load config");
960 assert_eq!(config.module.depth, Some(3));
961 assert_eq!(
962 config.module.roots,
963 Some(vec!["src".to_string(), "tests".to_string()])
964 );
965 }
966}