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-config)
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/// Scan options shared by all commands that invoke the scanner.
28///
29/// This mirrors the scan-relevant fields of `GlobalArgs` without any
30/// UI-specific fields (`verbose`, `no_progress`). Lower-tier crates
31/// (scan, format, model) depend on this instead of `tokmd-config`.
32#[derive(Debug, Clone, Default, Serialize, Deserialize)]
33pub struct ScanOptions {
34    /// Glob patterns to exclude.
35    #[serde(default)]
36    pub excluded: Vec<String>,
37
38    /// Whether to load scan config files (`tokei.toml` / `.tokeirc`).
39    #[serde(default)]
40    pub config: ConfigMode,
41
42    /// Count hidden files and directories.
43    #[serde(default)]
44    pub hidden: bool,
45
46    /// Don't respect ignore files (.gitignore, .ignore, etc.).
47    #[serde(default)]
48    pub no_ignore: bool,
49
50    /// Don't respect ignore files in parent directories.
51    #[serde(default)]
52    pub no_ignore_parent: bool,
53
54    /// Don't respect .ignore and .tokeignore files.
55    #[serde(default)]
56    pub no_ignore_dot: bool,
57
58    /// Don't respect VCS ignore files (.gitignore, .hgignore, etc.).
59    #[serde(default)]
60    pub no_ignore_vcs: bool,
61
62    /// Treat doc strings as comments.
63    #[serde(default)]
64    pub treat_doc_strings_as_comments: bool,
65}
66
67/// Global scan settings shared by all operations.
68#[derive(Debug, Clone, Default, Serialize, Deserialize)]
69pub struct ScanSettings {
70    /// Paths to scan (defaults to `["."]`).
71    #[serde(default)]
72    pub paths: Vec<String>,
73
74    /// Scan options (excludes, ignore flags, etc.).
75    #[serde(flatten)]
76    pub options: ScanOptions,
77}
78
79impl ScanSettings {
80    /// Create settings for scanning the current directory with defaults.
81    pub fn current_dir() -> Self {
82        Self {
83            paths: vec![".".to_string()],
84            ..Default::default()
85        }
86    }
87
88    /// Create settings for scanning specific paths.
89    pub fn for_paths(paths: Vec<String>) -> Self {
90        Self {
91            paths,
92            ..Default::default()
93        }
94    }
95}
96
97/// Settings for language summary (`tokmd lang`).
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct LangSettings {
100    /// Show only the top N rows (0 = all).
101    #[serde(default)]
102    pub top: usize,
103
104    /// Include file counts and average lines per file.
105    #[serde(default)]
106    pub files: bool,
107
108    /// How to handle embedded languages.
109    #[serde(default = "default_children_mode")]
110    pub children: ChildrenMode,
111
112    /// Redaction mode for output.
113    #[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/// Settings for module summary (`tokmd module`).
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct ModuleSettings {
135    /// Show only the top N modules (0 = all).
136    #[serde(default)]
137    pub top: usize,
138
139    /// Top-level directories as "module roots".
140    #[serde(default = "default_module_roots")]
141    pub module_roots: Vec<String>,
142
143    /// Path segments to include for module roots.
144    #[serde(default = "default_module_depth")]
145    pub module_depth: usize,
146
147    /// How to handle embedded languages.
148    #[serde(default = "default_child_include_mode")]
149    pub children: ChildIncludeMode,
150
151    /// Redaction mode for output.
152    #[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/// Settings for file-level export (`tokmd export`).
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct ExportSettings {
183    /// Output format.
184    #[serde(default = "default_export_format")]
185    pub format: ExportFormat,
186
187    /// Module roots (see `ModuleSettings`).
188    #[serde(default = "default_module_roots")]
189    pub module_roots: Vec<String>,
190
191    /// Module depth (see `ModuleSettings`).
192    #[serde(default = "default_module_depth")]
193    pub module_depth: usize,
194
195    /// How to handle embedded languages.
196    #[serde(default = "default_child_include_mode")]
197    pub children: ChildIncludeMode,
198
199    /// Drop rows with fewer than N code lines.
200    #[serde(default)]
201    pub min_code: usize,
202
203    /// Stop after emitting N rows (0 = unlimited).
204    #[serde(default)]
205    pub max_rows: usize,
206
207    /// Redaction mode.
208    #[serde(default = "default_redact_mode")]
209    pub redact: RedactMode,
210
211    /// Include a meta record.
212    #[serde(default = "default_meta")]
213    pub meta: bool,
214
215    /// Strip this prefix from paths.
216    #[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/// Settings for analysis (`tokmd analyze`).
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct AnalyzeSettings {
251    /// Analysis preset to run.
252    #[serde(default = "default_preset")]
253    pub preset: String,
254
255    /// Context window size (tokens) for utilization bars.
256    #[serde(default)]
257    pub window: Option<usize>,
258
259    /// Force-enable git-based metrics.
260    #[serde(default)]
261    pub git: Option<bool>,
262
263    /// Limit files walked for asset/deps/content scans.
264    #[serde(default)]
265    pub max_files: Option<usize>,
266
267    /// Limit total bytes read during content scans.
268    #[serde(default)]
269    pub max_bytes: Option<u64>,
270
271    /// Limit bytes per file during content scans.
272    #[serde(default)]
273    pub max_file_bytes: Option<u64>,
274
275    /// Limit commits scanned for git metrics.
276    #[serde(default)]
277    pub max_commits: Option<usize>,
278
279    /// Limit files per commit for git metrics.
280    #[serde(default)]
281    pub max_commit_files: Option<usize>,
282
283    /// Import graph granularity.
284    #[serde(default = "default_granularity")]
285    pub granularity: String,
286
287    /// Effort model for estimate calculations.
288    #[serde(default)]
289    pub effort_model: Option<String>,
290
291    /// Effort report layer.
292    #[serde(default)]
293    pub effort_layer: Option<String>,
294
295    /// Base reference for effort delta computation.
296    #[serde(default)]
297    pub effort_base_ref: Option<String>,
298
299    /// Head reference for effort delta computation.
300    #[serde(default)]
301    pub effort_head_ref: Option<String>,
302
303    /// Enable Monte Carlo uncertainty for effort estimation.
304    #[serde(default)]
305    pub effort_monte_carlo: Option<bool>,
306
307    /// Monte Carlo iterations for effort estimation.
308    #[serde(default)]
309    pub effort_mc_iterations: Option<usize>,
310
311    /// Monte Carlo seed for effort estimation.
312    #[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/// Settings for cockpit PR metrics (`tokmd cockpit`).
348#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct CockpitSettings {
350    /// Base ref to compare from.
351    #[serde(default = "default_cockpit_base")]
352    pub base: String,
353
354    /// Head ref to compare to.
355    #[serde(default = "default_cockpit_head")]
356    pub head: String,
357
358    /// Range mode: "two-dot" or "three-dot".
359    #[serde(default = "default_cockpit_range_mode")]
360    pub range_mode: String,
361
362    /// Optional baseline file path for trend comparison.
363    #[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/// Settings for diff comparison (`tokmd diff`).
391#[derive(Debug, Clone, Default, Serialize, Deserialize)]
392pub struct DiffSettings {
393    /// Base reference to compare from.
394    pub from: String,
395
396    /// Target reference to compare to.
397    pub to: String,
398}
399
400// =============================================================================
401// TOML Configuration File Structures
402// =============================================================================
403
404/// Root TOML configuration structure.
405#[derive(Debug, Clone, Default, Serialize, Deserialize)]
406#[serde(default)]
407pub struct TomlConfig {
408    /// Scan settings (applies to all commands).
409    pub scan: ScanConfig,
410
411    /// Module command settings.
412    pub module: ModuleConfig,
413
414    /// Export command settings.
415    pub export: ExportConfig,
416
417    /// Analyze command settings.
418    pub analyze: AnalyzeConfig,
419
420    /// Context command settings.
421    pub context: ContextConfig,
422
423    /// Badge command settings.
424    pub badge: BadgeConfig,
425
426    /// Gate command settings.
427    pub gate: GateConfig,
428
429    /// Named view profiles (e.g., [view.llm], [view.ci]).
430    #[serde(default)]
431    pub view: BTreeMap<String, ViewProfile>,
432}
433
434/// Scan settings shared by all commands.
435#[derive(Debug, Clone, Default, Serialize, Deserialize)]
436#[serde(default)]
437pub struct ScanConfig {
438    /// Paths to scan (default: ["."])
439    pub paths: Option<Vec<String>>,
440
441    /// Glob patterns to exclude.
442    pub exclude: Option<Vec<String>>,
443
444    /// Include hidden files and directories.
445    pub hidden: Option<bool>,
446
447    /// Config file strategy for tokei: "auto" or "none".
448    pub config: Option<String>,
449
450    /// Disable all ignore files.
451    pub no_ignore: Option<bool>,
452
453    /// Disable parent directory ignore file traversal.
454    pub no_ignore_parent: Option<bool>,
455
456    /// Disable .ignore/.tokeignore files.
457    pub no_ignore_dot: Option<bool>,
458
459    /// Disable .gitignore files.
460    pub no_ignore_vcs: Option<bool>,
461
462    /// Treat doc comments as comments instead of code.
463    pub doc_comments: Option<bool>,
464}
465
466/// Module command settings.
467#[derive(Debug, Clone, Default, Serialize, Deserialize)]
468#[serde(default)]
469pub struct ModuleConfig {
470    /// Root directories for module grouping.
471    pub roots: Option<Vec<String>>,
472
473    /// Depth for module grouping.
474    pub depth: Option<usize>,
475
476    /// Children handling: "collapse" or "separate".
477    pub children: Option<String>,
478}
479
480/// Export command settings.
481#[derive(Debug, Clone, Default, Serialize, Deserialize)]
482#[serde(default)]
483pub struct ExportConfig {
484    /// Minimum lines of code to include.
485    pub min_code: Option<usize>,
486
487    /// Maximum rows in output.
488    pub max_rows: Option<usize>,
489
490    /// Redaction mode: "none", "paths", or "all".
491    pub redact: Option<String>,
492
493    /// Output format: "jsonl", "csv", "json", "cyclonedx".
494    pub format: Option<String>,
495
496    /// Children handling: "collapse" or "separate".
497    pub children: Option<String>,
498}
499
500/// Analyze command settings.
501#[derive(Debug, Clone, Default, Serialize, Deserialize)]
502#[serde(default)]
503pub struct AnalyzeConfig {
504    /// Analysis preset.
505    pub preset: Option<String>,
506
507    /// Context window size for utilization analysis.
508    pub window: Option<usize>,
509
510    /// Output format.
511    pub format: Option<String>,
512
513    /// Force git metrics on/off.
514    pub git: Option<bool>,
515
516    /// Max files for asset/deps/content scans.
517    pub max_files: Option<usize>,
518
519    /// Max total bytes for content scans.
520    pub max_bytes: Option<u64>,
521
522    /// Max bytes per file for content scans.
523    pub max_file_bytes: Option<u64>,
524
525    /// Max commits for git metrics.
526    pub max_commits: Option<usize>,
527
528    /// Max files per commit for git metrics.
529    pub max_commit_files: Option<usize>,
530
531    /// Import graph granularity: "module" or "file".
532    pub granularity: Option<String>,
533
534    /// Effort model for estimate calculations.
535    pub effort_model: Option<String>,
536
537    /// Effort report layer.
538    pub effort_layer: Option<String>,
539
540    /// Base reference for effort delta computation.
541    pub effort_base_ref: Option<String>,
542
543    /// Head reference for effort delta computation.
544    pub effort_head_ref: Option<String>,
545
546    /// Enable Monte Carlo uncertainty for effort estimation.
547    pub effort_monte_carlo: Option<bool>,
548
549    /// Monte Carlo iterations for effort estimation.
550    pub effort_mc_iterations: Option<usize>,
551
552    /// Monte Carlo seed for effort estimation.
553    pub effort_mc_seed: Option<u64>,
554}
555
556/// Context command settings.
557#[derive(Debug, Clone, Default, Serialize, Deserialize)]
558#[serde(default)]
559pub struct ContextConfig {
560    /// Token budget with optional k/m suffix.
561    pub budget: Option<String>,
562
563    /// Packing strategy: "greedy" or "spread".
564    pub strategy: Option<String>,
565
566    /// Ranking metric: "code", "tokens", "churn", "hotspot".
567    pub rank_by: Option<String>,
568
569    /// Output mode: "list", "bundle", "json".
570    pub output: Option<String>,
571
572    /// Strip blank lines from bundle output.
573    pub compress: Option<bool>,
574}
575
576/// Badge command settings.
577#[derive(Debug, Clone, Default, Serialize, Deserialize)]
578#[serde(default)]
579pub struct BadgeConfig {
580    /// Default metric for badges.
581    pub metric: Option<String>,
582}
583
584/// Gate command settings.
585#[derive(Debug, Clone, Default, Serialize, Deserialize)]
586#[serde(default)]
587pub struct GateConfig {
588    /// Path to policy file.
589    pub policy: Option<String>,
590
591    /// Path to baseline file for ratchet comparison.
592    pub baseline: Option<String>,
593
594    /// Analysis preset for compute-then-gate mode.
595    pub preset: Option<String>,
596
597    /// Fail fast on first error.
598    pub fail_fast: Option<bool>,
599
600    /// Inline policy rules.
601    pub rules: Option<Vec<GateRule>>,
602
603    /// Inline ratchet rules for baseline comparison.
604    pub ratchet: Option<Vec<RatchetRuleConfig>>,
605
606    /// Allow missing baseline values (treat as pass).
607    pub allow_missing_baseline: Option<bool>,
608
609    /// Allow missing current values (treat as pass).
610    pub allow_missing_current: Option<bool>,
611}
612
613/// A single ratchet rule for baseline comparison (TOML configuration).
614#[derive(Debug, Clone, Serialize, Deserialize)]
615pub struct RatchetRuleConfig {
616    /// JSON Pointer to the metric (e.g., "/complexity/avg_cyclomatic").
617    pub pointer: String,
618
619    /// Maximum allowed percentage increase from baseline.
620    #[serde(default)]
621    pub max_increase_pct: Option<f64>,
622
623    /// Maximum allowed absolute value (hard ceiling).
624    #[serde(default)]
625    pub max_value: Option<f64>,
626
627    /// Rule severity level: "error" (default) or "warn".
628    #[serde(default)]
629    pub level: Option<String>,
630
631    /// Human-readable description of the rule.
632    #[serde(default)]
633    pub description: Option<String>,
634}
635
636/// A single gate policy rule (for inline TOML configuration).
637#[derive(Debug, Clone, Serialize, Deserialize)]
638pub struct GateRule {
639    /// Human-readable name for the rule.
640    pub name: String,
641
642    /// JSON Pointer to the value to check (RFC 6901).
643    pub pointer: String,
644
645    /// Comparison operator.
646    pub op: String,
647
648    /// Single value for comparison.
649    #[serde(default)]
650    pub value: Option<serde_json::Value>,
651
652    /// Multiple values for "in" operator.
653    #[serde(default)]
654    pub values: Option<Vec<serde_json::Value>>,
655
656    /// Negate the result.
657    #[serde(default)]
658    pub negate: bool,
659
660    /// Rule severity level: "error" or "warn".
661    #[serde(default)]
662    pub level: Option<String>,
663
664    /// Custom failure message.
665    #[serde(default)]
666    pub message: Option<String>,
667}
668
669/// A named view profile that can override settings for specific use cases.
670#[derive(Debug, Clone, Default, Serialize, Deserialize)]
671#[serde(default)]
672pub struct ViewProfile {
673    // Shared settings
674    /// Output format.
675    pub format: Option<String>,
676
677    /// Show only top N rows.
678    pub top: Option<usize>,
679
680    // Lang settings
681    /// Include file counts in lang output.
682    pub files: Option<bool>,
683
684    // Module / Export settings
685    /// Module roots for grouping.
686    pub module_roots: Option<Vec<String>>,
687
688    /// Module depth for grouping.
689    pub module_depth: Option<usize>,
690
691    /// Minimum lines of code.
692    pub min_code: Option<usize>,
693
694    /// Maximum rows in output.
695    pub max_rows: Option<usize>,
696
697    /// Redaction mode.
698    pub redact: Option<String>,
699
700    /// Include metadata record.
701    pub meta: Option<bool>,
702
703    /// Children handling mode.
704    pub children: Option<String>,
705
706    // Analyze settings
707    /// Analysis preset.
708    pub preset: Option<String>,
709
710    /// Context window size.
711    pub window: Option<usize>,
712
713    // Context settings
714    /// Token budget.
715    pub budget: Option<String>,
716
717    /// Packing strategy.
718    pub strategy: Option<String>,
719
720    /// Ranking metric.
721    pub rank_by: Option<String>,
722
723    /// Output mode for context.
724    pub output: Option<String>,
725
726    /// Strip blank lines.
727    pub compress: Option<bool>,
728
729    // Badge settings
730    /// Badge metric.
731    pub metric: Option<String>,
732}
733
734impl TomlConfig {
735    /// Load configuration from a TOML string.
736    pub fn parse(s: &str) -> Result<Self, toml::de::Error> {
737        toml::from_str(s)
738    }
739
740    /// Load configuration from a file path.
741    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
748/// Result type alias for TOML parsing errors.
749pub 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        // Verify that ScanOptions fields are accessible through ScanSettings
780        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}