1pub mod analyzer;
25pub mod converters;
26pub mod heuristics;
27pub mod suggester;
28pub mod writer;
29
30pub use analyzer::Analyzer;
31pub use converters::{DocStandardParser, ParsedDocumentation};
32pub use suggester::Suggester;
33pub use writer::{CommentStyle, Writer};
34
35use serde::{Deserialize, Serialize};
36
37use crate::ast::{SymbolKind, Visibility};
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum AnnotationType {
44 Module,
46 Summary,
48 Domain,
50 Layer,
52 Lock,
54 Stability,
56 Deprecated,
58 AiHint,
60 Ref,
62 Hack,
64 LockReason,
66}
67
68impl AnnotationType {
69 pub fn to_annotation_string(&self, value: &str) -> String {
78 match self {
79 Self::Module => format!("@acp:module \"{}\"", value),
80 Self::Summary => format!("@acp:summary \"{}\"", value),
81 Self::Domain => format!("@acp:domain {}", value),
82 Self::Layer => format!("@acp:layer {}", value),
83 Self::Lock => format!("@acp:lock {}", value),
84 Self::Stability => format!("@acp:stability {}", value),
85 Self::Deprecated => format!("@acp:deprecated \"{}\"", value),
86 Self::AiHint => format!("@acp:ai-hint \"{}\"", value),
87 Self::Ref => format!("@acp:ref \"{}\"", value),
88 Self::Hack => format!("@acp:hack {}", value),
89 Self::LockReason => format!("@acp:lock-reason \"{}\"", value),
90 }
91 }
92
93 pub fn namespace(&self) -> &'static str {
95 match self {
96 Self::Module => "module",
97 Self::Summary => "summary",
98 Self::Domain => "domain",
99 Self::Layer => "layer",
100 Self::Lock => "lock",
101 Self::Stability => "stability",
102 Self::Deprecated => "deprecated",
103 Self::AiHint => "ai-hint",
104 Self::Ref => "ref",
105 Self::Hack => "hack",
106 Self::LockReason => "lock-reason",
107 }
108 }
109}
110
111impl std::fmt::Display for AnnotationType {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 write!(f, "@acp:{}", self.namespace())
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
121#[serde(rename_all = "snake_case")]
122pub enum SuggestionSource {
123 Explicit = 0,
125 Converted = 1,
127 Heuristic = 2,
129}
130
131impl std::fmt::Display for SuggestionSource {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 match self {
134 Self::Explicit => write!(f, "explicit"),
135 Self::Converted => write!(f, "converted"),
136 Self::Heuristic => write!(f, "heuristic"),
137 }
138 }
139}
140
141#[derive(Debug, Clone)]
143pub struct ProvenanceConfig {
144 pub generation_id: Option<String>,
146 pub mark_needs_review: bool,
148 pub review_threshold: f32,
150 pub min_confidence: f32,
152}
153
154impl Default for ProvenanceConfig {
155 fn default() -> Self {
156 Self {
157 generation_id: None,
158 mark_needs_review: false,
159 review_threshold: 0.8,
160 min_confidence: 0.5,
161 }
162 }
163}
164
165impl ProvenanceConfig {
166 pub fn new() -> Self {
168 Self::default()
169 }
170
171 pub fn with_generation_id(mut self, id: impl Into<String>) -> Self {
173 self.generation_id = Some(id.into());
174 self
175 }
176
177 pub fn with_needs_review(mut self, needs_review: bool) -> Self {
179 self.mark_needs_review = needs_review;
180 self
181 }
182
183 pub fn with_review_threshold(mut self, threshold: f32) -> Self {
185 self.review_threshold = threshold;
186 self
187 }
188
189 pub fn with_min_confidence(mut self, confidence: f32) -> Self {
191 self.min_confidence = confidence;
192 self
193 }
194}
195
196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
199pub struct Suggestion {
200 pub target: String,
202
203 pub line: usize,
205
206 pub insertion_line: Option<usize>,
209
210 pub annotation_type: AnnotationType,
212
213 pub value: String,
215
216 pub source: SuggestionSource,
218
219 pub confidence: f32,
221}
222
223impl Suggestion {
224 pub fn new(
226 target: impl Into<String>,
227 line: usize,
228 annotation_type: AnnotationType,
229 value: impl Into<String>,
230 source: SuggestionSource,
231 ) -> Self {
232 Self {
233 target: target.into(),
234 line,
235 insertion_line: None,
236 annotation_type,
237 value: value.into(),
238 source,
239 confidence: 1.0,
240 }
241 }
242
243 pub fn with_insertion_line(mut self, line: usize) -> Self {
245 self.insertion_line = Some(line);
246 self
247 }
248
249 pub fn effective_insertion_line(&self) -> usize {
251 self.insertion_line.unwrap_or(self.line)
252 }
253
254 pub fn summary(
256 target: impl Into<String>,
257 line: usize,
258 value: impl Into<String>,
259 source: SuggestionSource,
260 ) -> Self {
261 Self::new(target, line, AnnotationType::Summary, value, source)
262 }
263
264 pub fn domain(
266 target: impl Into<String>,
267 line: usize,
268 value: impl Into<String>,
269 source: SuggestionSource,
270 ) -> Self {
271 Self::new(target, line, AnnotationType::Domain, value, source)
272 }
273
274 pub fn lock(
276 target: impl Into<String>,
277 line: usize,
278 value: impl Into<String>,
279 source: SuggestionSource,
280 ) -> Self {
281 Self::new(target, line, AnnotationType::Lock, value, source)
282 }
283
284 pub fn layer(
286 target: impl Into<String>,
287 line: usize,
288 value: impl Into<String>,
289 source: SuggestionSource,
290 ) -> Self {
291 Self::new(target, line, AnnotationType::Layer, value, source)
292 }
293
294 pub fn module(
296 target: impl Into<String>,
297 line: usize,
298 value: impl Into<String>,
299 source: SuggestionSource,
300 ) -> Self {
301 Self::new(target, line, AnnotationType::Module, value, source)
302 }
303
304 pub fn deprecated(
306 target: impl Into<String>,
307 line: usize,
308 value: impl Into<String>,
309 source: SuggestionSource,
310 ) -> Self {
311 Self::new(target, line, AnnotationType::Deprecated, value, source)
312 }
313
314 pub fn ai_hint(
316 target: impl Into<String>,
317 line: usize,
318 value: impl Into<String>,
319 source: SuggestionSource,
320 ) -> Self {
321 Self::new(target, line, AnnotationType::AiHint, value, source)
322 }
323
324 pub fn with_confidence(mut self, confidence: f32) -> Self {
326 self.confidence = confidence.clamp(0.0, 1.0);
327 self
328 }
329
330 pub fn is_file_level(&self) -> bool {
332 self.target.contains('/') || self.target.contains('\\')
334 }
335
336 pub fn to_annotation_string(&self) -> String {
338 self.annotation_type.to_annotation_string(&self.value)
339 }
340
341 pub fn to_annotation_strings_with_provenance(&self, config: &ProvenanceConfig) -> Vec<String> {
344 let mut lines = vec![self.annotation_type.to_annotation_string(&self.value)];
345
346 let source_value = match self.source {
348 SuggestionSource::Explicit => "explicit",
349 SuggestionSource::Converted => "converted",
350 SuggestionSource::Heuristic => "heuristic",
351 };
352 lines.push(format!("@acp:source {}", source_value));
353
354 if self.confidence < 1.0 {
356 lines.push(format!("@acp:source-confidence {:.2}", self.confidence));
357 }
358
359 let reviewed = match self.source {
362 SuggestionSource::Explicit => {
363 !config.mark_needs_review && self.confidence >= config.review_threshold
364 }
365 SuggestionSource::Converted | SuggestionSource::Heuristic => false,
366 };
367 lines.push(format!("@acp:source-reviewed {}", reviewed));
368
369 if let Some(ref gen_id) = config.generation_id {
371 lines.push(format!("@acp:source-id \"{}\"", gen_id));
372 }
373
374 lines
375 }
376}
377
378#[derive(Debug, Clone, Default, Serialize, Deserialize)]
381pub struct AnalysisResult {
382 pub file_path: String,
384
385 pub language: String,
387
388 pub existing_annotations: Vec<ExistingAnnotation>,
390
391 pub gaps: Vec<AnnotationGap>,
393
394 pub coverage: f32,
396}
397
398impl AnalysisResult {
399 pub fn new(file_path: impl Into<String>, language: impl Into<String>) -> Self {
401 Self {
402 file_path: file_path.into(),
403 language: language.into(),
404 existing_annotations: Vec::new(),
405 gaps: Vec::new(),
406 coverage: 0.0,
407 }
408 }
409
410 pub fn calculate_coverage(&mut self) {
412 let total = self.existing_annotations.len() + self.gaps.len();
413 if total == 0 {
414 self.coverage = 100.0;
415 } else {
416 self.coverage = (self.existing_annotations.len() as f32 / total as f32) * 100.0;
417 }
418 }
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct ExistingAnnotation {
424 pub target: String,
426
427 pub annotation_type: AnnotationType,
429
430 pub value: String,
432
433 pub line: usize,
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct AnnotationGap {
441 pub target: String,
443
444 pub symbol_kind: Option<SymbolKind>,
446
447 pub line: usize,
449
450 pub insertion_line: Option<usize>,
454
455 pub missing: Vec<AnnotationType>,
457
458 pub doc_comment: Option<String>,
460
461 pub doc_comment_range: Option<(usize, usize)>,
463
464 pub is_exported: bool,
466
467 pub visibility: Option<Visibility>,
469}
470
471impl AnnotationGap {
472 pub fn new(target: impl Into<String>, line: usize) -> Self {
474 Self {
475 target: target.into(),
476 symbol_kind: None,
477 line,
478 insertion_line: None,
479 missing: Vec::new(),
480 doc_comment: None,
481 doc_comment_range: None,
482 is_exported: false,
483 visibility: None,
484 }
485 }
486
487 pub fn with_symbol_kind(mut self, kind: SymbolKind) -> Self {
489 self.symbol_kind = Some(kind);
490 self
491 }
492
493 pub fn with_insertion_line(mut self, line: usize) -> Self {
495 self.insertion_line = Some(line);
496 self
497 }
498
499 pub fn effective_insertion_line(&self) -> usize {
501 self.insertion_line.unwrap_or(self.line)
502 }
503
504 pub fn with_doc_comment(mut self, doc: impl Into<String>) -> Self {
506 self.doc_comment = Some(doc.into());
507 self
508 }
509
510 pub fn with_doc_comment_range(
512 mut self,
513 doc: impl Into<String>,
514 start: usize,
515 end: usize,
516 ) -> Self {
517 self.doc_comment = Some(doc.into());
518 self.doc_comment_range = Some((start, end));
519 self
520 }
521
522 pub fn exported(mut self) -> Self {
524 self.is_exported = true;
525 self
526 }
527
528 pub fn with_visibility(mut self, visibility: Visibility) -> Self {
530 self.visibility = Some(visibility);
531 self
532 }
533
534 pub fn add_missing(&mut self, annotation_type: AnnotationType) {
536 if !self.missing.contains(&annotation_type) {
537 self.missing.push(annotation_type);
538 }
539 }
540}
541
542#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
545#[serde(rename_all = "snake_case")]
546pub enum AnnotateLevel {
547 Minimal,
549 #[default]
551 Standard,
552 Full,
554}
555
556impl AnnotateLevel {
557 pub fn included_types(&self) -> Vec<AnnotationType> {
559 match self {
560 Self::Minimal => vec![AnnotationType::Module, AnnotationType::Summary],
561 Self::Standard => vec![
562 AnnotationType::Module,
563 AnnotationType::Summary,
564 AnnotationType::Domain,
565 AnnotationType::Lock,
566 AnnotationType::Layer,
567 AnnotationType::Deprecated,
568 ],
569 Self::Full => vec![
570 AnnotationType::Module,
571 AnnotationType::Summary,
572 AnnotationType::Domain,
573 AnnotationType::Lock,
574 AnnotationType::Layer,
575 AnnotationType::Deprecated,
576 AnnotationType::Stability,
577 AnnotationType::AiHint,
578 AnnotationType::Ref,
579 AnnotationType::Hack,
580 AnnotationType::LockReason,
581 ],
582 }
583 }
584
585 pub fn includes(&self, annotation_type: AnnotationType) -> bool {
587 self.included_types().contains(&annotation_type)
588 }
589}
590
591#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
593#[serde(rename_all = "snake_case")]
594pub enum ConversionSource {
595 #[default]
597 Auto,
598 Jsdoc,
600 Tsdoc,
602 Docstring,
604 Rustdoc,
606 Godoc,
608 Javadoc,
610}
611
612impl ConversionSource {
613 pub fn for_language(language: &str) -> Self {
615 match language.to_lowercase().as_str() {
616 "typescript" | "tsx" => Self::Tsdoc,
617 "javascript" | "jsx" | "js" => Self::Jsdoc,
618 "python" | "py" => Self::Docstring,
619 "rust" | "rs" => Self::Rustdoc,
620 "go" => Self::Godoc,
621 "java" => Self::Javadoc,
622 _ => Self::Auto,
623 }
624 }
625}
626
627#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
629#[serde(rename_all = "snake_case")]
630pub enum OutputFormat {
631 #[default]
633 Diff,
634 Json,
636 Summary,
638}
639
640#[derive(Debug, Clone, Serialize, Deserialize)]
642pub struct FileChange {
643 pub file_path: String,
645
646 pub symbol_name: Option<String>,
648
649 pub line: usize,
651
652 pub annotations: Vec<Suggestion>,
654
655 pub existing_doc_start: Option<usize>,
657
658 pub existing_doc_end: Option<usize>,
660}
661
662impl FileChange {
663 pub fn new(file_path: impl Into<String>, line: usize) -> Self {
665 Self {
666 file_path: file_path.into(),
667 symbol_name: None,
668 line,
669 annotations: Vec::new(),
670 existing_doc_start: None,
671 existing_doc_end: None,
672 }
673 }
674
675 pub fn with_symbol(mut self, name: impl Into<String>) -> Self {
677 self.symbol_name = Some(name.into());
678 self
679 }
680
681 pub fn with_existing_doc(mut self, start: usize, end: usize) -> Self {
683 self.existing_doc_start = Some(start);
684 self.existing_doc_end = Some(end);
685 self
686 }
687
688 pub fn add_annotation(&mut self, suggestion: Suggestion) {
690 self.annotations.push(suggestion);
691 }
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697
698 #[test]
699 fn test_annotation_type_formatting() {
700 assert_eq!(
701 AnnotationType::Summary.to_annotation_string("Test summary"),
702 "@acp:summary \"Test summary\""
703 );
704 assert_eq!(
705 AnnotationType::Domain.to_annotation_string("authentication"),
706 "@acp:domain authentication"
707 );
708 assert_eq!(
709 AnnotationType::Lock.to_annotation_string("restricted"),
710 "@acp:lock restricted"
711 );
712 }
713
714 #[test]
715 fn test_suggestion_source_ordering() {
716 assert!(SuggestionSource::Explicit < SuggestionSource::Converted);
717 assert!(SuggestionSource::Converted < SuggestionSource::Heuristic);
718 }
719
720 #[test]
721 fn test_annotate_level_includes() {
722 assert!(AnnotateLevel::Minimal.includes(AnnotationType::Summary));
723 assert!(!AnnotateLevel::Minimal.includes(AnnotationType::Domain));
724 assert!(AnnotateLevel::Standard.includes(AnnotationType::Domain));
725 assert!(AnnotateLevel::Full.includes(AnnotationType::AiHint));
726 }
727
728 #[test]
729 fn test_suggestion_is_file_level() {
730 let file_suggestion =
731 Suggestion::summary("src/main.rs", 1, "Test", SuggestionSource::Heuristic);
732 let symbol_suggestion =
733 Suggestion::summary("MyClass", 10, "Test", SuggestionSource::Heuristic);
734
735 assert!(file_suggestion.is_file_level());
736 assert!(!symbol_suggestion.is_file_level());
737 }
738
739 #[test]
740 fn test_conversion_source_for_language() {
741 assert_eq!(
742 ConversionSource::for_language("typescript"),
743 ConversionSource::Tsdoc
744 );
745 assert_eq!(
746 ConversionSource::for_language("python"),
747 ConversionSource::Docstring
748 );
749 assert_eq!(
750 ConversionSource::for_language("rust"),
751 ConversionSource::Rustdoc
752 );
753 assert_eq!(
754 ConversionSource::for_language("unknown"),
755 ConversionSource::Auto
756 );
757 }
758}