1use 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
21pub fn normalize_path(path: &str) -> String {
39 let path = path.replace('\\', "/");
41
42 let mut components: Vec<&str> = Vec::new();
44
45 for part in path.split('/') {
46 match part {
47 "" | "." => continue, ".." => {
49 components.pop();
51 }
52 component => components.push(component),
53 }
54 }
55
56 components.join("/")
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Cache {
63 #[serde(rename = "$schema", default = "default_cache_schema")]
65 pub schema: String,
66 pub version: String,
68 pub generated_at: DateTime<Utc>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub git_commit: Option<String>,
73 pub project: ProjectInfo,
75 pub stats: Stats,
77 pub source_files: HashMap<String, DateTime<Utc>>,
79 pub files: HashMap<String, FileEntry>,
81 pub symbols: HashMap<String, SymbolEntry>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub graph: Option<CallGraph>,
86 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
88 pub domains: HashMap<String, DomainEntry>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub constraints: Option<ConstraintIndex>,
92 #[serde(default, skip_serializing_if = "ProvenanceStats::is_empty")]
94 pub provenance: ProvenanceStats,
95 #[serde(default, skip_serializing_if = "BridgeStats::is_empty")]
97 pub bridge: BridgeStats,
98 #[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 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 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 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 pub fn get_symbol(&self, name: &str) -> Option<&SymbolEntry> {
151 self.symbols.get(name)
152 }
153
154 pub fn get_file(&self, path: &str) -> Option<&FileEntry> {
162 if let Some(file) = self.files.get(path) {
164 return Some(file);
165 }
166
167 let normalized = normalize_path(path);
169
170 if let Some(file) = self.files.get(&normalized) {
172 return Some(file);
173 }
174
175 let with_prefix = format!("./{}", &normalized);
177 if let Some(file) = self.files.get(&with_prefix) {
178 return Some(file);
179 }
180
181 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 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 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 pub fn get_domain_files(&self, domain: &str) -> Option<&Vec<String>> {
203 self.domains.get(domain).map(|d| &d.files)
204 }
205
206 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
224pub 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 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#[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#[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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
310 pub languages: Vec<LanguageStat>,
311 #[serde(skip_serializing_if = "Option::is_none")]
313 pub primary_language: Option<String>,
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub indexed_at: Option<DateTime<Utc>>,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct LanguageStat {
322 pub name: String,
324 pub files: usize,
326 pub percentage: f64,
328}
329
330#[derive(Debug, Clone, Default, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct Conventions {
334 #[serde(default, skip_serializing_if = "Vec::is_empty")]
336 pub file_naming: Vec<FileNamingConvention>,
337 #[serde(skip_serializing_if = "Option::is_none")]
339 pub imports: Option<ImportConventions>,
340}
341
342impl Conventions {
343 pub fn is_empty(&self) -> bool {
345 self.file_naming.is_empty() && self.imports.is_none()
346 }
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct FileNamingConvention {
353 pub directory: String,
355 pub pattern: String,
357 pub confidence: f64,
359 #[serde(default, skip_serializing_if = "Vec::is_empty")]
361 pub examples: Vec<String>,
362 #[serde(default, skip_serializing_if = "Vec::is_empty")]
364 pub anti_patterns: Vec<String>,
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
369#[serde(rename_all = "camelCase")]
370pub struct ImportConventions {
371 #[serde(skip_serializing_if = "Option::is_none")]
373 pub module_system: Option<ModuleSystem>,
374 #[serde(skip_serializing_if = "Option::is_none")]
376 pub path_style: Option<PathStyle>,
377 #[serde(default, skip_serializing_if = "is_false")]
379 pub index_exports: bool,
380}
381
382#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct FileEntry {
404 pub path: String,
406 pub lines: usize,
408 pub language: Language,
410 #[serde(default)]
412 pub exports: Vec<String>,
413 #[serde(default)]
415 pub imports: Vec<String>,
416 #[serde(default, skip_serializing_if = "Vec::is_empty")]
418 pub imported_by: Vec<String>,
419 #[serde(skip_serializing_if = "Option::is_none")]
421 pub module: Option<String>,
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub summary: Option<String>,
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub purpose: Option<String>,
428 #[serde(skip_serializing_if = "Option::is_none")]
430 pub owner: Option<String>,
431 #[serde(default, skip_serializing_if = "Vec::is_empty")]
433 pub inline: Vec<InlineAnnotation>,
434 #[serde(default, skip_serializing_if = "Vec::is_empty")]
436 pub domains: Vec<String>,
437 #[serde(skip_serializing_if = "Option::is_none")]
439 pub layer: Option<String>,
440 #[serde(skip_serializing_if = "Option::is_none")]
442 pub stability: Option<Stability>,
443 #[serde(default, skip_serializing_if = "Vec::is_empty")]
445 pub ai_hints: Vec<String>,
446 #[serde(skip_serializing_if = "Option::is_none")]
448 pub git: Option<GitFileInfo>,
449 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
451 pub annotations: HashMap<String, AnnotationProvenance>,
452 #[serde(default, skip_serializing_if = "BridgeMetadata::is_empty")]
454 pub bridge: BridgeMetadata,
455 #[serde(skip_serializing_if = "Option::is_none")]
457 pub version: Option<String>,
458 #[serde(skip_serializing_if = "Option::is_none")]
460 pub since: Option<String>,
461 #[serde(skip_serializing_if = "Option::is_none")]
463 pub license: Option<String>,
464 #[serde(skip_serializing_if = "Option::is_none")]
466 pub author: Option<String>,
467 #[serde(skip_serializing_if = "Option::is_none")]
469 pub lifecycle: Option<LifecycleAnnotations>,
470 #[serde(default, skip_serializing_if = "Vec::is_empty")]
472 pub refs: Vec<RefEntry>,
473 #[serde(skip_serializing_if = "Option::is_none")]
475 pub style: Option<StyleEntry>,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize)]
480#[serde(rename_all = "camelCase")]
481pub struct RefEntry {
482 pub url: String,
484 #[serde(skip_serializing_if = "Option::is_none")]
486 pub source_id: Option<String>,
487 #[serde(skip_serializing_if = "Option::is_none")]
489 pub version: Option<String>,
490 #[serde(skip_serializing_if = "Option::is_none")]
492 pub section: Option<String>,
493 #[serde(default)]
495 pub fetch: bool,
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize)]
500#[serde(rename_all = "camelCase")]
501pub struct StyleEntry {
502 pub name: String,
504 #[serde(skip_serializing_if = "Option::is_none")]
506 pub extends: Option<String>,
507 #[serde(skip_serializing_if = "Option::is_none")]
509 pub source: Option<String>,
510 #[serde(skip_serializing_if = "Option::is_none")]
512 pub url: Option<String>,
513 #[serde(default, skip_serializing_if = "Vec::is_empty")]
515 pub rules: Vec<String>,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize)]
520pub struct InlineAnnotation {
521 pub line: usize,
523 #[serde(rename = "type")]
525 pub annotation_type: String,
526 #[serde(skip_serializing_if = "Option::is_none")]
528 pub value: Option<String>,
529 pub directive: String,
531 #[serde(skip_serializing_if = "Option::is_none")]
533 pub expires: Option<String>,
534 #[serde(skip_serializing_if = "Option::is_none")]
536 pub ticket: Option<String>,
537 #[serde(default, skip_serializing_if = "is_false")]
539 pub auto_generated: bool,
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct SymbolEntry {
545 pub name: String,
547 pub qualified_name: String,
549 #[serde(rename = "type")]
551 pub symbol_type: SymbolType,
552 pub file: String,
554 pub lines: [usize; 2],
556 pub exported: bool,
558 #[serde(skip_serializing_if = "Option::is_none")]
560 pub signature: Option<String>,
561 #[serde(skip_serializing_if = "Option::is_none")]
563 pub summary: Option<String>,
564 #[serde(skip_serializing_if = "Option::is_none")]
566 pub purpose: Option<String>,
567 #[serde(skip_serializing_if = "Option::is_none")]
569 pub constraints: Option<SymbolConstraint>,
570 #[serde(rename = "async", default, skip_serializing_if = "is_false")]
572 pub async_fn: bool,
573 #[serde(default, skip_serializing_if = "is_default_visibility")]
575 pub visibility: Visibility,
576 #[serde(default, skip_serializing_if = "Vec::is_empty")]
578 pub calls: Vec<String>,
579 #[serde(default, skip_serializing_if = "Vec::is_empty")]
581 pub called_by: Vec<String>,
582 #[serde(skip_serializing_if = "Option::is_none")]
584 pub git: Option<GitSymbolInfo>,
585 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
587 pub annotations: HashMap<String, AnnotationProvenance>,
588 #[serde(skip_serializing_if = "Option::is_none")]
590 pub behavioral: Option<BehavioralAnnotations>,
591 #[serde(skip_serializing_if = "Option::is_none")]
593 pub lifecycle: Option<LifecycleAnnotations>,
594 #[serde(skip_serializing_if = "Option::is_none")]
596 pub documentation: Option<DocumentationAnnotations>,
597 #[serde(skip_serializing_if = "Option::is_none")]
599 pub performance: Option<PerformanceAnnotations>,
600 #[serde(skip_serializing_if = "Option::is_none")]
602 pub type_info: Option<TypeInfo>,
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize)]
607pub struct SymbolConstraint {
608 pub level: String,
610 pub directive: String,
612 #[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#[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#[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#[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#[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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
682pub struct CallGraph {
683 #[serde(default)]
685 pub forward: HashMap<String, Vec<String>>,
686 #[serde(default)]
688 pub reverse: HashMap<String, Vec<String>>,
689}
690
691#[derive(Debug, Clone, Serialize, Deserialize)]
693pub struct DomainEntry {
694 pub name: String,
696 pub files: Vec<String>,
698 #[serde(default)]
700 pub symbols: Vec<String>,
701 #[serde(skip_serializing_if = "Option::is_none")]
703 pub description: Option<String>,
704}
705
706#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
713#[serde(rename_all = "snake_case")]
714pub enum TypeSource {
715 Acp,
717 TypeHint,
719 Jsdoc,
721 Docstring,
723 Rustdoc,
725 Javadoc,
727 Inferred,
729 Native,
731}
732
733#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
736#[serde(rename_all = "lowercase")]
737pub enum BridgeSource {
738 #[default]
740 Explicit,
741 Converted,
743 Merged,
745 Heuristic,
747}
748
749fn is_explicit_bridge(source: &BridgeSource) -> bool {
750 matches!(source, BridgeSource::Explicit)
751}
752
753#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
755#[serde(rename_all = "snake_case")]
756pub enum SourceFormat {
757 Acp,
759 Jsdoc,
761 #[serde(rename = "docstring:google")]
763 DocstringGoogle,
764 #[serde(rename = "docstring:numpy")]
766 DocstringNumpy,
767 #[serde(rename = "docstring:sphinx")]
769 DocstringSphinx,
770 Rustdoc,
772 Javadoc,
774 Godoc,
776 TypeHint,
778}
779
780#[derive(Debug, Clone, Serialize, Deserialize)]
782#[serde(rename_all = "camelCase")]
783pub struct ParamEntry {
784 pub name: String,
786 #[serde(skip_serializing_if = "Option::is_none")]
788 pub r#type: Option<String>,
789 #[serde(skip_serializing_if = "Option::is_none")]
791 pub type_source: Option<TypeSource>,
792 #[serde(skip_serializing_if = "Option::is_none")]
794 pub description: Option<String>,
795 #[serde(skip_serializing_if = "Option::is_none")]
797 pub directive: Option<String>,
798 #[serde(default, skip_serializing_if = "is_false")]
800 pub optional: bool,
801 #[serde(skip_serializing_if = "Option::is_none")]
803 pub default: Option<String>,
804 #[serde(default, skip_serializing_if = "is_explicit_bridge")]
806 pub source: BridgeSource,
807 #[serde(skip_serializing_if = "Option::is_none")]
809 pub source_format: Option<SourceFormat>,
810 #[serde(default, skip_serializing_if = "Vec::is_empty")]
812 pub source_formats: Vec<SourceFormat>,
813}
814
815#[derive(Debug, Clone, Serialize, Deserialize)]
817#[serde(rename_all = "camelCase")]
818pub struct ReturnsEntry {
819 #[serde(skip_serializing_if = "Option::is_none")]
821 pub r#type: Option<String>,
822 #[serde(skip_serializing_if = "Option::is_none")]
824 pub type_source: Option<TypeSource>,
825 #[serde(skip_serializing_if = "Option::is_none")]
827 pub description: Option<String>,
828 #[serde(skip_serializing_if = "Option::is_none")]
830 pub directive: Option<String>,
831 #[serde(default, skip_serializing_if = "is_explicit_bridge")]
833 pub source: BridgeSource,
834 #[serde(skip_serializing_if = "Option::is_none")]
836 pub source_format: Option<SourceFormat>,
837 #[serde(default, skip_serializing_if = "Vec::is_empty")]
839 pub source_formats: Vec<SourceFormat>,
840}
841
842#[derive(Debug, Clone, Serialize, Deserialize)]
844#[serde(rename_all = "camelCase")]
845pub struct ThrowsEntry {
846 pub exception: String,
848 #[serde(skip_serializing_if = "Option::is_none")]
850 pub description: Option<String>,
851 #[serde(skip_serializing_if = "Option::is_none")]
853 pub directive: Option<String>,
854 #[serde(default, skip_serializing_if = "is_explicit_bridge")]
856 pub source: BridgeSource,
857 #[serde(skip_serializing_if = "Option::is_none")]
859 pub source_format: Option<SourceFormat>,
860}
861
862#[derive(Debug, Clone, Default, Serialize, Deserialize)]
864#[serde(rename_all = "camelCase")]
865pub struct BridgeMetadata {
866 #[serde(default)]
868 pub enabled: bool,
869 #[serde(skip_serializing_if = "Option::is_none")]
871 pub detected_format: Option<SourceFormat>,
872 #[serde(default)]
874 pub converted_count: u64,
875 #[serde(default)]
877 pub merged_count: u64,
878 #[serde(default)]
880 pub explicit_count: u64,
881}
882
883impl BridgeMetadata {
884 pub fn is_empty(&self) -> bool {
886 !self.enabled && self.converted_count == 0 && self.merged_count == 0
887 }
888}
889
890#[derive(Debug, Clone, Default, Serialize, Deserialize)]
892#[serde(rename_all = "camelCase")]
893pub struct BridgeStats {
894 pub enabled: bool,
896 pub precedence: String,
898 pub summary: BridgeSummary,
900 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
902 pub by_format: HashMap<String, u64>,
903}
904
905impl BridgeStats {
906 pub fn is_empty(&self) -> bool {
908 !self.enabled && self.summary.total_annotations == 0
909 }
910}
911
912#[derive(Debug, Clone, Default, Serialize, Deserialize)]
914#[serde(rename_all = "camelCase")]
915pub struct BridgeSummary {
916 pub total_annotations: u64,
918 pub explicit_count: u64,
920 pub converted_count: u64,
922 pub merged_count: u64,
924}
925
926#[derive(Debug, Clone, Default, Serialize, Deserialize)]
932#[serde(rename_all = "camelCase")]
933pub struct BehavioralAnnotations {
934 #[serde(default, skip_serializing_if = "is_false")]
936 pub pure: bool,
937 #[serde(default, skip_serializing_if = "is_false")]
939 pub idempotent: bool,
940 #[serde(skip_serializing_if = "Option::is_none")]
942 pub memoized: Option<MemoizedValue>,
943 #[serde(default, skip_serializing_if = "is_false")]
945 pub r#async: bool,
946 #[serde(default, skip_serializing_if = "is_false")]
948 pub generator: bool,
949 #[serde(skip_serializing_if = "Option::is_none")]
951 pub throttled: Option<String>,
952 #[serde(default, skip_serializing_if = "is_false")]
954 pub transactional: bool,
955 #[serde(default, skip_serializing_if = "Vec::is_empty")]
957 pub side_effects: Vec<String>,
958}
959
960impl BehavioralAnnotations {
961 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#[derive(Debug, Clone, Serialize, Deserialize)]
976#[serde(untagged)]
977pub enum MemoizedValue {
978 Enabled(bool),
980 Duration(String),
982}
983
984#[derive(Debug, Clone, Default, Serialize, Deserialize)]
986#[serde(rename_all = "camelCase")]
987pub struct LifecycleAnnotations {
988 #[serde(skip_serializing_if = "Option::is_none")]
990 pub deprecated: Option<String>,
991 #[serde(default, skip_serializing_if = "is_false")]
993 pub experimental: bool,
994 #[serde(default, skip_serializing_if = "is_false")]
996 pub beta: bool,
997 #[serde(default, skip_serializing_if = "is_false")]
999 pub internal: bool,
1000 #[serde(default, skip_serializing_if = "is_false")]
1002 pub public_api: bool,
1003 #[serde(skip_serializing_if = "Option::is_none")]
1005 pub since: Option<String>,
1006}
1007
1008impl LifecycleAnnotations {
1009 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1022#[serde(rename_all = "camelCase")]
1023pub struct DocumentationAnnotations {
1024 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1026 pub examples: Vec<String>,
1027 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1029 pub see_also: Vec<String>,
1030 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1032 pub links: Vec<String>,
1033 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1035 pub notes: Vec<String>,
1036 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1038 pub warnings: Vec<String>,
1039 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1041 pub todos: Vec<String>,
1042}
1043
1044impl DocumentationAnnotations {
1045 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1058#[serde(rename_all = "camelCase")]
1059pub struct PerformanceAnnotations {
1060 #[serde(skip_serializing_if = "Option::is_none")]
1062 pub complexity: Option<String>,
1063 #[serde(skip_serializing_if = "Option::is_none")]
1065 pub memory: Option<String>,
1066 #[serde(skip_serializing_if = "Option::is_none")]
1068 pub cached: Option<String>,
1069}
1070
1071impl PerformanceAnnotations {
1072 pub fn is_empty(&self) -> bool {
1074 self.complexity.is_none() && self.memory.is_none() && self.cached.is_none()
1075 }
1076}
1077
1078#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1084#[serde(rename_all = "camelCase")]
1085pub struct TypeInfo {
1086 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1088 pub params: Vec<TypeParamInfo>,
1089 #[serde(skip_serializing_if = "Option::is_none")]
1091 pub returns: Option<TypeReturnInfo>,
1092 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1094 pub type_params: Vec<TypeTypeParam>,
1095}
1096
1097impl TypeInfo {
1098 pub fn is_empty(&self) -> bool {
1100 self.params.is_empty() && self.returns.is_none() && self.type_params.is_empty()
1101 }
1102}
1103
1104#[derive(Debug, Clone, Serialize, Deserialize)]
1106#[serde(rename_all = "camelCase")]
1107pub struct TypeParamInfo {
1108 pub name: String,
1110 #[serde(skip_serializing_if = "Option::is_none")]
1112 pub r#type: Option<String>,
1113 #[serde(skip_serializing_if = "Option::is_none")]
1115 pub type_source: Option<TypeSource>,
1116 #[serde(default, skip_serializing_if = "is_false")]
1118 pub optional: bool,
1119 #[serde(skip_serializing_if = "Option::is_none")]
1121 pub default: Option<String>,
1122 #[serde(skip_serializing_if = "Option::is_none")]
1124 pub directive: Option<String>,
1125}
1126
1127#[derive(Debug, Clone, Serialize, Deserialize)]
1129#[serde(rename_all = "camelCase")]
1130pub struct TypeReturnInfo {
1131 #[serde(skip_serializing_if = "Option::is_none")]
1133 pub r#type: Option<String>,
1134 #[serde(skip_serializing_if = "Option::is_none")]
1136 pub type_source: Option<TypeSource>,
1137 #[serde(skip_serializing_if = "Option::is_none")]
1139 pub directive: Option<String>,
1140}
1141
1142#[derive(Debug, Clone, Serialize, Deserialize)]
1144#[serde(rename_all = "camelCase")]
1145pub struct TypeTypeParam {
1146 pub name: String,
1148 #[serde(skip_serializing_if = "Option::is_none")]
1150 pub constraint: Option<String>,
1151 #[serde(skip_serializing_if = "Option::is_none")]
1153 pub directive: Option<String>,
1154}
1155
1156#[derive(Debug, Clone, Serialize, Deserialize)]
1162#[serde(rename_all = "camelCase")]
1163pub struct AnnotationProvenance {
1164 pub value: String,
1166 #[serde(default, skip_serializing_if = "is_explicit")]
1168 pub source: SourceOrigin,
1169 #[serde(skip_serializing_if = "Option::is_none")]
1171 pub confidence: Option<f64>,
1172 #[serde(default, skip_serializing_if = "is_false")]
1174 pub needs_review: bool,
1175 #[serde(default, skip_serializing_if = "is_false")]
1177 pub reviewed: bool,
1178 #[serde(skip_serializing_if = "Option::is_none")]
1180 pub reviewed_at: Option<String>,
1181 #[serde(skip_serializing_if = "Option::is_none")]
1183 pub generated_at: Option<String>,
1184 #[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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1195#[serde(rename_all = "camelCase")]
1196pub struct ProvenanceStats {
1197 pub summary: ProvenanceSummary,
1199 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1201 pub low_confidence: Vec<LowConfidenceEntry>,
1202 #[serde(skip_serializing_if = "Option::is_none")]
1204 pub last_generation: Option<GenerationInfo>,
1205}
1206
1207impl ProvenanceStats {
1208 pub fn is_empty(&self) -> bool {
1210 self.summary.total == 0
1211 }
1212}
1213
1214#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1216#[serde(rename_all = "camelCase")]
1217pub struct ProvenanceSummary {
1218 pub total: u64,
1220 pub by_source: SourceCounts,
1222 pub needs_review: u64,
1224 pub reviewed: u64,
1226 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1228 pub average_confidence: HashMap<String, f64>,
1229}
1230
1231#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1233pub struct SourceCounts {
1234 #[serde(default)]
1236 pub explicit: u64,
1237 #[serde(default)]
1239 pub converted: u64,
1240 #[serde(default)]
1242 pub heuristic: u64,
1243 #[serde(default)]
1245 pub refined: u64,
1246 #[serde(default)]
1248 pub inferred: u64,
1249}
1250
1251#[derive(Debug, Clone, Serialize, Deserialize)]
1253#[serde(rename_all = "camelCase")]
1254pub struct LowConfidenceEntry {
1255 pub target: String,
1257 pub annotation: String,
1259 pub confidence: f64,
1261 pub value: String,
1263}
1264
1265#[derive(Debug, Clone, Serialize, Deserialize)]
1267#[serde(rename_all = "camelCase")]
1268pub struct GenerationInfo {
1269 pub id: String,
1271 pub timestamp: String,
1273 pub annotations_generated: u64,
1275 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(), behavioral: None,
1305 lifecycle: None,
1306 documentation: None,
1307 performance: None,
1308 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 #[test]
1325 fn test_normalize_path_basic() {
1326 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 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 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 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 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 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 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 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![], 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(), bridge: BridgeMetadata::default(), version: None,
1409 since: None,
1410 license: None,
1411 author: None,
1412 lifecycle: None,
1413 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 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 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 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 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 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 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 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![], 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(), bridge: BridgeMetadata::default(), version: None,
1493 since: None,
1494 license: None,
1495 author: None,
1496 lifecycle: None,
1497 refs: vec![],
1499 style: None,
1500 },
1501 );
1502
1503 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}