1pub mod cockpit;
32
33use std::path::PathBuf;
34
35use serde::{Deserialize, Serialize};
36
37pub const SCHEMA_VERSION: u32 = 2;
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64pub struct Totals {
65 pub code: usize,
66 pub lines: usize,
67 pub files: usize,
68 pub bytes: usize,
69 pub tokens: usize,
70 pub avg_lines: usize,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
93pub struct LangRow {
94 pub lang: String,
95 pub code: usize,
96 pub lines: usize,
97 pub files: usize,
98 pub bytes: usize,
99 pub tokens: usize,
100 pub avg_lines: usize,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct LangReport {
139 pub rows: Vec<LangRow>,
140 pub total: Totals,
141 pub with_files: bool,
142 pub children: ChildrenMode,
143 pub top: usize,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
166pub struct ModuleRow {
167 pub module: String,
168 pub code: usize,
169 pub lines: usize,
170 pub files: usize,
171 pub bytes: usize,
172 pub tokens: usize,
173 pub avg_lines: usize,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct ModuleReport {
178 pub rows: Vec<ModuleRow>,
179 pub total: Totals,
180 pub module_roots: Vec<String>,
181 pub module_depth: usize,
182 pub children: ChildIncludeMode,
183 pub top: usize,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
187#[serde(rename_all = "snake_case")]
188pub enum FileKind {
189 Parent,
190 Child,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
217pub struct FileRow {
218 pub path: String,
219 pub module: String,
220 pub lang: String,
221 pub kind: FileKind,
222 pub code: usize,
223 pub comments: usize,
224 pub blanks: usize,
225 pub lines: usize,
226 pub bytes: usize,
227 pub tokens: usize,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct ExportData {
260 pub rows: Vec<FileRow>,
261 pub module_roots: Vec<String>,
262 pub module_depth: usize,
263 pub children: ChildIncludeMode,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct RunReceipt {
268 pub schema_version: u32,
269 pub generated_at_ms: u128,
270 pub lang_file: String,
271 pub module_file: String,
272 pub export_file: String,
273 }
275
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277#[serde(rename_all = "snake_case")]
278pub enum ScanStatus {
279 Complete,
280 Partial,
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
289#[serde(rename_all = "snake_case")]
290pub enum CommitIntentKind {
291 Feat,
292 Fix,
293 Refactor,
294 Docs,
295 Test,
296 Chore,
297 Ci,
298 Build,
299 Perf,
300 Style,
301 Revert,
302 Other,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, Default)]
306pub struct ToolInfo {
307 pub name: String,
308 pub version: String,
309}
310
311impl ToolInfo {
312 pub fn current() -> Self {
313 Self {
314 name: "tokmd".to_string(),
315 version: env!("CARGO_PKG_VERSION").to_string(),
316 }
317 }
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct ScanArgs {
322 pub paths: Vec<String>,
323 pub excluded: Vec<String>,
324 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
326 pub excluded_redacted: bool,
327 pub config: ConfigMode,
328 pub hidden: bool,
329 pub no_ignore: bool,
330 pub no_ignore_parent: bool,
331 pub no_ignore_dot: bool,
332 pub no_ignore_vcs: bool,
333 pub treat_doc_strings_as_comments: bool,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct LangArgsMeta {
338 pub format: String,
339 pub top: usize,
340 pub with_files: bool,
341 pub children: ChildrenMode,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct LangReceipt {
346 pub schema_version: u32,
347 pub generated_at_ms: u128,
348 pub tool: ToolInfo,
349 pub mode: String, pub status: ScanStatus,
351 pub warnings: Vec<String>,
352 pub scan: ScanArgs,
353 pub args: LangArgsMeta,
354 #[serde(flatten)]
355 pub report: LangReport,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct ModuleArgsMeta {
360 pub format: String,
361 pub module_roots: Vec<String>,
362 pub module_depth: usize,
363 pub children: ChildIncludeMode,
364 pub top: usize,
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct ModuleReceipt {
369 pub schema_version: u32,
370 pub generated_at_ms: u128,
371 pub tool: ToolInfo,
372 pub mode: String, pub status: ScanStatus,
374 pub warnings: Vec<String>,
375 pub scan: ScanArgs,
376 pub args: ModuleArgsMeta,
377 #[serde(flatten)]
378 pub report: ModuleReport,
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct ExportArgsMeta {
383 pub format: ExportFormat,
384 pub module_roots: Vec<String>,
385 pub module_depth: usize,
386 pub children: ChildIncludeMode,
387 pub min_code: usize,
388 pub max_rows: usize,
389 pub redact: RedactMode,
390 pub strip_prefix: Option<String>,
391 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
393 pub strip_prefix_redacted: bool,
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize)]
397pub struct ExportReceipt {
398 pub schema_version: u32,
399 pub generated_at_ms: u128,
400 pub tool: ToolInfo,
401 pub mode: String, pub status: ScanStatus,
403 pub warnings: Vec<String>,
404 pub scan: ScanArgs,
405 pub args: ExportArgsMeta,
406 #[serde(flatten)]
407 pub data: ExportData,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct LangArgs {
412 pub paths: Vec<PathBuf>,
413 pub format: TableFormat,
414 pub top: usize,
415 pub files: bool,
416 pub children: ChildrenMode,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct ModuleArgs {
421 pub paths: Vec<PathBuf>,
422 pub format: TableFormat,
423 pub top: usize,
424 pub module_roots: Vec<String>,
425 pub module_depth: usize,
426 pub children: ChildIncludeMode,
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct ExportArgs {
431 pub paths: Vec<PathBuf>,
432 pub format: ExportFormat,
433 pub output: Option<PathBuf>,
434 pub module_roots: Vec<String>,
435 pub module_depth: usize,
436 pub children: ChildIncludeMode,
437 pub min_code: usize,
438 pub max_rows: usize,
439 pub redact: RedactMode,
440 pub meta: bool,
441 pub strip_prefix: Option<PathBuf>,
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct ContextReceipt {
446 pub schema_version: u32,
447 pub generated_at_ms: u128,
448 pub tool: ToolInfo,
449 pub mode: String,
450 pub budget_tokens: usize,
451 pub used_tokens: usize,
452 pub utilization_pct: f64,
453 pub strategy: String,
454 pub rank_by: String,
455 pub file_count: usize,
456 pub files: Vec<ContextFileRow>,
457 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub rank_by_effective: Option<String>,
460 #[serde(default, skip_serializing_if = "Option::is_none")]
462 pub fallback_reason: Option<String>,
463 #[serde(default, skip_serializing_if = "Vec::is_empty")]
465 pub excluded_by_policy: Vec<PolicyExcludedFile>,
466 #[serde(default, skip_serializing_if = "Option::is_none")]
468 pub token_estimation: Option<TokenEstimationMeta>,
469 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub bundle_audit: Option<TokenAudit>,
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct ContextFileRow {
476 pub path: String,
477 pub module: String,
478 pub lang: String,
479 pub tokens: usize,
480 pub code: usize,
481 pub lines: usize,
482 pub bytes: usize,
483 pub value: usize,
484 #[serde(default, skip_serializing_if = "String::is_empty")]
485 pub rank_reason: String,
486 #[serde(default, skip_serializing_if = "is_default_policy")]
488 pub policy: InclusionPolicy,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
491 pub effective_tokens: Option<usize>,
492 #[serde(default, skip_serializing_if = "Option::is_none")]
494 pub policy_reason: Option<String>,
495 #[serde(default, skip_serializing_if = "Vec::is_empty")]
497 pub classifications: Vec<FileClassification>,
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
522pub struct DiffRow {
523 pub lang: String,
524 pub old_code: usize,
525 pub new_code: usize,
526 pub delta_code: i64,
527 pub old_lines: usize,
528 pub new_lines: usize,
529 pub delta_lines: i64,
530 pub old_files: usize,
531 pub new_files: usize,
532 pub delta_files: i64,
533 pub old_bytes: usize,
534 pub new_bytes: usize,
535 pub delta_bytes: i64,
536 pub old_tokens: usize,
537 pub new_tokens: usize,
538 pub delta_tokens: i64,
539}
540
541#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
554pub struct DiffTotals {
555 pub old_code: usize,
556 pub new_code: usize,
557 pub delta_code: i64,
558 pub old_lines: usize,
559 pub new_lines: usize,
560 pub delta_lines: i64,
561 pub old_files: usize,
562 pub new_files: usize,
563 pub delta_files: i64,
564 pub old_bytes: usize,
565 pub new_bytes: usize,
566 pub delta_bytes: i64,
567 pub old_tokens: usize,
568 pub new_tokens: usize,
569 pub delta_tokens: i64,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
574pub struct DiffReceipt {
575 pub schema_version: u32,
576 pub generated_at_ms: u128,
577 pub tool: ToolInfo,
578 pub mode: String,
579 pub from_source: String,
580 pub to_source: String,
581 pub diff_rows: Vec<DiffRow>,
582 pub totals: DiffTotals,
583}
584
585#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
590#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
591#[serde(rename_all = "kebab-case")]
592pub enum TableFormat {
593 Md,
595 Tsv,
597 Json,
599}
600
601#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
602#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
603#[serde(rename_all = "kebab-case")]
604pub enum ExportFormat {
605 Csv,
607 Jsonl,
609 Json,
611 Cyclonedx,
613}
614
615#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
616#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
617#[serde(rename_all = "kebab-case")]
618pub enum ConfigMode {
619 #[default]
621 Auto,
622 None,
624}
625
626#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
627#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
628#[serde(rename_all = "kebab-case")]
629pub enum ChildrenMode {
630 Collapse,
632 Separate,
634}
635
636#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
637#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
638#[serde(rename_all = "kebab-case")]
639pub enum ChildIncludeMode {
640 Separate,
642 ParentsOnly,
644}
645
646#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
647#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
648#[serde(rename_all = "kebab-case")]
649pub enum RedactMode {
650 None,
652 Paths,
654 All,
656}
657
658#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
659#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
660#[serde(rename_all = "kebab-case")]
661pub enum AnalysisFormat {
662 Md,
663 Json,
664 Jsonld,
665 Xml,
666 Svg,
667 Mermaid,
668 Obj,
669 Midi,
670 Tree,
671 Html,
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize)]
677pub struct ContextLogRecord {
678 pub schema_version: u32,
679 pub generated_at_ms: u128,
680 pub tool: ToolInfo,
681 pub budget_tokens: usize,
682 pub used_tokens: usize,
683 pub utilization_pct: f64,
684 pub strategy: String,
685 pub rank_by: String,
686 pub file_count: usize,
687 pub total_bytes: usize,
688 pub output_destination: String,
689}
690
691pub const HANDOFF_SCHEMA_VERSION: u32 = 5;
701
702pub const CONTEXT_BUNDLE_SCHEMA_VERSION: u32 = 2;
708
709pub const CONTEXT_SCHEMA_VERSION: u32 = 4;
715
716#[derive(Debug, Clone, Serialize, Deserialize)]
728pub struct TokenEstimationMeta {
729 pub bytes_per_token_est: f64,
731 pub bytes_per_token_low: f64,
733 pub bytes_per_token_high: f64,
735 #[serde(alias = "tokens_high")]
737 pub tokens_min: usize,
738 pub tokens_est: usize,
740 #[serde(alias = "tokens_low")]
742 pub tokens_max: usize,
743 pub source_bytes: usize,
745}
746
747impl TokenEstimationMeta {
748 pub const DEFAULT_BPT_EST: f64 = 4.0;
750 pub const DEFAULT_BPT_LOW: f64 = 3.0;
751 pub const DEFAULT_BPT_HIGH: f64 = 5.0;
752
753 pub fn from_bytes(bytes: usize, bpt: f64) -> Self {
768 Self::from_bytes_with_bounds(bytes, bpt, Self::DEFAULT_BPT_LOW, Self::DEFAULT_BPT_HIGH)
769 }
770
771 pub fn from_bytes_with_bounds(bytes: usize, bpt_est: f64, bpt_low: f64, bpt_high: f64) -> Self {
773 Self {
774 bytes_per_token_est: bpt_est,
775 bytes_per_token_low: bpt_low,
776 bytes_per_token_high: bpt_high,
777 tokens_min: (bytes as f64 / bpt_high).ceil() as usize,
778 tokens_est: (bytes as f64 / bpt_est).ceil() as usize,
779 tokens_max: (bytes as f64 / bpt_low).ceil() as usize,
780 source_bytes: bytes,
781 }
782 }
783}
784
785#[derive(Debug, Clone, Serialize, Deserialize)]
787pub struct TokenAudit {
788 pub output_bytes: u64,
790 #[serde(alias = "tokens_high")]
792 pub tokens_min: usize,
793 pub tokens_est: usize,
795 #[serde(alias = "tokens_low")]
797 pub tokens_max: usize,
798 pub overhead_bytes: u64,
800 pub overhead_pct: f64,
802}
803
804impl TokenAudit {
805 pub fn from_output(output_bytes: u64, content_bytes: u64) -> Self {
818 Self::from_output_with_divisors(
819 output_bytes,
820 content_bytes,
821 TokenEstimationMeta::DEFAULT_BPT_EST,
822 TokenEstimationMeta::DEFAULT_BPT_LOW,
823 TokenEstimationMeta::DEFAULT_BPT_HIGH,
824 )
825 }
826
827 pub fn from_output_with_divisors(
829 output_bytes: u64,
830 content_bytes: u64,
831 bpt_est: f64,
832 bpt_low: f64,
833 bpt_high: f64,
834 ) -> Self {
835 let overhead_bytes = output_bytes.saturating_sub(content_bytes);
836 let overhead_pct = if output_bytes > 0 {
837 overhead_bytes as f64 / output_bytes as f64
838 } else {
839 0.0
840 };
841 Self {
842 output_bytes,
843 tokens_min: (output_bytes as f64 / bpt_high).ceil() as usize,
844 tokens_est: (output_bytes as f64 / bpt_est).ceil() as usize,
845 tokens_max: (output_bytes as f64 / bpt_low).ceil() as usize,
846 overhead_bytes,
847 overhead_pct,
848 }
849 }
850}
851
852#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
858#[serde(rename_all = "snake_case")]
859pub enum FileClassification {
860 Generated,
862 Fixture,
864 Vendored,
866 Lockfile,
868 Minified,
870 DataBlob,
872 Sourcemap,
874}
875
876#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
878#[serde(rename_all = "snake_case")]
879pub enum InclusionPolicy {
880 #[default]
882 Full,
883 HeadTail,
885 Summary,
887 Skip,
889}
890
891fn is_default_policy(policy: &InclusionPolicy) -> bool {
893 *policy == InclusionPolicy::Full
894}
895
896#[derive(Debug, Clone, Serialize, Deserialize)]
898pub struct PolicyExcludedFile {
899 pub path: String,
900 pub original_tokens: usize,
901 pub policy: InclusionPolicy,
902 pub reason: String,
903 pub classifications: Vec<FileClassification>,
904}
905
906#[derive(Debug, Clone, Serialize, Deserialize)]
908pub struct HandoffManifest {
909 pub schema_version: u32,
910 pub generated_at_ms: u128,
911 pub tool: ToolInfo,
912 pub mode: String,
913 pub inputs: Vec<String>,
914 pub output_dir: String,
915 pub budget_tokens: usize,
916 pub used_tokens: usize,
917 pub utilization_pct: f64,
918 pub strategy: String,
919 pub rank_by: String,
920 pub capabilities: Vec<CapabilityStatus>,
921 pub artifacts: Vec<ArtifactEntry>,
922 pub included_files: Vec<ContextFileRow>,
923 pub excluded_paths: Vec<HandoffExcludedPath>,
924 pub excluded_patterns: Vec<String>,
925 pub smart_excluded_files: Vec<SmartExcludedFile>,
926 pub total_files: usize,
927 pub bundled_files: usize,
928 pub intelligence_preset: String,
929 #[serde(default, skip_serializing_if = "Option::is_none")]
931 pub rank_by_effective: Option<String>,
932 #[serde(default, skip_serializing_if = "Option::is_none")]
934 pub fallback_reason: Option<String>,
935 #[serde(default, skip_serializing_if = "Vec::is_empty")]
937 pub excluded_by_policy: Vec<PolicyExcludedFile>,
938 #[serde(default, skip_serializing_if = "Option::is_none")]
940 pub token_estimation: Option<TokenEstimationMeta>,
941 #[serde(default, skip_serializing_if = "Option::is_none")]
943 pub code_audit: Option<TokenAudit>,
944}
945
946#[derive(Debug, Clone, Serialize, Deserialize)]
948pub struct SmartExcludedFile {
949 pub path: String,
950 pub reason: String,
951 pub tokens: usize,
952}
953
954#[derive(Debug, Clone, Serialize, Deserialize)]
956pub struct ContextBundleManifest {
957 pub schema_version: u32,
958 pub generated_at_ms: u128,
959 pub tool: ToolInfo,
960 pub mode: String,
961 pub budget_tokens: usize,
962 pub used_tokens: usize,
963 pub utilization_pct: f64,
964 pub strategy: String,
965 pub rank_by: String,
966 pub file_count: usize,
967 pub bundle_bytes: usize,
968 pub artifacts: Vec<ArtifactEntry>,
969 pub included_files: Vec<ContextFileRow>,
970 pub excluded_paths: Vec<ContextExcludedPath>,
971 pub excluded_patterns: Vec<String>,
972 #[serde(default, skip_serializing_if = "Option::is_none")]
974 pub rank_by_effective: Option<String>,
975 #[serde(default, skip_serializing_if = "Option::is_none")]
977 pub fallback_reason: Option<String>,
978 #[serde(default, skip_serializing_if = "Vec::is_empty")]
980 pub excluded_by_policy: Vec<PolicyExcludedFile>,
981 #[serde(default, skip_serializing_if = "Option::is_none")]
983 pub token_estimation: Option<TokenEstimationMeta>,
984 #[serde(default, skip_serializing_if = "Option::is_none")]
986 pub bundle_audit: Option<TokenAudit>,
987}
988
989#[derive(Debug, Clone, Serialize, Deserialize)]
991pub struct ContextExcludedPath {
992 pub path: String,
993 pub reason: String,
994}
995
996#[derive(Debug, Clone, Serialize, Deserialize)]
998pub struct HandoffIntelligence {
999 pub tree: Option<String>,
1000 pub tree_depth: Option<usize>,
1001 pub hotspots: Option<Vec<HandoffHotspot>>,
1002 pub complexity: Option<HandoffComplexity>,
1003 pub derived: Option<HandoffDerived>,
1004 pub warnings: Vec<String>,
1005}
1006
1007#[derive(Debug, Clone, Serialize, Deserialize)]
1009pub struct HandoffExcludedPath {
1010 pub path: String,
1011 pub reason: String,
1012}
1013
1014#[derive(Debug, Clone, Serialize, Deserialize)]
1016pub struct HandoffHotspot {
1017 pub path: String,
1018 pub commits: usize,
1019 pub lines: usize,
1020 pub score: usize,
1021}
1022
1023#[derive(Debug, Clone, Serialize, Deserialize)]
1025pub struct HandoffComplexity {
1026 pub total_functions: usize,
1027 pub avg_function_length: f64,
1028 pub max_function_length: usize,
1029 pub avg_cyclomatic: f64,
1030 pub max_cyclomatic: usize,
1031 pub high_risk_files: usize,
1032}
1033
1034#[derive(Debug, Clone, Serialize, Deserialize)]
1036pub struct HandoffDerived {
1037 pub total_files: usize,
1038 pub total_code: usize,
1039 pub total_lines: usize,
1040 pub total_tokens: usize,
1041 pub lang_count: usize,
1042 pub dominant_lang: String,
1043 pub dominant_pct: f64,
1044}
1045
1046#[derive(Debug, Clone, Serialize, Deserialize)]
1048pub struct CapabilityStatus {
1049 pub name: String,
1050 pub status: CapabilityState,
1051 #[serde(skip_serializing_if = "Option::is_none")]
1052 pub reason: Option<String>,
1053}
1054
1055#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1057#[serde(rename_all = "snake_case")]
1058pub enum CapabilityState {
1059 Available,
1061 Skipped,
1063 Unavailable,
1065}
1066
1067#[derive(Debug, Clone, Serialize, Deserialize)]
1069pub struct ArtifactEntry {
1070 pub name: String,
1071 pub path: String,
1072 pub description: String,
1073 pub bytes: u64,
1074 #[serde(skip_serializing_if = "Option::is_none")]
1075 pub hash: Option<ArtifactHash>,
1076}
1077
1078#[derive(Debug, Clone, Serialize, Deserialize)]
1080pub struct ArtifactHash {
1081 pub algo: String,
1082 pub hash: String,
1083}
1084
1085#[cfg(test)]
1086mod tests {
1087 use super::*;
1088
1089 #[test]
1091 fn schema_version_constants() {
1092 assert_eq!(SCHEMA_VERSION, 2);
1093 assert_eq!(HANDOFF_SCHEMA_VERSION, 5);
1094 assert_eq!(CONTEXT_BUNDLE_SCHEMA_VERSION, 2);
1095 assert_eq!(CONTEXT_SCHEMA_VERSION, 4);
1096 }
1097
1098 #[test]
1100 fn config_mode_default_is_auto() {
1101 assert_eq!(ConfigMode::default(), ConfigMode::Auto);
1102 }
1103
1104 #[test]
1105 fn inclusion_policy_default_is_full() {
1106 assert_eq!(InclusionPolicy::default(), InclusionPolicy::Full);
1107 }
1108
1109 #[test]
1110 fn diff_totals_default_is_zeroed() {
1111 let dt = DiffTotals::default();
1112 assert_eq!(dt.old_code, 0);
1113 assert_eq!(dt.new_code, 0);
1114 assert_eq!(dt.delta_code, 0);
1115 assert_eq!(dt.delta_tokens, 0);
1116 }
1117
1118 #[test]
1119 fn tool_info_default_is_empty() {
1120 let ti = ToolInfo::default();
1121 assert!(ti.name.is_empty());
1122 assert!(ti.version.is_empty());
1123 }
1124
1125 #[test]
1126 fn tool_info_current() {
1127 let ti = ToolInfo::current();
1128 assert_eq!(ti.name, "tokmd");
1129 assert!(!ti.version.is_empty());
1130 }
1131
1132 #[test]
1134 fn table_format_serde_roundtrip() {
1135 for variant in [TableFormat::Md, TableFormat::Tsv, TableFormat::Json] {
1136 let json = serde_json::to_string(&variant).unwrap();
1137 let back: TableFormat = serde_json::from_str(&json).unwrap();
1138 assert_eq!(back, variant);
1139 }
1140 }
1141
1142 #[test]
1143 fn export_format_serde_roundtrip() {
1144 for variant in [
1145 ExportFormat::Csv,
1146 ExportFormat::Jsonl,
1147 ExportFormat::Json,
1148 ExportFormat::Cyclonedx,
1149 ] {
1150 let json = serde_json::to_string(&variant).unwrap();
1151 let back: ExportFormat = serde_json::from_str(&json).unwrap();
1152 assert_eq!(back, variant);
1153 }
1154 }
1155
1156 #[test]
1157 fn config_mode_serde_roundtrip() {
1158 for variant in [ConfigMode::Auto, ConfigMode::None] {
1159 let json = serde_json::to_string(&variant).unwrap();
1160 let back: ConfigMode = serde_json::from_str(&json).unwrap();
1161 assert_eq!(back, variant);
1162 }
1163 }
1164
1165 #[test]
1166 fn children_mode_serde_roundtrip() {
1167 for variant in [ChildrenMode::Collapse, ChildrenMode::Separate] {
1168 let json = serde_json::to_string(&variant).unwrap();
1169 let back: ChildrenMode = serde_json::from_str(&json).unwrap();
1170 assert_eq!(back, variant);
1171 }
1172 }
1173
1174 #[test]
1175 fn redact_mode_serde_roundtrip() {
1176 for variant in [RedactMode::None, RedactMode::Paths, RedactMode::All] {
1177 let json = serde_json::to_string(&variant).unwrap();
1178 let back: RedactMode = serde_json::from_str(&json).unwrap();
1179 assert_eq!(back, variant);
1180 }
1181 }
1182
1183 #[test]
1184 fn file_kind_serde_roundtrip() {
1185 for variant in [FileKind::Parent, FileKind::Child] {
1186 let json = serde_json::to_string(&variant).unwrap();
1187 let back: FileKind = serde_json::from_str(&json).unwrap();
1188 assert_eq!(back, variant);
1189 }
1190 }
1191
1192 #[test]
1193 fn scan_status_serde_roundtrip() {
1194 let json = serde_json::to_string(&ScanStatus::Complete).unwrap();
1195 assert_eq!(json, "\"complete\"");
1196 let back: ScanStatus = serde_json::from_str(&json).unwrap();
1197 assert!(matches!(back, ScanStatus::Complete));
1198 }
1199
1200 #[test]
1201 fn file_classification_serde_roundtrip() {
1202 for variant in [
1203 FileClassification::Generated,
1204 FileClassification::Fixture,
1205 FileClassification::Vendored,
1206 FileClassification::Lockfile,
1207 FileClassification::Minified,
1208 FileClassification::DataBlob,
1209 FileClassification::Sourcemap,
1210 ] {
1211 let json = serde_json::to_string(&variant).unwrap();
1212 let back: FileClassification = serde_json::from_str(&json).unwrap();
1213 assert_eq!(back, variant);
1214 }
1215 }
1216
1217 #[test]
1218 fn inclusion_policy_serde_roundtrip() {
1219 for variant in [
1220 InclusionPolicy::Full,
1221 InclusionPolicy::HeadTail,
1222 InclusionPolicy::Summary,
1223 InclusionPolicy::Skip,
1224 ] {
1225 let json = serde_json::to_string(&variant).unwrap();
1226 let back: InclusionPolicy = serde_json::from_str(&json).unwrap();
1227 assert_eq!(back, variant);
1228 }
1229 }
1230
1231 #[test]
1232 fn capability_state_serde_roundtrip() {
1233 for variant in [
1234 CapabilityState::Available,
1235 CapabilityState::Skipped,
1236 CapabilityState::Unavailable,
1237 ] {
1238 let json = serde_json::to_string(&variant).unwrap();
1239 let back: CapabilityState = serde_json::from_str(&json).unwrap();
1240 assert_eq!(back, variant);
1241 }
1242 }
1243
1244 #[test]
1245 fn analysis_format_serde_roundtrip() {
1246 for variant in [
1247 AnalysisFormat::Md,
1248 AnalysisFormat::Json,
1249 AnalysisFormat::Jsonld,
1250 AnalysisFormat::Xml,
1251 AnalysisFormat::Svg,
1252 AnalysisFormat::Mermaid,
1253 AnalysisFormat::Obj,
1254 AnalysisFormat::Midi,
1255 AnalysisFormat::Tree,
1256 AnalysisFormat::Html,
1257 ] {
1258 let json = serde_json::to_string(&variant).unwrap();
1259 let back: AnalysisFormat = serde_json::from_str(&json).unwrap();
1260 assert_eq!(back, variant);
1261 }
1262 }
1263
1264 #[test]
1265 fn commit_intent_kind_serde_roundtrip() {
1266 for variant in [
1267 CommitIntentKind::Feat,
1268 CommitIntentKind::Fix,
1269 CommitIntentKind::Refactor,
1270 CommitIntentKind::Docs,
1271 CommitIntentKind::Test,
1272 CommitIntentKind::Chore,
1273 CommitIntentKind::Ci,
1274 CommitIntentKind::Other,
1275 ] {
1276 let json = serde_json::to_string(&variant).unwrap();
1277 let back: CommitIntentKind = serde_json::from_str(&json).unwrap();
1278 assert_eq!(back, variant);
1279 }
1280 }
1281
1282 #[test]
1284 fn is_default_policy_works() {
1285 assert!(is_default_policy(&InclusionPolicy::Full));
1286 assert!(!is_default_policy(&InclusionPolicy::Skip));
1287 assert!(!is_default_policy(&InclusionPolicy::Summary));
1288 assert!(!is_default_policy(&InclusionPolicy::HeadTail));
1289 }
1290
1291 #[test]
1293 fn totals_serde_roundtrip() {
1294 let t = Totals {
1295 code: 100,
1296 lines: 200,
1297 files: 10,
1298 bytes: 5000,
1299 tokens: 250,
1300 avg_lines: 20,
1301 };
1302 let json = serde_json::to_string(&t).unwrap();
1303 let back: Totals = serde_json::from_str(&json).unwrap();
1304 assert_eq!(back, t);
1305 }
1306
1307 #[test]
1308 fn lang_row_serde_roundtrip() {
1309 let r = LangRow {
1310 lang: "Rust".into(),
1311 code: 100,
1312 lines: 150,
1313 files: 5,
1314 bytes: 3000,
1315 tokens: 200,
1316 avg_lines: 30,
1317 };
1318 let json = serde_json::to_string(&r).unwrap();
1319 let back: LangRow = serde_json::from_str(&json).unwrap();
1320 assert_eq!(back, r);
1321 }
1322
1323 #[test]
1324 fn diff_row_serde_roundtrip() {
1325 let r = DiffRow {
1326 lang: "Rust".into(),
1327 old_code: 100,
1328 new_code: 120,
1329 delta_code: 20,
1330 old_lines: 200,
1331 new_lines: 220,
1332 delta_lines: 20,
1333 old_files: 10,
1334 new_files: 11,
1335 delta_files: 1,
1336 old_bytes: 5000,
1337 new_bytes: 6000,
1338 delta_bytes: 1000,
1339 old_tokens: 250,
1340 new_tokens: 300,
1341 delta_tokens: 50,
1342 };
1343 let json = serde_json::to_string(&r).unwrap();
1344 let back: DiffRow = serde_json::from_str(&json).unwrap();
1345 assert_eq!(back, r);
1346 }
1347
1348 #[test]
1349 fn diff_totals_serde_roundtrip() {
1350 let t = DiffTotals {
1351 old_code: 100,
1352 new_code: 120,
1353 delta_code: 20,
1354 ..DiffTotals::default()
1355 };
1356 let json = serde_json::to_string(&t).unwrap();
1357 let back: DiffTotals = serde_json::from_str(&json).unwrap();
1358 assert_eq!(back, t);
1359 }
1360
1361 #[test]
1363 fn token_estimation_from_bytes_defaults() {
1364 let est = TokenEstimationMeta::from_bytes(4000, TokenEstimationMeta::DEFAULT_BPT_EST);
1365 assert_eq!(est.source_bytes, 4000);
1366 assert_eq!(est.tokens_est, 1000); assert_eq!(est.tokens_min, 800);
1369 assert_eq!(est.tokens_max, 1334);
1371 }
1372
1373 #[test]
1374 fn token_estimation_invariant_min_le_est_le_max() {
1375 let est = TokenEstimationMeta::from_bytes(12345, 4.0);
1376 assert!(est.tokens_min <= est.tokens_est);
1377 assert!(est.tokens_est <= est.tokens_max);
1378 }
1379
1380 #[test]
1381 fn token_estimation_zero_bytes() {
1382 let est = TokenEstimationMeta::from_bytes(0, 4.0);
1383 assert_eq!(est.tokens_min, 0);
1384 assert_eq!(est.tokens_est, 0);
1385 assert_eq!(est.tokens_max, 0);
1386 }
1387
1388 #[test]
1389 fn token_estimation_with_custom_bounds() {
1390 let est = TokenEstimationMeta::from_bytes_with_bounds(1000, 4.0, 2.0, 8.0);
1391 assert_eq!(est.bytes_per_token_est, 4.0);
1392 assert_eq!(est.bytes_per_token_low, 2.0);
1393 assert_eq!(est.bytes_per_token_high, 8.0);
1394 assert_eq!(est.tokens_est, 250); assert_eq!(est.tokens_min, 125); assert_eq!(est.tokens_max, 500); }
1398
1399 #[test]
1401 fn token_audit_from_output_basic() {
1402 let audit = TokenAudit::from_output(1000, 800);
1403 assert_eq!(audit.output_bytes, 1000);
1404 assert_eq!(audit.overhead_bytes, 200);
1405 assert!((audit.overhead_pct - 0.2).abs() < f64::EPSILON);
1406 }
1407
1408 #[test]
1409 fn token_audit_from_output_with_divisors() {
1410 let audit = TokenAudit::from_output_with_divisors(1000, 800, 4.0, 2.0, 8.0);
1411
1412 assert_eq!(audit.output_bytes, 1000);
1413 assert_eq!(audit.overhead_bytes, 200);
1414 assert_eq!(audit.tokens_est, 250);
1415 assert_eq!(audit.tokens_min, 125);
1416 assert_eq!(audit.tokens_max, 500);
1417 }
1418
1419 #[test]
1420 fn token_audit_zero_output() {
1421 let audit = TokenAudit::from_output(0, 0);
1422 assert_eq!(audit.output_bytes, 0);
1423 assert_eq!(audit.overhead_bytes, 0);
1424 assert_eq!(audit.overhead_pct, 0.0);
1425 }
1426
1427 #[test]
1428 fn token_audit_content_exceeds_output() {
1429 let audit = TokenAudit::from_output(100, 200);
1431 assert_eq!(audit.overhead_bytes, 0);
1432 assert_eq!(audit.overhead_pct, 0.0);
1433 }
1434
1435 #[test]
1436 fn token_audit_serde_roundtrip() {
1437 let audit = TokenAudit::from_output(5000, 4500);
1438 let json = serde_json::to_string(&audit).unwrap();
1439 let back: TokenAudit = serde_json::from_str(&json).unwrap();
1440 assert_eq!(back.output_bytes, 5000);
1441 assert_eq!(back.overhead_bytes, 500);
1442 }
1443
1444 #[test]
1446 fn table_format_uses_kebab_case() {
1447 assert_eq!(serde_json::to_string(&TableFormat::Md).unwrap(), "\"md\"");
1448 assert_eq!(serde_json::to_string(&TableFormat::Tsv).unwrap(), "\"tsv\"");
1449 }
1450
1451 #[test]
1452 fn export_format_uses_kebab_case() {
1453 assert_eq!(
1454 serde_json::to_string(&ExportFormat::Cyclonedx).unwrap(),
1455 "\"cyclonedx\""
1456 );
1457 }
1458
1459 #[test]
1460 fn redact_mode_uses_kebab_case() {
1461 assert_eq!(
1462 serde_json::to_string(&RedactMode::Paths).unwrap(),
1463 "\"paths\""
1464 );
1465 }
1466
1467 #[test]
1469 fn file_row_serde_roundtrip() {
1470 let r = FileRow {
1471 path: "src/main.rs".into(),
1472 module: "src".into(),
1473 lang: "Rust".into(),
1474 kind: FileKind::Parent,
1475 code: 50,
1476 comments: 10,
1477 blanks: 5,
1478 lines: 65,
1479 bytes: 2000,
1480 tokens: 100,
1481 };
1482 let json = serde_json::to_string(&r).unwrap();
1483 let back: FileRow = serde_json::from_str(&json).unwrap();
1484 assert_eq!(back, r);
1485 }
1486}
1487
1488#[cfg(doctest)]
1489#[doc = include_str!("../README.md")]
1490pub mod readme_doctests {}