acp/cache/
types.rs

1//! @acp:module "Cache Types"
2//! @acp:summary "Data structures matching the .acp.cache.json schema (RFC-001/RFC-003 compliant)"
3//! @acp:domain cli
4//! @acp:layer model
5//!
6//! These types serialize directly to/from `.acp.cache.json`
7//! Includes RFC-003 annotation provenance tracking support.
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::fs::File;
13use std::io::{BufReader, BufWriter};
14use std::path::Path;
15
16use crate::constraints::ConstraintIndex;
17use crate::error::Result;
18use crate::git::{GitFileInfo, GitSymbolInfo};
19use crate::parse::SourceOrigin;
20
21/// @acp:summary "Normalize a file path for cross-platform compatibility"
22///
23/// Handles:
24/// - Windows backslashes → forward slashes
25/// - Redundant slashes (`//` → `/`)
26/// - Relative components (`.` and `..`)
27/// - Leading `./` prefix normalization
28///
29/// # Examples
30/// ```
31/// use acp::cache::normalize_path;
32///
33/// assert_eq!(normalize_path("src/file.ts"), "src/file.ts");
34/// assert_eq!(normalize_path("./src/file.ts"), "src/file.ts");
35/// assert_eq!(normalize_path("src\\file.ts"), "src/file.ts");
36/// assert_eq!(normalize_path("src/../src/file.ts"), "src/file.ts");
37/// ```
38pub fn normalize_path(path: &str) -> String {
39    // Convert backslashes to forward slashes (Windows compatibility)
40    let path = path.replace('\\', "/");
41
42    // Split into components and resolve . and ..
43    let mut components: Vec<&str> = Vec::new();
44
45    for part in path.split('/') {
46        match part {
47            "" | "." => continue, // Skip empty and current directory
48            ".." => {
49                // Go up one directory if possible
50                components.pop();
51            }
52            component => components.push(component),
53        }
54    }
55
56    components.join("/")
57}
58
59/// @acp:summary "Complete ACP cache file structure (schema-compliant)"
60/// @acp:lock normal
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Cache {
63    /// JSON Schema URL for validation
64    #[serde(rename = "$schema", default = "default_cache_schema")]
65    pub schema: String,
66    /// Schema version (required)
67    pub version: String,
68    /// Generation timestamp (required)
69    pub generated_at: DateTime<Utc>,
70    /// Git commit SHA (optional)
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub git_commit: Option<String>,
73    /// Project metadata (required)
74    pub project: ProjectInfo,
75    /// Aggregate statistics (required)
76    pub stats: Stats,
77    /// Map of file paths to modification times for staleness detection (required)
78    pub source_files: HashMap<String, DateTime<Utc>>,
79    /// Files indexed by path (required)
80    pub files: HashMap<String, FileEntry>,
81    /// Symbols indexed by name (required)
82    pub symbols: HashMap<String, SymbolEntry>,
83    /// Call graph relationships (optional)
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub graph: Option<CallGraph>,
86    /// Domain groupings (optional)
87    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
88    pub domains: HashMap<String, DomainEntry>,
89    /// AI behavioral constraints (optional)
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub constraints: Option<ConstraintIndex>,
92    /// RFC-0003: Annotation provenance statistics (optional)
93    #[serde(default, skip_serializing_if = "ProvenanceStats::is_empty")]
94    pub provenance: ProvenanceStats,
95    /// RFC-0006: Bridge statistics (optional)
96    #[serde(default, skip_serializing_if = "BridgeStats::is_empty")]
97    pub bridge: BridgeStats,
98    /// RFC-0015: Auto-detected naming and import conventions (optional)
99    #[serde(default, skip_serializing_if = "Conventions::is_empty")]
100    pub conventions: Conventions,
101}
102
103fn default_cache_schema() -> String {
104    "https://acp-protocol.dev/schemas/v1/cache.schema.json".to_string()
105}
106
107impl Cache {
108    /// @acp:summary "Create a new empty cache"
109    pub fn new(project_name: &str, root: &str) -> Self {
110        Self {
111            schema: default_cache_schema(),
112            version: crate::VERSION.to_string(),
113            generated_at: Utc::now(),
114            git_commit: None,
115            project: ProjectInfo {
116                name: project_name.to_string(),
117                root: root.to_string(),
118                description: None,
119            },
120            stats: Stats::default(),
121            source_files: HashMap::new(),
122            files: HashMap::new(),
123            symbols: HashMap::new(),
124            graph: Some(CallGraph::default()),
125            domains: HashMap::new(),
126            constraints: None,
127            provenance: ProvenanceStats::default(),
128            bridge: BridgeStats::default(),
129            conventions: Conventions::default(),
130        }
131    }
132
133    /// @acp:summary "Load cache from JSON file"
134    pub fn from_json<P: AsRef<Path>>(path: P) -> Result<Self> {
135        let file = File::open(path)?;
136        let reader = BufReader::new(file);
137        let cache = serde_json::from_reader(reader)?;
138        Ok(cache)
139    }
140
141    /// @acp:summary "Write cache to JSON file"
142    pub fn write_json<P: AsRef<Path>>(&self, path: P) -> Result<()> {
143        let file = File::create(path)?;
144        let writer = BufWriter::new(file);
145        serde_json::to_writer_pretty(writer, self)?;
146        Ok(())
147    }
148
149    /// @acp:summary "Get a symbol by name - O(1) lookup"
150    pub fn get_symbol(&self, name: &str) -> Option<&SymbolEntry> {
151        self.symbols.get(name)
152    }
153
154    /// @acp:summary "Get a file by path - O(1) lookup with cross-platform path normalization"
155    ///
156    /// Handles various path formats:
157    /// - With or without `./` prefix: `src/file.ts` and `./src/file.ts`
158    /// - Windows backslashes: `src\file.ts`
159    /// - Redundant separators: `src//file.ts`
160    /// - Relative components: `src/../src/file.ts`
161    pub fn get_file(&self, path: &str) -> Option<&FileEntry> {
162        // Try exact match first (fastest path)
163        if let Some(file) = self.files.get(path) {
164            return Some(file);
165        }
166
167        // Normalize and try variations
168        let normalized = normalize_path(path);
169
170        // Try normalized path directly
171        if let Some(file) = self.files.get(&normalized) {
172            return Some(file);
173        }
174
175        // Try with ./ prefix
176        let with_prefix = format!("./{}", &normalized);
177        if let Some(file) = self.files.get(&with_prefix) {
178            return Some(file);
179        }
180
181        // Try stripping ./ prefix from normalized
182        if let Some(stripped) = normalized.strip_prefix("./") {
183            if let Some(file) = self.files.get(stripped) {
184                return Some(file);
185            }
186        }
187
188        None
189    }
190
191    /// @acp:summary "Get callers of a symbol from reverse call graph"
192    pub fn get_callers(&self, symbol: &str) -> Option<&Vec<String>> {
193        self.graph.as_ref().and_then(|g| g.reverse.get(symbol))
194    }
195
196    /// @acp:summary "Get callees of a symbol from forward call graph"
197    pub fn get_callees(&self, symbol: &str) -> Option<&Vec<String>> {
198        self.graph.as_ref().and_then(|g| g.forward.get(symbol))
199    }
200
201    /// @acp:summary "Get all files in a domain"
202    pub fn get_domain_files(&self, domain: &str) -> Option<&Vec<String>> {
203        self.domains.get(domain).map(|d| &d.files)
204    }
205
206    /// @acp:summary "Recalculate statistics after indexing"
207    pub fn update_stats(&mut self) {
208        self.stats.files = self.files.len();
209        self.stats.symbols = self.symbols.len();
210        self.stats.lines = self.files.values().map(|f| f.lines).sum();
211
212        let annotated = self
213            .symbols
214            .values()
215            .filter(|s| s.summary.is_some())
216            .count();
217
218        if self.stats.symbols > 0 {
219            self.stats.annotation_coverage = (annotated as f64 / self.stats.symbols as f64) * 100.0;
220        }
221    }
222}
223
224/// @acp:summary "Builder for incremental cache construction"
225pub struct CacheBuilder {
226    cache: Cache,
227}
228
229impl CacheBuilder {
230    pub fn new(project_name: &str, root: &str) -> Self {
231        Self {
232            cache: Cache::new(project_name, root),
233        }
234    }
235
236    pub fn add_file(mut self, file: FileEntry) -> Self {
237        let path = file.path.clone();
238        self.cache.files.insert(path, file);
239        self
240    }
241
242    pub fn add_symbol(mut self, symbol: SymbolEntry) -> Self {
243        let name = symbol.name.clone();
244        self.cache.symbols.insert(name, symbol);
245        self
246    }
247
248    pub fn add_call_edge(mut self, from: &str, to: Vec<String>) -> Self {
249        let graph = self.cache.graph.get_or_insert_with(CallGraph::default);
250        graph.forward.insert(from.to_string(), to.clone());
251
252        // Build reverse graph
253        for callee in to {
254            graph
255                .reverse
256                .entry(callee)
257                .or_default()
258                .push(from.to_string());
259        }
260        self
261    }
262
263    pub fn add_source_file(mut self, path: String, modified_at: DateTime<Utc>) -> Self {
264        self.cache.source_files.insert(path, modified_at);
265        self
266    }
267
268    pub fn add_domain(mut self, domain: DomainEntry) -> Self {
269        let name = domain.name.clone();
270        self.cache.domains.insert(name, domain);
271        self
272    }
273
274    pub fn set_constraints(mut self, constraints: ConstraintIndex) -> Self {
275        self.cache.constraints = Some(constraints);
276        self
277    }
278
279    pub fn set_git_commit(mut self, commit: String) -> Self {
280        self.cache.git_commit = Some(commit);
281        self
282    }
283
284    pub fn build(mut self) -> Cache {
285        self.cache.update_stats();
286        self.cache
287    }
288}
289
290/// @acp:summary "Project metadata"
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ProjectInfo {
293    pub name: String,
294    pub root: String,
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub description: Option<String>,
297}
298
299/// @acp:summary "Aggregate statistics"
300#[derive(Debug, Clone, Default, Serialize, Deserialize)]
301#[serde(rename_all = "camelCase")]
302pub struct Stats {
303    pub files: usize,
304    pub symbols: usize,
305    pub lines: usize,
306    #[serde(default)]
307    pub annotation_coverage: f64,
308    /// RFC-0015: Language distribution statistics
309    #[serde(default, skip_serializing_if = "Vec::is_empty")]
310    pub languages: Vec<LanguageStat>,
311    /// RFC-0015: Primary language of the project (highest percentage)
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub primary_language: Option<String>,
314    /// RFC-0015: When the cache was last indexed
315    #[serde(skip_serializing_if = "Option::is_none")]
316    pub indexed_at: Option<DateTime<Utc>>,
317}
318
319/// @acp:summary "RFC-0015: Language statistics entry"
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct LanguageStat {
322    /// Programming language identifier
323    pub name: String,
324    /// Number of files in this language
325    pub files: usize,
326    /// Percentage of total files
327    pub percentage: f64,
328}
329
330/// @acp:summary "RFC-0015: Auto-detected naming and import conventions"
331#[derive(Debug, Clone, Default, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct Conventions {
334    /// File naming patterns detected per directory
335    #[serde(default, skip_serializing_if = "Vec::is_empty")]
336    pub file_naming: Vec<FileNamingConvention>,
337    /// Import/module style conventions
338    #[serde(skip_serializing_if = "Option::is_none")]
339    pub imports: Option<ImportConventions>,
340}
341
342impl Conventions {
343    /// Check if conventions are empty (for serialization skip)
344    pub fn is_empty(&self) -> bool {
345        self.file_naming.is_empty() && self.imports.is_none()
346    }
347}
348
349/// @acp:summary "RFC-0015: File naming pattern for a directory"
350#[derive(Debug, Clone, Serialize, Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct FileNamingConvention {
353    /// Relative directory path
354    pub directory: String,
355    /// Glob-style naming pattern (e.g., '*.ts', '*.route.ts')
356    pub pattern: String,
357    /// Detection confidence (0.0-1.0, threshold 0.7)
358    pub confidence: f64,
359    /// Example filenames matching this pattern
360    #[serde(default, skip_serializing_if = "Vec::is_empty")]
361    pub examples: Vec<String>,
362    /// Similar patterns NOT used (to avoid confusion)
363    #[serde(default, skip_serializing_if = "Vec::is_empty")]
364    pub anti_patterns: Vec<String>,
365}
366
367/// @acp:summary "RFC-0015: Import/module style conventions"
368#[derive(Debug, Clone, Serialize, Deserialize)]
369#[serde(rename_all = "camelCase")]
370pub struct ImportConventions {
371    /// JavaScript/TypeScript module system
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub module_system: Option<ModuleSystem>,
374    /// Import path style preference
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub path_style: Option<PathStyle>,
377    /// Whether index files re-export from subdirectories
378    #[serde(default, skip_serializing_if = "is_false")]
379    pub index_exports: bool,
380}
381
382/// @acp:summary "RFC-0015: JavaScript/TypeScript module system"
383#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
384#[serde(rename_all = "lowercase")]
385pub enum ModuleSystem {
386    Esm,
387    Commonjs,
388    Mixed,
389}
390
391/// @acp:summary "RFC-0015: Import path style preference"
392#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
393#[serde(rename_all = "lowercase")]
394pub enum PathStyle {
395    Relative,
396    Absolute,
397    Alias,
398    Mixed,
399}
400
401/// @acp:summary "File entry with metadata (RFC-001 compliant)"
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct FileEntry {
404    /// Relative path from project root (required)
405    pub path: String,
406    /// Line count (required)
407    pub lines: usize,
408    /// Programming language identifier (required)
409    pub language: Language,
410    /// Exported symbols (required)
411    #[serde(default)]
412    pub exports: Vec<String>,
413    /// Imported modules (required)
414    #[serde(default)]
415    pub imports: Vec<String>,
416    /// RFC-0015: Files that import this file (reverse import graph)
417    #[serde(default, skip_serializing_if = "Vec::is_empty")]
418    pub imported_by: Vec<String>,
419    /// Human-readable module name (optional)
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub module: Option<String>,
422    /// Brief file description (optional)
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub summary: Option<String>,
425    /// RFC-001: File purpose from @acp:purpose annotation
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub purpose: Option<String>,
428    /// RFC-001: File owner from @acp:owner annotation
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub owner: Option<String>,
431    /// RFC-001: Inline annotations (hack, todo, fixme, critical, perf)
432    #[serde(default, skip_serializing_if = "Vec::is_empty")]
433    pub inline: Vec<InlineAnnotation>,
434    /// Domain classifications (optional)
435    #[serde(default, skip_serializing_if = "Vec::is_empty")]
436    pub domains: Vec<String>,
437    /// Architectural layer (optional)
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub layer: Option<String>,
440    /// Stability level (optional, null if not specified)
441    #[serde(skip_serializing_if = "Option::is_none")]
442    pub stability: Option<Stability>,
443    /// AI behavioral hints (e.g., "ai-careful", "ai-readonly")
444    #[serde(default, skip_serializing_if = "Vec::is_empty")]
445    pub ai_hints: Vec<String>,
446    /// Git metadata (optional - last commit, author, contributors)
447    #[serde(skip_serializing_if = "Option::is_none")]
448    pub git: Option<GitFileInfo>,
449    /// RFC-0003: Annotation provenance tracking
450    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
451    pub annotations: HashMap<String, AnnotationProvenance>,
452    /// RFC-0006: Bridge metadata for this file
453    #[serde(default, skip_serializing_if = "BridgeMetadata::is_empty")]
454    pub bridge: BridgeMetadata,
455    /// RFC-0009: File version (from @acp:version)
456    #[serde(skip_serializing_if = "Option::is_none")]
457    pub version: Option<String>,
458    /// RFC-0009: Version when introduced (from @acp:since)
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub since: Option<String>,
461    /// RFC-0009: File license (from @acp:license)
462    #[serde(skip_serializing_if = "Option::is_none")]
463    pub license: Option<String>,
464    /// RFC-0009: File author (from @acp:author)
465    #[serde(skip_serializing_if = "Option::is_none")]
466    pub author: Option<String>,
467    /// RFC-0009: Lifecycle status
468    #[serde(skip_serializing_if = "Option::is_none")]
469    pub lifecycle: Option<LifecycleAnnotations>,
470    /// RFC-0002: Documentation references
471    #[serde(default, skip_serializing_if = "Vec::is_empty")]
472    pub refs: Vec<RefEntry>,
473    /// RFC-0002: Style guide configuration
474    #[serde(skip_serializing_if = "Option::is_none")]
475    pub style: Option<StyleEntry>,
476}
477
478/// @acp:summary "RFC-0002: Documentation reference entry"
479#[derive(Debug, Clone, Serialize, Deserialize)]
480#[serde(rename_all = "camelCase")]
481pub struct RefEntry {
482    /// Documentation URL
483    pub url: String,
484    /// Approved source ID from config (if applicable)
485    #[serde(skip_serializing_if = "Option::is_none")]
486    pub source_id: Option<String>,
487    /// Documentation version (from @acp:ref-version)
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub version: Option<String>,
490    /// Section within documentation (from @acp:ref-section)
491    #[serde(skip_serializing_if = "Option::is_none")]
492    pub section: Option<String>,
493    /// Whether AI should fetch this reference (from @acp:ref-fetch)
494    #[serde(default)]
495    pub fetch: bool,
496}
497
498/// @acp:summary "RFC-0002: Style guide configuration entry"
499#[derive(Debug, Clone, Serialize, Deserialize)]
500#[serde(rename_all = "camelCase")]
501pub struct StyleEntry {
502    /// Style guide name or ID
503    pub name: String,
504    /// Parent style guide (from @acp:style-extends)
505    #[serde(skip_serializing_if = "Option::is_none")]
506    pub extends: Option<String>,
507    /// Documentation source ID for this style
508    #[serde(skip_serializing_if = "Option::is_none")]
509    pub source: Option<String>,
510    /// Direct URL to style guide documentation
511    #[serde(skip_serializing_if = "Option::is_none")]
512    pub url: Option<String>,
513    /// Style rules (from @acp:style-rules)
514    #[serde(default, skip_serializing_if = "Vec::is_empty")]
515    pub rules: Vec<String>,
516}
517
518/// @acp:summary "RFC-001: Inline annotation (hack, todo, fixme, critical, perf)"
519#[derive(Debug, Clone, Serialize, Deserialize)]
520pub struct InlineAnnotation {
521    /// Line number (1-indexed)
522    pub line: usize,
523    /// Annotation type (hack, todo, fixme, critical, perf)
524    #[serde(rename = "type")]
525    pub annotation_type: String,
526    /// Annotation value/description
527    #[serde(skip_serializing_if = "Option::is_none")]
528    pub value: Option<String>,
529    /// RFC-001: Self-documenting directive
530    pub directive: String,
531    /// Expiry date for hacks (optional)
532    #[serde(skip_serializing_if = "Option::is_none")]
533    pub expires: Option<String>,
534    /// Related ticket/issue (optional)
535    #[serde(skip_serializing_if = "Option::is_none")]
536    pub ticket: Option<String>,
537    /// Whether directive was auto-generated
538    #[serde(default, skip_serializing_if = "is_false")]
539    pub auto_generated: bool,
540}
541
542/// @acp:summary "Symbol entry with metadata (RFC-001 compliant)"
543#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct SymbolEntry {
545    /// Simple symbol name (required)
546    pub name: String,
547    /// Qualified name: file_path:class.symbol (required)
548    pub qualified_name: String,
549    /// Symbol type (required)
550    #[serde(rename = "type")]
551    pub symbol_type: SymbolType,
552    /// Containing file path (required)
553    pub file: String,
554    /// [start_line, end_line] (required)
555    pub lines: [usize; 2],
556    /// Whether exported (required)
557    pub exported: bool,
558    /// Function signature (optional)
559    #[serde(skip_serializing_if = "Option::is_none")]
560    pub signature: Option<String>,
561    /// Brief description (optional)
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub summary: Option<String>,
564    /// RFC-001: Symbol purpose from @acp:fn/@acp:class/@acp:method directive
565    #[serde(skip_serializing_if = "Option::is_none")]
566    pub purpose: Option<String>,
567    /// RFC-001: Symbol-level constraints
568    #[serde(skip_serializing_if = "Option::is_none")]
569    pub constraints: Option<SymbolConstraint>,
570    /// Whether async (optional, default false)
571    #[serde(rename = "async", default, skip_serializing_if = "is_false")]
572    pub async_fn: bool,
573    /// Symbol visibility (optional, default public)
574    #[serde(default, skip_serializing_if = "is_default_visibility")]
575    pub visibility: Visibility,
576    /// Symbols this calls (optional)
577    #[serde(default, skip_serializing_if = "Vec::is_empty")]
578    pub calls: Vec<String>,
579    /// Symbols calling this (optional)
580    #[serde(default, skip_serializing_if = "Vec::is_empty")]
581    pub called_by: Vec<String>,
582    /// Git metadata (optional - last commit, author, code age)
583    #[serde(skip_serializing_if = "Option::is_none")]
584    pub git: Option<GitSymbolInfo>,
585    /// RFC-0003: Annotation provenance tracking
586    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
587    pub annotations: HashMap<String, AnnotationProvenance>,
588    /// RFC-0009: Behavioral characteristics
589    #[serde(skip_serializing_if = "Option::is_none")]
590    pub behavioral: Option<BehavioralAnnotations>,
591    /// RFC-0009: Lifecycle status
592    #[serde(skip_serializing_if = "Option::is_none")]
593    pub lifecycle: Option<LifecycleAnnotations>,
594    /// RFC-0009: Documentation metadata
595    #[serde(skip_serializing_if = "Option::is_none")]
596    pub documentation: Option<DocumentationAnnotations>,
597    /// RFC-0009: Performance characteristics
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub performance: Option<PerformanceAnnotations>,
600    /// RFC-0008: Type annotation information
601    #[serde(skip_serializing_if = "Option::is_none")]
602    pub type_info: Option<TypeInfo>,
603}
604
605/// @acp:summary "RFC-001: Symbol-level constraint"
606#[derive(Debug, Clone, Serialize, Deserialize)]
607pub struct SymbolConstraint {
608    /// Lock level for this symbol
609    pub level: String,
610    /// Self-documenting directive
611    pub directive: String,
612    /// Whether directive was auto-generated
613    #[serde(default, skip_serializing_if = "is_false")]
614    pub auto_generated: bool,
615}
616
617fn is_false(b: &bool) -> bool {
618    !*b
619}
620
621fn is_default_visibility(v: &Visibility) -> bool {
622    *v == Visibility::Public
623}
624
625/// @acp:summary "Symbol type enumeration (schema-compliant)"
626#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
627#[serde(rename_all = "lowercase")]
628pub enum SymbolType {
629    #[default]
630    Function,
631    Method,
632    Class,
633    Interface,
634    Type,
635    Enum,
636    Struct,
637    Trait,
638    Const,
639}
640
641/// @acp:summary "Symbol visibility"
642#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
643#[serde(rename_all = "lowercase")]
644pub enum Visibility {
645    #[default]
646    Public,
647    Private,
648    Protected,
649}
650
651/// @acp:summary "Stability classification (schema-compliant)"
652#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
653#[serde(rename_all = "lowercase")]
654pub enum Stability {
655    Stable,
656    Experimental,
657    Deprecated,
658}
659
660/// @acp:summary "Programming language identifier (schema-compliant)"
661#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
662#[serde(rename_all = "lowercase")]
663pub enum Language {
664    Typescript,
665    Javascript,
666    Python,
667    Rust,
668    Go,
669    Java,
670    #[serde(rename = "c-sharp")]
671    CSharp,
672    Cpp,
673    C,
674    Ruby,
675    Php,
676    Swift,
677    Kotlin,
678}
679
680/// @acp:summary "Bidirectional call graph"
681#[derive(Debug, Clone, Default, Serialize, Deserialize)]
682pub struct CallGraph {
683    /// Forward: caller -> [callees]
684    #[serde(default)]
685    pub forward: HashMap<String, Vec<String>>,
686    /// Reverse: callee -> [callers]
687    #[serde(default)]
688    pub reverse: HashMap<String, Vec<String>>,
689}
690
691/// @acp:summary "Domain grouping (schema-compliant)"
692#[derive(Debug, Clone, Serialize, Deserialize)]
693pub struct DomainEntry {
694    /// Domain identifier (required)
695    pub name: String,
696    /// Files in this domain (required)
697    pub files: Vec<String>,
698    /// Symbols in this domain (required)
699    #[serde(default)]
700    pub symbols: Vec<String>,
701    /// Human description (optional)
702    #[serde(skip_serializing_if = "Option::is_none")]
703    pub description: Option<String>,
704}
705
706// ============================================================================
707// RFC-0006: Documentation System Bridging Types
708// ============================================================================
709
710/// @acp:summary "Source of type information (RFC-0006, RFC-0008)"
711/// Indicates where type information was extracted from.
712#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
713#[serde(rename_all = "snake_case")]
714pub enum TypeSource {
715    /// Type from ACP annotation {Type} (RFC-0008)
716    Acp,
717    /// Inline type annotation (TypeScript, Python type hint)
718    TypeHint,
719    /// JSDoc @param {Type} or @returns {Type}
720    Jsdoc,
721    /// Python docstring type specification
722    Docstring,
723    /// Rust doc comment type specification
724    Rustdoc,
725    /// Javadoc @param type specification
726    Javadoc,
727    /// Inferred from usage or default values
728    Inferred,
729    /// Type bridged from native docs - general category (RFC-0008)
730    Native,
731}
732
733/// @acp:summary "Source of bridged documentation (RFC-0006)"
734/// Indicates how documentation was obtained.
735#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
736#[serde(rename_all = "lowercase")]
737pub enum BridgeSource {
738    /// Pure ACP annotation (human-written)
739    #[default]
740    Explicit,
741    /// Converted from native documentation
742    Converted,
743    /// Combined from native + ACP
744    Merged,
745    /// Auto-generated through inference
746    Heuristic,
747}
748
749fn is_explicit_bridge(source: &BridgeSource) -> bool {
750    matches!(source, BridgeSource::Explicit)
751}
752
753/// @acp:summary "Original documentation format (RFC-0006)"
754#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
755#[serde(rename_all = "snake_case")]
756pub enum SourceFormat {
757    /// Pure ACP annotation
758    Acp,
759    /// JSDoc/TSDoc
760    Jsdoc,
761    /// Google-style Python docstring
762    #[serde(rename = "docstring:google")]
763    DocstringGoogle,
764    /// NumPy-style Python docstring
765    #[serde(rename = "docstring:numpy")]
766    DocstringNumpy,
767    /// Sphinx/reST-style Python docstring
768    #[serde(rename = "docstring:sphinx")]
769    DocstringSphinx,
770    /// Rust doc comments
771    Rustdoc,
772    /// Javadoc comments
773    Javadoc,
774    /// Go doc comments
775    Godoc,
776    /// Inline type annotation (TypeScript, Python type hints)
777    TypeHint,
778}
779
780/// @acp:summary "Parameter entry with bridge provenance (RFC-0006)"
781#[derive(Debug, Clone, Serialize, Deserialize)]
782#[serde(rename_all = "camelCase")]
783pub struct ParamEntry {
784    /// Parameter name
785    pub name: String,
786    /// Type annotation (if available)
787    #[serde(skip_serializing_if = "Option::is_none")]
788    pub r#type: Option<String>,
789    /// Source of type information
790    #[serde(skip_serializing_if = "Option::is_none")]
791    pub type_source: Option<TypeSource>,
792    /// Parameter description
793    #[serde(skip_serializing_if = "Option::is_none")]
794    pub description: Option<String>,
795    /// AI behavioral directive
796    #[serde(skip_serializing_if = "Option::is_none")]
797    pub directive: Option<String>,
798    /// Whether parameter is optional
799    #[serde(default, skip_serializing_if = "is_false")]
800    pub optional: bool,
801    /// Default value (if any)
802    #[serde(skip_serializing_if = "Option::is_none")]
803    pub default: Option<String>,
804    /// Source of documentation
805    #[serde(default, skip_serializing_if = "is_explicit_bridge")]
806    pub source: BridgeSource,
807    /// Single source format (when from one source)
808    #[serde(skip_serializing_if = "Option::is_none")]
809    pub source_format: Option<SourceFormat>,
810    /// Multiple source formats (when merged)
811    #[serde(default, skip_serializing_if = "Vec::is_empty")]
812    pub source_formats: Vec<SourceFormat>,
813}
814
815/// @acp:summary "Returns entry with bridge provenance (RFC-0006)"
816#[derive(Debug, Clone, Serialize, Deserialize)]
817#[serde(rename_all = "camelCase")]
818pub struct ReturnsEntry {
819    /// Return type (if available)
820    #[serde(skip_serializing_if = "Option::is_none")]
821    pub r#type: Option<String>,
822    /// Source of type information
823    #[serde(skip_serializing_if = "Option::is_none")]
824    pub type_source: Option<TypeSource>,
825    /// Return value description
826    #[serde(skip_serializing_if = "Option::is_none")]
827    pub description: Option<String>,
828    /// AI behavioral directive
829    #[serde(skip_serializing_if = "Option::is_none")]
830    pub directive: Option<String>,
831    /// Source of documentation
832    #[serde(default, skip_serializing_if = "is_explicit_bridge")]
833    pub source: BridgeSource,
834    /// Single source format (when from one source)
835    #[serde(skip_serializing_if = "Option::is_none")]
836    pub source_format: Option<SourceFormat>,
837    /// Multiple source formats (when merged)
838    #[serde(default, skip_serializing_if = "Vec::is_empty")]
839    pub source_formats: Vec<SourceFormat>,
840}
841
842/// @acp:summary "Throws/Raises entry with bridge provenance (RFC-0006)"
843#[derive(Debug, Clone, Serialize, Deserialize)]
844#[serde(rename_all = "camelCase")]
845pub struct ThrowsEntry {
846    /// Exception/error type
847    pub exception: String,
848    /// Description of when/why thrown
849    #[serde(skip_serializing_if = "Option::is_none")]
850    pub description: Option<String>,
851    /// AI behavioral directive
852    #[serde(skip_serializing_if = "Option::is_none")]
853    pub directive: Option<String>,
854    /// Source of documentation
855    #[serde(default, skip_serializing_if = "is_explicit_bridge")]
856    pub source: BridgeSource,
857    /// Original documentation format
858    #[serde(skip_serializing_if = "Option::is_none")]
859    pub source_format: Option<SourceFormat>,
860}
861
862/// @acp:summary "Per-file bridge metadata (RFC-0006)"
863#[derive(Debug, Clone, Default, Serialize, Deserialize)]
864#[serde(rename_all = "camelCase")]
865pub struct BridgeMetadata {
866    /// Whether bridging was enabled for this file
867    #[serde(default)]
868    pub enabled: bool,
869    /// Detected documentation format (auto-detected or configured)
870    #[serde(skip_serializing_if = "Option::is_none")]
871    pub detected_format: Option<SourceFormat>,
872    /// Count of converted annotations
873    #[serde(default)]
874    pub converted_count: u64,
875    /// Count of merged annotations
876    #[serde(default)]
877    pub merged_count: u64,
878    /// Count of explicit ACP annotations
879    #[serde(default)]
880    pub explicit_count: u64,
881}
882
883impl BridgeMetadata {
884    /// Check if bridge metadata should be serialized
885    pub fn is_empty(&self) -> bool {
886        !self.enabled && self.converted_count == 0 && self.merged_count == 0
887    }
888}
889
890/// @acp:summary "Top-level bridge statistics (RFC-0006)"
891#[derive(Debug, Clone, Default, Serialize, Deserialize)]
892#[serde(rename_all = "camelCase")]
893pub struct BridgeStats {
894    /// Whether bridging is enabled project-wide
895    pub enabled: bool,
896    /// Precedence mode (acp-first, native-first, merge)
897    pub precedence: String,
898    /// Summary statistics
899    pub summary: BridgeSummary,
900    /// Counts by source format
901    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
902    pub by_format: HashMap<String, u64>,
903}
904
905impl BridgeStats {
906    /// Check if bridge stats are empty (for serialization skip)
907    pub fn is_empty(&self) -> bool {
908        !self.enabled && self.summary.total_annotations == 0
909    }
910}
911
912/// @acp:summary "Bridge summary counts (RFC-0006)"
913#[derive(Debug, Clone, Default, Serialize, Deserialize)]
914#[serde(rename_all = "camelCase")]
915pub struct BridgeSummary {
916    /// Total annotations
917    pub total_annotations: u64,
918    /// Explicit ACP annotations
919    pub explicit_count: u64,
920    /// Converted from native docs
921    pub converted_count: u64,
922    /// Merged ACP + native
923    pub merged_count: u64,
924}
925
926// ============================================================================
927// RFC-0009: Extended Annotation Types
928// ============================================================================
929
930/// @acp:summary "Behavioral characteristics of a symbol (RFC-0009)"
931#[derive(Debug, Clone, Default, Serialize, Deserialize)]
932#[serde(rename_all = "camelCase")]
933pub struct BehavioralAnnotations {
934    /// Function has no side effects (from @acp:pure)
935    #[serde(default, skip_serializing_if = "is_false")]
936    pub pure: bool,
937    /// Function is safe to call multiple times (from @acp:idempotent)
938    #[serde(default, skip_serializing_if = "is_false")]
939    pub idempotent: bool,
940    /// Results are cached; string for duration (from @acp:memoized)
941    #[serde(skip_serializing_if = "Option::is_none")]
942    pub memoized: Option<MemoizedValue>,
943    /// Function is asynchronous (from @acp:async)
944    #[serde(default, skip_serializing_if = "is_false")]
945    pub r#async: bool,
946    /// Function is a generator (from @acp:generator)
947    #[serde(default, skip_serializing_if = "is_false")]
948    pub generator: bool,
949    /// Rate limit specification (from @acp:throttled)
950    #[serde(skip_serializing_if = "Option::is_none")]
951    pub throttled: Option<String>,
952    /// Function runs in a database transaction (from @acp:transactional)
953    #[serde(default, skip_serializing_if = "is_false")]
954    pub transactional: bool,
955    /// List of side effects (from @acp:side-effects)
956    #[serde(default, skip_serializing_if = "Vec::is_empty")]
957    pub side_effects: Vec<String>,
958}
959
960impl BehavioralAnnotations {
961    /// Check if behavioral annotations are empty (for skip_serializing)
962    pub fn is_empty(&self) -> bool {
963        !self.pure
964            && !self.idempotent
965            && self.memoized.is_none()
966            && !self.r#async
967            && !self.generator
968            && self.throttled.is_none()
969            && !self.transactional
970            && self.side_effects.is_empty()
971    }
972}
973
974/// @acp:summary "Memoized value - either boolean or string duration (RFC-0009)"
975#[derive(Debug, Clone, Serialize, Deserialize)]
976#[serde(untagged)]
977pub enum MemoizedValue {
978    /// Simple memoization flag
979    Enabled(bool),
980    /// Memoization with duration (e.g., "5min", "1h")
981    Duration(String),
982}
983
984/// @acp:summary "Lifecycle status of a symbol or file (RFC-0009)"
985#[derive(Debug, Clone, Default, Serialize, Deserialize)]
986#[serde(rename_all = "camelCase")]
987pub struct LifecycleAnnotations {
988    /// Deprecation message with version/replacement (from @acp:deprecated)
989    #[serde(skip_serializing_if = "Option::is_none")]
990    pub deprecated: Option<String>,
991    /// API may change without notice (from @acp:experimental)
992    #[serde(default, skip_serializing_if = "is_false")]
993    pub experimental: bool,
994    /// Feature in beta testing (from @acp:beta)
995    #[serde(default, skip_serializing_if = "is_false")]
996    pub beta: bool,
997    /// Not intended for external use (from @acp:internal)
998    #[serde(default, skip_serializing_if = "is_false")]
999    pub internal: bool,
1000    /// Stable public interface (from @acp:public-api)
1001    #[serde(default, skip_serializing_if = "is_false")]
1002    pub public_api: bool,
1003    /// Version when introduced (from @acp:since)
1004    #[serde(skip_serializing_if = "Option::is_none")]
1005    pub since: Option<String>,
1006}
1007
1008impl LifecycleAnnotations {
1009    /// Check if lifecycle annotations are empty (for skip_serializing)
1010    pub fn is_empty(&self) -> bool {
1011        self.deprecated.is_none()
1012            && !self.experimental
1013            && !self.beta
1014            && !self.internal
1015            && !self.public_api
1016            && self.since.is_none()
1017    }
1018}
1019
1020/// @acp:summary "Documentation metadata for a symbol (RFC-0009)"
1021#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1022#[serde(rename_all = "camelCase")]
1023pub struct DocumentationAnnotations {
1024    /// Code examples (from @acp:example)
1025    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1026    pub examples: Vec<String>,
1027    /// References to related symbols (from @acp:see)
1028    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1029    pub see_also: Vec<String>,
1030    /// External documentation URLs (from @acp:link)
1031    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1032    pub links: Vec<String>,
1033    /// Important notes (from @acp:note)
1034    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1035    pub notes: Vec<String>,
1036    /// Warnings about usage (from @acp:warning)
1037    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1038    pub warnings: Vec<String>,
1039    /// Pending work items (from @acp:todo)
1040    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1041    pub todos: Vec<String>,
1042}
1043
1044impl DocumentationAnnotations {
1045    /// Check if documentation annotations are empty (for skip_serializing)
1046    pub fn is_empty(&self) -> bool {
1047        self.examples.is_empty()
1048            && self.see_also.is_empty()
1049            && self.links.is_empty()
1050            && self.notes.is_empty()
1051            && self.warnings.is_empty()
1052            && self.todos.is_empty()
1053    }
1054}
1055
1056/// @acp:summary "Performance characteristics of a symbol (RFC-0009)"
1057#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1058#[serde(rename_all = "camelCase")]
1059pub struct PerformanceAnnotations {
1060    /// Time complexity notation (from @acp:perf)
1061    #[serde(skip_serializing_if = "Option::is_none")]
1062    pub complexity: Option<String>,
1063    /// Space complexity notation (from @acp:memory)
1064    #[serde(skip_serializing_if = "Option::is_none")]
1065    pub memory: Option<String>,
1066    /// Caching duration or strategy (from @acp:cached)
1067    #[serde(skip_serializing_if = "Option::is_none")]
1068    pub cached: Option<String>,
1069}
1070
1071impl PerformanceAnnotations {
1072    /// Check if performance annotations are empty (for skip_serializing)
1073    pub fn is_empty(&self) -> bool {
1074        self.complexity.is_none() && self.memory.is_none() && self.cached.is_none()
1075    }
1076}
1077
1078// ============================================================================
1079// RFC-0008: Type Annotation Types
1080// ============================================================================
1081
1082/// @acp:summary "Type annotation information for a symbol (RFC-0008)"
1083#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1084#[serde(rename_all = "camelCase")]
1085pub struct TypeInfo {
1086    /// Parameter type information from @acp:param {Type}
1087    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1088    pub params: Vec<TypeParamInfo>,
1089    /// Return type information from @acp:returns {Type}
1090    #[serde(skip_serializing_if = "Option::is_none")]
1091    pub returns: Option<TypeReturnInfo>,
1092    /// Generic type parameters from @acp:template
1093    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1094    pub type_params: Vec<TypeTypeParam>,
1095}
1096
1097impl TypeInfo {
1098    /// Check if type info is empty (for skip_serializing)
1099    pub fn is_empty(&self) -> bool {
1100        self.params.is_empty() && self.returns.is_none() && self.type_params.is_empty()
1101    }
1102}
1103
1104/// @acp:summary "Parameter type information (RFC-0008)"
1105#[derive(Debug, Clone, Serialize, Deserialize)]
1106#[serde(rename_all = "camelCase")]
1107pub struct TypeParamInfo {
1108    /// Parameter name (required)
1109    pub name: String,
1110    /// Type expression (e.g., "string", "Promise<User>")
1111    #[serde(skip_serializing_if = "Option::is_none")]
1112    pub r#type: Option<String>,
1113    /// Where the type came from: acp, inferred, native
1114    #[serde(skip_serializing_if = "Option::is_none")]
1115    pub type_source: Option<TypeSource>,
1116    /// Whether parameter is optional (from [name] syntax)
1117    #[serde(default, skip_serializing_if = "is_false")]
1118    pub optional: bool,
1119    /// Default value (from [name=default] syntax)
1120    #[serde(skip_serializing_if = "Option::is_none")]
1121    pub default: Option<String>,
1122    /// Directive text for this parameter
1123    #[serde(skip_serializing_if = "Option::is_none")]
1124    pub directive: Option<String>,
1125}
1126
1127/// @acp:summary "Return type information (RFC-0008)"
1128#[derive(Debug, Clone, Serialize, Deserialize)]
1129#[serde(rename_all = "camelCase")]
1130pub struct TypeReturnInfo {
1131    /// Return type expression
1132    #[serde(skip_serializing_if = "Option::is_none")]
1133    pub r#type: Option<String>,
1134    /// Where the type came from: acp, inferred, native
1135    #[serde(skip_serializing_if = "Option::is_none")]
1136    pub type_source: Option<TypeSource>,
1137    /// Directive text for return value
1138    #[serde(skip_serializing_if = "Option::is_none")]
1139    pub directive: Option<String>,
1140}
1141
1142/// @acp:summary "Generic type parameter (RFC-0008)"
1143#[derive(Debug, Clone, Serialize, Deserialize)]
1144#[serde(rename_all = "camelCase")]
1145pub struct TypeTypeParam {
1146    /// Type parameter name (e.g., "T")
1147    pub name: String,
1148    /// Constraint type from 'extends' clause
1149    #[serde(skip_serializing_if = "Option::is_none")]
1150    pub constraint: Option<String>,
1151    /// Directive text for type parameter
1152    #[serde(skip_serializing_if = "Option::is_none")]
1153    pub directive: Option<String>,
1154}
1155
1156// ============================================================================
1157// RFC-0003: Annotation Provenance Types
1158// ============================================================================
1159
1160/// Provenance metadata for a single annotation value (RFC-0003)
1161#[derive(Debug, Clone, Serialize, Deserialize)]
1162#[serde(rename_all = "camelCase")]
1163pub struct AnnotationProvenance {
1164    /// The annotation value
1165    pub value: String,
1166    /// Origin of the annotation
1167    #[serde(default, skip_serializing_if = "is_explicit")]
1168    pub source: SourceOrigin,
1169    /// Confidence score (0.0-1.0), only for auto-generated
1170    #[serde(skip_serializing_if = "Option::is_none")]
1171    pub confidence: Option<f64>,
1172    /// Whether annotation is flagged for review
1173    #[serde(default, skip_serializing_if = "is_false")]
1174    pub needs_review: bool,
1175    /// Whether annotation has been reviewed
1176    #[serde(default, skip_serializing_if = "is_false")]
1177    pub reviewed: bool,
1178    /// When the annotation was reviewed (ISO 8601)
1179    #[serde(skip_serializing_if = "Option::is_none")]
1180    pub reviewed_at: Option<String>,
1181    /// When the annotation was generated (ISO 8601)
1182    #[serde(skip_serializing_if = "Option::is_none")]
1183    pub generated_at: Option<String>,
1184    /// Generation batch identifier
1185    #[serde(skip_serializing_if = "Option::is_none")]
1186    pub generation_id: Option<String>,
1187}
1188
1189fn is_explicit(source: &SourceOrigin) -> bool {
1190    matches!(source, SourceOrigin::Explicit)
1191}
1192
1193/// Top-level provenance statistics (RFC-0003)
1194#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1195#[serde(rename_all = "camelCase")]
1196pub struct ProvenanceStats {
1197    /// Summary counts by source type
1198    pub summary: ProvenanceSummary,
1199    /// Annotations below confidence threshold
1200    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1201    pub low_confidence: Vec<LowConfidenceEntry>,
1202    /// Information about the last generation run
1203    #[serde(skip_serializing_if = "Option::is_none")]
1204    pub last_generation: Option<GenerationInfo>,
1205}
1206
1207impl ProvenanceStats {
1208    /// Check if provenance stats are empty (for serialization skip)
1209    pub fn is_empty(&self) -> bool {
1210        self.summary.total == 0
1211    }
1212}
1213
1214/// Summary of annotation provenance counts (RFC-0003)
1215#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1216#[serde(rename_all = "camelCase")]
1217pub struct ProvenanceSummary {
1218    /// Total annotations tracked
1219    pub total: u64,
1220    /// Counts by source type
1221    pub by_source: SourceCounts,
1222    /// Count needing review
1223    pub needs_review: u64,
1224    /// Count already reviewed
1225    pub reviewed: u64,
1226    /// Average confidence by source type
1227    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1228    pub average_confidence: HashMap<String, f64>,
1229}
1230
1231/// Counts of annotations by source origin (RFC-0003)
1232#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1233pub struct SourceCounts {
1234    /// Human-written annotations
1235    #[serde(default)]
1236    pub explicit: u64,
1237    /// Converted from existing docs
1238    #[serde(default)]
1239    pub converted: u64,
1240    /// Inferred via heuristics
1241    #[serde(default)]
1242    pub heuristic: u64,
1243    /// Refined by AI
1244    #[serde(default)]
1245    pub refined: u64,
1246    /// Fully AI-inferred
1247    #[serde(default)]
1248    pub inferred: u64,
1249}
1250
1251/// Entry for low-confidence annotation tracking (RFC-0003)
1252#[derive(Debug, Clone, Serialize, Deserialize)]
1253#[serde(rename_all = "camelCase")]
1254pub struct LowConfidenceEntry {
1255    /// Target file or symbol (file:symbol format)
1256    pub target: String,
1257    /// Annotation key (e.g., "@acp:summary")
1258    pub annotation: String,
1259    /// Confidence score
1260    pub confidence: f64,
1261    /// Annotation value
1262    pub value: String,
1263}
1264
1265/// Information about a generation run (RFC-0003)
1266#[derive(Debug, Clone, Serialize, Deserialize)]
1267#[serde(rename_all = "camelCase")]
1268pub struct GenerationInfo {
1269    /// Unique batch identifier
1270    pub id: String,
1271    /// When generation occurred (ISO 8601)
1272    pub timestamp: String,
1273    /// Number of annotations generated
1274    pub annotations_generated: u64,
1275    /// Number of files affected
1276    pub files_affected: u64,
1277}
1278
1279#[cfg(test)]
1280mod tests {
1281    use super::*;
1282
1283    #[test]
1284    fn test_cache_roundtrip() {
1285        let cache = CacheBuilder::new("test", "/test")
1286            .add_symbol(SymbolEntry {
1287                name: "test_fn".to_string(),
1288                qualified_name: "test.rs:test_fn".to_string(),
1289                symbol_type: SymbolType::Function,
1290                file: "test.rs".to_string(),
1291                lines: [1, 10],
1292                exported: true,
1293                signature: None,
1294                summary: Some("Test function".to_string()),
1295                purpose: None,
1296                constraints: None,
1297                async_fn: false,
1298                visibility: Visibility::Public,
1299                calls: vec![],
1300                called_by: vec![],
1301                git: None,
1302                annotations: HashMap::new(), // RFC-0003
1303                // RFC-0009: Extended annotation types
1304                behavioral: None,
1305                lifecycle: None,
1306                documentation: None,
1307                performance: None,
1308                // RFC-0008: Type annotation info
1309                type_info: None,
1310            })
1311            .build();
1312
1313        let json = serde_json::to_string_pretty(&cache).unwrap();
1314        let parsed: Cache = serde_json::from_str(&json).unwrap();
1315
1316        assert_eq!(parsed.project.name, "test");
1317        assert!(parsed.symbols.contains_key("test_fn"));
1318    }
1319
1320    // ========================================================================
1321    // Path Normalization Tests
1322    // ========================================================================
1323
1324    #[test]
1325    fn test_normalize_path_basic() {
1326        // Simple paths should pass through
1327        assert_eq!(normalize_path("src/file.ts"), "src/file.ts");
1328        assert_eq!(normalize_path("file.ts"), "file.ts");
1329        assert_eq!(normalize_path("a/b/c/file.ts"), "a/b/c/file.ts");
1330    }
1331
1332    #[test]
1333    fn test_normalize_path_dot_prefix() {
1334        // Leading ./ should be stripped
1335        assert_eq!(normalize_path("./src/file.ts"), "src/file.ts");
1336        assert_eq!(normalize_path("./file.ts"), "file.ts");
1337        assert_eq!(normalize_path("././src/file.ts"), "src/file.ts");
1338    }
1339
1340    #[test]
1341    fn test_normalize_path_windows_backslash() {
1342        // Windows backslashes should be converted
1343        assert_eq!(normalize_path("src\\file.ts"), "src/file.ts");
1344        assert_eq!(normalize_path(".\\src\\file.ts"), "src/file.ts");
1345        assert_eq!(normalize_path("a\\b\\c\\file.ts"), "a/b/c/file.ts");
1346    }
1347
1348    #[test]
1349    fn test_normalize_path_double_slashes() {
1350        // Double slashes should be normalized
1351        assert_eq!(normalize_path("src//file.ts"), "src/file.ts");
1352        assert_eq!(normalize_path("a//b//c//file.ts"), "a/b/c/file.ts");
1353        assert_eq!(normalize_path(".//src/file.ts"), "src/file.ts");
1354    }
1355
1356    #[test]
1357    fn test_normalize_path_parent_refs() {
1358        // Parent directory references should be resolved
1359        assert_eq!(normalize_path("src/../src/file.ts"), "src/file.ts");
1360        assert_eq!(normalize_path("a/b/../c/file.ts"), "a/c/file.ts");
1361        assert_eq!(normalize_path("a/b/c/../../d/file.ts"), "a/d/file.ts");
1362        assert_eq!(normalize_path("./src/../src/file.ts"), "src/file.ts");
1363    }
1364
1365    #[test]
1366    fn test_normalize_path_current_dir() {
1367        // Current directory references should be removed
1368        assert_eq!(normalize_path("src/./file.ts"), "src/file.ts");
1369        assert_eq!(normalize_path("./src/./file.ts"), "src/file.ts");
1370        assert_eq!(normalize_path("a/./b/./c/file.ts"), "a/b/c/file.ts");
1371    }
1372
1373    #[test]
1374    fn test_normalize_path_mixed() {
1375        // Mixed cases
1376        assert_eq!(normalize_path(".\\src/../src\\file.ts"), "src/file.ts");
1377        assert_eq!(normalize_path("./a\\b//../c//file.ts"), "a/c/file.ts");
1378    }
1379
1380    // ========================================================================
1381    // get_file Tests with Various Path Formats
1382    // ========================================================================
1383
1384    fn create_test_cache_with_file() -> Cache {
1385        let mut cache = Cache::new("test", ".");
1386        cache.files.insert(
1387            "./src/sample.ts".to_string(),
1388            FileEntry {
1389                path: "./src/sample.ts".to_string(),
1390                lines: 100,
1391                language: Language::Typescript,
1392                exports: vec![],
1393                imports: vec![],
1394                imported_by: vec![], // RFC-0015
1395                module: None,
1396                summary: None,
1397                purpose: None,
1398                owner: None,
1399                inline: vec![],
1400                domains: vec![],
1401                layer: None,
1402                stability: None,
1403                ai_hints: vec![],
1404                git: None,
1405                annotations: HashMap::new(),       // RFC-0003
1406                bridge: BridgeMetadata::default(), // RFC-0006
1407                // RFC-0009: Extended file-level annotations
1408                version: None,
1409                since: None,
1410                license: None,
1411                author: None,
1412                lifecycle: None,
1413                // RFC-0002: Documentation references and style
1414                refs: vec![],
1415                style: None,
1416            },
1417        );
1418        cache
1419    }
1420
1421    #[test]
1422    fn test_get_file_exact_match() {
1423        let cache = create_test_cache_with_file();
1424        // Exact match should work
1425        assert!(cache.get_file("./src/sample.ts").is_some());
1426    }
1427
1428    #[test]
1429    fn test_get_file_without_prefix() {
1430        let cache = create_test_cache_with_file();
1431        // Without ./ prefix should work
1432        assert!(cache.get_file("src/sample.ts").is_some());
1433    }
1434
1435    #[test]
1436    fn test_get_file_windows_path() {
1437        let cache = create_test_cache_with_file();
1438        // Windows-style path should work
1439        assert!(cache.get_file("src\\sample.ts").is_some());
1440        assert!(cache.get_file(".\\src\\sample.ts").is_some());
1441    }
1442
1443    #[test]
1444    fn test_get_file_with_parent_ref() {
1445        let cache = create_test_cache_with_file();
1446        // Path with .. should work
1447        assert!(cache.get_file("src/../src/sample.ts").is_some());
1448        assert!(cache.get_file("./src/../src/sample.ts").is_some());
1449    }
1450
1451    #[test]
1452    fn test_get_file_double_slash() {
1453        let cache = create_test_cache_with_file();
1454        // Double slashes should work
1455        assert!(cache.get_file("src//sample.ts").is_some());
1456    }
1457
1458    #[test]
1459    fn test_get_file_not_found() {
1460        let cache = create_test_cache_with_file();
1461        // Non-existent files should return None
1462        assert!(cache.get_file("src/other.ts").is_none());
1463        assert!(cache.get_file("other/sample.ts").is_none());
1464    }
1465
1466    #[test]
1467    fn test_get_file_stored_without_prefix() {
1468        // Test when cache stores paths without ./ prefix
1469        let mut cache = Cache::new("test", ".");
1470        cache.files.insert(
1471            "src/sample.ts".to_string(),
1472            FileEntry {
1473                path: "src/sample.ts".to_string(),
1474                lines: 100,
1475                language: Language::Typescript,
1476                exports: vec![],
1477                imports: vec![],
1478                imported_by: vec![], // RFC-0015
1479                module: None,
1480                summary: None,
1481                purpose: None,
1482                owner: None,
1483                inline: vec![],
1484                domains: vec![],
1485                layer: None,
1486                stability: None,
1487                ai_hints: vec![],
1488                git: None,
1489                annotations: HashMap::new(),       // RFC-0003
1490                bridge: BridgeMetadata::default(), // RFC-0006
1491                // RFC-0009: Extended file-level annotations
1492                version: None,
1493                since: None,
1494                license: None,
1495                author: None,
1496                lifecycle: None,
1497                // RFC-0002: Documentation references and style
1498                refs: vec![],
1499                style: None,
1500            },
1501        );
1502
1503        // All formats should find it
1504        assert!(cache.get_file("src/sample.ts").is_some());
1505        assert!(cache.get_file("./src/sample.ts").is_some());
1506        assert!(cache.get_file("src\\sample.ts").is_some());
1507    }
1508}