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 annotation_type: AnnotationType,
208
209 pub value: String,
211
212 pub source: SuggestionSource,
214
215 pub confidence: f32,
217}
218
219impl Suggestion {
220 pub fn new(
222 target: impl Into<String>,
223 line: usize,
224 annotation_type: AnnotationType,
225 value: impl Into<String>,
226 source: SuggestionSource,
227 ) -> Self {
228 Self {
229 target: target.into(),
230 line,
231 annotation_type,
232 value: value.into(),
233 source,
234 confidence: 1.0,
235 }
236 }
237
238 pub fn summary(
240 target: impl Into<String>,
241 line: usize,
242 value: impl Into<String>,
243 source: SuggestionSource,
244 ) -> Self {
245 Self::new(target, line, AnnotationType::Summary, value, source)
246 }
247
248 pub fn domain(
250 target: impl Into<String>,
251 line: usize,
252 value: impl Into<String>,
253 source: SuggestionSource,
254 ) -> Self {
255 Self::new(target, line, AnnotationType::Domain, value, source)
256 }
257
258 pub fn lock(
260 target: impl Into<String>,
261 line: usize,
262 value: impl Into<String>,
263 source: SuggestionSource,
264 ) -> Self {
265 Self::new(target, line, AnnotationType::Lock, value, source)
266 }
267
268 pub fn layer(
270 target: impl Into<String>,
271 line: usize,
272 value: impl Into<String>,
273 source: SuggestionSource,
274 ) -> Self {
275 Self::new(target, line, AnnotationType::Layer, value, source)
276 }
277
278 pub fn deprecated(
280 target: impl Into<String>,
281 line: usize,
282 value: impl Into<String>,
283 source: SuggestionSource,
284 ) -> Self {
285 Self::new(target, line, AnnotationType::Deprecated, value, source)
286 }
287
288 pub fn ai_hint(
290 target: impl Into<String>,
291 line: usize,
292 value: impl Into<String>,
293 source: SuggestionSource,
294 ) -> Self {
295 Self::new(target, line, AnnotationType::AiHint, value, source)
296 }
297
298 pub fn with_confidence(mut self, confidence: f32) -> Self {
300 self.confidence = confidence.clamp(0.0, 1.0);
301 self
302 }
303
304 pub fn is_file_level(&self) -> bool {
306 self.target.contains('/') || self.target.contains('\\')
308 }
309
310 pub fn to_annotation_string(&self) -> String {
312 self.annotation_type.to_annotation_string(&self.value)
313 }
314
315 pub fn to_annotation_strings_with_provenance(&self, config: &ProvenanceConfig) -> Vec<String> {
318 let mut lines = vec![self.annotation_type.to_annotation_string(&self.value)];
319
320 let source_value = match self.source {
322 SuggestionSource::Explicit => "explicit",
323 SuggestionSource::Converted => "converted",
324 SuggestionSource::Heuristic => "heuristic",
325 };
326 lines.push(format!("@acp:source {}", source_value));
327
328 if self.confidence < 1.0 {
330 lines.push(format!("@acp:source-confidence {:.2}", self.confidence));
331 }
332
333 let reviewed = !config.mark_needs_review && self.confidence >= config.review_threshold;
335 lines.push(format!("@acp:source-reviewed {}", reviewed));
336
337 if let Some(ref gen_id) = config.generation_id {
339 lines.push(format!("@acp:source-id \"{}\"", gen_id));
340 }
341
342 lines
343 }
344}
345
346#[derive(Debug, Clone, Default, Serialize, Deserialize)]
349pub struct AnalysisResult {
350 pub file_path: String,
352
353 pub language: String,
355
356 pub existing_annotations: Vec<ExistingAnnotation>,
358
359 pub gaps: Vec<AnnotationGap>,
361
362 pub coverage: f32,
364}
365
366impl AnalysisResult {
367 pub fn new(file_path: impl Into<String>, language: impl Into<String>) -> Self {
369 Self {
370 file_path: file_path.into(),
371 language: language.into(),
372 existing_annotations: Vec::new(),
373 gaps: Vec::new(),
374 coverage: 0.0,
375 }
376 }
377
378 pub fn calculate_coverage(&mut self) {
380 let total = self.existing_annotations.len() + self.gaps.len();
381 if total == 0 {
382 self.coverage = 100.0;
383 } else {
384 self.coverage = (self.existing_annotations.len() as f32 / total as f32) * 100.0;
385 }
386 }
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct ExistingAnnotation {
392 pub target: String,
394
395 pub annotation_type: AnnotationType,
397
398 pub value: String,
400
401 pub line: usize,
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct AnnotationGap {
409 pub target: String,
411
412 pub symbol_kind: Option<SymbolKind>,
414
415 pub line: usize,
417
418 pub missing: Vec<AnnotationType>,
420
421 pub doc_comment: Option<String>,
423
424 pub doc_comment_range: Option<(usize, usize)>,
426
427 pub is_exported: bool,
429
430 pub visibility: Option<Visibility>,
432}
433
434impl AnnotationGap {
435 pub fn new(target: impl Into<String>, line: usize) -> Self {
437 Self {
438 target: target.into(),
439 symbol_kind: None,
440 line,
441 missing: Vec::new(),
442 doc_comment: None,
443 doc_comment_range: None,
444 is_exported: false,
445 visibility: None,
446 }
447 }
448
449 pub fn with_symbol_kind(mut self, kind: SymbolKind) -> Self {
451 self.symbol_kind = Some(kind);
452 self
453 }
454
455 pub fn with_doc_comment(mut self, doc: impl Into<String>) -> Self {
457 self.doc_comment = Some(doc.into());
458 self
459 }
460
461 pub fn with_doc_comment_range(
463 mut self,
464 doc: impl Into<String>,
465 start: usize,
466 end: usize,
467 ) -> Self {
468 self.doc_comment = Some(doc.into());
469 self.doc_comment_range = Some((start, end));
470 self
471 }
472
473 pub fn exported(mut self) -> Self {
475 self.is_exported = true;
476 self
477 }
478
479 pub fn with_visibility(mut self, visibility: Visibility) -> Self {
481 self.visibility = Some(visibility);
482 self
483 }
484
485 pub fn add_missing(&mut self, annotation_type: AnnotationType) {
487 if !self.missing.contains(&annotation_type) {
488 self.missing.push(annotation_type);
489 }
490 }
491}
492
493#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
496#[serde(rename_all = "snake_case")]
497pub enum AnnotateLevel {
498 Minimal,
500 #[default]
502 Standard,
503 Full,
505}
506
507impl AnnotateLevel {
508 pub fn included_types(&self) -> Vec<AnnotationType> {
510 match self {
511 Self::Minimal => vec![AnnotationType::Module, AnnotationType::Summary],
512 Self::Standard => vec![
513 AnnotationType::Module,
514 AnnotationType::Summary,
515 AnnotationType::Domain,
516 AnnotationType::Lock,
517 AnnotationType::Layer,
518 AnnotationType::Deprecated,
519 ],
520 Self::Full => vec![
521 AnnotationType::Module,
522 AnnotationType::Summary,
523 AnnotationType::Domain,
524 AnnotationType::Lock,
525 AnnotationType::Layer,
526 AnnotationType::Deprecated,
527 AnnotationType::Stability,
528 AnnotationType::AiHint,
529 AnnotationType::Ref,
530 AnnotationType::Hack,
531 AnnotationType::LockReason,
532 ],
533 }
534 }
535
536 pub fn includes(&self, annotation_type: AnnotationType) -> bool {
538 self.included_types().contains(&annotation_type)
539 }
540}
541
542#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
544#[serde(rename_all = "snake_case")]
545pub enum ConversionSource {
546 #[default]
548 Auto,
549 Jsdoc,
551 Tsdoc,
553 Docstring,
555 Rustdoc,
557 Godoc,
559 Javadoc,
561}
562
563impl ConversionSource {
564 pub fn for_language(language: &str) -> Self {
566 match language.to_lowercase().as_str() {
567 "typescript" | "tsx" => Self::Tsdoc,
568 "javascript" | "jsx" | "js" => Self::Jsdoc,
569 "python" | "py" => Self::Docstring,
570 "rust" | "rs" => Self::Rustdoc,
571 "go" => Self::Godoc,
572 "java" => Self::Javadoc,
573 _ => Self::Auto,
574 }
575 }
576}
577
578#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
580#[serde(rename_all = "snake_case")]
581pub enum OutputFormat {
582 #[default]
584 Diff,
585 Json,
587 Summary,
589}
590
591#[derive(Debug, Clone, Serialize, Deserialize)]
593pub struct FileChange {
594 pub file_path: String,
596
597 pub symbol_name: Option<String>,
599
600 pub line: usize,
602
603 pub annotations: Vec<Suggestion>,
605
606 pub existing_doc_start: Option<usize>,
608
609 pub existing_doc_end: Option<usize>,
611}
612
613impl FileChange {
614 pub fn new(file_path: impl Into<String>, line: usize) -> Self {
616 Self {
617 file_path: file_path.into(),
618 symbol_name: None,
619 line,
620 annotations: Vec::new(),
621 existing_doc_start: None,
622 existing_doc_end: None,
623 }
624 }
625
626 pub fn with_symbol(mut self, name: impl Into<String>) -> Self {
628 self.symbol_name = Some(name.into());
629 self
630 }
631
632 pub fn with_existing_doc(mut self, start: usize, end: usize) -> Self {
634 self.existing_doc_start = Some(start);
635 self.existing_doc_end = Some(end);
636 self
637 }
638
639 pub fn add_annotation(&mut self, suggestion: Suggestion) {
641 self.annotations.push(suggestion);
642 }
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648
649 #[test]
650 fn test_annotation_type_formatting() {
651 assert_eq!(
652 AnnotationType::Summary.to_annotation_string("Test summary"),
653 "@acp:summary \"Test summary\""
654 );
655 assert_eq!(
656 AnnotationType::Domain.to_annotation_string("authentication"),
657 "@acp:domain authentication"
658 );
659 assert_eq!(
660 AnnotationType::Lock.to_annotation_string("restricted"),
661 "@acp:lock restricted"
662 );
663 }
664
665 #[test]
666 fn test_suggestion_source_ordering() {
667 assert!(SuggestionSource::Explicit < SuggestionSource::Converted);
668 assert!(SuggestionSource::Converted < SuggestionSource::Heuristic);
669 }
670
671 #[test]
672 fn test_annotate_level_includes() {
673 assert!(AnnotateLevel::Minimal.includes(AnnotationType::Summary));
674 assert!(!AnnotateLevel::Minimal.includes(AnnotationType::Domain));
675 assert!(AnnotateLevel::Standard.includes(AnnotationType::Domain));
676 assert!(AnnotateLevel::Full.includes(AnnotationType::AiHint));
677 }
678
679 #[test]
680 fn test_suggestion_is_file_level() {
681 let file_suggestion =
682 Suggestion::summary("src/main.rs", 1, "Test", SuggestionSource::Heuristic);
683 let symbol_suggestion =
684 Suggestion::summary("MyClass", 10, "Test", SuggestionSource::Heuristic);
685
686 assert!(file_suggestion.is_file_level());
687 assert!(!symbol_suggestion.is_file_level());
688 }
689
690 #[test]
691 fn test_conversion_source_for_language() {
692 assert_eq!(
693 ConversionSource::for_language("typescript"),
694 ConversionSource::Tsdoc
695 );
696 assert_eq!(
697 ConversionSource::for_language("python"),
698 ConversionSource::Docstring
699 );
700 assert_eq!(
701 ConversionSource::for_language("rust"),
702 ConversionSource::Rustdoc
703 );
704 assert_eq!(
705 ConversionSource::for_language("unknown"),
706 ConversionSource::Auto
707 );
708 }
709}