Skip to main content

tokmd_settings/
lib.rs

1//! # tokmd-settings
2//!
3//! **Tier 0 (Pure Settings)**
4//!
5//! Clap-free settings types for the scan and format layers.
6//! These types mirror CLI arguments without Clap dependencies,
7//! making them suitable for FFI boundaries and library consumers.
8//!
9//! ## What belongs here
10//! * Pure data types with Serde derive
11//! * Scan, language, module, export, analyze, diff settings
12//! * Default values and conversions
13//!
14//! ## What does NOT belong here
15//! * Clap parsing (use `tokmd::cli`)
16//! * I/O operations
17//! * Business logic
18
19use std::collections::BTreeMap;
20use std::path::Path;
21
22use serde::{Deserialize, Serialize};
23
24// Re-export types from tokmd_types for convenience
25pub use tokmd_types::{ChildIncludeMode, ChildrenMode, ConfigMode, ExportFormat, RedactMode};
26
27// Legacy profile contract persisted by the historical `config.json` format.
28//
29// These types are intentionally kept in `tokmd-settings` so that the config
30// profile contract is available without CLI parsing dependencies.
31
32/// Legacy profile map used by historical `config.json` files.
33#[derive(Debug, Clone, Default, Serialize, Deserialize)]
34pub struct UserConfig {
35    pub profiles: BTreeMap<String, Profile>,
36    pub repos: BTreeMap<String, String>, // "owner/repo" -> "profile_name"
37}
38
39/// Legacy profile options shared by configuration consumers.
40#[derive(Debug, Clone, Default, Serialize, Deserialize)]
41pub struct Profile {
42    // Shared
43    pub format: Option<String>, // "json", "md", "tsv", "csv", "jsonl"
44    pub top: Option<usize>,
45
46    // Lang
47    pub files: Option<bool>,
48
49    // Module / Export
50    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    // "children" can be ChildrenMode or ChildIncludeMode string
58    pub children: Option<String>,
59}
60
61/// Scan options shared by all commands that invoke the scanner.
62///
63/// This mirrors the scan-relevant fields of CLI global args without any
64/// UI-specific fields (`verbose`, `no_progress`). Lower-tier crates
65/// (scan, format, model) depend on this instead of the CLI parser.
66#[derive(Debug, Clone, Default, Serialize, Deserialize)]
67pub struct ScanOptions {
68    /// Glob patterns to exclude.
69    #[serde(default)]
70    pub excluded: Vec<String>,
71
72    /// Whether to load scan config files (`tokei.toml` / `.tokeirc`).
73    #[serde(default)]
74    pub config: ConfigMode,
75
76    /// Count hidden files and directories.
77    #[serde(default)]
78    pub hidden: bool,
79
80    /// Don't respect ignore files (.gitignore, .ignore, etc.).
81    #[serde(default)]
82    pub no_ignore: bool,
83
84    /// Don't respect ignore files in parent directories.
85    #[serde(default)]
86    pub no_ignore_parent: bool,
87
88    /// Don't respect .ignore and .tokeignore files.
89    #[serde(default)]
90    pub no_ignore_dot: bool,
91
92    /// Don't respect VCS ignore files (.gitignore, .hgignore, etc.).
93    #[serde(default)]
94    pub no_ignore_vcs: bool,
95
96    /// Treat doc strings as comments.
97    #[serde(default)]
98    pub treat_doc_strings_as_comments: bool,
99}
100
101/// Global scan settings shared by all operations.
102#[derive(Debug, Clone, Default, Serialize, Deserialize)]
103pub struct ScanSettings {
104    /// Paths to scan (defaults to `["."]`).
105    #[serde(default)]
106    pub paths: Vec<String>,
107
108    /// Scan options (excludes, ignore flags, etc.).
109    #[serde(flatten)]
110    pub options: ScanOptions,
111}
112
113impl ScanSettings {
114    /// Create settings for scanning the current directory with defaults.
115    pub fn current_dir() -> Self {
116        Self {
117            paths: vec![".".to_string()],
118            ..Default::default()
119        }
120    }
121
122    /// Create settings for scanning specific paths.
123    pub fn for_paths(paths: Vec<String>) -> Self {
124        Self {
125            paths,
126            ..Default::default()
127        }
128    }
129}
130
131/// Settings for language summary (`tokmd lang`).
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct LangSettings {
134    /// Show only the top N rows (0 = all).
135    #[serde(default)]
136    pub top: usize,
137
138    /// Include file counts and average lines per file.
139    #[serde(default)]
140    pub files: bool,
141
142    /// How to handle embedded languages.
143    #[serde(default = "default_children_mode")]
144    pub children: ChildrenMode,
145
146    /// Redaction mode for output.
147    #[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/// Settings for module summary (`tokmd module`).
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct ModuleSettings {
169    /// Show only the top N modules (0 = all).
170    #[serde(default)]
171    pub top: usize,
172
173    /// Top-level directories as "module roots".
174    #[serde(default = "default_module_roots")]
175    pub module_roots: Vec<String>,
176
177    /// Path segments to include for module roots.
178    #[serde(default = "default_module_depth")]
179    pub module_depth: usize,
180
181    /// How to handle embedded languages.
182    #[serde(default = "default_child_include_mode")]
183    pub children: ChildIncludeMode,
184
185    /// Redaction mode for output.
186    #[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/// Settings for file-level export (`tokmd export`).
215#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct ExportSettings {
217    /// Output format.
218    #[serde(default = "default_export_format")]
219    pub format: ExportFormat,
220
221    /// Module roots (see `ModuleSettings`).
222    #[serde(default = "default_module_roots")]
223    pub module_roots: Vec<String>,
224
225    /// Module depth (see `ModuleSettings`).
226    #[serde(default = "default_module_depth")]
227    pub module_depth: usize,
228
229    /// How to handle embedded languages.
230    #[serde(default = "default_child_include_mode")]
231    pub children: ChildIncludeMode,
232
233    /// Drop rows with fewer than N code lines.
234    #[serde(default)]
235    pub min_code: usize,
236
237    /// Stop after emitting N rows (0 = unlimited).
238    #[serde(default)]
239    pub max_rows: usize,
240
241    /// Redaction mode.
242    #[serde(default = "default_redact_mode")]
243    pub redact: RedactMode,
244
245    /// Include a meta record.
246    #[serde(default = "default_meta")]
247    pub meta: bool,
248
249    /// Strip this prefix from paths.
250    #[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/// Settings for analysis (`tokmd analyze`).
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct AnalyzeSettings {
285    /// Analysis preset to run.
286    #[serde(default = "default_preset")]
287    pub preset: String,
288
289    /// Context window size (tokens) for utilization bars.
290    #[serde(default)]
291    pub window: Option<usize>,
292
293    /// Force-enable git-based metrics.
294    #[serde(default)]
295    pub git: Option<bool>,
296
297    /// Limit files walked for asset/deps/content scans.
298    #[serde(default)]
299    pub max_files: Option<usize>,
300
301    /// Limit total bytes read during content scans.
302    #[serde(default)]
303    pub max_bytes: Option<u64>,
304
305    /// Limit bytes per file during content scans.
306    #[serde(default)]
307    pub max_file_bytes: Option<u64>,
308
309    /// Limit commits scanned for git metrics.
310    #[serde(default)]
311    pub max_commits: Option<usize>,
312
313    /// Limit files per commit for git metrics.
314    #[serde(default)]
315    pub max_commit_files: Option<usize>,
316
317    /// Import graph granularity.
318    #[serde(default = "default_granularity")]
319    pub granularity: String,
320
321    /// Effort model for estimate calculations.
322    #[serde(default)]
323    pub effort_model: Option<String>,
324
325    /// Effort report layer.
326    #[serde(default)]
327    pub effort_layer: Option<String>,
328
329    /// Base reference for effort delta computation.
330    #[serde(default)]
331    pub effort_base_ref: Option<String>,
332
333    /// Head reference for effort delta computation.
334    #[serde(default)]
335    pub effort_head_ref: Option<String>,
336
337    /// Enable Monte Carlo uncertainty for effort estimation.
338    #[serde(default)]
339    pub effort_monte_carlo: Option<bool>,
340
341    /// Monte Carlo iterations for effort estimation.
342    #[serde(default)]
343    pub effort_mc_iterations: Option<usize>,
344
345    /// Monte Carlo seed for effort estimation.
346    #[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/// Settings for cockpit PR metrics (`tokmd cockpit`).
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct CockpitSettings {
384    /// Base ref to compare from.
385    #[serde(default = "default_cockpit_base")]
386    pub base: String,
387
388    /// Head ref to compare to.
389    #[serde(default = "default_cockpit_head")]
390    pub head: String,
391
392    /// Range mode: "two-dot" or "three-dot".
393    #[serde(default = "default_cockpit_range_mode")]
394    pub range_mode: String,
395
396    /// Optional baseline file path for trend comparison.
397    #[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/// Settings for diff comparison (`tokmd diff`).
425#[derive(Debug, Clone, Default, Serialize, Deserialize)]
426pub struct DiffSettings {
427    /// Base reference to compare from.
428    pub from: String,
429
430    /// Target reference to compare to.
431    pub to: String,
432}
433
434// =============================================================================
435// TOML Configuration File Structures
436// =============================================================================
437
438/// Root TOML configuration structure.
439#[derive(Debug, Clone, Default, Serialize, Deserialize)]
440#[serde(default)]
441pub struct TomlConfig {
442    /// Scan settings (applies to all commands).
443    pub scan: ScanConfig,
444
445    /// Module command settings.
446    pub module: ModuleConfig,
447
448    /// Export command settings.
449    pub export: ExportConfig,
450
451    /// Analyze command settings.
452    pub analyze: AnalyzeConfig,
453
454    /// Context command settings.
455    pub context: ContextConfig,
456
457    /// Badge command settings.
458    pub badge: BadgeConfig,
459
460    /// Gate command settings.
461    pub gate: GateConfig,
462
463    /// Named view profiles (e.g., [view.llm], [view.ci]).
464    #[serde(default)]
465    pub view: BTreeMap<String, ViewProfile>,
466}
467
468/// Scan settings shared by all commands.
469#[derive(Debug, Clone, Default, Serialize, Deserialize)]
470#[serde(default)]
471pub struct ScanConfig {
472    /// Paths to scan (default: ["."])
473    pub paths: Option<Vec<String>>,
474
475    /// Glob patterns to exclude.
476    pub exclude: Option<Vec<String>>,
477
478    /// Include hidden files and directories.
479    pub hidden: Option<bool>,
480
481    /// Config file strategy for tokei: "auto" or "none".
482    pub config: Option<String>,
483
484    /// Disable all ignore files.
485    pub no_ignore: Option<bool>,
486
487    /// Disable parent directory ignore file traversal.
488    pub no_ignore_parent: Option<bool>,
489
490    /// Disable .ignore/.tokeignore files.
491    pub no_ignore_dot: Option<bool>,
492
493    /// Disable .gitignore files.
494    pub no_ignore_vcs: Option<bool>,
495
496    /// Treat doc comments as comments instead of code.
497    pub doc_comments: Option<bool>,
498}
499
500/// Module command settings.
501#[derive(Debug, Clone, Default, Serialize, Deserialize)]
502#[serde(default)]
503pub struct ModuleConfig {
504    /// Root directories for module grouping.
505    pub roots: Option<Vec<String>>,
506
507    /// Depth for module grouping.
508    pub depth: Option<usize>,
509
510    /// Children handling: "collapse" or "separate".
511    pub children: Option<String>,
512}
513
514/// Export command settings.
515#[derive(Debug, Clone, Default, Serialize, Deserialize)]
516#[serde(default)]
517pub struct ExportConfig {
518    /// Minimum lines of code to include.
519    pub min_code: Option<usize>,
520
521    /// Maximum rows in output.
522    pub max_rows: Option<usize>,
523
524    /// Redaction mode: "none", "paths", or "all".
525    pub redact: Option<String>,
526
527    /// Output format: "jsonl", "csv", "json", "cyclonedx".
528    pub format: Option<String>,
529
530    /// Children handling: "collapse" or "separate".
531    pub children: Option<String>,
532}
533
534/// Analyze command settings.
535#[derive(Debug, Clone, Default, Serialize, Deserialize)]
536#[serde(default)]
537pub struct AnalyzeConfig {
538    /// Analysis preset.
539    pub preset: Option<String>,
540
541    /// Context window size for utilization analysis.
542    pub window: Option<usize>,
543
544    /// Output format.
545    pub format: Option<String>,
546
547    /// Force git metrics on/off.
548    pub git: Option<bool>,
549
550    /// Max files for asset/deps/content scans.
551    pub max_files: Option<usize>,
552
553    /// Max total bytes for content scans.
554    pub max_bytes: Option<u64>,
555
556    /// Max bytes per file for content scans.
557    pub max_file_bytes: Option<u64>,
558
559    /// Max commits for git metrics.
560    pub max_commits: Option<usize>,
561
562    /// Max files per commit for git metrics.
563    pub max_commit_files: Option<usize>,
564
565    /// Import graph granularity: "module" or "file".
566    pub granularity: Option<String>,
567
568    /// Effort model for estimate calculations.
569    pub effort_model: Option<String>,
570
571    /// Effort report layer.
572    pub effort_layer: Option<String>,
573
574    /// Base reference for effort delta computation.
575    pub effort_base_ref: Option<String>,
576
577    /// Head reference for effort delta computation.
578    pub effort_head_ref: Option<String>,
579
580    /// Enable Monte Carlo uncertainty for effort estimation.
581    pub effort_monte_carlo: Option<bool>,
582
583    /// Monte Carlo iterations for effort estimation.
584    pub effort_mc_iterations: Option<usize>,
585
586    /// Monte Carlo seed for effort estimation.
587    pub effort_mc_seed: Option<u64>,
588}
589
590/// Context command settings.
591#[derive(Debug, Clone, Default, Serialize, Deserialize)]
592#[serde(default)]
593pub struct ContextConfig {
594    /// Token budget with optional k/m suffix.
595    pub budget: Option<String>,
596
597    /// Packing strategy: "greedy" or "spread".
598    pub strategy: Option<String>,
599
600    /// Ranking metric: "code", "tokens", "churn", "hotspot".
601    pub rank_by: Option<String>,
602
603    /// Output mode: "list", "bundle", "json".
604    pub output: Option<String>,
605
606    /// Strip blank lines from bundle output.
607    pub compress: Option<bool>,
608}
609
610/// Badge command settings.
611#[derive(Debug, Clone, Default, Serialize, Deserialize)]
612#[serde(default)]
613pub struct BadgeConfig {
614    /// Default metric for badges.
615    pub metric: Option<String>,
616}
617
618/// Gate command settings.
619#[derive(Debug, Clone, Default, Serialize, Deserialize)]
620#[serde(default)]
621pub struct GateConfig {
622    /// Path to policy file.
623    pub policy: Option<String>,
624
625    /// Path to baseline file for ratchet comparison.
626    pub baseline: Option<String>,
627
628    /// Analysis preset for compute-then-gate mode.
629    pub preset: Option<String>,
630
631    /// Fail fast on first error.
632    pub fail_fast: Option<bool>,
633
634    /// Inline policy rules.
635    pub rules: Option<Vec<GateRule>>,
636
637    /// Inline ratchet rules for baseline comparison.
638    pub ratchet: Option<Vec<RatchetRuleConfig>>,
639
640    /// Allow missing baseline values (treat as pass).
641    pub allow_missing_baseline: Option<bool>,
642
643    /// Allow missing current values (treat as pass).
644    pub allow_missing_current: Option<bool>,
645}
646
647/// A single ratchet rule for baseline comparison (TOML configuration).
648#[derive(Debug, Clone, Serialize, Deserialize)]
649pub struct RatchetRuleConfig {
650    /// JSON Pointer to the metric (e.g., "/complexity/avg_cyclomatic").
651    pub pointer: String,
652
653    /// Maximum allowed percentage increase from baseline.
654    #[serde(default)]
655    pub max_increase_pct: Option<f64>,
656
657    /// Maximum allowed absolute value (hard ceiling).
658    #[serde(default)]
659    pub max_value: Option<f64>,
660
661    /// Rule severity level: "error" (default) or "warn".
662    #[serde(default)]
663    pub level: Option<String>,
664
665    /// Human-readable description of the rule.
666    #[serde(default)]
667    pub description: Option<String>,
668}
669
670/// A single gate policy rule (for inline TOML configuration).
671#[derive(Debug, Clone, Serialize, Deserialize)]
672pub struct GateRule {
673    /// Human-readable name for the rule.
674    pub name: String,
675
676    /// JSON Pointer to the value to check (RFC 6901).
677    pub pointer: String,
678
679    /// Comparison operator.
680    pub op: String,
681
682    /// Single value for comparison.
683    #[serde(default)]
684    pub value: Option<serde_json::Value>,
685
686    /// Multiple values for "in" operator.
687    #[serde(default)]
688    pub values: Option<Vec<serde_json::Value>>,
689
690    /// Negate the result.
691    #[serde(default)]
692    pub negate: bool,
693
694    /// Rule severity level: "error" or "warn".
695    #[serde(default)]
696    pub level: Option<String>,
697
698    /// Custom failure message.
699    #[serde(default)]
700    pub message: Option<String>,
701}
702
703/// A named view profile that can override settings for specific use cases.
704#[derive(Debug, Clone, Default, Serialize, Deserialize)]
705#[serde(default)]
706pub struct ViewProfile {
707    // Shared settings
708    /// Output format.
709    pub format: Option<String>,
710
711    /// Show only top N rows.
712    pub top: Option<usize>,
713
714    // Lang settings
715    /// Include file counts in lang output.
716    pub files: Option<bool>,
717
718    // Module / Export settings
719    /// Module roots for grouping.
720    pub module_roots: Option<Vec<String>>,
721
722    /// Module depth for grouping.
723    pub module_depth: Option<usize>,
724
725    /// Minimum lines of code.
726    pub min_code: Option<usize>,
727
728    /// Maximum rows in output.
729    pub max_rows: Option<usize>,
730
731    /// Redaction mode.
732    pub redact: Option<String>,
733
734    /// Include metadata record.
735    pub meta: Option<bool>,
736
737    /// Children handling mode.
738    pub children: Option<String>,
739
740    // Analyze settings
741    /// Analysis preset.
742    pub preset: Option<String>,
743
744    /// Context window size.
745    pub window: Option<usize>,
746
747    // Context settings
748    /// Token budget.
749    pub budget: Option<String>,
750
751    /// Packing strategy.
752    pub strategy: Option<String>,
753
754    /// Ranking metric.
755    pub rank_by: Option<String>,
756
757    /// Output mode for context.
758    pub output: Option<String>,
759
760    /// Strip blank lines.
761    pub compress: Option<bool>,
762
763    // Badge settings
764    /// Badge metric.
765    pub metric: Option<String>,
766}
767
768impl TomlConfig {
769    /// Load configuration from a TOML string.
770    pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
771        toml::from_str(s)
772    }
773
774    /// Load configuration from a file path.
775    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
782/// Result type alias for TOML parsing errors.
783pub 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        // Verify that ScanOptions fields are accessible through ScanSettings
814        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}