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
31use std::path::PathBuf;
32
33use serde::{Deserialize, Serialize};
34
35/// The current schema version for all receipt types.
36pub const SCHEMA_VERSION: u32 = 2;
37
38/// A small totals struct shared by summary outputs.
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
40pub struct Totals {
41    pub code: usize,
42    pub lines: usize,
43    pub files: usize,
44    pub bytes: usize,
45    pub tokens: usize,
46    pub avg_lines: usize,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub struct LangRow {
51    pub lang: String,
52    pub code: usize,
53    pub lines: usize,
54    pub files: usize,
55    pub bytes: usize,
56    pub tokens: usize,
57    pub avg_lines: usize,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct LangReport {
62    pub rows: Vec<LangRow>,
63    pub total: Totals,
64    pub with_files: bool,
65    pub children: ChildrenMode,
66    pub top: usize,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70pub struct ModuleRow {
71    pub module: String,
72    pub code: usize,
73    pub lines: usize,
74    pub files: usize,
75    pub bytes: usize,
76    pub tokens: usize,
77    pub avg_lines: usize,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ModuleReport {
82    pub rows: Vec<ModuleRow>,
83    pub total: Totals,
84    pub module_roots: Vec<String>,
85    pub module_depth: usize,
86    pub children: ChildIncludeMode,
87    pub top: usize,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum FileKind {
93    Parent,
94    Child,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
98pub struct FileRow {
99    pub path: String,
100    pub module: String,
101    pub lang: String,
102    pub kind: FileKind,
103    pub code: usize,
104    pub comments: usize,
105    pub blanks: usize,
106    pub lines: usize,
107    pub bytes: usize,
108    pub tokens: usize,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ExportData {
113    pub rows: Vec<FileRow>,
114    pub module_roots: Vec<String>,
115    pub module_depth: usize,
116    pub children: ChildIncludeMode,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct RunReceipt {
121    pub schema_version: u32,
122    pub generated_at_ms: u128,
123    pub lang_file: String,
124    pub module_file: String,
125    pub export_file: String,
126    // We could store the scan args here too
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130#[serde(rename_all = "snake_case")]
131pub enum ScanStatus {
132    Complete,
133    Partial,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, Default)]
137pub struct ToolInfo {
138    pub name: String,
139    pub version: String,
140}
141
142impl ToolInfo {
143    pub fn current() -> Self {
144        Self {
145            name: "tokmd".to_string(),
146            version: env!("CARGO_PKG_VERSION").to_string(),
147        }
148    }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct ScanArgs {
153    pub paths: Vec<String>,
154    pub excluded: Vec<String>,
155    /// True if `excluded` patterns were redacted (replaced with hashes).
156    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
157    pub excluded_redacted: bool,
158    pub config: ConfigMode,
159    pub hidden: bool,
160    pub no_ignore: bool,
161    pub no_ignore_parent: bool,
162    pub no_ignore_dot: bool,
163    pub no_ignore_vcs: bool,
164    pub treat_doc_strings_as_comments: bool,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct LangArgsMeta {
169    pub format: String,
170    pub top: usize,
171    pub with_files: bool,
172    pub children: ChildrenMode,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct LangReceipt {
177    pub schema_version: u32,
178    pub generated_at_ms: u128,
179    pub tool: ToolInfo,
180    pub mode: String, // "lang"
181    pub status: ScanStatus,
182    pub warnings: Vec<String>,
183    pub scan: ScanArgs,
184    pub args: LangArgsMeta,
185    #[serde(flatten)]
186    pub report: LangReport,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct ModuleArgsMeta {
191    pub format: String,
192    pub module_roots: Vec<String>,
193    pub module_depth: usize,
194    pub children: ChildIncludeMode,
195    pub top: usize,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct ModuleReceipt {
200    pub schema_version: u32,
201    pub generated_at_ms: u128,
202    pub tool: ToolInfo,
203    pub mode: String, // "module"
204    pub status: ScanStatus,
205    pub warnings: Vec<String>,
206    pub scan: ScanArgs,
207    pub args: ModuleArgsMeta,
208    #[serde(flatten)]
209    pub report: ModuleReport,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct ExportArgsMeta {
214    pub format: ExportFormat,
215    pub module_roots: Vec<String>,
216    pub module_depth: usize,
217    pub children: ChildIncludeMode,
218    pub min_code: usize,
219    pub max_rows: usize,
220    pub redact: RedactMode,
221    pub strip_prefix: Option<String>,
222    /// True if `strip_prefix` was redacted (replaced with a hash).
223    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
224    pub strip_prefix_redacted: bool,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct ExportReceipt {
229    pub schema_version: u32,
230    pub generated_at_ms: u128,
231    pub tool: ToolInfo,
232    pub mode: String, // "export"
233    pub status: ScanStatus,
234    pub warnings: Vec<String>,
235    pub scan: ScanArgs,
236    pub args: ExportArgsMeta,
237    #[serde(flatten)]
238    pub data: ExportData,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct LangArgs {
243    pub paths: Vec<PathBuf>,
244    pub format: TableFormat,
245    pub top: usize,
246    pub files: bool,
247    pub children: ChildrenMode,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct ModuleArgs {
252    pub paths: Vec<PathBuf>,
253    pub format: TableFormat,
254    pub top: usize,
255    pub module_roots: Vec<String>,
256    pub module_depth: usize,
257    pub children: ChildIncludeMode,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct ExportArgs {
262    pub paths: Vec<PathBuf>,
263    pub format: ExportFormat,
264    pub output: Option<PathBuf>,
265    pub module_roots: Vec<String>,
266    pub module_depth: usize,
267    pub children: ChildIncludeMode,
268    pub min_code: usize,
269    pub max_rows: usize,
270    pub redact: RedactMode,
271    pub meta: bool,
272    pub strip_prefix: Option<PathBuf>,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct ContextReceipt {
277    pub schema_version: u32,
278    pub generated_at_ms: u128,
279    pub tool: ToolInfo,
280    pub mode: String,
281    pub budget_tokens: usize,
282    pub used_tokens: usize,
283    pub utilization_pct: f64,
284    pub strategy: String,
285    pub rank_by: String,
286    pub file_count: usize,
287    pub files: Vec<ContextFileRow>,
288    /// Effective ranking metric (may differ from rank_by if fallback occurred).
289    #[serde(default, skip_serializing_if = "Option::is_none")]
290    pub rank_by_effective: Option<String>,
291    /// Reason for fallback if rank_by_effective differs from rank_by.
292    #[serde(default, skip_serializing_if = "Option::is_none")]
293    pub fallback_reason: Option<String>,
294    /// Files excluded by per-file cap / classification policy.
295    #[serde(default, skip_serializing_if = "Vec::is_empty")]
296    pub excluded_by_policy: Vec<PolicyExcludedFile>,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct ContextFileRow {
301    pub path: String,
302    pub module: String,
303    pub lang: String,
304    pub tokens: usize,
305    pub code: usize,
306    pub lines: usize,
307    pub bytes: usize,
308    pub value: usize,
309    #[serde(default, skip_serializing_if = "String::is_empty")]
310    pub rank_reason: String,
311    /// Inclusion policy applied to this file.
312    #[serde(default, skip_serializing_if = "is_default_policy")]
313    pub policy: InclusionPolicy,
314    /// Effective token count when policy != Full (None means same as `tokens`).
315    #[serde(default, skip_serializing_if = "Option::is_none")]
316    pub effective_tokens: Option<usize>,
317    /// Reason for the applied policy.
318    #[serde(default, skip_serializing_if = "Option::is_none")]
319    pub policy_reason: Option<String>,
320    /// File classifications detected by hygiene analysis.
321    #[serde(default, skip_serializing_if = "Vec::is_empty")]
322    pub classifications: Vec<FileClassification>,
323}
324
325// -----------------------
326// Diff types
327// -----------------------
328
329/// A row in the diff output showing changes for a single language.
330#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
331pub struct DiffRow {
332    pub lang: String,
333    pub old_code: usize,
334    pub new_code: usize,
335    pub delta_code: i64,
336    pub old_lines: usize,
337    pub new_lines: usize,
338    pub delta_lines: i64,
339    pub old_files: usize,
340    pub new_files: usize,
341    pub delta_files: i64,
342    pub old_bytes: usize,
343    pub new_bytes: usize,
344    pub delta_bytes: i64,
345    pub old_tokens: usize,
346    pub new_tokens: usize,
347    pub delta_tokens: i64,
348}
349
350/// Aggregate totals for the diff.
351#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
352pub struct DiffTotals {
353    pub old_code: usize,
354    pub new_code: usize,
355    pub delta_code: i64,
356    pub old_lines: usize,
357    pub new_lines: usize,
358    pub delta_lines: i64,
359    pub old_files: usize,
360    pub new_files: usize,
361    pub delta_files: i64,
362    pub old_bytes: usize,
363    pub new_bytes: usize,
364    pub delta_bytes: i64,
365    pub old_tokens: usize,
366    pub new_tokens: usize,
367    pub delta_tokens: i64,
368}
369
370/// JSON receipt for diff output with envelope metadata.
371#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct DiffReceipt {
373    pub schema_version: u32,
374    pub generated_at_ms: u128,
375    pub tool: ToolInfo,
376    pub mode: String,
377    pub from_source: String,
378    pub to_source: String,
379    pub diff_rows: Vec<DiffRow>,
380    pub totals: DiffTotals,
381}
382
383// -----------------------------------------------------------------------------
384// Enums shared with CLI (moved from tokmd-config)
385// -----------------------------------------------------------------------------
386
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
388#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
389#[serde(rename_all = "kebab-case")]
390pub enum TableFormat {
391    /// Markdown table (great for pasting into ChatGPT).
392    Md,
393    /// Tab-separated values (good for piping to other tools).
394    Tsv,
395    /// JSON (compact).
396    Json,
397}
398
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
400#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
401#[serde(rename_all = "kebab-case")]
402pub enum ExportFormat {
403    /// CSV with a header row.
404    Csv,
405    /// One JSON object per line.
406    Jsonl,
407    /// A single JSON array.
408    Json,
409    /// CycloneDX 1.6 JSON SBOM format.
410    Cyclonedx,
411}
412
413#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
414#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
415#[serde(rename_all = "kebab-case")]
416pub enum ConfigMode {
417    /// Read `tokei.toml` / `.tokeirc` if present.
418    #[default]
419    Auto,
420    /// Ignore config files.
421    None,
422}
423
424#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
425#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
426#[serde(rename_all = "kebab-case")]
427pub enum ChildrenMode {
428    /// Merge embedded content into the parent language totals.
429    Collapse,
430    /// Show embedded languages as separate "(embedded)" rows.
431    Separate,
432}
433
434#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
435#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
436#[serde(rename_all = "kebab-case")]
437pub enum ChildIncludeMode {
438    /// Include embedded languages as separate contributions.
439    Separate,
440    /// Ignore embedded languages.
441    ParentsOnly,
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
445#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
446#[serde(rename_all = "kebab-case")]
447pub enum RedactMode {
448    /// Do not redact.
449    None,
450    /// Redact file paths.
451    Paths,
452    /// Redact file paths and module names.
453    All,
454}
455
456/// Log record for context command JSONL append mode.
457/// Contains metadata only (not file contents) for lightweight logging.
458#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct ContextLogRecord {
460    pub schema_version: u32,
461    pub generated_at_ms: u128,
462    pub tool: ToolInfo,
463    pub budget_tokens: usize,
464    pub used_tokens: usize,
465    pub utilization_pct: f64,
466    pub strategy: String,
467    pub rank_by: String,
468    pub file_count: usize,
469    pub total_bytes: usize,
470    pub output_destination: String,
471}
472
473// -----------------------
474// Handoff types
475// -----------------------
476
477/// Schema version for handoff receipts.
478pub const HANDOFF_SCHEMA_VERSION: u32 = 4;
479
480/// Schema version for context bundle manifests.
481pub const CONTEXT_BUNDLE_SCHEMA_VERSION: u32 = 2;
482
483/// Schema version for context receipts (separate from SCHEMA_VERSION used by lang/module/export/diff).
484pub const CONTEXT_SCHEMA_VERSION: u32 = 3;
485
486// -----------------------
487// Bundle hygiene types
488// -----------------------
489
490/// Classification of a file for bundle hygiene purposes.
491#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
492#[serde(rename_all = "snake_case")]
493pub enum FileClassification {
494    /// Protobuf output, parser tables, node-types.json, etc.
495    Generated,
496    /// Test fixtures, golden snapshots.
497    Fixture,
498    /// Third-party vendored code.
499    Vendored,
500    /// Cargo.lock, package-lock.json, etc.
501    Lockfile,
502    /// *.min.js, *.min.css.
503    Minified,
504    /// Files with very high tokens-per-line ratio.
505    DataBlob,
506    /// *.js.map, *.css.map.
507    Sourcemap,
508}
509
510/// How a file is included in the context/handoff bundle.
511#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
512#[serde(rename_all = "snake_case")]
513pub enum InclusionPolicy {
514    /// Full file content.
515    #[default]
516    Full,
517    /// First N + last N lines.
518    HeadTail,
519    /// Structural summary (placeholder, behaves as Skip for now).
520    Summary,
521    /// Excluded from payload entirely.
522    Skip,
523}
524
525/// Helper for serde skip_serializing_if on InclusionPolicy.
526fn is_default_policy(policy: &InclusionPolicy) -> bool {
527    *policy == InclusionPolicy::Full
528}
529
530/// A file excluded by per-file cap / classification policy.
531#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct PolicyExcludedFile {
533    pub path: String,
534    pub original_tokens: usize,
535    pub policy: InclusionPolicy,
536    pub reason: String,
537    pub classifications: Vec<FileClassification>,
538}
539
540/// Manifest for a handoff bundle containing LLM-ready artifacts.
541#[derive(Debug, Clone, Serialize, Deserialize)]
542pub struct HandoffManifest {
543    pub schema_version: u32,
544    pub generated_at_ms: u128,
545    pub tool: ToolInfo,
546    pub mode: String,
547    pub inputs: Vec<String>,
548    pub output_dir: String,
549    pub budget_tokens: usize,
550    pub used_tokens: usize,
551    pub utilization_pct: f64,
552    pub strategy: String,
553    pub rank_by: String,
554    pub capabilities: Vec<CapabilityStatus>,
555    pub artifacts: Vec<ArtifactEntry>,
556    pub included_files: Vec<ContextFileRow>,
557    pub excluded_paths: Vec<HandoffExcludedPath>,
558    pub excluded_patterns: Vec<String>,
559    pub smart_excluded_files: Vec<SmartExcludedFile>,
560    pub total_files: usize,
561    pub bundled_files: usize,
562    pub intelligence_preset: String,
563    /// Effective ranking metric (may differ from rank_by if fallback occurred).
564    #[serde(default, skip_serializing_if = "Option::is_none")]
565    pub rank_by_effective: Option<String>,
566    /// Reason for fallback if rank_by_effective differs from rank_by.
567    #[serde(default, skip_serializing_if = "Option::is_none")]
568    pub fallback_reason: Option<String>,
569    /// Files excluded by per-file cap / classification policy.
570    #[serde(default, skip_serializing_if = "Vec::is_empty")]
571    pub excluded_by_policy: Vec<PolicyExcludedFile>,
572}
573
574/// A file excluded by smart-exclude heuristics (lockfiles, minified, etc.).
575#[derive(Debug, Clone, Serialize, Deserialize)]
576pub struct SmartExcludedFile {
577    pub path: String,
578    pub reason: String,
579    pub tokens: usize,
580}
581
582/// Manifest for a context bundle directory (bundle.txt + receipt.json + manifest.json).
583#[derive(Debug, Clone, Serialize, Deserialize)]
584pub struct ContextBundleManifest {
585    pub schema_version: u32,
586    pub generated_at_ms: u128,
587    pub tool: ToolInfo,
588    pub mode: String,
589    pub budget_tokens: usize,
590    pub used_tokens: usize,
591    pub utilization_pct: f64,
592    pub strategy: String,
593    pub rank_by: String,
594    pub file_count: usize,
595    pub bundle_bytes: usize,
596    pub artifacts: Vec<ArtifactEntry>,
597    pub included_files: Vec<ContextFileRow>,
598    pub excluded_paths: Vec<ContextExcludedPath>,
599    pub excluded_patterns: Vec<String>,
600    /// Effective ranking metric (may differ from rank_by if fallback occurred).
601    #[serde(default, skip_serializing_if = "Option::is_none")]
602    pub rank_by_effective: Option<String>,
603    /// Reason for fallback if rank_by_effective differs from rank_by.
604    #[serde(default, skip_serializing_if = "Option::is_none")]
605    pub fallback_reason: Option<String>,
606    /// Files excluded by per-file cap / classification policy.
607    #[serde(default, skip_serializing_if = "Vec::is_empty")]
608    pub excluded_by_policy: Vec<PolicyExcludedFile>,
609}
610
611/// Explicitly excluded path with reason for context bundles.
612#[derive(Debug, Clone, Serialize, Deserialize)]
613pub struct ContextExcludedPath {
614    pub path: String,
615    pub reason: String,
616}
617
618/// Intelligence bundle for handoff containing tree, hotspots, complexity, and derived metrics.
619#[derive(Debug, Clone, Serialize, Deserialize)]
620pub struct HandoffIntelligence {
621    pub tree: Option<String>,
622    pub tree_depth: Option<usize>,
623    pub hotspots: Option<Vec<HandoffHotspot>>,
624    pub complexity: Option<HandoffComplexity>,
625    pub derived: Option<HandoffDerived>,
626    pub warnings: Vec<String>,
627}
628
629/// Explicitly excluded path with reason.
630#[derive(Debug, Clone, Serialize, Deserialize)]
631pub struct HandoffExcludedPath {
632    pub path: String,
633    pub reason: String,
634}
635
636/// Simplified hotspot row for handoff intelligence.
637#[derive(Debug, Clone, Serialize, Deserialize)]
638pub struct HandoffHotspot {
639    pub path: String,
640    pub commits: usize,
641    pub lines: usize,
642    pub score: usize,
643}
644
645/// Simplified complexity report for handoff intelligence.
646#[derive(Debug, Clone, Serialize, Deserialize)]
647pub struct HandoffComplexity {
648    pub total_functions: usize,
649    pub avg_function_length: f64,
650    pub max_function_length: usize,
651    pub avg_cyclomatic: f64,
652    pub max_cyclomatic: usize,
653    pub high_risk_files: usize,
654}
655
656/// Simplified derived metrics for handoff intelligence.
657#[derive(Debug, Clone, Serialize, Deserialize)]
658pub struct HandoffDerived {
659    pub total_files: usize,
660    pub total_code: usize,
661    pub total_lines: usize,
662    pub total_tokens: usize,
663    pub lang_count: usize,
664    pub dominant_lang: String,
665    pub dominant_pct: f64,
666}
667
668/// Status of a detected capability.
669#[derive(Debug, Clone, Serialize, Deserialize)]
670pub struct CapabilityStatus {
671    pub name: String,
672    pub status: CapabilityState,
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub reason: Option<String>,
675}
676
677/// State of a capability: available, skipped, or unavailable.
678#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
679#[serde(rename_all = "snake_case")]
680pub enum CapabilityState {
681    /// Capability is available and was used.
682    Available,
683    /// Capability is available but was skipped (e.g., --no-git flag).
684    Skipped,
685    /// Capability is unavailable (e.g., not in a git repo).
686    Unavailable,
687}
688
689/// Entry describing an artifact in the handoff bundle.
690#[derive(Debug, Clone, Serialize, Deserialize)]
691pub struct ArtifactEntry {
692    pub name: String,
693    pub path: String,
694    pub description: String,
695    pub bytes: u64,
696    #[serde(skip_serializing_if = "Option::is_none")]
697    pub hash: Option<ArtifactHash>,
698}
699
700/// Hash for artifact integrity.
701#[derive(Debug, Clone, Serialize, Deserialize)]
702pub struct ArtifactHash {
703    pub algo: String,
704    pub hash: String,
705}