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