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}
99
100fn default_cache_schema() -> String {
101 "https://acp-protocol.dev/schemas/v1/cache.schema.json".to_string()
102}
103
104impl Cache {
105 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 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 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 pub fn get_symbol(&self, name: &str) -> Option<&SymbolEntry> {
147 self.symbols.get(name)
148 }
149
150 pub fn get_file(&self, path: &str) -> Option<&FileEntry> {
158 if let Some(file) = self.files.get(path) {
160 return Some(file);
161 }
162
163 let normalized = normalize_path(path);
165
166 if let Some(file) = self.files.get(&normalized) {
168 return Some(file);
169 }
170
171 let with_prefix = format!("./{}", &normalized);
173 if let Some(file) = self.files.get(&with_prefix) {
174 return Some(file);
175 }
176
177 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 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 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 pub fn get_domain_files(&self, domain: &str) -> Option<&Vec<String>> {
199 self.domains.get(domain).map(|d| &d.files)
200 }
201
202 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
220pub 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 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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct FileEntry {
308 pub path: String,
310 pub lines: usize,
312 pub language: Language,
314 #[serde(default)]
316 pub exports: Vec<String>,
317 #[serde(default)]
319 pub imports: Vec<String>,
320 #[serde(skip_serializing_if = "Option::is_none")]
322 pub module: Option<String>,
323 #[serde(skip_serializing_if = "Option::is_none")]
325 pub summary: Option<String>,
326 #[serde(skip_serializing_if = "Option::is_none")]
328 pub purpose: Option<String>,
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub owner: Option<String>,
332 #[serde(default, skip_serializing_if = "Vec::is_empty")]
334 pub inline: Vec<InlineAnnotation>,
335 #[serde(default, skip_serializing_if = "Vec::is_empty")]
337 pub domains: Vec<String>,
338 #[serde(skip_serializing_if = "Option::is_none")]
340 pub layer: Option<String>,
341 #[serde(skip_serializing_if = "Option::is_none")]
343 pub stability: Option<Stability>,
344 #[serde(default, skip_serializing_if = "Vec::is_empty")]
346 pub ai_hints: Vec<String>,
347 #[serde(skip_serializing_if = "Option::is_none")]
349 pub git: Option<GitFileInfo>,
350 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
352 pub annotations: HashMap<String, AnnotationProvenance>,
353 #[serde(default, skip_serializing_if = "BridgeMetadata::is_empty")]
355 pub bridge: BridgeMetadata,
356 #[serde(skip_serializing_if = "Option::is_none")]
358 pub version: Option<String>,
359 #[serde(skip_serializing_if = "Option::is_none")]
361 pub since: Option<String>,
362 #[serde(skip_serializing_if = "Option::is_none")]
364 pub license: Option<String>,
365 #[serde(skip_serializing_if = "Option::is_none")]
367 pub author: Option<String>,
368 #[serde(skip_serializing_if = "Option::is_none")]
370 pub lifecycle: Option<LifecycleAnnotations>,
371 #[serde(default, skip_serializing_if = "Vec::is_empty")]
373 pub refs: Vec<RefEntry>,
374 #[serde(skip_serializing_if = "Option::is_none")]
376 pub style: Option<StyleEntry>,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
381#[serde(rename_all = "camelCase")]
382pub struct RefEntry {
383 pub url: String,
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub source_id: Option<String>,
388 #[serde(skip_serializing_if = "Option::is_none")]
390 pub version: Option<String>,
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub section: Option<String>,
394 #[serde(default)]
396 pub fetch: bool,
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
401#[serde(rename_all = "camelCase")]
402pub struct StyleEntry {
403 pub name: String,
405 #[serde(skip_serializing_if = "Option::is_none")]
407 pub extends: Option<String>,
408 #[serde(skip_serializing_if = "Option::is_none")]
410 pub source: Option<String>,
411 #[serde(skip_serializing_if = "Option::is_none")]
413 pub url: Option<String>,
414 #[serde(default, skip_serializing_if = "Vec::is_empty")]
416 pub rules: Vec<String>,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct InlineAnnotation {
422 pub line: usize,
424 #[serde(rename = "type")]
426 pub annotation_type: String,
427 #[serde(skip_serializing_if = "Option::is_none")]
429 pub value: Option<String>,
430 pub directive: String,
432 #[serde(skip_serializing_if = "Option::is_none")]
434 pub expires: Option<String>,
435 #[serde(skip_serializing_if = "Option::is_none")]
437 pub ticket: Option<String>,
438 #[serde(default, skip_serializing_if = "is_false")]
440 pub auto_generated: bool,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct SymbolEntry {
446 pub name: String,
448 pub qualified_name: String,
450 #[serde(rename = "type")]
452 pub symbol_type: SymbolType,
453 pub file: String,
455 pub lines: [usize; 2],
457 pub exported: bool,
459 #[serde(skip_serializing_if = "Option::is_none")]
461 pub signature: Option<String>,
462 #[serde(skip_serializing_if = "Option::is_none")]
464 pub summary: Option<String>,
465 #[serde(skip_serializing_if = "Option::is_none")]
467 pub purpose: Option<String>,
468 #[serde(skip_serializing_if = "Option::is_none")]
470 pub constraints: Option<SymbolConstraint>,
471 #[serde(rename = "async", default, skip_serializing_if = "is_false")]
473 pub async_fn: bool,
474 #[serde(default, skip_serializing_if = "is_default_visibility")]
476 pub visibility: Visibility,
477 #[serde(default, skip_serializing_if = "Vec::is_empty")]
479 pub calls: Vec<String>,
480 #[serde(default, skip_serializing_if = "Vec::is_empty")]
482 pub called_by: Vec<String>,
483 #[serde(skip_serializing_if = "Option::is_none")]
485 pub git: Option<GitSymbolInfo>,
486 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
488 pub annotations: HashMap<String, AnnotationProvenance>,
489 #[serde(skip_serializing_if = "Option::is_none")]
491 pub behavioral: Option<BehavioralAnnotations>,
492 #[serde(skip_serializing_if = "Option::is_none")]
494 pub lifecycle: Option<LifecycleAnnotations>,
495 #[serde(skip_serializing_if = "Option::is_none")]
497 pub documentation: Option<DocumentationAnnotations>,
498 #[serde(skip_serializing_if = "Option::is_none")]
500 pub performance: Option<PerformanceAnnotations>,
501 #[serde(skip_serializing_if = "Option::is_none")]
503 pub type_info: Option<TypeInfo>,
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct SymbolConstraint {
509 pub level: String,
511 pub directive: String,
513 #[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#[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#[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#[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#[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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
583pub struct CallGraph {
584 #[serde(default)]
586 pub forward: HashMap<String, Vec<String>>,
587 #[serde(default)]
589 pub reverse: HashMap<String, Vec<String>>,
590}
591
592#[derive(Debug, Clone, Serialize, Deserialize)]
594pub struct DomainEntry {
595 pub name: String,
597 pub files: Vec<String>,
599 #[serde(default)]
601 pub symbols: Vec<String>,
602 #[serde(skip_serializing_if = "Option::is_none")]
604 pub description: Option<String>,
605}
606
607#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
614#[serde(rename_all = "snake_case")]
615pub enum TypeSource {
616 Acp,
618 TypeHint,
620 Jsdoc,
622 Docstring,
624 Rustdoc,
626 Javadoc,
628 Inferred,
630 Native,
632}
633
634#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
637#[serde(rename_all = "lowercase")]
638pub enum BridgeSource {
639 #[default]
641 Explicit,
642 Converted,
644 Merged,
646 Heuristic,
648}
649
650fn is_explicit_bridge(source: &BridgeSource) -> bool {
651 matches!(source, BridgeSource::Explicit)
652}
653
654#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
656#[serde(rename_all = "snake_case")]
657pub enum SourceFormat {
658 Acp,
660 Jsdoc,
662 #[serde(rename = "docstring:google")]
664 DocstringGoogle,
665 #[serde(rename = "docstring:numpy")]
667 DocstringNumpy,
668 #[serde(rename = "docstring:sphinx")]
670 DocstringSphinx,
671 Rustdoc,
673 Javadoc,
675 Godoc,
677 TypeHint,
679}
680
681#[derive(Debug, Clone, Serialize, Deserialize)]
683#[serde(rename_all = "camelCase")]
684pub struct ParamEntry {
685 pub name: String,
687 #[serde(skip_serializing_if = "Option::is_none")]
689 pub r#type: Option<String>,
690 #[serde(skip_serializing_if = "Option::is_none")]
692 pub type_source: Option<TypeSource>,
693 #[serde(skip_serializing_if = "Option::is_none")]
695 pub description: Option<String>,
696 #[serde(skip_serializing_if = "Option::is_none")]
698 pub directive: Option<String>,
699 #[serde(default, skip_serializing_if = "is_false")]
701 pub optional: bool,
702 #[serde(skip_serializing_if = "Option::is_none")]
704 pub default: Option<String>,
705 #[serde(default, skip_serializing_if = "is_explicit_bridge")]
707 pub source: BridgeSource,
708 #[serde(skip_serializing_if = "Option::is_none")]
710 pub source_format: Option<SourceFormat>,
711 #[serde(default, skip_serializing_if = "Vec::is_empty")]
713 pub source_formats: Vec<SourceFormat>,
714}
715
716#[derive(Debug, Clone, Serialize, Deserialize)]
718#[serde(rename_all = "camelCase")]
719pub struct ReturnsEntry {
720 #[serde(skip_serializing_if = "Option::is_none")]
722 pub r#type: Option<String>,
723 #[serde(skip_serializing_if = "Option::is_none")]
725 pub type_source: Option<TypeSource>,
726 #[serde(skip_serializing_if = "Option::is_none")]
728 pub description: Option<String>,
729 #[serde(skip_serializing_if = "Option::is_none")]
731 pub directive: Option<String>,
732 #[serde(default, skip_serializing_if = "is_explicit_bridge")]
734 pub source: BridgeSource,
735 #[serde(skip_serializing_if = "Option::is_none")]
737 pub source_format: Option<SourceFormat>,
738 #[serde(default, skip_serializing_if = "Vec::is_empty")]
740 pub source_formats: Vec<SourceFormat>,
741}
742
743#[derive(Debug, Clone, Serialize, Deserialize)]
745#[serde(rename_all = "camelCase")]
746pub struct ThrowsEntry {
747 pub exception: String,
749 #[serde(skip_serializing_if = "Option::is_none")]
751 pub description: Option<String>,
752 #[serde(skip_serializing_if = "Option::is_none")]
754 pub directive: Option<String>,
755 #[serde(default, skip_serializing_if = "is_explicit_bridge")]
757 pub source: BridgeSource,
758 #[serde(skip_serializing_if = "Option::is_none")]
760 pub source_format: Option<SourceFormat>,
761}
762
763#[derive(Debug, Clone, Default, Serialize, Deserialize)]
765#[serde(rename_all = "camelCase")]
766pub struct BridgeMetadata {
767 #[serde(default)]
769 pub enabled: bool,
770 #[serde(skip_serializing_if = "Option::is_none")]
772 pub detected_format: Option<SourceFormat>,
773 #[serde(default)]
775 pub converted_count: u64,
776 #[serde(default)]
778 pub merged_count: u64,
779 #[serde(default)]
781 pub explicit_count: u64,
782}
783
784impl BridgeMetadata {
785 pub fn is_empty(&self) -> bool {
787 !self.enabled && self.converted_count == 0 && self.merged_count == 0
788 }
789}
790
791#[derive(Debug, Clone, Default, Serialize, Deserialize)]
793#[serde(rename_all = "camelCase")]
794pub struct BridgeStats {
795 pub enabled: bool,
797 pub precedence: String,
799 pub summary: BridgeSummary,
801 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
803 pub by_format: HashMap<String, u64>,
804}
805
806impl BridgeStats {
807 pub fn is_empty(&self) -> bool {
809 !self.enabled && self.summary.total_annotations == 0
810 }
811}
812
813#[derive(Debug, Clone, Default, Serialize, Deserialize)]
815#[serde(rename_all = "camelCase")]
816pub struct BridgeSummary {
817 pub total_annotations: u64,
819 pub explicit_count: u64,
821 pub converted_count: u64,
823 pub merged_count: u64,
825}
826
827#[derive(Debug, Clone, Default, Serialize, Deserialize)]
833#[serde(rename_all = "camelCase")]
834pub struct BehavioralAnnotations {
835 #[serde(default, skip_serializing_if = "is_false")]
837 pub pure: bool,
838 #[serde(default, skip_serializing_if = "is_false")]
840 pub idempotent: bool,
841 #[serde(skip_serializing_if = "Option::is_none")]
843 pub memoized: Option<MemoizedValue>,
844 #[serde(default, skip_serializing_if = "is_false")]
846 pub r#async: bool,
847 #[serde(default, skip_serializing_if = "is_false")]
849 pub generator: bool,
850 #[serde(skip_serializing_if = "Option::is_none")]
852 pub throttled: Option<String>,
853 #[serde(default, skip_serializing_if = "is_false")]
855 pub transactional: bool,
856 #[serde(default, skip_serializing_if = "Vec::is_empty")]
858 pub side_effects: Vec<String>,
859}
860
861impl BehavioralAnnotations {
862 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#[derive(Debug, Clone, Serialize, Deserialize)]
877#[serde(untagged)]
878pub enum MemoizedValue {
879 Enabled(bool),
881 Duration(String),
883}
884
885#[derive(Debug, Clone, Default, Serialize, Deserialize)]
887#[serde(rename_all = "camelCase")]
888pub struct LifecycleAnnotations {
889 #[serde(skip_serializing_if = "Option::is_none")]
891 pub deprecated: Option<String>,
892 #[serde(default, skip_serializing_if = "is_false")]
894 pub experimental: bool,
895 #[serde(default, skip_serializing_if = "is_false")]
897 pub beta: bool,
898 #[serde(default, skip_serializing_if = "is_false")]
900 pub internal: bool,
901 #[serde(default, skip_serializing_if = "is_false")]
903 pub public_api: bool,
904 #[serde(skip_serializing_if = "Option::is_none")]
906 pub since: Option<String>,
907}
908
909impl LifecycleAnnotations {
910 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
923#[serde(rename_all = "camelCase")]
924pub struct DocumentationAnnotations {
925 #[serde(default, skip_serializing_if = "Vec::is_empty")]
927 pub examples: Vec<String>,
928 #[serde(default, skip_serializing_if = "Vec::is_empty")]
930 pub see_also: Vec<String>,
931 #[serde(default, skip_serializing_if = "Vec::is_empty")]
933 pub links: Vec<String>,
934 #[serde(default, skip_serializing_if = "Vec::is_empty")]
936 pub notes: Vec<String>,
937 #[serde(default, skip_serializing_if = "Vec::is_empty")]
939 pub warnings: Vec<String>,
940 #[serde(default, skip_serializing_if = "Vec::is_empty")]
942 pub todos: Vec<String>,
943}
944
945impl DocumentationAnnotations {
946 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
959#[serde(rename_all = "camelCase")]
960pub struct PerformanceAnnotations {
961 #[serde(skip_serializing_if = "Option::is_none")]
963 pub complexity: Option<String>,
964 #[serde(skip_serializing_if = "Option::is_none")]
966 pub memory: Option<String>,
967 #[serde(skip_serializing_if = "Option::is_none")]
969 pub cached: Option<String>,
970}
971
972impl PerformanceAnnotations {
973 pub fn is_empty(&self) -> bool {
975 self.complexity.is_none() && self.memory.is_none() && self.cached.is_none()
976 }
977}
978
979#[derive(Debug, Clone, Default, Serialize, Deserialize)]
985#[serde(rename_all = "camelCase")]
986pub struct TypeInfo {
987 #[serde(default, skip_serializing_if = "Vec::is_empty")]
989 pub params: Vec<TypeParamInfo>,
990 #[serde(skip_serializing_if = "Option::is_none")]
992 pub returns: Option<TypeReturnInfo>,
993 #[serde(default, skip_serializing_if = "Vec::is_empty")]
995 pub type_params: Vec<TypeTypeParam>,
996}
997
998impl TypeInfo {
999 pub fn is_empty(&self) -> bool {
1001 self.params.is_empty() && self.returns.is_none() && self.type_params.is_empty()
1002 }
1003}
1004
1005#[derive(Debug, Clone, Serialize, Deserialize)]
1007#[serde(rename_all = "camelCase")]
1008pub struct TypeParamInfo {
1009 pub name: String,
1011 #[serde(skip_serializing_if = "Option::is_none")]
1013 pub r#type: Option<String>,
1014 #[serde(skip_serializing_if = "Option::is_none")]
1016 pub type_source: Option<TypeSource>,
1017 #[serde(default, skip_serializing_if = "is_false")]
1019 pub optional: bool,
1020 #[serde(skip_serializing_if = "Option::is_none")]
1022 pub default: Option<String>,
1023 #[serde(skip_serializing_if = "Option::is_none")]
1025 pub directive: Option<String>,
1026}
1027
1028#[derive(Debug, Clone, Serialize, Deserialize)]
1030#[serde(rename_all = "camelCase")]
1031pub struct TypeReturnInfo {
1032 #[serde(skip_serializing_if = "Option::is_none")]
1034 pub r#type: Option<String>,
1035 #[serde(skip_serializing_if = "Option::is_none")]
1037 pub type_source: Option<TypeSource>,
1038 #[serde(skip_serializing_if = "Option::is_none")]
1040 pub directive: Option<String>,
1041}
1042
1043#[derive(Debug, Clone, Serialize, Deserialize)]
1045#[serde(rename_all = "camelCase")]
1046pub struct TypeTypeParam {
1047 pub name: String,
1049 #[serde(skip_serializing_if = "Option::is_none")]
1051 pub constraint: Option<String>,
1052 #[serde(skip_serializing_if = "Option::is_none")]
1054 pub directive: Option<String>,
1055}
1056
1057#[derive(Debug, Clone, Serialize, Deserialize)]
1063#[serde(rename_all = "camelCase")]
1064pub struct AnnotationProvenance {
1065 pub value: String,
1067 #[serde(default, skip_serializing_if = "is_explicit")]
1069 pub source: SourceOrigin,
1070 #[serde(skip_serializing_if = "Option::is_none")]
1072 pub confidence: Option<f64>,
1073 #[serde(default, skip_serializing_if = "is_false")]
1075 pub needs_review: bool,
1076 #[serde(default, skip_serializing_if = "is_false")]
1078 pub reviewed: bool,
1079 #[serde(skip_serializing_if = "Option::is_none")]
1081 pub reviewed_at: Option<String>,
1082 #[serde(skip_serializing_if = "Option::is_none")]
1084 pub generated_at: Option<String>,
1085 #[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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1096#[serde(rename_all = "camelCase")]
1097pub struct ProvenanceStats {
1098 pub summary: ProvenanceSummary,
1100 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1102 pub low_confidence: Vec<LowConfidenceEntry>,
1103 #[serde(skip_serializing_if = "Option::is_none")]
1105 pub last_generation: Option<GenerationInfo>,
1106}
1107
1108impl ProvenanceStats {
1109 pub fn is_empty(&self) -> bool {
1111 self.summary.total == 0
1112 }
1113}
1114
1115#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1117#[serde(rename_all = "camelCase")]
1118pub struct ProvenanceSummary {
1119 pub total: u64,
1121 pub by_source: SourceCounts,
1123 pub needs_review: u64,
1125 pub reviewed: u64,
1127 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1129 pub average_confidence: HashMap<String, f64>,
1130}
1131
1132#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1134pub struct SourceCounts {
1135 #[serde(default)]
1137 pub explicit: u64,
1138 #[serde(default)]
1140 pub converted: u64,
1141 #[serde(default)]
1143 pub heuristic: u64,
1144 #[serde(default)]
1146 pub refined: u64,
1147 #[serde(default)]
1149 pub inferred: u64,
1150}
1151
1152#[derive(Debug, Clone, Serialize, Deserialize)]
1154#[serde(rename_all = "camelCase")]
1155pub struct LowConfidenceEntry {
1156 pub target: String,
1158 pub annotation: String,
1160 pub confidence: f64,
1162 pub value: String,
1164}
1165
1166#[derive(Debug, Clone, Serialize, Deserialize)]
1168#[serde(rename_all = "camelCase")]
1169pub struct GenerationInfo {
1170 pub id: String,
1172 pub timestamp: String,
1174 pub annotations_generated: u64,
1176 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(), behavioral: None,
1206 lifecycle: None,
1207 documentation: None,
1208 performance: None,
1209 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 #[test]
1226 fn test_normalize_path_basic() {
1227 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 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 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 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 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 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 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 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(), bridge: BridgeMetadata::default(), version: None,
1309 since: None,
1310 license: None,
1311 author: None,
1312 lifecycle: None,
1313 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 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 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 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 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 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 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 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(), bridge: BridgeMetadata::default(), version: None,
1392 since: None,
1393 license: None,
1394 author: None,
1395 lifecycle: None,
1396 refs: vec![],
1398 style: None,
1399 },
1400 );
1401
1402 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}