cadi_core/
atomic.rs

1//! Atomic Chunk System
2//!
3//! This module provides the AtomicChunk type - a smart, reusable code unit with:
4//! - Human-readable aliases for easy reference
5//! - Automatic categorization and tagging
6//! - Platform/architecture constraints
7//! - Composition support (chunks made of other chunks)
8//! - Granularity levels for different reuse patterns
9
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13/// Granularity level of an atomic chunk
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16#[derive(Default)]
17pub enum ChunkGranularity {
18    /// Single function, method, or small utility
19    Function,
20    /// A class, struct, trait, or interface
21    Type,
22    /// A complete module or file
23    #[default]
24    Module,
25    /// A package or crate (multiple modules)
26    Package,
27    /// An entire project or workspace
28    Project,
29}
30
31
32/// Category of code chunk
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35#[derive(Default)]
36pub enum ChunkCategory {
37    /// Core logic and business rules
38    #[default]
39    Logic,
40    /// Data types and structures
41    Data,
42    /// Utility and helper functions
43    Utility,
44    /// API and interface definitions
45    Api,
46    /// Configuration and constants
47    Config,
48    /// Tests and test utilities
49    Test,
50    /// Documentation
51    Docs,
52    /// Build and tooling scripts
53    Build,
54    /// UI/Frontend components
55    Ui,
56    /// Backend/Server code
57    Backend,
58    /// Database and persistence
59    Database,
60    /// Custom category
61    Custom(String),
62}
63
64
65/// Platform constraint for a chunk
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct PlatformConstraint {
68    /// Target OS (any, linux, macos, windows, etc.)
69    #[serde(default = "default_any")]
70    pub os: String,
71    /// Target architecture (any, x86_64, aarch64, wasm32, etc.)
72    #[serde(default = "default_any")]
73    pub arch: String,
74    /// Required runtime/environment (node, browser, native, etc.)
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub runtime: Option<String>,
77    /// Minimum version requirements
78    #[serde(default)]
79    pub min_versions: HashMap<String, String>,
80}
81
82fn default_any() -> String {
83    "any".to_string()
84}
85
86impl Default for PlatformConstraint {
87    fn default() -> Self {
88        Self {
89            os: "any".to_string(),
90            arch: "any".to_string(),
91            runtime: None,
92            min_versions: HashMap::new(),
93        }
94    }
95}
96
97/// An alias for a chunk - human readable path
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct ChunkAlias {
100    /// The alias path (e.g., "utils/string-helpers")
101    pub path: String,
102    /// Namespace/scope (e.g., "my-org")
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub namespace: Option<String>,
105    /// Whether this is the primary/canonical alias
106    #[serde(default)]
107    pub primary: bool,
108    /// When this alias was registered
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub registered_at: Option<String>,
111}
112
113impl ChunkAlias {
114    /// Create a new primary alias
115    pub fn new(path: impl Into<String>) -> Self {
116        Self {
117            path: path.into(),
118            namespace: None,
119            primary: true,
120            registered_at: Some(chrono::Utc::now().to_rfc3339()),
121        }
122    }
123
124    /// With namespace
125    pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
126        self.namespace = Some(namespace.into());
127        self
128    }
129
130    /// Full qualified path
131    pub fn full_path(&self) -> String {
132        match &self.namespace {
133            Some(ns) => format!("{}/{}", ns, self.path),
134            None => self.path.clone(),
135        }
136    }
137}
138
139/// Reference to another chunk (for composition)
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ChunkReference {
142    /// The chunk ID being referenced
143    pub chunk_id: String,
144    /// Optional alias for easier reference
145    #[serde(skip_serializing_if = "Option::is_none")]
146    pub alias: Option<String>,
147    /// Whether this is required (vs optional)
148    #[serde(default = "default_true")]
149    pub required: bool,
150    /// Specific items imported from this chunk
151    #[serde(default)]
152    pub imports: Vec<String>,
153}
154
155fn default_true() -> bool {
156    true
157}
158
159/// Composition information - how this chunk relates to others
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ChunkComposition {
162    /// Chunks that this chunk is composed of (dependencies)
163    #[serde(default)]
164    pub composed_of: Vec<ChunkReference>,
165    /// Chunks that compose/include this chunk
166    #[serde(default)]
167    pub composed_by: Vec<String>,
168    /// Whether this chunk is atomic (not composed of other CADI chunks)
169    #[serde(default = "default_true")]
170    pub is_atomic: bool,
171    /// If composed, the strategy used
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub composition_strategy: Option<String>,
174}
175
176impl Default for ChunkComposition {
177    fn default() -> Self {
178        Self {
179            composed_of: Vec::new(),
180            composed_by: Vec::new(),
181            is_atomic: true,
182            composition_strategy: None,
183        }
184    }
185}
186
187/// Quality and reusability metrics
188#[derive(Debug, Clone, Default, Serialize, Deserialize)]
189pub struct ChunkMetrics {
190    /// Lines of code
191    #[serde(default)]
192    pub loc: usize,
193    /// Cyclomatic complexity estimate
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub complexity: Option<f32>,
196    /// Reusability score (0-1, higher = more reusable)
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub reusability_score: Option<f32>,
199    /// Number of public exports
200    #[serde(default)]
201    pub export_count: usize,
202    /// Number of dependencies
203    #[serde(default)]
204    pub dependency_count: usize,
205    /// Coupling score (0-1, lower = better)
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub coupling: Option<f32>,
208}
209
210/// Source location information
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct SourceLocation {
213    /// Original file path (relative)
214    pub file: String,
215    /// Start line (1-indexed)
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub start_line: Option<usize>,
218    /// End line (1-indexed)
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub end_line: Option<usize>,
221    /// Start column
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub start_col: Option<usize>,
224    /// End column
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub end_col: Option<usize>,
227}
228
229/// An atomic chunk - the fundamental unit of reusable code
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct AtomicChunk {
232    /// Content-addressed chunk ID (chunk:sha256:...)
233    pub chunk_id: String,
234
235    /// Human-readable aliases for this chunk
236    #[serde(default)]
237    pub aliases: Vec<ChunkAlias>,
238
239    /// Name of the chunk
240    pub name: String,
241
242    /// Description of what this chunk does
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub description: Option<String>,
245
246    /// Primary language
247    pub language: String,
248
249    /// Granularity level
250    #[serde(default)]
251    pub granularity: ChunkGranularity,
252
253    /// Categories this chunk belongs to
254    #[serde(default)]
255    pub categories: Vec<ChunkCategory>,
256
257    /// Tags for search and discovery
258    #[serde(default)]
259    pub tags: Vec<String>,
260
261    /// Concepts this chunk implements
262    #[serde(default)]
263    pub concepts: Vec<String>,
264
265    /// Interfaces/APIs provided
266    #[serde(default)]
267    pub provides: Vec<String>,
268
269    /// Interfaces/APIs required
270    #[serde(default)]
271    pub requires: Vec<String>,
272
273    /// Platform constraints
274    #[serde(default)]
275    pub platform: PlatformConstraint,
276
277    /// Composition information
278    #[serde(default)]
279    pub composition: ChunkComposition,
280
281    /// Quality metrics
282    #[serde(default)]
283    pub metrics: ChunkMetrics,
284
285    /// Source location(s)
286    #[serde(default)]
287    pub sources: Vec<SourceLocation>,
288
289    /// Content hash (sha256)
290    pub content_hash: String,
291
292    /// Size in bytes
293    pub size: usize,
294
295    /// License
296    #[serde(default = "default_license")]
297    pub license: String,
298
299    /// Creation timestamp
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub created_at: Option<String>,
302
303    /// Version if applicable
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub version: Option<String>,
306}
307
308fn default_license() -> String {
309    "MIT".to_string()
310}
311
312impl AtomicChunk {
313    /// Create a new atomic chunk
314    pub fn new(
315        chunk_id: String,
316        name: String,
317        language: String,
318        content_hash: String,
319        size: usize,
320    ) -> Self {
321        Self {
322            chunk_id,
323            aliases: Vec::new(),
324            name,
325            description: None,
326            language,
327            granularity: ChunkGranularity::default(),
328            categories: Vec::new(),
329            tags: Vec::new(),
330            concepts: Vec::new(),
331            provides: Vec::new(),
332            requires: Vec::new(),
333            platform: PlatformConstraint::default(),
334            composition: ChunkComposition::default(),
335            metrics: ChunkMetrics::default(),
336            sources: Vec::new(),
337            content_hash,
338            size,
339            license: "MIT".to_string(),
340            created_at: Some(chrono::Utc::now().to_rfc3339()),
341            version: None,
342        }
343    }
344
345    /// Add a primary alias
346    pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
347        self.aliases.push(ChunkAlias::new(alias));
348        self
349    }
350
351    /// Add multiple aliases
352    pub fn with_aliases(mut self, aliases: Vec<String>) -> Self {
353        for (i, alias) in aliases.into_iter().enumerate() {
354            let mut a = ChunkAlias::new(alias);
355            a.primary = i == 0;
356            self.aliases.push(a);
357        }
358        self
359    }
360
361    /// Set categories
362    pub fn with_categories(mut self, categories: Vec<ChunkCategory>) -> Self {
363        self.categories = categories;
364        self
365    }
366
367    /// Set granularity
368    pub fn with_granularity(mut self, granularity: ChunkGranularity) -> Self {
369        self.granularity = granularity;
370        self
371    }
372
373    /// Add concepts
374    pub fn with_concepts(mut self, concepts: Vec<String>) -> Self {
375        self.concepts = concepts;
376        self
377    }
378
379    /// Mark as composed of other chunks
380    pub fn composed_of(mut self, chunks: Vec<ChunkReference>) -> Self {
381        let is_empty = chunks.is_empty();
382        self.composition.composed_of = chunks;
383        self.composition.is_atomic = is_empty;
384        self
385    }
386
387    /// Get the primary alias, if any
388    pub fn primary_alias(&self) -> Option<&ChunkAlias> {
389        self.aliases.iter().find(|a| a.primary)
390    }
391
392    /// Get the display name (alias or chunk name)
393    pub fn display_name(&self) -> String {
394        self.primary_alias()
395            .map(|a| a.full_path())
396            .unwrap_or_else(|| self.name.clone())
397    }
398
399    /// Check if chunk is atomic (not composed of other chunks)
400    pub fn is_atomic(&self) -> bool {
401        self.composition.is_atomic && self.composition.composed_of.is_empty()
402    }
403}
404
405/// Alias registry for tracking used aliases
406#[derive(Debug, Clone, Default, Serialize, Deserialize)]
407pub struct AliasRegistry {
408    /// Map of alias path -> chunk_id
409    pub aliases: HashMap<String, String>,
410    /// Map of chunk_id -> list of aliases
411    pub chunks: HashMap<String, Vec<String>>,
412    /// Reserved aliases that cannot be used
413    #[serde(default)]
414    pub reserved: Vec<String>,
415}
416
417impl AliasRegistry {
418    /// Create a new empty registry
419    pub fn new() -> Self {
420        Self::default()
421    }
422
423    /// Check if an alias is available
424    pub fn is_available(&self, alias: &str) -> bool {
425        !self.aliases.contains_key(alias) && !self.reserved.contains(&alias.to_string())
426    }
427
428    /// Register an alias for a chunk
429    pub fn register(&mut self, alias: impl Into<String>, chunk_id: impl Into<String>) -> bool {
430        let alias = alias.into();
431        let chunk_id = chunk_id.into();
432
433        if !self.is_available(&alias) {
434            return false;
435        }
436
437        self.aliases.insert(alias.clone(), chunk_id.clone());
438        self.chunks
439            .entry(chunk_id)
440            .or_default()
441            .push(alias);
442
443        true
444    }
445
446    /// Resolve an alias to a chunk ID
447    pub fn resolve(&self, alias: &str) -> Option<&String> {
448        self.aliases.get(alias)
449    }
450
451    /// Get all aliases for a chunk
452    pub fn get_aliases(&self, chunk_id: &str) -> Option<&Vec<String>> {
453        self.chunks.get(chunk_id)
454    }
455
456    /// Generate a unique alias from a base name
457    pub fn generate_unique(&self, base: &str) -> String {
458        if self.is_available(base) {
459            return base.to_string();
460        }
461
462        let mut counter = 1;
463        loop {
464            let candidate = format!("{}-{}", base, counter);
465            if self.is_available(&candidate) {
466                return candidate;
467            }
468            counter += 1;
469        }
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    #[test]
478    fn test_chunk_alias() {
479        let alias = ChunkAlias::new("utils/string-helpers")
480            .with_namespace("my-org");
481        
482        assert_eq!(alias.full_path(), "my-org/utils/string-helpers");
483        assert!(alias.primary);
484    }
485
486    #[test]
487    fn test_alias_registry() {
488        let mut registry = AliasRegistry::new();
489        
490        assert!(registry.is_available("test/chunk"));
491        assert!(registry.register("test/chunk", "chunk:sha256:abc123"));
492        assert!(!registry.is_available("test/chunk"));
493        
494        let resolved = registry.resolve("test/chunk");
495        assert_eq!(resolved, Some(&"chunk:sha256:abc123".to_string()));
496    }
497
498    #[test]
499    fn test_atomic_chunk() {
500        let chunk = AtomicChunk::new(
501            "chunk:sha256:abc123".to_string(),
502            "string-helpers".to_string(),
503            "rust".to_string(),
504            "abc123".to_string(),
505            1024,
506        )
507        .with_alias("utils/string-helpers")
508        .with_granularity(ChunkGranularity::Module)
509        .with_categories(vec![ChunkCategory::Utility]);
510
511        // Ensure composition defaults to atomic
512        assert!(ChunkComposition::default().is_atomic);
513
514        assert!(chunk.is_atomic());
515        assert_eq!(chunk.display_name(), "utils/string-helpers");
516    }
517}