acp/bridge/
merger.rs

1//! @acp:module "Bridge Merger"
2//! @acp:summary "RFC-0006: Merges native documentation with ACP annotations"
3//! @acp:domain cli
4//! @acp:layer service
5
6use super::config::{BridgeConfig, Precedence};
7use super::BridgeResult;
8use crate::annotate::converters::ParsedDocumentation;
9use crate::cache::{BridgeSource, ParamEntry, ReturnsEntry, SourceFormat, ThrowsEntry};
10
11/// @acp:summary "Parsed ACP annotations for a symbol"
12#[derive(Debug, Clone, Default)]
13pub struct AcpAnnotations {
14    /// Summary from @acp:summary or @acp:fn
15    pub summary: Option<String>,
16    /// Directive from @acp:fn or @acp:method
17    pub directive: Option<String>,
18    /// Parameter directives: (name, directive)
19    pub params: Vec<(String, String)>,
20    /// Returns directive
21    pub returns: Option<String>,
22    /// Throws entries: (exception, directive)
23    pub throws: Vec<(String, String)>,
24}
25
26/// @acp:summary "Merges native documentation with ACP annotations"
27pub struct BridgeMerger {
28    config: BridgeConfig,
29}
30
31impl BridgeMerger {
32    /// @acp:summary "Create a new merger with configuration"
33    pub fn new(config: &BridgeConfig) -> Self {
34        Self {
35            config: config.clone(),
36        }
37    }
38
39    /// @acp:summary "Merge native documentation with ACP annotations"
40    pub fn merge(
41        &self,
42        native: Option<&ParsedDocumentation>,
43        native_format: SourceFormat,
44        acp: &AcpAnnotations,
45    ) -> BridgeResult {
46        // If no native docs, return ACP-only result
47        if native.is_none() || !self.config.enabled {
48            return self.build_acp_only(acp);
49        }
50
51        let native = native.unwrap();
52
53        // If native docs are empty, return ACP-only result
54        if native.is_empty() {
55            return self.build_acp_only(acp);
56        }
57
58        // If no ACP annotations, return native-only result
59        if self.is_acp_empty(acp) {
60            return BridgeResult::from_native(native, native_format);
61        }
62
63        // Merge based on precedence mode
64        match self.config.precedence {
65            Precedence::AcpFirst => self.merge_acp_first(native, native_format, acp),
66            Precedence::NativeFirst => self.merge_native_first(native, native_format, acp),
67            Precedence::Merge => self.merge_combined(native, native_format, acp),
68        }
69    }
70
71    /// @acp:summary "Create result from ACP annotations only"
72    fn build_acp_only(&self, acp: &AcpAnnotations) -> BridgeResult {
73        let mut result = BridgeResult {
74            summary: acp.summary.clone(),
75            directive: acp.directive.clone(),
76            source: BridgeSource::Explicit,
77            source_formats: vec![SourceFormat::Acp],
78            ..Default::default()
79        };
80
81        // Convert ACP params
82        for (name, directive) in &acp.params {
83            result.params.push(ParamEntry {
84                name: name.clone(),
85                r#type: None,
86                type_source: None,
87                description: None,
88                directive: Some(directive.clone()),
89                optional: false,
90                default: None,
91                source: BridgeSource::Explicit,
92                source_format: Some(SourceFormat::Acp),
93                source_formats: vec![],
94            });
95        }
96
97        // Convert ACP returns
98        if let Some(directive) = &acp.returns {
99            result.returns = Some(ReturnsEntry {
100                r#type: None,
101                type_source: None,
102                description: None,
103                directive: Some(directive.clone()),
104                source: BridgeSource::Explicit,
105                source_format: Some(SourceFormat::Acp),
106                source_formats: vec![],
107            });
108        }
109
110        // Convert ACP throws
111        for (exception, directive) in &acp.throws {
112            result.throws.push(ThrowsEntry {
113                exception: exception.clone(),
114                description: None,
115                directive: Some(directive.clone()),
116                source: BridgeSource::Explicit,
117                source_format: Some(SourceFormat::Acp),
118            });
119        }
120
121        result
122    }
123
124    /// @acp:summary "Check if ACP annotations are empty"
125    fn is_acp_empty(&self, acp: &AcpAnnotations) -> bool {
126        acp.summary.is_none()
127            && acp.directive.is_none()
128            && acp.params.is_empty()
129            && acp.returns.is_none()
130            && acp.throws.is_empty()
131    }
132
133    /// @acp:summary "Merge with ACP taking precedence"
134    /// ACP annotations win; native docs fill gaps.
135    fn merge_acp_first(
136        &self,
137        native: &ParsedDocumentation,
138        native_format: SourceFormat,
139        acp: &AcpAnnotations,
140    ) -> BridgeResult {
141        let mut result = BridgeResult {
142            // Summary: prefer native (as per spec 15.3.1)
143            summary: native.summary.clone().or_else(|| acp.summary.clone()),
144            // Directive: use ACP
145            directive: acp.directive.clone(),
146            source: BridgeSource::Merged,
147            source_formats: vec![native_format, SourceFormat::Acp],
148            examples: native.examples.clone(),
149            ..Default::default()
150        };
151
152        // Merge params: native descriptions + ACP directives
153        result.params = self.merge_params(native, native_format, acp);
154
155        // Merge returns
156        result.returns = self.merge_returns(native, native_format, acp);
157
158        // Merge throws
159        result.throws = self.merge_throws(native, native_format, acp);
160
161        result
162    }
163
164    /// @acp:summary "Merge with native taking precedence"
165    /// Native docs are authoritative; ACP adds directives only.
166    fn merge_native_first(
167        &self,
168        native: &ParsedDocumentation,
169        native_format: SourceFormat,
170        acp: &AcpAnnotations,
171    ) -> BridgeResult {
172        let mut result = BridgeResult::from_native(native, native_format);
173
174        // Layer on ACP directives only
175        result.directive = acp.directive.clone();
176        result.source = BridgeSource::Merged;
177        result.source_formats = vec![native_format, SourceFormat::Acp];
178
179        // Add directives to params
180        for param in &mut result.params {
181            if let Some((_, directive)) = acp.params.iter().find(|(n, _)| n == &param.name) {
182                param.directive = Some(directive.clone());
183                param.source = BridgeSource::Merged;
184                param.source_formats = vec![native_format, SourceFormat::Acp];
185            }
186        }
187
188        // Add directive to returns
189        if let Some(returns) = &mut result.returns {
190            if let Some(directive) = &acp.returns {
191                returns.directive = Some(directive.clone());
192                returns.source = BridgeSource::Merged;
193                returns.source_formats = vec![native_format, SourceFormat::Acp];
194            }
195        }
196
197        result
198    }
199
200    /// @acp:summary "Intelligently combine both sources"
201    fn merge_combined(
202        &self,
203        native: &ParsedDocumentation,
204        native_format: SourceFormat,
205        acp: &AcpAnnotations,
206    ) -> BridgeResult {
207        // For merge mode, combine descriptions from both if they provide different info
208        let summary = match (&native.summary, &acp.summary) {
209            (Some(n), Some(a)) if n != a => Some(format!("{} {}", n, a)),
210            (Some(n), _) => Some(n.clone()),
211            (_, Some(a)) => Some(a.clone()),
212            _ => None,
213        };
214
215        let mut result = BridgeResult {
216            summary,
217            directive: acp.directive.clone(),
218            source: BridgeSource::Merged,
219            source_formats: vec![native_format, SourceFormat::Acp],
220            examples: native.examples.clone(),
221            ..Default::default()
222        };
223
224        result.params = self.merge_params(native, native_format, acp);
225        result.returns = self.merge_returns(native, native_format, acp);
226        result.throws = self.merge_throws(native, native_format, acp);
227
228        result
229    }
230
231    /// @acp:summary "Merge parameter entries"
232    fn merge_params(
233        &self,
234        native: &ParsedDocumentation,
235        native_format: SourceFormat,
236        acp: &AcpAnnotations,
237    ) -> Vec<ParamEntry> {
238        let mut params = Vec::new();
239
240        // Start with native params
241        for (name, type_str, desc) in &native.params {
242            let directive = acp
243                .params
244                .iter()
245                .find(|(n, _)| n == name)
246                .map(|(_, d)| d.clone());
247
248            let source = if directive.is_some() {
249                BridgeSource::Merged
250            } else {
251                BridgeSource::Converted
252            };
253
254            let source_formats = if directive.is_some() {
255                vec![native_format, SourceFormat::Acp]
256            } else {
257                vec![]
258            };
259
260            params.push(ParamEntry {
261                name: name.clone(),
262                r#type: type_str.clone(),
263                type_source: type_source_from_format(native_format),
264                description: desc.clone(),
265                directive,
266                optional: false,
267                default: None,
268                source,
269                source_format: if source_formats.is_empty() {
270                    Some(native_format)
271                } else {
272                    None
273                },
274                source_formats,
275            });
276        }
277
278        // Add ACP-only params (not in native)
279        for (name, directive) in &acp.params {
280            if !params.iter().any(|p| &p.name == name) {
281                params.push(ParamEntry {
282                    name: name.clone(),
283                    r#type: None,
284                    type_source: None,
285                    description: None,
286                    directive: Some(directive.clone()),
287                    optional: false,
288                    default: None,
289                    source: BridgeSource::Explicit,
290                    source_format: Some(SourceFormat::Acp),
291                    source_formats: vec![],
292                });
293            }
294        }
295
296        params
297    }
298
299    /// @acp:summary "Merge returns entry"
300    fn merge_returns(
301        &self,
302        native: &ParsedDocumentation,
303        native_format: SourceFormat,
304        acp: &AcpAnnotations,
305    ) -> Option<ReturnsEntry> {
306        match (&native.returns, &acp.returns) {
307            (Some((type_str, desc)), Some(directive)) => {
308                // Both exist - merge
309                Some(ReturnsEntry {
310                    r#type: type_str.clone(),
311                    type_source: type_source_from_format(native_format),
312                    description: desc.clone(),
313                    directive: Some(directive.clone()),
314                    source: BridgeSource::Merged,
315                    source_format: None,
316                    source_formats: vec![native_format, SourceFormat::Acp],
317                })
318            }
319            (Some((type_str, desc)), None) => {
320                // Native only
321                Some(ReturnsEntry {
322                    r#type: type_str.clone(),
323                    type_source: type_source_from_format(native_format),
324                    description: desc.clone(),
325                    directive: None,
326                    source: BridgeSource::Converted,
327                    source_format: Some(native_format),
328                    source_formats: vec![],
329                })
330            }
331            (None, Some(directive)) => {
332                // ACP only
333                Some(ReturnsEntry {
334                    r#type: None,
335                    type_source: None,
336                    description: None,
337                    directive: Some(directive.clone()),
338                    source: BridgeSource::Explicit,
339                    source_format: Some(SourceFormat::Acp),
340                    source_formats: vec![],
341                })
342            }
343            (None, None) => None,
344        }
345    }
346
347    /// @acp:summary "Merge throws entries"
348    fn merge_throws(
349        &self,
350        native: &ParsedDocumentation,
351        native_format: SourceFormat,
352        acp: &AcpAnnotations,
353    ) -> Vec<ThrowsEntry> {
354        let mut throws = Vec::new();
355
356        // Start with native throws
357        for (exc_type, desc) in &native.throws {
358            let directive = acp
359                .throws
360                .iter()
361                .find(|(e, _)| e == exc_type)
362                .map(|(_, d)| d.clone());
363
364            let source = if directive.is_some() {
365                BridgeSource::Merged
366            } else {
367                BridgeSource::Converted
368            };
369
370            throws.push(ThrowsEntry {
371                exception: exc_type.clone(),
372                description: desc.clone(),
373                directive,
374                source,
375                source_format: Some(native_format),
376            });
377        }
378
379        // Add ACP-only throws
380        for (exception, directive) in &acp.throws {
381            if !throws.iter().any(|t| &t.exception == exception) {
382                throws.push(ThrowsEntry {
383                    exception: exception.clone(),
384                    description: None,
385                    directive: Some(directive.clone()),
386                    source: BridgeSource::Explicit,
387                    source_format: Some(SourceFormat::Acp),
388                });
389            }
390        }
391
392        throws
393    }
394}
395
396/// @acp:summary "Determine TypeSource from SourceFormat"
397fn type_source_from_format(format: SourceFormat) -> Option<crate::cache::TypeSource> {
398    use crate::cache::TypeSource;
399    match format {
400        SourceFormat::Jsdoc => Some(TypeSource::Jsdoc),
401        SourceFormat::DocstringGoogle
402        | SourceFormat::DocstringNumpy
403        | SourceFormat::DocstringSphinx => Some(TypeSource::Docstring),
404        SourceFormat::Rustdoc => Some(TypeSource::Rustdoc),
405        SourceFormat::Javadoc => Some(TypeSource::Javadoc),
406        SourceFormat::TypeHint => Some(TypeSource::TypeHint),
407        _ => None,
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    fn test_config() -> BridgeConfig {
416        BridgeConfig::enabled()
417    }
418
419    #[test]
420    fn test_merge_acp_only() {
421        let merger = BridgeMerger::new(&test_config());
422        let acp = AcpAnnotations {
423            summary: Some("Test function".to_string()),
424            directive: Some("MUST validate input".to_string()),
425            params: vec![("userId".to_string(), "MUST be UUID".to_string())],
426            returns: Some("MAY be null".to_string()),
427            throws: vec![],
428        };
429
430        let result = merger.merge(None, SourceFormat::Acp, &acp);
431
432        assert_eq!(result.summary, Some("Test function".to_string()));
433        assert_eq!(result.directive, Some("MUST validate input".to_string()));
434        assert_eq!(result.source, BridgeSource::Explicit);
435        assert_eq!(result.params.len(), 1);
436        assert_eq!(result.params[0].directive, Some("MUST be UUID".to_string()));
437    }
438
439    #[test]
440    fn test_merge_native_only() {
441        let merger = BridgeMerger::new(&test_config());
442        let mut native = ParsedDocumentation::new();
443        native.summary = Some("Native summary".to_string());
444        native.params.push((
445            "userId".to_string(),
446            Some("string".to_string()),
447            Some("User ID".to_string()),
448        ));
449
450        let acp = AcpAnnotations::default();
451        let result = merger.merge(Some(&native), SourceFormat::Jsdoc, &acp);
452
453        assert_eq!(result.summary, Some("Native summary".to_string()));
454        assert_eq!(result.source, BridgeSource::Converted);
455        assert_eq!(result.params.len(), 1);
456        assert!(result.params[0].directive.is_none());
457    }
458
459    #[test]
460    fn test_merge_acp_first() {
461        let config = BridgeConfig::enabled();
462        let merger = BridgeMerger::new(&config);
463
464        let mut native = ParsedDocumentation::new();
465        native.summary = Some("Native summary".to_string());
466        native.params.push((
467            "userId".to_string(),
468            Some("string".to_string()),
469            Some("User ID".to_string()),
470        ));
471        native.returns = Some((
472            Some("User".to_string()),
473            Some("The user object".to_string()),
474        ));
475
476        let acp = AcpAnnotations {
477            summary: Some("ACP summary".to_string()),
478            directive: Some("MUST authenticate".to_string()),
479            params: vec![("userId".to_string(), "MUST be UUID".to_string())],
480            returns: Some("MAY be cached".to_string()),
481            throws: vec![],
482        };
483
484        let result = merger.merge(Some(&native), SourceFormat::Jsdoc, &acp);
485
486        // Summary should be from native (per spec 15.3.1)
487        assert_eq!(result.summary, Some("Native summary".to_string()));
488        // Directive from ACP
489        assert_eq!(result.directive, Some("MUST authenticate".to_string()));
490        assert_eq!(result.source, BridgeSource::Merged);
491
492        // Param should have native description + ACP directive
493        assert_eq!(result.params.len(), 1);
494        assert_eq!(result.params[0].description, Some("User ID".to_string()));
495        assert_eq!(result.params[0].directive, Some("MUST be UUID".to_string()));
496        assert_eq!(result.params[0].source, BridgeSource::Merged);
497
498        // Returns should be merged
499        let returns = result.returns.unwrap();
500        assert_eq!(returns.description, Some("The user object".to_string()));
501        assert_eq!(returns.directive, Some("MAY be cached".to_string()));
502    }
503
504    #[test]
505    fn test_merge_native_first() {
506        let mut config = BridgeConfig::enabled();
507        config.precedence = Precedence::NativeFirst;
508        let merger = BridgeMerger::new(&config);
509
510        let mut native = ParsedDocumentation::new();
511        native.summary = Some("Native summary".to_string());
512        native.params.push((
513            "userId".to_string(),
514            Some("string".to_string()),
515            Some("User ID".to_string()),
516        ));
517
518        let acp = AcpAnnotations {
519            directive: Some("MUST authenticate".to_string()),
520            params: vec![("userId".to_string(), "MUST be UUID".to_string())],
521            ..Default::default()
522        };
523
524        let result = merger.merge(Some(&native), SourceFormat::Jsdoc, &acp);
525
526        // Should use native summary
527        assert_eq!(result.summary, Some("Native summary".to_string()));
528        // But layer ACP directive
529        assert_eq!(result.directive, Some("MUST authenticate".to_string()));
530        // Param should be merged
531        assert_eq!(result.params[0].directive, Some("MUST be UUID".to_string()));
532    }
533
534    #[test]
535    fn test_merge_disabled() {
536        let config = BridgeConfig::new(); // disabled
537        let merger = BridgeMerger::new(&config);
538
539        let mut native = ParsedDocumentation::new();
540        native.summary = Some("Native".to_string());
541
542        let acp = AcpAnnotations {
543            summary: Some("ACP".to_string()),
544            ..Default::default()
545        };
546
547        let result = merger.merge(Some(&native), SourceFormat::Jsdoc, &acp);
548
549        // Should only use ACP when bridging disabled
550        assert_eq!(result.summary, Some("ACP".to_string()));
551        assert_eq!(result.source, BridgeSource::Explicit);
552    }
553}