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)]
33pub struct ScanOptions {
34 #[serde(default)]
36 pub excluded: Vec<String>,
37
38 #[serde(default)]
40 pub config: ConfigMode,
41
42 #[serde(default)]
44 pub hidden: bool,
45
46 #[serde(default)]
48 pub no_ignore: bool,
49
50 #[serde(default)]
52 pub no_ignore_parent: bool,
53
54 #[serde(default)]
56 pub no_ignore_dot: bool,
57
58 #[serde(default)]
60 pub no_ignore_vcs: bool,
61
62 #[serde(default)]
64 pub treat_doc_strings_as_comments: bool,
65}
66
67#[derive(Debug, Clone, Default, Serialize, Deserialize)]
69pub struct ScanSettings {
70 #[serde(default)]
72 pub paths: Vec<String>,
73
74 #[serde(flatten)]
76 pub options: ScanOptions,
77}
78
79impl ScanSettings {
80 pub fn current_dir() -> Self {
82 Self {
83 paths: vec![".".to_string()],
84 ..Default::default()
85 }
86 }
87
88 pub fn for_paths(paths: Vec<String>) -> Self {
90 Self {
91 paths,
92 ..Default::default()
93 }
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct LangSettings {
100 #[serde(default)]
102 pub top: usize,
103
104 #[serde(default)]
106 pub files: bool,
107
108 #[serde(default = "default_children_mode")]
110 pub children: ChildrenMode,
111
112 #[serde(default)]
114 pub redact: Option<RedactMode>,
115}
116
117impl Default for LangSettings {
118 fn default() -> Self {
119 Self {
120 top: 0,
121 files: false,
122 children: ChildrenMode::Collapse,
123 redact: None,
124 }
125 }
126}
127
128fn default_children_mode() -> ChildrenMode {
129 ChildrenMode::Collapse
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct ModuleSettings {
135 #[serde(default)]
137 pub top: usize,
138
139 #[serde(default = "default_module_roots")]
141 pub module_roots: Vec<String>,
142
143 #[serde(default = "default_module_depth")]
145 pub module_depth: usize,
146
147 #[serde(default = "default_child_include_mode")]
149 pub children: ChildIncludeMode,
150
151 #[serde(default)]
153 pub redact: Option<RedactMode>,
154}
155
156fn default_module_roots() -> Vec<String> {
157 vec!["crates".to_string(), "packages".to_string()]
158}
159
160fn default_module_depth() -> usize {
161 2
162}
163
164fn default_child_include_mode() -> ChildIncludeMode {
165 ChildIncludeMode::Separate
166}
167
168impl Default for ModuleSettings {
169 fn default() -> Self {
170 Self {
171 top: 0,
172 module_roots: default_module_roots(),
173 module_depth: default_module_depth(),
174 children: default_child_include_mode(),
175 redact: None,
176 }
177 }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct ExportSettings {
183 #[serde(default = "default_export_format")]
185 pub format: ExportFormat,
186
187 #[serde(default = "default_module_roots")]
189 pub module_roots: Vec<String>,
190
191 #[serde(default = "default_module_depth")]
193 pub module_depth: usize,
194
195 #[serde(default = "default_child_include_mode")]
197 pub children: ChildIncludeMode,
198
199 #[serde(default)]
201 pub min_code: usize,
202
203 #[serde(default)]
205 pub max_rows: usize,
206
207 #[serde(default = "default_redact_mode")]
209 pub redact: RedactMode,
210
211 #[serde(default = "default_meta")]
213 pub meta: bool,
214
215 #[serde(default)]
217 pub strip_prefix: Option<String>,
218}
219
220fn default_redact_mode() -> RedactMode {
221 RedactMode::None
222}
223
224fn default_export_format() -> ExportFormat {
225 ExportFormat::Jsonl
226}
227
228fn default_meta() -> bool {
229 true
230}
231
232impl Default for ExportSettings {
233 fn default() -> Self {
234 Self {
235 format: default_export_format(),
236 module_roots: default_module_roots(),
237 module_depth: default_module_depth(),
238 children: default_child_include_mode(),
239 min_code: 0,
240 max_rows: 0,
241 redact: RedactMode::None,
242 meta: true,
243 strip_prefix: None,
244 }
245 }
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct AnalyzeSettings {
251 #[serde(default = "default_preset")]
253 pub preset: String,
254
255 #[serde(default)]
257 pub window: Option<usize>,
258
259 #[serde(default)]
261 pub git: Option<bool>,
262
263 #[serde(default)]
265 pub max_files: Option<usize>,
266
267 #[serde(default)]
269 pub max_bytes: Option<u64>,
270
271 #[serde(default)]
273 pub max_file_bytes: Option<u64>,
274
275 #[serde(default)]
277 pub max_commits: Option<usize>,
278
279 #[serde(default)]
281 pub max_commit_files: Option<usize>,
282
283 #[serde(default = "default_granularity")]
285 pub granularity: String,
286
287 #[serde(default)]
289 pub effort_model: Option<String>,
290
291 #[serde(default)]
293 pub effort_layer: Option<String>,
294
295 #[serde(default)]
297 pub effort_base_ref: Option<String>,
298
299 #[serde(default)]
301 pub effort_head_ref: Option<String>,
302
303 #[serde(default)]
305 pub effort_monte_carlo: Option<bool>,
306
307 #[serde(default)]
309 pub effort_mc_iterations: Option<usize>,
310
311 #[serde(default)]
313 pub effort_mc_seed: Option<u64>,
314}
315
316fn default_preset() -> String {
317 "receipt".to_string()
318}
319
320fn default_granularity() -> String {
321 "module".to_string()
322}
323
324impl Default for AnalyzeSettings {
325 fn default() -> Self {
326 Self {
327 preset: default_preset(),
328 window: None,
329 git: None,
330 max_files: None,
331 max_bytes: None,
332 max_file_bytes: None,
333 max_commits: None,
334 max_commit_files: None,
335 granularity: default_granularity(),
336 effort_model: None,
337 effort_layer: None,
338 effort_base_ref: None,
339 effort_head_ref: None,
340 effort_monte_carlo: None,
341 effort_mc_iterations: None,
342 effort_mc_seed: None,
343 }
344 }
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct CockpitSettings {
350 #[serde(default = "default_cockpit_base")]
352 pub base: String,
353
354 #[serde(default = "default_cockpit_head")]
356 pub head: String,
357
358 #[serde(default = "default_cockpit_range_mode")]
360 pub range_mode: String,
361
362 #[serde(default)]
364 pub baseline: Option<String>,
365}
366
367fn default_cockpit_base() -> String {
368 "main".to_string()
369}
370
371fn default_cockpit_head() -> String {
372 "HEAD".to_string()
373}
374
375fn default_cockpit_range_mode() -> String {
376 "two-dot".to_string()
377}
378
379impl Default for CockpitSettings {
380 fn default() -> Self {
381 Self {
382 base: default_cockpit_base(),
383 head: default_cockpit_head(),
384 range_mode: default_cockpit_range_mode(),
385 baseline: None,
386 }
387 }
388}
389
390#[derive(Debug, Clone, Default, Serialize, Deserialize)]
392pub struct DiffSettings {
393 pub from: String,
395
396 pub to: String,
398}
399
400#[derive(Debug, Clone, Default, Serialize, Deserialize)]
406#[serde(default)]
407pub struct TomlConfig {
408 pub scan: ScanConfig,
410
411 pub module: ModuleConfig,
413
414 pub export: ExportConfig,
416
417 pub analyze: AnalyzeConfig,
419
420 pub context: ContextConfig,
422
423 pub badge: BadgeConfig,
425
426 pub gate: GateConfig,
428
429 #[serde(default)]
431 pub view: BTreeMap<String, ViewProfile>,
432}
433
434#[derive(Debug, Clone, Default, Serialize, Deserialize)]
436#[serde(default)]
437pub struct ScanConfig {
438 pub paths: Option<Vec<String>>,
440
441 pub exclude: Option<Vec<String>>,
443
444 pub hidden: Option<bool>,
446
447 pub config: Option<String>,
449
450 pub no_ignore: Option<bool>,
452
453 pub no_ignore_parent: Option<bool>,
455
456 pub no_ignore_dot: Option<bool>,
458
459 pub no_ignore_vcs: Option<bool>,
461
462 pub doc_comments: Option<bool>,
464}
465
466#[derive(Debug, Clone, Default, Serialize, Deserialize)]
468#[serde(default)]
469pub struct ModuleConfig {
470 pub roots: Option<Vec<String>>,
472
473 pub depth: Option<usize>,
475
476 pub children: Option<String>,
478}
479
480#[derive(Debug, Clone, Default, Serialize, Deserialize)]
482#[serde(default)]
483pub struct ExportConfig {
484 pub min_code: Option<usize>,
486
487 pub max_rows: Option<usize>,
489
490 pub redact: Option<String>,
492
493 pub format: Option<String>,
495
496 pub children: Option<String>,
498}
499
500#[derive(Debug, Clone, Default, Serialize, Deserialize)]
502#[serde(default)]
503pub struct AnalyzeConfig {
504 pub preset: Option<String>,
506
507 pub window: Option<usize>,
509
510 pub format: Option<String>,
512
513 pub git: Option<bool>,
515
516 pub max_files: Option<usize>,
518
519 pub max_bytes: Option<u64>,
521
522 pub max_file_bytes: Option<u64>,
524
525 pub max_commits: Option<usize>,
527
528 pub max_commit_files: Option<usize>,
530
531 pub granularity: Option<String>,
533
534 pub effort_model: Option<String>,
536
537 pub effort_layer: Option<String>,
539
540 pub effort_base_ref: Option<String>,
542
543 pub effort_head_ref: Option<String>,
545
546 pub effort_monte_carlo: Option<bool>,
548
549 pub effort_mc_iterations: Option<usize>,
551
552 pub effort_mc_seed: Option<u64>,
554}
555
556#[derive(Debug, Clone, Default, Serialize, Deserialize)]
558#[serde(default)]
559pub struct ContextConfig {
560 pub budget: Option<String>,
562
563 pub strategy: Option<String>,
565
566 pub rank_by: Option<String>,
568
569 pub output: Option<String>,
571
572 pub compress: Option<bool>,
574}
575
576#[derive(Debug, Clone, Default, Serialize, Deserialize)]
578#[serde(default)]
579pub struct BadgeConfig {
580 pub metric: Option<String>,
582}
583
584#[derive(Debug, Clone, Default, Serialize, Deserialize)]
586#[serde(default)]
587pub struct GateConfig {
588 pub policy: Option<String>,
590
591 pub baseline: Option<String>,
593
594 pub preset: Option<String>,
596
597 pub fail_fast: Option<bool>,
599
600 pub rules: Option<Vec<GateRule>>,
602
603 pub ratchet: Option<Vec<RatchetRuleConfig>>,
605
606 pub allow_missing_baseline: Option<bool>,
608
609 pub allow_missing_current: Option<bool>,
611}
612
613#[derive(Debug, Clone, Serialize, Deserialize)]
615pub struct RatchetRuleConfig {
616 pub pointer: String,
618
619 #[serde(default)]
621 pub max_increase_pct: Option<f64>,
622
623 #[serde(default)]
625 pub max_value: Option<f64>,
626
627 #[serde(default)]
629 pub level: Option<String>,
630
631 #[serde(default)]
633 pub description: Option<String>,
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize)]
638pub struct GateRule {
639 pub name: String,
641
642 pub pointer: String,
644
645 pub op: String,
647
648 #[serde(default)]
650 pub value: Option<serde_json::Value>,
651
652 #[serde(default)]
654 pub values: Option<Vec<serde_json::Value>>,
655
656 #[serde(default)]
658 pub negate: bool,
659
660 #[serde(default)]
662 pub level: Option<String>,
663
664 #[serde(default)]
666 pub message: Option<String>,
667}
668
669#[derive(Debug, Clone, Default, Serialize, Deserialize)]
671#[serde(default)]
672pub struct ViewProfile {
673 pub format: Option<String>,
676
677 pub top: Option<usize>,
679
680 pub files: Option<bool>,
683
684 pub module_roots: Option<Vec<String>>,
687
688 pub module_depth: Option<usize>,
690
691 pub min_code: Option<usize>,
693
694 pub max_rows: Option<usize>,
696
697 pub redact: Option<String>,
699
700 pub meta: Option<bool>,
702
703 pub children: Option<String>,
705
706 pub preset: Option<String>,
709
710 pub window: Option<usize>,
712
713 pub budget: Option<String>,
716
717 pub strategy: Option<String>,
719
720 pub rank_by: Option<String>,
722
723 pub output: Option<String>,
725
726 pub compress: Option<bool>,
728
729 pub metric: Option<String>,
732}
733
734impl TomlConfig {
735 pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
737 toml::from_str(s)
738 }
739
740 pub fn from_file(path: &Path) -> std::io::Result<Self> {
742 let content = std::fs::read_to_string(path)?;
743 toml::from_str(&content)
744 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
745 }
746}
747
748pub type TomlResult<T> = Result<T, toml::de::Error>;
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754 use std::io::Write;
755 use tempfile::NamedTempFile;
756
757 #[test]
758 fn scan_options_default() {
759 let opts = ScanOptions::default();
760 assert!(opts.excluded.is_empty());
761 assert!(!opts.hidden);
762 assert!(!opts.no_ignore);
763 }
764
765 #[test]
766 fn scan_settings_current_dir() {
767 let s = ScanSettings::current_dir();
768 assert_eq!(s.paths, vec!["."]);
769 }
770
771 #[test]
772 fn scan_settings_for_paths() {
773 let s = ScanSettings::for_paths(vec!["src".into(), "lib".into()]);
774 assert_eq!(s.paths.len(), 2);
775 }
776
777 #[test]
778 fn scan_settings_flatten() {
779 let s = ScanSettings {
781 paths: vec![".".into()],
782 options: ScanOptions {
783 hidden: true,
784 ..Default::default()
785 },
786 };
787 assert!(s.options.hidden);
788 }
789
790 #[test]
791 fn serde_roundtrip_scan_options() {
792 let opts = ScanOptions {
793 excluded: vec!["target".into()],
794 config: ConfigMode::None,
795 hidden: true,
796 no_ignore: false,
797 no_ignore_parent: true,
798 no_ignore_dot: false,
799 no_ignore_vcs: true,
800 treat_doc_strings_as_comments: true,
801 };
802 let json = serde_json::to_string(&opts).unwrap();
803 let back: ScanOptions = serde_json::from_str(&json).unwrap();
804 assert_eq!(back.excluded, opts.excluded);
805 assert!(back.hidden);
806 assert!(back.no_ignore_parent);
807 assert!(back.no_ignore_vcs);
808 assert!(back.treat_doc_strings_as_comments);
809 }
810
811 #[test]
812 fn serde_roundtrip_scan_settings() {
813 let s = ScanSettings {
814 paths: vec!["src".into()],
815 options: ScanOptions {
816 excluded: vec!["*.bak".into()],
817 ..Default::default()
818 },
819 };
820 let json = serde_json::to_string(&s).unwrap();
821 let back: ScanSettings = serde_json::from_str(&json).unwrap();
822 assert_eq!(back.paths, s.paths);
823 assert_eq!(back.options.excluded, s.options.excluded);
824 }
825
826 #[test]
827 fn serde_roundtrip_lang_settings() {
828 let s = LangSettings {
829 top: 10,
830 files: true,
831 children: ChildrenMode::Separate,
832 redact: Some(RedactMode::Paths),
833 };
834 let json = serde_json::to_string(&s).unwrap();
835 let back: LangSettings = serde_json::from_str(&json).unwrap();
836 assert_eq!(back.top, 10);
837 assert!(back.files);
838 }
839
840 #[test]
841 fn serde_roundtrip_export_settings() {
842 let s = ExportSettings::default();
843 let json = serde_json::to_string(&s).unwrap();
844 let back: ExportSettings = serde_json::from_str(&json).unwrap();
845 assert_eq!(back.min_code, 0);
846 assert!(back.meta);
847 }
848
849 #[test]
850 fn serde_roundtrip_analyze_settings() {
851 let s = AnalyzeSettings::default();
852 let json = serde_json::to_string(&s).unwrap();
853 let back: AnalyzeSettings = serde_json::from_str(&json).unwrap();
854 assert_eq!(back.preset, "receipt");
855 assert_eq!(back.granularity, "module");
856 }
857
858 #[test]
859 fn serde_roundtrip_cockpit_settings() {
860 let s = CockpitSettings::default();
861 let json = serde_json::to_string(&s).unwrap();
862 let back: CockpitSettings = serde_json::from_str(&json).unwrap();
863 assert_eq!(back.base, "main");
864 assert_eq!(back.head, "HEAD");
865 assert_eq!(back.range_mode, "two-dot");
866 assert!(back.baseline.is_none());
867 }
868
869 #[test]
870 fn serde_roundtrip_cockpit_settings_with_baseline() {
871 let s = CockpitSettings {
872 base: "v1.0".into(),
873 head: "feature".into(),
874 range_mode: "three-dot".into(),
875 baseline: Some("baseline.json".into()),
876 };
877 let json = serde_json::to_string(&s).unwrap();
878 let back: CockpitSettings = serde_json::from_str(&json).unwrap();
879 assert_eq!(back.base, "v1.0");
880 assert_eq!(back.baseline, Some("baseline.json".to_string()));
881 }
882
883 #[test]
884 fn serde_roundtrip_diff_settings() {
885 let s = DiffSettings {
886 from: "v1.0".into(),
887 to: "v2.0".into(),
888 };
889 let json = serde_json::to_string(&s).unwrap();
890 let back: DiffSettings = serde_json::from_str(&json).unwrap();
891 assert_eq!(back.from, "v1.0");
892 assert_eq!(back.to, "v2.0");
893 }
894
895 #[test]
896 fn toml_parse_and_view_profiles() {
897 let toml_str = r#"
898[scan]
899hidden = true
900
901[view.llm]
902format = "json"
903top = 10
904"#;
905 let config = TomlConfig::parse(toml_str).expect("parse config");
906 assert_eq!(config.scan.hidden, Some(true));
907 let llm = config.view.get("llm").expect("llm profile");
908 assert_eq!(llm.format.as_deref(), Some("json"));
909 assert_eq!(llm.top, Some(10));
910 }
911
912 #[test]
913 fn toml_from_file_roundtrip() {
914 let toml_content = r#"
915[module]
916depth = 3
917roots = ["src", "tests"]
918"#;
919
920 let mut temp_file = NamedTempFile::new().expect("temp file");
921 temp_file
922 .write_all(toml_content.as_bytes())
923 .expect("write config");
924
925 let config = TomlConfig::from_file(temp_file.path()).expect("load config");
926 assert_eq!(config.module.depth, Some(3));
927 assert_eq!(
928 config.module.roots,
929 Some(vec!["src".to_string(), "tests".to_string()])
930 );
931 }
932}