Skip to main content

tokmd_config/
lib.rs

1//! # tokmd-config
2//!
3//! **Tier 4 (Configuration)**
4//!
5//! This crate defines the CLI arguments and configuration file structures.
6//! Currently it couples strict configuration schemas with Clap CLI parsing.
7//!
8//! ## What belongs here
9//! * Clap `Parser`, `Args`, `Subcommand` structs
10//! * Configuration file struct definitions (Serde)
11//! * Default values and enums
12//!
13//! ## What does NOT belong here
14//! * Business logic
15//! * I/O operations (except config file parsing)
16//! * Higher-tier crate dependencies
17//!
18//! ## Future Direction
19//! * Split into `tokmd-settings` (pure config) and `tokmd-cli` (Clap parsing)
20
21use 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/// tokmd — code awareness for AI contexts
33///
34/// A small, chat-friendly wrapper around tokei for extracting, summarizing, and shaping code telemetry.
35/// Run `tokmd` in any directory to get a high-level summary of the code.
36/// Use `tokmd [COMMAND] --help` for detailed help.
37///
38/// Default mode (no subcommand) prints a language summary.
39#[derive(Parser, Debug)]
40#[command(name = "tokmd", version, long_about = None)]
41pub struct Cli {
42    #[command(flatten)]
43    pub global: GlobalArgs,
44
45    /// Default options for the implicit `lang` mode (when no subcommand is provided).
46    #[command(flatten)]
47    pub lang: CliLangArgs,
48
49    #[command(subcommand)]
50    pub command: Option<Commands>,
51
52    /// Configuration profile to use (e.g., "llm_safe", "ci").
53    #[arg(long, visible_alias = "view", global = true)]
54    pub profile: Option<String>,
55}
56
57#[derive(Args, Debug, Clone, Default)]
58pub struct GlobalArgs {
59    /// Exclude pattern(s) using gitignore syntax. Repeatable.
60    ///
61    /// Examples:
62    ///   --exclude target
63    ///   --exclude "**/*.min.js"
64    #[arg(
65        long = "exclude",
66        visible_alias = "ignore",
67        value_name = "PATTERN",
68        global = true
69    )]
70    pub excluded: Vec<String>,
71
72    /// Whether to load scan config files (`tokei.toml` / `.tokeirc`).
73    #[arg(long, value_enum, value_name = "MODE", default_value_t = ConfigMode::Auto)]
74    pub config: ConfigMode,
75
76    /// Count hidden files and directories.
77    #[arg(long)]
78    pub hidden: bool,
79
80    /// Don't respect ignore files (.gitignore, .ignore, etc.).
81    ///
82    /// Implies --no-ignore-parent, --no-ignore-dot, and --no-ignore-vcs.
83    #[arg(long)]
84    pub no_ignore: bool,
85
86    /// Don't respect ignore files in parent directories.
87    #[arg(long)]
88    pub no_ignore_parent: bool,
89
90    /// Don't respect .ignore and .tokeignore files (including in parent directories).
91    #[arg(long)]
92    pub no_ignore_dot: bool,
93
94    /// Don't respect VCS ignore files (.gitignore, .hgignore, etc.), including in parents.
95    #[arg(long, visible_alias = "no-ignore-git")]
96    pub no_ignore_vcs: bool,
97
98    /// Treat doc strings as comments (language-dependent).
99    #[arg(long)]
100    pub treat_doc_strings_as_comments: bool,
101
102    /// Verbose output (repeat for more detail).
103    #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
104    pub verbose: u8,
105
106    /// Disable progress spinners.
107    #[arg(long, global = true)]
108    pub no_progress: bool,
109}
110
111#[derive(Subcommand, Debug, Clone)]
112pub enum Commands {
113    /// Language summary (default).
114    Lang(CliLangArgs),
115
116    /// Module summary (group by path prefixes like `crates/<name>` or `packages/<name>`).
117    Module(CliModuleArgs),
118
119    /// Export a file-level dataset (CSV / JSONL / JSON).
120    Export(CliExportArgs),
121
122    /// Analyze receipts or paths to produce derived metrics.
123    Analyze(CliAnalyzeArgs),
124
125    /// Render a simple SVG badge for a metric.
126    Badge(BadgeArgs),
127
128    /// Write a `.tokeignore` template to the target directory.
129    Init(InitArgs),
130
131    /// Generate shell completions.
132    Completions(CompletionsArgs),
133
134    /// Run a full scan and save receipts to a state directory.
135    Run(RunArgs),
136
137    /// Compare two receipts or runs.
138    Diff(DiffArgs),
139
140    /// Pack files into an LLM context window within a token budget.
141    Context(CliContextArgs),
142
143    /// Check why a file is being ignored (for troubleshooting).
144    CheckIgnore(CliCheckIgnoreArgs),
145
146    /// Output CLI schema as JSON for AI agents.
147    Tools(ToolsArgs),
148
149    /// Evaluate policy rules against analysis receipts.
150    Gate(CliGateArgs),
151
152    /// Generate PR cockpit metrics for code review.
153    Cockpit(CockpitArgs),
154
155    /// Generate a complexity baseline for trend tracking.
156    Baseline(BaselineArgs),
157
158    /// Bundle codebase for LLM handoff.
159    Handoff(HandoffArgs),
160
161    /// Run as a conforming sensor, producing a SensorReport.
162    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>, // "owner/repo" -> "profile_name"
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, Default)]
172pub struct Profile {
173    // Shared
174    pub format: Option<String>, // "json", "md", "tsv", "csv", "jsonl"
175    pub top: Option<usize>,
176
177    // Lang
178    pub files: Option<bool>,
179
180    // Module / Export
181    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    // "children" can be ChildrenMode or ChildIncludeMode string
189    pub children: Option<String>,
190}
191
192#[derive(Args, Debug, Clone)]
193pub struct RunArgs {
194    /// Paths to scan.
195    #[arg(value_name = "PATH", default_value = ".")]
196    pub paths: Vec<PathBuf>,
197
198    /// Output directory for artifacts (defaults to `.runs/tokmd` inside the repo, or system temp if not possible).
199    #[arg(long)]
200    pub output_dir: Option<PathBuf>,
201
202    /// Tag or name for this run.
203    #[arg(long)]
204    pub name: Option<String>,
205
206    /// Also emit analysis receipts using this preset.
207    #[arg(long, value_enum)]
208    pub analysis: Option<AnalysisPreset>,
209
210    /// Redact paths (and optionally module names) for safer copy/paste into LLMs.
211    #[arg(long, value_enum)]
212    pub redact: Option<RedactMode>,
213}
214
215#[derive(Args, Debug, Clone)]
216pub struct DiffArgs {
217    /// Base receipt/run or git ref to compare from.
218    #[arg(long)]
219    pub from: Option<String>,
220
221    /// Target receipt/run or git ref to compare to.
222    #[arg(long)]
223    pub to: Option<String>,
224
225    /// Two refs/paths to compare (positional).
226    #[arg(value_name = "REF", num_args = 2)]
227    pub refs: Vec<String>,
228
229    /// Output format.
230    #[arg(long, value_enum, default_value_t = DiffFormat::Md)]
231    pub format: DiffFormat,
232
233    /// Compact output for narrow terminals (summary table only).
234    #[arg(long)]
235    pub compact: bool,
236
237    /// Color policy for terminal output.
238    #[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    /// Markdown table output.
246    #[default]
247    Md,
248    /// JSON receipt with envelope metadata.
249    Json,
250}
251
252#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
253#[serde(rename_all = "kebab-case")]
254pub enum ColorMode {
255    /// Enable color when stdout is a TTY and color env vars allow it.
256    #[default]
257    Auto,
258    /// Always emit ANSI color.
259    Always,
260    /// Never emit ANSI color.
261    Never,
262}
263
264#[derive(Args, Debug, Clone)]
265pub struct CompletionsArgs {
266    /// Shell to generate completions for.
267    #[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    /// Paths to scan (directories, files, or globs). Defaults to "."
284    #[arg(value_name = "PATH")]
285    pub paths: Option<Vec<PathBuf>>,
286
287    /// Output format [default: md].
288    #[arg(long, value_enum)]
289    pub format: Option<TableFormat>,
290
291    /// Show only the top N rows (by code lines), plus an "Other" row if needed.
292    /// Use 0 to show all rows.
293    #[arg(long)]
294    pub top: Option<usize>,
295
296    /// Include file counts and average lines per file.
297    #[arg(long)]
298    pub files: bool,
299
300    /// How to handle embedded languages (tokei "children" / blobs) [default: collapse].
301    #[arg(long, value_enum)]
302    pub children: Option<ChildrenMode>,
303}
304
305#[derive(Args, Debug, Clone)]
306pub struct CliModuleArgs {
307    /// Paths to scan (directories, files, or globs). Defaults to "."
308    #[arg(value_name = "PATH")]
309    pub paths: Option<Vec<PathBuf>>,
310
311    /// Output format [default: md].
312    #[arg(long, value_enum)]
313    pub format: Option<TableFormat>,
314
315    /// Show only the top N modules (by code lines), plus an "Other" row if needed.
316    /// Use 0 to show all rows.
317    #[arg(long)]
318    pub top: Option<usize>,
319
320    /// Treat these top-level directories as "module roots" [default: crates,packages].
321    ///
322    /// If a file path starts with one of these roots, the module key will include
323    /// `module_depth` segments. Otherwise, the module key is the top-level directory.
324    #[arg(long, value_delimiter = ',')]
325    pub module_roots: Option<Vec<String>>,
326
327    /// How many path segments to include for module roots [default: 2].
328    ///
329    /// Example:
330    ///   crates/foo/src/lib.rs  (depth=2) => crates/foo
331    ///   crates/foo/src/lib.rs  (depth=1) => crates
332    #[arg(long)]
333    pub module_depth: Option<usize>,
334
335    /// Whether to include embedded languages (tokei "children" / blobs) in module totals [default: separate].
336    #[arg(long, value_enum)]
337    pub children: Option<ChildIncludeMode>,
338}
339
340#[derive(Args, Debug, Clone)]
341pub struct CliExportArgs {
342    /// Paths to scan (directories, files, or globs). Defaults to "."
343    #[arg(value_name = "PATH")]
344    pub paths: Option<Vec<PathBuf>>,
345
346    /// Output format [default: jsonl].
347    #[arg(long, value_enum)]
348    pub format: Option<ExportFormat>,
349
350    /// Write output to this file instead of stdout.
351    #[arg(long, value_name = "PATH", visible_alias = "out")]
352    pub output: Option<PathBuf>,
353
354    /// Module roots (see `tokmd module`) [default: crates,packages].
355    #[arg(long, value_delimiter = ',')]
356    pub module_roots: Option<Vec<String>>,
357
358    /// Module depth (see `tokmd module`) [default: 2].
359    #[arg(long)]
360    pub module_depth: Option<usize>,
361
362    /// Whether to include embedded languages (tokei "children" / blobs) [default: separate].
363    #[arg(long, value_enum)]
364    pub children: Option<ChildIncludeMode>,
365
366    /// Drop rows with fewer than N code lines [default: 0].
367    #[arg(long)]
368    pub min_code: Option<usize>,
369
370    /// Stop after emitting N rows (0 = unlimited) [default: 0].
371    #[arg(long)]
372    pub max_rows: Option<usize>,
373
374    /// Include a meta record (JSON / JSONL only). Enabled by default.
375    #[arg(long, action = clap::ArgAction::Set)]
376    pub meta: Option<bool>,
377
378    /// Redact paths (and optionally module names) for safer copy/paste into LLMs [default: none].
379    #[arg(long, value_enum)]
380    pub redact: Option<RedactMode>,
381
382    /// Strip this prefix from paths before output (helps when paths are absolute).
383    #[arg(long, value_name = "PATH")]
384    pub strip_prefix: Option<PathBuf>,
385}
386
387#[derive(Args, Debug, Clone)]
388pub struct CliAnalyzeArgs {
389    /// Inputs to analyze (run dir, receipt.json, export.jsonl, or paths).
390    #[arg(value_name = "INPUT", default_value = ".")]
391    pub inputs: Vec<PathBuf>,
392
393    /// Analysis preset to run [default: receipt].
394    #[arg(long, value_enum)]
395    pub preset: Option<AnalysisPreset>,
396
397    /// Output format [default: md].
398    #[arg(long, value_enum)]
399    pub format: Option<AnalysisFormat>,
400
401    /// Context window size (tokens) for utilization bars.
402    #[arg(long)]
403    pub window: Option<usize>,
404
405    /// Force-enable git-based metrics.
406    #[arg(long, action = clap::ArgAction::SetTrue, conflicts_with = "no_git")]
407    pub git: bool,
408
409    /// Disable git-based metrics.
410    #[arg(long = "no-git", action = clap::ArgAction::SetTrue, conflicts_with = "git")]
411    pub no_git: bool,
412
413    /// Output directory for analysis artifacts.
414    #[arg(long)]
415    pub output_dir: Option<PathBuf>,
416
417    /// Limit how many files are walked for asset/deps/content scans.
418    #[arg(long)]
419    pub max_files: Option<usize>,
420
421    /// Limit total bytes read during content scans.
422    #[arg(long)]
423    pub max_bytes: Option<u64>,
424
425    /// Limit bytes per file during content scans.
426    #[arg(long)]
427    pub max_file_bytes: Option<u64>,
428
429    /// Limit how many commits are scanned for git metrics.
430    #[arg(long)]
431    pub max_commits: Option<usize>,
432
433    /// Limit files per commit when scanning git history.
434    #[arg(long)]
435    pub max_commit_files: Option<usize>,
436
437    /// Import graph granularity [default: module].
438    #[arg(long, value_enum)]
439    pub granularity: Option<ImportGranularity>,
440
441    /// Effort model for estimate calculations [default: cocomo81-basic].
442    #[arg(long)]
443    pub effort_model: Option<EffortModelKind>,
444
445    /// Effort layer for report detail [default: full].
446    #[arg(long)]
447    pub effort_layer: Option<EffortLayer>,
448
449    /// Base reference for effort delta computation.
450    #[arg(long = "effort-base-ref")]
451    pub effort_base_ref: Option<String>,
452
453    /// Head reference for effort delta computation.
454    #[arg(long = "effort-head-ref")]
455    pub effort_head_ref: Option<String>,
456
457    /// Enable Monte Carlo simulation for effort estimation.
458    #[arg(long)]
459    pub monte_carlo: bool,
460
461    /// Monte Carlo iterations when effort estimation is enabled [default: 10000].
462    #[arg(long = "mc-iterations")]
463    pub mc_iterations: Option<usize>,
464
465    /// Monte Carlo seed for deterministic effort estimation.
466    #[arg(long = "mc-seed")]
467    pub mc_seed: Option<u64>,
468
469    /// Include function-level complexity details in output.
470    #[arg(long)]
471    pub detail_functions: bool,
472
473    /// Enable near-duplicate file detection (opt-in).
474    #[arg(long)]
475    pub near_dup: bool,
476
477    /// Near-duplicate similarity threshold (0.0–1.0) [default: 0.80].
478    #[arg(long, default_value = "0.80")]
479    pub near_dup_threshold: f64,
480
481    /// Maximum files to analyze for near-duplicates [default: 2000].
482    #[arg(long, default_value = "2000")]
483    pub near_dup_max_files: usize,
484
485    /// Near-duplicate comparison scope [default: module].
486    #[arg(long, value_enum)]
487    pub near_dup_scope: Option<NearDupScope>,
488
489    /// Maximum near-duplicate pairs to emit (truncation guardrail) [default: 10000].
490    #[arg(long, default_value = "10000")]
491    pub near_dup_max_pairs: usize,
492
493    /// Exclude files matching this glob pattern from near-duplicate analysis. Repeatable.
494    #[arg(long, value_name = "GLOB")]
495    pub near_dup_exclude: Vec<String>,
496
497    /// Explain a metric or finding key and exit.
498    #[arg(long, value_name = "KEY")]
499    pub explain: Option<String>,
500}
501
502#[derive(Args, Debug, Clone)]
503pub struct BadgeArgs {
504    /// Inputs to analyze (run dir, receipt.json, export.jsonl, or paths).
505    #[arg(value_name = "INPUT", default_value = ".")]
506    pub inputs: Vec<PathBuf>,
507
508    /// Metric to render.
509    #[arg(long, value_enum)]
510    pub metric: BadgeMetric,
511
512    /// Optional analysis preset to use for the badge.
513    #[arg(long, value_enum)]
514    pub preset: Option<AnalysisPreset>,
515
516    /// Force-enable git-based metrics.
517    #[arg(long, action = clap::ArgAction::SetTrue, conflicts_with = "no_git")]
518    pub git: bool,
519
520    /// Disable git-based metrics.
521    #[arg(long = "no-git", action = clap::ArgAction::SetTrue, conflicts_with = "git")]
522    pub no_git: bool,
523
524    /// Limit how many commits are scanned for git metrics.
525    #[arg(long)]
526    pub max_commits: Option<usize>,
527
528    /// Limit files per commit when scanning git history.
529    #[arg(long)]
530    pub max_commit_files: Option<usize>,
531
532    /// Output file for the badge (defaults to stdout).
533    #[arg(long, visible_alias = "out")]
534    pub output: Option<PathBuf>,
535}
536
537#[derive(Args, Debug, Clone)]
538pub struct InitArgs {
539    /// Target directory (defaults to ".").
540    #[arg(long, value_name = "DIR", default_value = ".")]
541    pub dir: PathBuf,
542
543    /// Overwrite an existing `.tokeignore`.
544    #[arg(long)]
545    pub force: bool,
546
547    /// Print the template to stdout instead of writing a file.
548    #[arg(long)]
549    pub print: bool,
550
551    /// Which template profile to use.
552    #[arg(long, value_enum, default_value_t = InitProfile::Default)]
553    pub template: InitProfile,
554
555    /// Skip interactive wizard and use defaults.
556    #[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    /// Paths to scan (directories, files, or globs). Defaults to "."
626    #[arg(value_name = "PATH")]
627    pub paths: Option<Vec<PathBuf>>,
628
629    /// Token budget with optional k/m/g suffix, or 'unlimited' (e.g., "128k", "1m", "1g", "unlimited").
630    #[arg(long, default_value = "128k")]
631    pub budget: String,
632
633    /// Packing strategy.
634    #[arg(long, value_enum, default_value_t = ContextStrategy::Greedy)]
635    pub strategy: ContextStrategy,
636
637    /// Metric to rank files by.
638    #[arg(long, value_enum, default_value_t = ValueMetric::Code)]
639    pub rank_by: ValueMetric,
640
641    /// Output mode.
642    #[arg(long = "mode", value_enum, default_value_t = ContextOutput::List)]
643    pub output_mode: ContextOutput,
644
645    /// Strip blank lines from bundle output.
646    #[arg(long)]
647    pub compress: bool,
648
649    /// Disable smart exclusion of lockfiles, minified files, and generated artifacts.
650    #[arg(long)]
651    pub no_smart_exclude: bool,
652
653    /// Module roots (see `tokmd module`).
654    #[arg(long, value_delimiter = ',')]
655    pub module_roots: Option<Vec<String>>,
656
657    /// Module depth (see `tokmd module`).
658    #[arg(long)]
659    pub module_depth: Option<usize>,
660
661    /// Enable git-based ranking (required for churn/hotspot).
662    #[arg(long)]
663    pub git: bool,
664
665    /// Disable git-based ranking.
666    #[arg(long = "no-git")]
667    pub no_git: bool,
668
669    /// Maximum commits to scan for git metrics.
670    #[arg(long, default_value = "1000")]
671    pub max_commits: usize,
672
673    /// Maximum files per commit to process.
674    #[arg(long, default_value = "100")]
675    pub max_commit_files: usize,
676
677    /// Write output to file instead of stdout.
678    #[arg(long, value_name = "PATH", visible_alias = "out")]
679    pub output: Option<PathBuf>,
680
681    /// Overwrite existing output file.
682    #[arg(long)]
683    pub force: bool,
684
685    /// Write bundle to directory with manifest (for large outputs).
686    #[arg(long, value_name = "DIR", conflicts_with = "output")]
687    pub bundle_dir: Option<PathBuf>,
688
689    /// Warn if output exceeds N bytes (default: 10MB, 0=disable).
690    #[arg(long, default_value = "10485760")]
691    pub max_output_bytes: u64,
692
693    /// Append JSONL record to log file (metadata only, not content).
694    #[arg(long, value_name = "PATH")]
695    pub log: Option<PathBuf>,
696
697    /// Maximum fraction of budget a single file may consume (0.0–1.0).
698    #[arg(long, default_value = "0.15")]
699    pub max_file_pct: f64,
700
701    /// Hard cap on tokens per file (overrides percentage-based cap).
702    #[arg(long)]
703    pub max_file_tokens: Option<usize>,
704
705    /// Error if git scores are unavailable when using churn/hotspot ranking.
706    #[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    /// Select files by value until budget is exhausted.
714    #[default]
715    Greedy,
716    /// Round-robin across modules/languages for coverage, then greedy fill.
717    Spread,
718}
719
720#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
721#[serde(rename_all = "kebab-case")]
722pub enum ValueMetric {
723    /// Rank by lines of code.
724    #[default]
725    Code,
726    /// Rank by token count.
727    Tokens,
728    /// Rank by git churn (requires git feature).
729    Churn,
730    /// Rank by hotspot score (requires git feature).
731    Hotspot,
732}
733
734#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
735#[serde(rename_all = "kebab-case")]
736pub enum ContextOutput {
737    /// Print list of selected files with stats.
738    #[default]
739    List,
740    /// Concatenate file contents into a single bundle.
741    Bundle,
742    /// Output JSON receipt with selection details.
743    Json,
744}
745
746#[derive(Args, Debug, Clone)]
747pub struct CliCheckIgnoreArgs {
748    /// File path(s) to check.
749    #[arg(value_name = "PATH", required = true)]
750    pub paths: Vec<PathBuf>,
751
752    /// Show verbose output with rule sources.
753    #[arg(long, short = 'v')]
754    pub verbose: bool,
755}
756
757#[derive(Args, Debug, Clone)]
758pub struct ToolsArgs {
759    /// Output format for the tool schema.
760    #[arg(long, value_enum, default_value_t = ToolSchemaFormat::Jsonschema)]
761    pub format: ToolSchemaFormat,
762
763    /// Pretty-print JSON output.
764    #[arg(long)]
765    pub pretty: bool,
766}
767
768#[derive(Args, Debug, Clone)]
769pub struct CliGateArgs {
770    /// Input analysis receipt or path to scan.
771    #[arg(value_name = "INPUT")]
772    pub input: Option<PathBuf>,
773
774    /// Path to policy file (TOML format).
775    #[arg(long)]
776    pub policy: Option<PathBuf>,
777
778    /// Path to baseline receipt for ratchet comparison.
779    ///
780    /// When provided, gate will evaluate ratchet rules comparing current
781    /// metrics against the baseline values.
782    #[arg(long, value_name = "PATH")]
783    pub baseline: Option<PathBuf>,
784
785    /// Path to ratchet config file (TOML format).
786    ///
787    /// Defines rules for comparing current metrics against baseline.
788    /// Can also be specified inline in tokmd.toml under [[gate.ratchet]].
789    #[arg(long, value_name = "PATH")]
790    pub ratchet_config: Option<PathBuf>,
791
792    /// Analysis preset (for compute-then-gate mode).
793    #[arg(long, value_enum)]
794    pub preset: Option<AnalysisPreset>,
795
796    /// Output format.
797    #[arg(long, value_enum, default_value_t = GateFormat::Text)]
798    pub format: GateFormat,
799
800    /// Fail fast on first error.
801    #[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    /// Human-readable text output.
809    #[default]
810    Text,
811    /// JSON output.
812    Json,
813}
814
815#[derive(Args, Debug, Clone)]
816pub struct CockpitArgs {
817    /// Base reference to compare from (default: main).
818    #[arg(long, default_value = "main")]
819    pub base: String,
820
821    /// Head reference to compare to (default: HEAD).
822    #[arg(long, default_value = "HEAD")]
823    pub head: String,
824
825    /// Output format.
826    #[arg(long, value_enum, default_value_t = CockpitFormat::Json)]
827    pub format: CockpitFormat,
828
829    /// Output file (stdout if omitted).
830    #[arg(long, value_name = "PATH")]
831    pub output: Option<std::path::PathBuf>,
832
833    /// Write cockpit artifacts (`cockpit.json`, `report.json`, `comment.md`) to directory.
834    #[arg(long, value_name = "DIR")]
835    pub artifacts_dir: Option<std::path::PathBuf>,
836
837    /// Path to baseline receipt for trend comparison.
838    ///
839    /// When provided, cockpit will compute delta metrics showing how
840    /// the current state compares to the baseline.
841    #[arg(long, value_name = "PATH")]
842    pub baseline: Option<std::path::PathBuf>,
843
844    /// Diff range syntax: two-dot (default) or three-dot.
845    #[arg(long, value_enum, default_value_t = DiffRangeMode::TwoDot)]
846    pub diff_range: DiffRangeMode,
847
848    /// Run in sensor mode for CI integration.
849    ///
850    /// When enabled:
851    /// - Writes only sensor.report.v1 envelope to artifacts_dir/report.json
852    /// - Exits 0 if receipt written successfully (verdict in envelope instead of exit code)
853    #[arg(long)]
854    pub sensor_mode: bool,
855}
856
857#[derive(Args, Debug, Clone)]
858pub struct BaselineArgs {
859    /// Target path to analyze.
860    #[arg(default_value = ".")]
861    pub path: PathBuf,
862
863    /// Output path for baseline file.
864    #[arg(long, default_value = ".tokmd/baseline.json")]
865    pub output: PathBuf,
866
867    /// Include determinism baseline (hash build artifacts).
868    #[arg(long)]
869    pub determinism: bool,
870
871    /// Force overwrite existing baseline.
872    #[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    /// JSON output with full metrics.
880    #[default]
881    Json,
882    /// Markdown output for human readability.
883    Md,
884    /// Section-based output for PR template filling.
885    Sections,
886}
887
888#[derive(Args, Debug, Clone)]
889pub struct HandoffArgs {
890    /// Paths to scan (directories, files, or globs). Defaults to ".".
891    #[arg(value_name = "PATH")]
892    pub paths: Option<Vec<PathBuf>>,
893
894    /// Output directory for handoff artifacts.
895    #[arg(long, default_value = ".handoff")]
896    pub out_dir: PathBuf,
897
898    /// Token budget with optional k/m/g suffix, or 'unlimited' (e.g., "128k", "1m", "1g", "unlimited").
899    #[arg(long, default_value = "128k")]
900    pub budget: String,
901
902    /// Packing strategy for code bundle.
903    #[arg(long, value_enum, default_value_t = ContextStrategy::Greedy)]
904    pub strategy: ContextStrategy,
905
906    /// Metric to rank files by for packing.
907    #[arg(long, value_enum, default_value_t = ValueMetric::Hotspot)]
908    pub rank_by: ValueMetric,
909
910    /// Intelligence preset level.
911    #[arg(long, value_enum, default_value_t = HandoffPreset::Risk)]
912    pub preset: HandoffPreset,
913
914    /// Module roots (see `tokmd module`).
915    #[arg(long, value_delimiter = ',')]
916    pub module_roots: Option<Vec<String>>,
917
918    /// Module depth (see `tokmd module`).
919    #[arg(long)]
920    pub module_depth: Option<usize>,
921
922    /// Overwrite existing output directory.
923    #[arg(long)]
924    pub force: bool,
925
926    /// Strip blank lines from code bundle.
927    #[arg(long)]
928    pub compress: bool,
929
930    /// Disable smart exclusion of lockfiles, minified files, and generated artifacts.
931    #[arg(long)]
932    pub no_smart_exclude: bool,
933
934    /// Disable git-based features.
935    #[arg(long = "no-git")]
936    pub no_git: bool,
937
938    /// Maximum commits to scan for git metrics.
939    #[arg(long, default_value = "1000")]
940    pub max_commits: usize,
941
942    /// Maximum files per commit to process.
943    #[arg(long, default_value = "100")]
944    pub max_commit_files: usize,
945
946    /// Maximum fraction of budget a single file may consume (0.0–1.0).
947    #[arg(long, default_value = "0.15")]
948    pub max_file_pct: f64,
949
950    /// Hard cap on tokens per file (overrides percentage-based cap).
951    #[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: tree + map only.
959    Minimal,
960    /// Standard: + complexity, derived.
961    Standard,
962    /// Risk: + hotspots, coupling (default).
963    #[default]
964    Risk,
965    /// Deep: everything.
966    Deep,
967}
968
969#[derive(Args, Debug, Clone, Serialize, Deserialize)]
970pub struct SensorArgs {
971    /// Base reference to compare from (default: main).
972    #[arg(long, default_value = "main")]
973    pub base: String,
974
975    /// Head reference to compare to (default: HEAD).
976    #[arg(long, default_value = "HEAD")]
977    pub head: String,
978
979    /// Output file for the sensor report.
980    #[arg(
981        long,
982        value_name = "PATH",
983        default_value = "artifacts/tokmd/report.json"
984    )]
985    pub output: std::path::PathBuf,
986
987    /// Output format.
988    #[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    /// JSON sensor report.
996    #[default]
997    Json,
998    /// Markdown summary.
999    Md,
1000}
1001
1002#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1003#[serde(rename_all = "kebab-case")]
1004pub enum NearDupScope {
1005    /// Compare files within the same module.
1006    #[default]
1007    Module,
1008    /// Compare files within the same language.
1009    Lang,
1010    /// Compare all files globally.
1011    Global,
1012}
1013
1014#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1015#[serde(rename_all = "kebab-case")]
1016pub enum DiffRangeMode {
1017    /// Two-dot syntax (A..B) - direct diff between commits.
1018    #[default]
1019    TwoDot,
1020    /// Three-dot syntax (A...B) - diff from merge-base.
1021    ThreeDot,
1022}
1023
1024// =============================================================================
1025// TOML Configuration File Structures (re-exported from tokmd-settings)
1026// =============================================================================
1027
1028pub use tokmd_settings::{
1029    AnalyzeConfig, BadgeConfig, ContextConfig, ExportConfig, GateConfig, GateRule, ModuleConfig,
1030    RatchetRuleConfig, ScanConfig, TomlConfig, TomlResult, ViewProfile,
1031};
1032
1033// ============================================================
1034// Conversions between CLI GlobalArgs and Tier-0 ScanOptions
1035// ============================================================
1036
1037impl 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    // ── Default impls ─────────────────────────────────────────────────
1063    #[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    // ── Enum serde roundtrips ─────────────────────────────────────────
1106    #[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    // ── Serde naming ──────────────────────────────────────────────────
1193    #[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    // ── UserConfig serde roundtrip ────────────────────────────────────
1226    #[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    // ── GlobalArgs → ScanOptions conversion ───────────────────────────
1248    #[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}