Skip to main content

tokmd_types/
lib.rs

1//! # tokmd-types
2//!
3//! **Tier 0 (Core Types)**
4//!
5//! This crate defines the core data structures and contracts for `tokmd`.
6//! It contains only data types, Serde definitions, and `schema_version`.
7//!
8//! ## Stability Policy
9//!
10//! **JSON-first stability**: The primary contract is the JSON schema, not Rust struct literals.
11//!
12//! - **JSON consumers**: Stable. New fields have sensible defaults; removed/renamed fields
13//!   bump `SCHEMA_VERSION`.
14//! - **Rust library consumers**: Semi-stable. New fields may be added in minor versions,
15//!   which can break struct literal construction. Use `Default` + field mutation or
16//!   `..Default::default()` patterns for forward compatibility.
17//!
18//! If you need strict Rust API stability, pin to an exact version.
19//!
20//! ## What belongs here
21//! * Pure data structs (Receipts, Rows, Reports)
22//! * Serialization/Deserialization logic
23//! * Stability markers (SCHEMA_VERSION)
24//!
25//! ## What does NOT belong here
26//! * File I/O
27//! * CLI argument parsing
28//! * Complex business logic
29//! * Tokei dependencies
30
31pub mod cockpit;
32
33use std::path::PathBuf;
34
35use serde::{Deserialize, Serialize};
36
37/// The current schema version for all receipt types.
38pub const SCHEMA_VERSION: u32 = 2;
39
40/// A small totals struct shared by summary outputs.
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42pub struct Totals {
43    pub code: usize,
44    pub lines: usize,
45    pub files: usize,
46    pub bytes: usize,
47    pub tokens: usize,
48    pub avg_lines: usize,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52pub struct LangRow {
53    pub lang: String,
54    pub code: usize,
55    pub lines: usize,
56    pub files: usize,
57    pub bytes: usize,
58    pub tokens: usize,
59    pub avg_lines: usize,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct LangReport {
64    pub rows: Vec<LangRow>,
65    pub total: Totals,
66    pub with_files: bool,
67    pub children: ChildrenMode,
68    pub top: usize,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
72pub struct ModuleRow {
73    pub module: String,
74    pub code: usize,
75    pub lines: usize,
76    pub files: usize,
77    pub bytes: usize,
78    pub tokens: usize,
79    pub avg_lines: usize,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct ModuleReport {
84    pub rows: Vec<ModuleRow>,
85    pub total: Totals,
86    pub module_roots: Vec<String>,
87    pub module_depth: usize,
88    pub children: ChildIncludeMode,
89    pub top: usize,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
93#[serde(rename_all = "snake_case")]
94pub enum FileKind {
95    Parent,
96    Child,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
100pub struct FileRow {
101    pub path: String,
102    pub module: String,
103    pub lang: String,
104    pub kind: FileKind,
105    pub code: usize,
106    pub comments: usize,
107    pub blanks: usize,
108    pub lines: usize,
109    pub bytes: usize,
110    pub tokens: usize,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct ExportData {
115    pub rows: Vec<FileRow>,
116    pub module_roots: Vec<String>,
117    pub module_depth: usize,
118    pub children: ChildIncludeMode,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct RunReceipt {
123    pub schema_version: u32,
124    pub generated_at_ms: u128,
125    pub lang_file: String,
126    pub module_file: String,
127    pub export_file: String,
128    // We could store the scan args here too
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(rename_all = "snake_case")]
133pub enum ScanStatus {
134    Complete,
135    Partial,
136}
137
138/// Classification of a commit's intent, derived from subject line.
139///
140/// Lives in `tokmd-types` (Tier 0) so that both `tokmd-git` (Tier 2) and
141/// `tokmd-analysis-types` (Tier 0) can reference it without creating
142/// upward dependency edges.
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
144#[serde(rename_all = "snake_case")]
145pub enum CommitIntentKind {
146    Feat,
147    Fix,
148    Refactor,
149    Docs,
150    Test,
151    Chore,
152    Ci,
153    Build,
154    Perf,
155    Style,
156    Revert,
157    Other,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, Default)]
161pub struct ToolInfo {
162    pub name: String,
163    pub version: String,
164}
165
166impl ToolInfo {
167    pub fn current() -> Self {
168        Self {
169            name: "tokmd".to_string(),
170            version: env!("CARGO_PKG_VERSION").to_string(),
171        }
172    }
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct ScanArgs {
177    pub paths: Vec<String>,
178    pub excluded: Vec<String>,
179    /// True if `excluded` patterns were redacted (replaced with hashes).
180    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
181    pub excluded_redacted: bool,
182    pub config: ConfigMode,
183    pub hidden: bool,
184    pub no_ignore: bool,
185    pub no_ignore_parent: bool,
186    pub no_ignore_dot: bool,
187    pub no_ignore_vcs: bool,
188    pub treat_doc_strings_as_comments: bool,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct LangArgsMeta {
193    pub format: String,
194    pub top: usize,
195    pub with_files: bool,
196    pub children: ChildrenMode,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct LangReceipt {
201    pub schema_version: u32,
202    pub generated_at_ms: u128,
203    pub tool: ToolInfo,
204    pub mode: String, // "lang"
205    pub status: ScanStatus,
206    pub warnings: Vec<String>,
207    pub scan: ScanArgs,
208    pub args: LangArgsMeta,
209    #[serde(flatten)]
210    pub report: LangReport,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct ModuleArgsMeta {
215    pub format: String,
216    pub module_roots: Vec<String>,
217    pub module_depth: usize,
218    pub children: ChildIncludeMode,
219    pub top: usize,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct ModuleReceipt {
224    pub schema_version: u32,
225    pub generated_at_ms: u128,
226    pub tool: ToolInfo,
227    pub mode: String, // "module"
228    pub status: ScanStatus,
229    pub warnings: Vec<String>,
230    pub scan: ScanArgs,
231    pub args: ModuleArgsMeta,
232    #[serde(flatten)]
233    pub report: ModuleReport,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct ExportArgsMeta {
238    pub format: ExportFormat,
239    pub module_roots: Vec<String>,
240    pub module_depth: usize,
241    pub children: ChildIncludeMode,
242    pub min_code: usize,
243    pub max_rows: usize,
244    pub redact: RedactMode,
245    pub strip_prefix: Option<String>,
246    /// True if `strip_prefix` was redacted (replaced with a hash).
247    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
248    pub strip_prefix_redacted: bool,
249}
250
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct ExportReceipt {
253    pub schema_version: u32,
254    pub generated_at_ms: u128,
255    pub tool: ToolInfo,
256    pub mode: String, // "export"
257    pub status: ScanStatus,
258    pub warnings: Vec<String>,
259    pub scan: ScanArgs,
260    pub args: ExportArgsMeta,
261    #[serde(flatten)]
262    pub data: ExportData,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct LangArgs {
267    pub paths: Vec<PathBuf>,
268    pub format: TableFormat,
269    pub top: usize,
270    pub files: bool,
271    pub children: ChildrenMode,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct ModuleArgs {
276    pub paths: Vec<PathBuf>,
277    pub format: TableFormat,
278    pub top: usize,
279    pub module_roots: Vec<String>,
280    pub module_depth: usize,
281    pub children: ChildIncludeMode,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct ExportArgs {
286    pub paths: Vec<PathBuf>,
287    pub format: ExportFormat,
288    pub output: Option<PathBuf>,
289    pub module_roots: Vec<String>,
290    pub module_depth: usize,
291    pub children: ChildIncludeMode,
292    pub min_code: usize,
293    pub max_rows: usize,
294    pub redact: RedactMode,
295    pub meta: bool,
296    pub strip_prefix: Option<PathBuf>,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct ContextReceipt {
301    pub schema_version: u32,
302    pub generated_at_ms: u128,
303    pub tool: ToolInfo,
304    pub mode: String,
305    pub budget_tokens: usize,
306    pub used_tokens: usize,
307    pub utilization_pct: f64,
308    pub strategy: String,
309    pub rank_by: String,
310    pub file_count: usize,
311    pub files: Vec<ContextFileRow>,
312    /// Effective ranking metric (may differ from rank_by if fallback occurred).
313    #[serde(default, skip_serializing_if = "Option::is_none")]
314    pub rank_by_effective: Option<String>,
315    /// Reason for fallback if rank_by_effective differs from rank_by.
316    #[serde(default, skip_serializing_if = "Option::is_none")]
317    pub fallback_reason: Option<String>,
318    /// Files excluded by per-file cap / classification policy.
319    #[serde(default, skip_serializing_if = "Vec::is_empty")]
320    pub excluded_by_policy: Vec<PolicyExcludedFile>,
321    /// Token estimation envelope with uncertainty bounds.
322    #[serde(default, skip_serializing_if = "Option::is_none")]
323    pub token_estimation: Option<TokenEstimationMeta>,
324    /// Post-bundle audit comparing actual bytes to estimates.
325    #[serde(default, skip_serializing_if = "Option::is_none")]
326    pub bundle_audit: Option<TokenAudit>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct ContextFileRow {
331    pub path: String,
332    pub module: String,
333    pub lang: String,
334    pub tokens: usize,
335    pub code: usize,
336    pub lines: usize,
337    pub bytes: usize,
338    pub value: usize,
339    #[serde(default, skip_serializing_if = "String::is_empty")]
340    pub rank_reason: String,
341    /// Inclusion policy applied to this file.
342    #[serde(default, skip_serializing_if = "is_default_policy")]
343    pub policy: InclusionPolicy,
344    /// Effective token count when policy != Full (None means same as `tokens`).
345    #[serde(default, skip_serializing_if = "Option::is_none")]
346    pub effective_tokens: Option<usize>,
347    /// Reason for the applied policy.
348    #[serde(default, skip_serializing_if = "Option::is_none")]
349    pub policy_reason: Option<String>,
350    /// File classifications detected by hygiene analysis.
351    #[serde(default, skip_serializing_if = "Vec::is_empty")]
352    pub classifications: Vec<FileClassification>,
353}
354
355// -----------------------
356// Diff types
357// -----------------------
358
359/// A row in the diff output showing changes for a single language.
360#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
361pub struct DiffRow {
362    pub lang: String,
363    pub old_code: usize,
364    pub new_code: usize,
365    pub delta_code: i64,
366    pub old_lines: usize,
367    pub new_lines: usize,
368    pub delta_lines: i64,
369    pub old_files: usize,
370    pub new_files: usize,
371    pub delta_files: i64,
372    pub old_bytes: usize,
373    pub new_bytes: usize,
374    pub delta_bytes: i64,
375    pub old_tokens: usize,
376    pub new_tokens: usize,
377    pub delta_tokens: i64,
378}
379
380/// Aggregate totals for the diff.
381#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
382pub struct DiffTotals {
383    pub old_code: usize,
384    pub new_code: usize,
385    pub delta_code: i64,
386    pub old_lines: usize,
387    pub new_lines: usize,
388    pub delta_lines: i64,
389    pub old_files: usize,
390    pub new_files: usize,
391    pub delta_files: i64,
392    pub old_bytes: usize,
393    pub new_bytes: usize,
394    pub delta_bytes: i64,
395    pub old_tokens: usize,
396    pub new_tokens: usize,
397    pub delta_tokens: i64,
398}
399
400/// JSON receipt for diff output with envelope metadata.
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct DiffReceipt {
403    pub schema_version: u32,
404    pub generated_at_ms: u128,
405    pub tool: ToolInfo,
406    pub mode: String,
407    pub from_source: String,
408    pub to_source: String,
409    pub diff_rows: Vec<DiffRow>,
410    pub totals: DiffTotals,
411}
412
413// -----------------------------------------------------------------------------
414// Enums shared with CLI (moved from tokmd-config)
415// -----------------------------------------------------------------------------
416
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
418#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
419#[serde(rename_all = "kebab-case")]
420pub enum TableFormat {
421    /// Markdown table (great for pasting into ChatGPT).
422    Md,
423    /// Tab-separated values (good for piping to other tools).
424    Tsv,
425    /// JSON (compact).
426    Json,
427}
428
429#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
430#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
431#[serde(rename_all = "kebab-case")]
432pub enum ExportFormat {
433    /// CSV with a header row.
434    Csv,
435    /// One JSON object per line.
436    Jsonl,
437    /// A single JSON array.
438    Json,
439    /// CycloneDX 1.6 JSON SBOM format.
440    Cyclonedx,
441}
442
443#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
444#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
445#[serde(rename_all = "kebab-case")]
446pub enum ConfigMode {
447    /// Read `tokei.toml` / `.tokeirc` if present.
448    #[default]
449    Auto,
450    /// Ignore config files.
451    None,
452}
453
454#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
455#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
456#[serde(rename_all = "kebab-case")]
457pub enum ChildrenMode {
458    /// Merge embedded content into the parent language totals.
459    Collapse,
460    /// Show embedded languages as separate "(embedded)" rows.
461    Separate,
462}
463
464#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
465#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
466#[serde(rename_all = "kebab-case")]
467pub enum ChildIncludeMode {
468    /// Include embedded languages as separate contributions.
469    Separate,
470    /// Ignore embedded languages.
471    ParentsOnly,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
475#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
476#[serde(rename_all = "kebab-case")]
477pub enum RedactMode {
478    /// Do not redact.
479    None,
480    /// Redact file paths.
481    Paths,
482    /// Redact file paths and module names.
483    All,
484}
485
486#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
487#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
488#[serde(rename_all = "kebab-case")]
489pub enum AnalysisFormat {
490    Md,
491    Json,
492    Jsonld,
493    Xml,
494    Svg,
495    Mermaid,
496    Obj,
497    Midi,
498    Tree,
499    Html,
500}
501
502/// Log record for context command JSONL append mode.
503/// Contains metadata only (not file contents) for lightweight logging.
504#[derive(Debug, Clone, Serialize, Deserialize)]
505pub struct ContextLogRecord {
506    pub schema_version: u32,
507    pub generated_at_ms: u128,
508    pub tool: ToolInfo,
509    pub budget_tokens: usize,
510    pub used_tokens: usize,
511    pub utilization_pct: f64,
512    pub strategy: String,
513    pub rank_by: String,
514    pub file_count: usize,
515    pub total_bytes: usize,
516    pub output_destination: String,
517}
518
519// -----------------------
520// Handoff types
521// -----------------------
522
523/// Schema version for handoff receipts.
524pub const HANDOFF_SCHEMA_VERSION: u32 = 5;
525
526/// Schema version for context bundle manifests.
527pub const CONTEXT_BUNDLE_SCHEMA_VERSION: u32 = 2;
528
529/// Schema version for context receipts (separate from SCHEMA_VERSION used by lang/module/export/diff).
530pub const CONTEXT_SCHEMA_VERSION: u32 = 4;
531
532// -----------------------
533// Token estimation types
534// -----------------------
535
536/// Metadata about how token estimates were produced.
537///
538/// Rails are NOT guaranteed bounds — they are heuristic fences.
539/// Default divisors: est=4.0, low=3.0 (conservative → more tokens),
540/// high=5.0 (optimistic → fewer tokens).
541///
542/// **Invariant**: `tokens_min <= tokens_est <= tokens_max`.
543#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct TokenEstimationMeta {
545    /// Divisor used for main estimate (default 4.0).
546    pub bytes_per_token_est: f64,
547    /// Conservative divisor — more tokens (default 3.0).
548    pub bytes_per_token_low: f64,
549    /// Optimistic divisor — fewer tokens (default 5.0).
550    pub bytes_per_token_high: f64,
551    /// tokens = source_bytes / bytes_per_token_high (optimistic, fewest tokens).
552    #[serde(alias = "tokens_high")]
553    pub tokens_min: usize,
554    /// tokens = source_bytes / bytes_per_token_est.
555    pub tokens_est: usize,
556    /// tokens = source_bytes / bytes_per_token_low (conservative, most tokens).
557    #[serde(alias = "tokens_low")]
558    pub tokens_max: usize,
559    /// Total source bytes used to compute estimates.
560    pub source_bytes: usize,
561}
562
563impl TokenEstimationMeta {
564    /// Default bytes-per-token divisors.
565    pub const DEFAULT_BPT_EST: f64 = 4.0;
566    pub const DEFAULT_BPT_LOW: f64 = 3.0;
567    pub const DEFAULT_BPT_HIGH: f64 = 5.0;
568
569    /// Create estimation from source byte count using default divisors.
570    pub fn from_bytes(bytes: usize, bpt: f64) -> Self {
571        Self::from_bytes_with_bounds(bytes, bpt, Self::DEFAULT_BPT_LOW, Self::DEFAULT_BPT_HIGH)
572    }
573
574    /// Create estimation from source byte count with explicit low/high divisors.
575    pub fn from_bytes_with_bounds(bytes: usize, bpt_est: f64, bpt_low: f64, bpt_high: f64) -> Self {
576        Self {
577            bytes_per_token_est: bpt_est,
578            bytes_per_token_low: bpt_low,
579            bytes_per_token_high: bpt_high,
580            tokens_min: (bytes as f64 / bpt_high).ceil() as usize,
581            tokens_est: (bytes as f64 / bpt_est).ceil() as usize,
582            tokens_max: (bytes as f64 / bpt_low).ceil() as usize,
583            source_bytes: bytes,
584        }
585    }
586}
587
588/// Post-write audit comparing actual output to estimates.
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct TokenAudit {
591    /// Actual bytes written to the output bundle.
592    pub output_bytes: u64,
593    /// tokens = output_bytes / bytes_per_token_high (optimistic, fewest tokens).
594    #[serde(alias = "tokens_high")]
595    pub tokens_min: usize,
596    /// tokens = output_bytes / bytes_per_token_est.
597    pub tokens_est: usize,
598    /// tokens = output_bytes / bytes_per_token_low (conservative, most tokens).
599    #[serde(alias = "tokens_low")]
600    pub tokens_max: usize,
601    /// Bytes of framing/separators/headers (output_bytes - content_bytes).
602    pub overhead_bytes: u64,
603    /// overhead_bytes / output_bytes (0.0-1.0).
604    pub overhead_pct: f64,
605}
606
607impl TokenAudit {
608    /// Create an audit from output bytes and content bytes.
609    pub fn from_output(output_bytes: u64, content_bytes: u64) -> Self {
610        Self::from_output_with_divisors(
611            output_bytes,
612            content_bytes,
613            TokenEstimationMeta::DEFAULT_BPT_EST,
614            TokenEstimationMeta::DEFAULT_BPT_LOW,
615            TokenEstimationMeta::DEFAULT_BPT_HIGH,
616        )
617    }
618
619    /// Create an audit from output bytes with explicit divisors.
620    pub fn from_output_with_divisors(
621        output_bytes: u64,
622        content_bytes: u64,
623        bpt_est: f64,
624        bpt_low: f64,
625        bpt_high: f64,
626    ) -> Self {
627        let overhead_bytes = output_bytes.saturating_sub(content_bytes);
628        let overhead_pct = if output_bytes > 0 {
629            overhead_bytes as f64 / output_bytes as f64
630        } else {
631            0.0
632        };
633        Self {
634            output_bytes,
635            tokens_min: (output_bytes as f64 / bpt_high).ceil() as usize,
636            tokens_est: (output_bytes as f64 / bpt_est).ceil() as usize,
637            tokens_max: (output_bytes as f64 / bpt_low).ceil() as usize,
638            overhead_bytes,
639            overhead_pct,
640        }
641    }
642}
643
644// -----------------------
645// Bundle hygiene types
646// -----------------------
647
648/// Classification of a file for bundle hygiene purposes.
649#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
650#[serde(rename_all = "snake_case")]
651pub enum FileClassification {
652    /// Protobuf output, parser tables, node-types.json, etc.
653    Generated,
654    /// Test fixtures, golden snapshots.
655    Fixture,
656    /// Third-party vendored code.
657    Vendored,
658    /// Cargo.lock, package-lock.json, etc.
659    Lockfile,
660    /// *.min.js, *.min.css.
661    Minified,
662    /// Files with very high tokens-per-line ratio.
663    DataBlob,
664    /// *.js.map, *.css.map.
665    Sourcemap,
666}
667
668/// How a file is included in the context/handoff bundle.
669#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
670#[serde(rename_all = "snake_case")]
671pub enum InclusionPolicy {
672    /// Full file content.
673    #[default]
674    Full,
675    /// First N + last N lines.
676    HeadTail,
677    /// Structural summary (placeholder, behaves as Skip for now).
678    Summary,
679    /// Excluded from payload entirely.
680    Skip,
681}
682
683/// Helper for serde skip_serializing_if on InclusionPolicy.
684fn is_default_policy(policy: &InclusionPolicy) -> bool {
685    *policy == InclusionPolicy::Full
686}
687
688/// A file excluded by per-file cap / classification policy.
689#[derive(Debug, Clone, Serialize, Deserialize)]
690pub struct PolicyExcludedFile {
691    pub path: String,
692    pub original_tokens: usize,
693    pub policy: InclusionPolicy,
694    pub reason: String,
695    pub classifications: Vec<FileClassification>,
696}
697
698/// Manifest for a handoff bundle containing LLM-ready artifacts.
699#[derive(Debug, Clone, Serialize, Deserialize)]
700pub struct HandoffManifest {
701    pub schema_version: u32,
702    pub generated_at_ms: u128,
703    pub tool: ToolInfo,
704    pub mode: String,
705    pub inputs: Vec<String>,
706    pub output_dir: String,
707    pub budget_tokens: usize,
708    pub used_tokens: usize,
709    pub utilization_pct: f64,
710    pub strategy: String,
711    pub rank_by: String,
712    pub capabilities: Vec<CapabilityStatus>,
713    pub artifacts: Vec<ArtifactEntry>,
714    pub included_files: Vec<ContextFileRow>,
715    pub excluded_paths: Vec<HandoffExcludedPath>,
716    pub excluded_patterns: Vec<String>,
717    pub smart_excluded_files: Vec<SmartExcludedFile>,
718    pub total_files: usize,
719    pub bundled_files: usize,
720    pub intelligence_preset: String,
721    /// Effective ranking metric (may differ from rank_by if fallback occurred).
722    #[serde(default, skip_serializing_if = "Option::is_none")]
723    pub rank_by_effective: Option<String>,
724    /// Reason for fallback if rank_by_effective differs from rank_by.
725    #[serde(default, skip_serializing_if = "Option::is_none")]
726    pub fallback_reason: Option<String>,
727    /// Files excluded by per-file cap / classification policy.
728    #[serde(default, skip_serializing_if = "Vec::is_empty")]
729    pub excluded_by_policy: Vec<PolicyExcludedFile>,
730    /// Token estimation envelope with uncertainty bounds.
731    #[serde(default, skip_serializing_if = "Option::is_none")]
732    pub token_estimation: Option<TokenEstimationMeta>,
733    /// Post-bundle audit comparing actual code bundle bytes to estimates.
734    #[serde(default, skip_serializing_if = "Option::is_none")]
735    pub code_audit: Option<TokenAudit>,
736}
737
738/// A file excluded by smart-exclude heuristics (lockfiles, minified, etc.).
739#[derive(Debug, Clone, Serialize, Deserialize)]
740pub struct SmartExcludedFile {
741    pub path: String,
742    pub reason: String,
743    pub tokens: usize,
744}
745
746/// Manifest for a context bundle directory (bundle.txt + receipt.json + manifest.json).
747#[derive(Debug, Clone, Serialize, Deserialize)]
748pub struct ContextBundleManifest {
749    pub schema_version: u32,
750    pub generated_at_ms: u128,
751    pub tool: ToolInfo,
752    pub mode: String,
753    pub budget_tokens: usize,
754    pub used_tokens: usize,
755    pub utilization_pct: f64,
756    pub strategy: String,
757    pub rank_by: String,
758    pub file_count: usize,
759    pub bundle_bytes: usize,
760    pub artifacts: Vec<ArtifactEntry>,
761    pub included_files: Vec<ContextFileRow>,
762    pub excluded_paths: Vec<ContextExcludedPath>,
763    pub excluded_patterns: Vec<String>,
764    /// Effective ranking metric (may differ from rank_by if fallback occurred).
765    #[serde(default, skip_serializing_if = "Option::is_none")]
766    pub rank_by_effective: Option<String>,
767    /// Reason for fallback if rank_by_effective differs from rank_by.
768    #[serde(default, skip_serializing_if = "Option::is_none")]
769    pub fallback_reason: Option<String>,
770    /// Files excluded by per-file cap / classification policy.
771    #[serde(default, skip_serializing_if = "Vec::is_empty")]
772    pub excluded_by_policy: Vec<PolicyExcludedFile>,
773    /// Token estimation envelope with uncertainty bounds.
774    #[serde(default, skip_serializing_if = "Option::is_none")]
775    pub token_estimation: Option<TokenEstimationMeta>,
776    /// Post-bundle audit comparing actual bundle bytes to estimates.
777    #[serde(default, skip_serializing_if = "Option::is_none")]
778    pub bundle_audit: Option<TokenAudit>,
779}
780
781/// Explicitly excluded path with reason for context bundles.
782#[derive(Debug, Clone, Serialize, Deserialize)]
783pub struct ContextExcludedPath {
784    pub path: String,
785    pub reason: String,
786}
787
788/// Intelligence bundle for handoff containing tree, hotspots, complexity, and derived metrics.
789#[derive(Debug, Clone, Serialize, Deserialize)]
790pub struct HandoffIntelligence {
791    pub tree: Option<String>,
792    pub tree_depth: Option<usize>,
793    pub hotspots: Option<Vec<HandoffHotspot>>,
794    pub complexity: Option<HandoffComplexity>,
795    pub derived: Option<HandoffDerived>,
796    pub warnings: Vec<String>,
797}
798
799/// Explicitly excluded path with reason.
800#[derive(Debug, Clone, Serialize, Deserialize)]
801pub struct HandoffExcludedPath {
802    pub path: String,
803    pub reason: String,
804}
805
806/// Simplified hotspot row for handoff intelligence.
807#[derive(Debug, Clone, Serialize, Deserialize)]
808pub struct HandoffHotspot {
809    pub path: String,
810    pub commits: usize,
811    pub lines: usize,
812    pub score: usize,
813}
814
815/// Simplified complexity report for handoff intelligence.
816#[derive(Debug, Clone, Serialize, Deserialize)]
817pub struct HandoffComplexity {
818    pub total_functions: usize,
819    pub avg_function_length: f64,
820    pub max_function_length: usize,
821    pub avg_cyclomatic: f64,
822    pub max_cyclomatic: usize,
823    pub high_risk_files: usize,
824}
825
826/// Simplified derived metrics for handoff intelligence.
827#[derive(Debug, Clone, Serialize, Deserialize)]
828pub struct HandoffDerived {
829    pub total_files: usize,
830    pub total_code: usize,
831    pub total_lines: usize,
832    pub total_tokens: usize,
833    pub lang_count: usize,
834    pub dominant_lang: String,
835    pub dominant_pct: f64,
836}
837
838/// Status of a detected capability.
839#[derive(Debug, Clone, Serialize, Deserialize)]
840pub struct CapabilityStatus {
841    pub name: String,
842    pub status: CapabilityState,
843    #[serde(skip_serializing_if = "Option::is_none")]
844    pub reason: Option<String>,
845}
846
847/// State of a capability: available, skipped, or unavailable.
848#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
849#[serde(rename_all = "snake_case")]
850pub enum CapabilityState {
851    /// Capability is available and was used.
852    Available,
853    /// Capability is available but was skipped (e.g., --no-git flag).
854    Skipped,
855    /// Capability is unavailable (e.g., not in a git repo).
856    Unavailable,
857}
858
859/// Entry describing an artifact in the handoff bundle.
860#[derive(Debug, Clone, Serialize, Deserialize)]
861pub struct ArtifactEntry {
862    pub name: String,
863    pub path: String,
864    pub description: String,
865    pub bytes: u64,
866    #[serde(skip_serializing_if = "Option::is_none")]
867    pub hash: Option<ArtifactHash>,
868}
869
870/// Hash for artifact integrity.
871#[derive(Debug, Clone, Serialize, Deserialize)]
872pub struct ArtifactHash {
873    pub algo: String,
874    pub hash: String,
875}