acp/parse/
mod.rs

1//! @acp:module "Parser"
2//! @acp:summary "Source code parsing and annotation extraction (RFC-001/RFC-003 compliant)"
3//! @acp:domain cli
4//! @acp:layer service
5//!
6//! Parses source files to extract symbols, calls, and documentation.
7//! Supports RFC-001 self-documenting annotations with directive extraction.
8//! Supports RFC-003 annotation provenance tracking.
9//! Currently uses regex-based parsing with tree-sitter support planned.
10
11use std::path::Path;
12use std::sync::LazyLock;
13
14use regex::Regex;
15use serde::{Deserialize, Serialize};
16
17use crate::cache::{
18    BehavioralAnnotations, DocumentationAnnotations, FileEntry, InlineAnnotation,
19    LifecycleAnnotations, MemoizedValue, PerformanceAnnotations, SymbolEntry, SymbolType, TypeInfo,
20    TypeParamInfo, TypeReturnInfo, TypeSource, TypeTypeParam, Visibility,
21};
22use crate::error::{AcpError, Result};
23use crate::index::detect_language;
24
25/// Regex pattern for parsing @acp: annotations with directive support (RFC-001)
26/// Matches: @acp:name [value] [- directive]
27/// Groups: 1=name, 2=value (before dash), 3=directive (after dash)
28static ANNOTATION_PATTERN: LazyLock<Regex> =
29    LazyLock::new(|| Regex::new(r"@acp:([\w-]+)(?:\s+([^-\n]+?))?(?:\s+-\s+(.+))?$").unwrap());
30
31/// Regex for detecting comment continuation lines (for multiline directives)
32static CONTINUATION_PATTERN: LazyLock<Regex> =
33    LazyLock::new(|| Regex::new(r"^(?://|#|/?\*)\s{2,}(.+)$").unwrap());
34
35// ============================================================================
36// RFC-0003: Annotation Provenance Tracking
37// ============================================================================
38
39/// Regex for @acp:source annotation (RFC-0003)
40static SOURCE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
41    Regex::new(r"@acp:source\s+(explicit|converted|heuristic|refined|inferred)(?:\s+-\s+(.+))?$")
42        .unwrap()
43});
44
45/// Regex for @acp:source-confidence annotation (RFC-0003)
46static CONFIDENCE_PATTERN: LazyLock<Regex> =
47    LazyLock::new(|| Regex::new(r"@acp:source-confidence\s+(\d+\.?\d*)(?:\s+-\s+(.+))?$").unwrap());
48
49/// Regex for @acp:source-reviewed annotation (RFC-0003)
50static REVIEWED_PATTERN: LazyLock<Regex> =
51    LazyLock::new(|| Regex::new(r"@acp:source-reviewed\s+(true|false)(?:\s+-\s+(.+))?$").unwrap());
52
53/// Regex for @acp:source-id annotation (RFC-0003)
54static ID_PATTERN: LazyLock<Regex> =
55    LazyLock::new(|| Regex::new(r"@acp:source-id\s+([a-zA-Z0-9\-]+)(?:\s+-\s+(.+))?$").unwrap());
56
57/// Source origin for annotation provenance (RFC-0003)
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
59#[serde(rename_all = "lowercase")]
60pub enum SourceOrigin {
61    /// Annotation was written by a human developer
62    #[default]
63    Explicit,
64    /// Annotation was converted from existing documentation (JSDoc, rustdoc, etc.)
65    Converted,
66    /// Annotation was inferred using heuristic analysis
67    Heuristic,
68    /// Annotation was refined by AI from lower-quality source
69    Refined,
70    /// Annotation was fully inferred by AI
71    Inferred,
72}
73
74impl SourceOrigin {
75    /// Get string representation for serialization
76    pub fn as_str(&self) -> &'static str {
77        match self {
78            SourceOrigin::Explicit => "explicit",
79            SourceOrigin::Converted => "converted",
80            SourceOrigin::Heuristic => "heuristic",
81            SourceOrigin::Refined => "refined",
82            SourceOrigin::Inferred => "inferred",
83        }
84    }
85}
86
87impl std::str::FromStr for SourceOrigin {
88    type Err = String;
89
90    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
91        match s.to_lowercase().as_str() {
92            "explicit" => Ok(SourceOrigin::Explicit),
93            "converted" => Ok(SourceOrigin::Converted),
94            "heuristic" => Ok(SourceOrigin::Heuristic),
95            "refined" => Ok(SourceOrigin::Refined),
96            "inferred" => Ok(SourceOrigin::Inferred),
97            _ => Err(format!("Unknown source origin: {}", s)),
98        }
99    }
100}
101
102impl std::fmt::Display for SourceOrigin {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        write!(f, "{}", self.as_str())
105    }
106}
107
108/// Provenance metadata for an annotation (RFC-0003)
109#[derive(Debug, Clone, Serialize, Deserialize, Default)]
110pub struct ProvenanceMarker {
111    /// Source origin (explicit, converted, heuristic, refined, inferred)
112    pub source: SourceOrigin,
113    /// Confidence score (0.0-1.0), only for auto-generated
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub confidence: Option<f64>,
116    /// Whether annotation has been reviewed
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub reviewed: Option<bool>,
119    /// Generation batch identifier
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub generation_id: Option<String>,
122}
123
124/// Extended annotation with provenance (RFC-0003)
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct AnnotationWithProvenance {
127    /// The base annotation
128    pub annotation: Annotation,
129    /// Optional provenance metadata
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub provenance: Option<ProvenanceMarker>,
132}
133
134/// @acp:summary "Result of parsing a source file"
135#[derive(Debug, Clone)]
136pub struct ParseResult {
137    pub file: FileEntry,
138    pub symbols: Vec<SymbolEntry>,
139    pub calls: Vec<(String, Vec<String>)>, // (caller, callees)
140    pub lock_level: Option<String>,        // from @acp:lock
141    pub lock_directive: Option<String>,    // RFC-001: directive text for lock
142    pub ai_hints: Vec<String>,             // from @acp:ai-careful, @acp:ai-readonly, etc.
143    pub hacks: Vec<HackAnnotation>,        // from @acp:hack
144    pub inline_annotations: Vec<InlineAnnotation>, // RFC-001: inline annotations (todo, fixme, critical, perf)
145    pub purpose: Option<String>,                   // RFC-001: file purpose from @acp:purpose
146    pub owner: Option<String>,                     // RFC-001: file owner from @acp:owner
147}
148
149/// @acp:summary "Parsed hack annotation"
150#[derive(Debug, Clone)]
151pub struct HackAnnotation {
152    pub line: usize,
153    pub expires: Option<String>,
154    pub ticket: Option<String>,
155    pub reason: Option<String>,
156}
157
158/// @acp:summary "Parser for source files"
159pub struct Parser {
160    // tree-sitter parsers would be initialized here
161    // For now, this is a stub implementation
162}
163
164impl Parser {
165    pub fn new() -> Self {
166        Self {}
167    }
168
169    /// @acp:summary "Parse a source file and extract metadata"
170    pub fn parse<P: AsRef<Path>>(&self, path: P) -> Result<ParseResult> {
171        let path = path.as_ref();
172        let content = std::fs::read_to_string(path)?;
173        let file_path = path.to_string_lossy().to_string();
174
175        let language = detect_language(&file_path).ok_or_else(|| {
176            AcpError::UnsupportedLanguage(
177                path.extension()
178                    .map(|e| e.to_string_lossy().to_string())
179                    .unwrap_or_default(),
180            )
181        })?;
182
183        let lines = content.lines().count();
184        let _file_name = path
185            .file_stem()
186            .map(|s| s.to_string_lossy().to_string())
187            .unwrap_or_default();
188
189        // Parse @acp: annotations from source
190        let annotations = self.parse_annotations(&content);
191
192        // Extract file-level metadata from annotations
193        let mut module_name = None;
194        let mut file_summary = None;
195        let mut domains = vec![];
196        let mut layer = None;
197        let mut symbols = vec![];
198        let mut exports = vec![];
199        let mut imports = vec![];
200        let mut calls = vec![];
201        let mut lock_level = None;
202        let mut lock_directive = None;
203        let mut ai_hints = vec![];
204        let mut hacks = vec![];
205        let mut inline_annotations = vec![];
206        let mut purpose = None;
207        let mut owner = None;
208
209        // RFC-0009: File-level extended annotation accumulators
210        let mut file_version: Option<String> = None;
211        let mut file_since: Option<String> = None;
212        let mut file_license: Option<String> = None;
213        let mut file_author: Option<String> = None;
214        let mut file_lifecycle = LifecycleAnnotations::default();
215
216        // Track current symbol context for multi-line annotations
217        let mut current_symbol: Option<SymbolBuilder> = None;
218
219        for ann in &annotations {
220            match ann.name.as_str() {
221                "module" => {
222                    if let Some(val) = &ann.value {
223                        module_name = Some(val.trim_matches('"').to_string());
224                    }
225                }
226                "summary" => {
227                    if let Some(ref mut builder) = current_symbol {
228                        if let Some(val) = &ann.value {
229                            builder.summary = Some(val.trim_matches('"').to_string());
230                        }
231                    } else if let Some(val) = &ann.value {
232                        // File-level summary
233                        file_summary = Some(val.trim_matches('"').to_string());
234                    }
235                }
236                "domain" => {
237                    if let Some(val) = &ann.value {
238                        domains.push(val.trim_matches('"').to_string());
239                    }
240                }
241                "layer" => {
242                    if let Some(val) = &ann.value {
243                        layer = Some(val.trim_matches('"').to_string());
244                    }
245                }
246                "lock" => {
247                    if let Some(val) = &ann.value {
248                        lock_level = Some(val.trim_matches('"').to_string());
249                    }
250                    // RFC-001: Capture directive for lock annotation
251                    lock_directive = ann.directive.clone();
252                }
253                // RFC-001: File purpose annotation
254                "purpose" => {
255                    if let Some(val) = &ann.value {
256                        purpose = Some(val.trim_matches('"').to_string());
257                    } else if let Some(dir) = &ann.directive {
258                        purpose = Some(dir.clone());
259                    }
260                }
261                // RFC-001: File owner annotation
262                "owner" => {
263                    if let Some(val) = &ann.value {
264                        owner = Some(val.trim_matches('"').to_string());
265                    }
266                }
267                "ai-careful" | "ai-readonly" | "ai-avoid" | "ai-no-modify" => {
268                    let hint = if let Some(val) = &ann.value {
269                        format!("{}: {}", ann.name, val.trim_matches('"'))
270                    } else {
271                        ann.name.clone()
272                    };
273                    ai_hints.push(hint);
274                }
275                "hack" => {
276                    // Parse hack annotation: @acp:hack expires=2025-03-01 ticket=JIRA-123 "reason"
277                    let mut expires = None;
278                    let mut ticket = None;
279                    let mut reason = None;
280
281                    if let Some(val) = &ann.value {
282                        // Parse key=value pairs and quoted reason
283                        for part in val.split_whitespace() {
284                            if let Some(exp) = part.strip_prefix("expires=") {
285                                expires = Some(exp.to_string());
286                            } else if let Some(tkt) = part.strip_prefix("ticket=") {
287                                ticket = Some(tkt.to_string());
288                            } else if part.starts_with('"') {
289                                // Capture the rest as reason
290                                reason = Some(val.split('"').nth(1).unwrap_or("").to_string());
291                                break;
292                            }
293                        }
294                    }
295
296                    let hack = HackAnnotation {
297                        line: ann.line,
298                        expires: expires.clone(),
299                        ticket: ticket.clone(),
300                        reason,
301                    };
302                    hacks.push(hack);
303
304                    // RFC-001: Also add to inline annotations
305                    inline_annotations.push(InlineAnnotation {
306                        line: ann.line,
307                        annotation_type: "hack".to_string(),
308                        value: ann.value.clone(),
309                        directive: ann
310                            .directive
311                            .clone()
312                            .unwrap_or_else(|| "Temporary workaround".to_string()),
313                        expires,
314                        ticket,
315                        auto_generated: ann.auto_generated,
316                    });
317                }
318                // RFC-001: Inline annotation types
319                "todo" | "fixme" | "critical" => {
320                    inline_annotations.push(InlineAnnotation {
321                        line: ann.line,
322                        annotation_type: ann.name.clone(),
323                        value: ann.value.clone(),
324                        directive: ann.directive.clone().unwrap_or_else(|| {
325                            match ann.name.as_str() {
326                                "todo" => "Pending work item".to_string(),
327                                "fixme" => "Known issue requiring fix".to_string(),
328                                "critical" => {
329                                    "Critical section - extra review required".to_string()
330                                }
331                                _ => "".to_string(),
332                            }
333                        }),
334                        expires: None,
335                        ticket: None,
336                        auto_generated: ann.auto_generated,
337                    });
338                    // RFC-0009: Also add to symbol documentation.todos
339                    if ann.name == "todo" {
340                        if let Some(ref mut builder) = current_symbol {
341                            let todo_text = ann
342                                .directive
343                                .clone()
344                                .or_else(|| {
345                                    ann.value.clone().map(|v| v.trim_matches('"').to_string())
346                                })
347                                .unwrap_or_else(|| "Pending work item".to_string());
348                            builder.documentation.todos.push(todo_text);
349                        }
350                    }
351                }
352                // RFC-0009: Performance annotation (extends RFC-001 perf)
353                "perf" => {
354                    inline_annotations.push(InlineAnnotation {
355                        line: ann.line,
356                        annotation_type: ann.name.clone(),
357                        value: ann.value.clone(),
358                        directive: ann
359                            .directive
360                            .clone()
361                            .unwrap_or_else(|| "Performance-sensitive code".to_string()),
362                        expires: None,
363                        ticket: None,
364                        auto_generated: ann.auto_generated,
365                    });
366                    // RFC-0009: Also set symbol performance.complexity
367                    if let Some(ref mut builder) = current_symbol {
368                        if let Some(val) = &ann.value {
369                            builder.performance.complexity =
370                                Some(val.trim_matches('"').to_string());
371                        }
372                    }
373                }
374                "symbol" => {
375                    // Save previous symbol if exists
376                    if let Some(builder) = current_symbol.take() {
377                        let sym = builder.build(&file_path);
378                        exports.push(sym.name.clone());
379                        symbols.push(sym);
380                    }
381                    // Start new symbol
382                    if let Some(val) = &ann.value {
383                        current_symbol = Some(SymbolBuilder::new(
384                            val.trim_matches('"').to_string(),
385                            ann.line,
386                            &file_path,
387                        ));
388                    }
389                }
390                // RFC-001: Symbol-level annotations
391                "fn" | "function" | "class" | "method" => {
392                    // Save previous symbol if exists
393                    if let Some(builder) = current_symbol.take() {
394                        let sym = builder.build(&file_path);
395                        exports.push(sym.name.clone());
396                        symbols.push(sym);
397                    }
398                    // Start new symbol with RFC-001 type
399                    if let Some(val) = &ann.value {
400                        let mut builder = SymbolBuilder::new(
401                            val.trim_matches('"').to_string(),
402                            ann.line,
403                            &file_path,
404                        );
405                        builder.symbol_type = match ann.name.as_str() {
406                            "fn" | "function" => SymbolType::Function,
407                            "class" => SymbolType::Class,
408                            "method" => SymbolType::Method,
409                            _ => SymbolType::Function,
410                        };
411                        builder.purpose = ann.directive.clone();
412                        current_symbol = Some(builder);
413                    }
414                }
415                "calls" => {
416                    if let Some(ref mut builder) = current_symbol {
417                        if let Some(val) = &ann.value {
418                            let callees: Vec<String> = val
419                                .split(',')
420                                .map(|s| s.trim().trim_matches('"').to_string())
421                                .collect();
422                            builder.calls.extend(callees);
423                        }
424                    }
425                }
426                "imports" | "depends" => {
427                    if let Some(val) = &ann.value {
428                        let import_list: Vec<String> = val
429                            .split(',')
430                            .map(|s| s.trim().trim_matches('"').to_string())
431                            .collect();
432                        imports.extend(import_list);
433                    }
434                }
435
436                // ================================================================
437                // RFC-0008: Type Annotations
438                // ================================================================
439                "param" => {
440                    if let Some(ref mut builder) = current_symbol {
441                        // Parse type, name, optional marker, and default from value
442                        // Value format: "{Type} [name]=default" or just "name"
443                        if let Some(val) = &ann.value {
444                            let val = val.trim();
445                            let (type_expr, rest) = if val.starts_with('{') {
446                                // Extract type from {Type}
447                                if let Some(close_idx) = val.find('}') {
448                                    let type_str = val[1..close_idx].trim().to_string();
449                                    let remaining = val[close_idx + 1..].trim();
450                                    (Some(type_str), remaining)
451                                } else {
452                                    (None, val)
453                                }
454                            } else {
455                                (None, val)
456                            };
457
458                            // Parse optional marker and name: [name]=default or name
459                            let (optional, name, default) = if rest.starts_with('[') {
460                                // Optional parameter: [name] or [name=default]
461                                if let Some(close_idx) = rest.find(']') {
462                                    let inner = &rest[1..close_idx];
463                                    if let Some(eq_idx) = inner.find('=') {
464                                        let n = inner[..eq_idx].trim().to_string();
465                                        let d = inner[eq_idx + 1..].trim().to_string();
466                                        (true, n, Some(d))
467                                    } else {
468                                        (true, inner.trim().to_string(), None)
469                                    }
470                                } else {
471                                    (false, rest.trim_matches('"').to_string(), None)
472                                }
473                            } else {
474                                // Required parameter
475                                let name = rest
476                                    .split_whitespace()
477                                    .next()
478                                    .unwrap_or("")
479                                    .trim_matches('"')
480                                    .to_string();
481                                (false, name, None)
482                            };
483
484                            if !name.is_empty() {
485                                builder.type_info.params.push(TypeParamInfo {
486                                    name,
487                                    r#type: type_expr.clone(),
488                                    type_source: type_expr.as_ref().map(|_| TypeSource::Acp),
489                                    optional,
490                                    default,
491                                    directive: ann.directive.clone(),
492                                });
493                            }
494                        }
495                    }
496                }
497                "returns" | "return" => {
498                    if let Some(ref mut builder) = current_symbol {
499                        // Parse type from value: "{Type}" or empty
500                        let type_expr = ann.value.as_ref().and_then(|val| {
501                            let val = val.trim();
502                            if val.starts_with('{') {
503                                val.find('}')
504                                    .map(|close_idx| val[1..close_idx].trim().to_string())
505                            } else {
506                                None
507                            }
508                        });
509
510                        builder.type_info.returns = Some(TypeReturnInfo {
511                            r#type: type_expr.clone(),
512                            type_source: type_expr.as_ref().map(|_| TypeSource::Acp),
513                            directive: ann.directive.clone(),
514                        });
515                    }
516                }
517                "template" => {
518                    if let Some(ref mut builder) = current_symbol {
519                        // Parse @acp:template T [extends Constraint] from value
520                        if let Some(val) = &ann.value {
521                            let val = val.trim();
522                            // Check for "extends" keyword
523                            let (name, constraint) =
524                                if let Some(extends_idx) = val.find(" extends ") {
525                                    let n = val[..extends_idx].trim().to_string();
526                                    let c = val[extends_idx + 9..].trim().to_string();
527                                    (n, Some(c))
528                                } else {
529                                    (
530                                        val.split_whitespace().next().unwrap_or("").to_string(),
531                                        None,
532                                    )
533                                };
534
535                            if !name.is_empty() {
536                                builder.type_info.type_params.push(TypeTypeParam {
537                                    name,
538                                    constraint,
539                                    directive: ann.directive.clone(),
540                                });
541                            }
542                        }
543                    }
544                }
545
546                // ================================================================
547                // RFC-0009: Behavioral Annotations
548                // ================================================================
549                "pure" => {
550                    if let Some(ref mut builder) = current_symbol {
551                        builder.behavioral.pure = true;
552                    }
553                }
554                "idempotent" => {
555                    if let Some(ref mut builder) = current_symbol {
556                        builder.behavioral.idempotent = true;
557                    }
558                }
559                "memoized" => {
560                    if let Some(ref mut builder) = current_symbol {
561                        if let Some(val) = &ann.value {
562                            builder.behavioral.memoized =
563                                Some(MemoizedValue::Duration(val.trim_matches('"').to_string()));
564                        } else {
565                            builder.behavioral.memoized = Some(MemoizedValue::Enabled(true));
566                        }
567                    }
568                }
569                "async" => {
570                    if let Some(ref mut builder) = current_symbol {
571                        builder.behavioral.r#async = true;
572                    }
573                }
574                "generator" => {
575                    if let Some(ref mut builder) = current_symbol {
576                        builder.behavioral.generator = true;
577                    }
578                }
579                "throttled" => {
580                    if let Some(ref mut builder) = current_symbol {
581                        if let Some(val) = &ann.value {
582                            builder.behavioral.throttled = Some(val.trim_matches('"').to_string());
583                        }
584                    }
585                }
586                "transactional" => {
587                    if let Some(ref mut builder) = current_symbol {
588                        builder.behavioral.transactional = true;
589                    }
590                }
591                "side-effects" => {
592                    if let Some(ref mut builder) = current_symbol {
593                        if let Some(val) = &ann.value {
594                            let effects: Vec<String> = val
595                                .split(',')
596                                .map(|s| s.trim().trim_matches('"').to_string())
597                                .collect();
598                            builder.behavioral.side_effects.extend(effects);
599                        }
600                    }
601                }
602
603                // ================================================================
604                // RFC-0009: Lifecycle Annotations (file and symbol level)
605                // ================================================================
606                "deprecated" => {
607                    let message = ann
608                        .directive
609                        .clone()
610                        .or_else(|| ann.value.clone().map(|v| v.trim_matches('"').to_string()))
611                        .unwrap_or_else(|| "Deprecated".to_string());
612                    if let Some(ref mut builder) = current_symbol {
613                        builder.lifecycle.deprecated = Some(message);
614                    } else {
615                        file_lifecycle.deprecated = Some(message);
616                    }
617                }
618                "experimental" => {
619                    if let Some(ref mut builder) = current_symbol {
620                        builder.lifecycle.experimental = true;
621                    } else {
622                        file_lifecycle.experimental = true;
623                    }
624                }
625                "beta" => {
626                    if let Some(ref mut builder) = current_symbol {
627                        builder.lifecycle.beta = true;
628                    } else {
629                        file_lifecycle.beta = true;
630                    }
631                }
632                "internal" => {
633                    if let Some(ref mut builder) = current_symbol {
634                        builder.lifecycle.internal = true;
635                    } else {
636                        file_lifecycle.internal = true;
637                    }
638                }
639                "public-api" => {
640                    if let Some(ref mut builder) = current_symbol {
641                        builder.lifecycle.public_api = true;
642                    } else {
643                        file_lifecycle.public_api = true;
644                    }
645                }
646                "since" => {
647                    if let Some(val) = &ann.value {
648                        let version = val.trim_matches('"').to_string();
649                        if let Some(ref mut builder) = current_symbol {
650                            builder.lifecycle.since = Some(version);
651                        } else {
652                            file_since = Some(version);
653                        }
654                    }
655                }
656
657                // ================================================================
658                // RFC-0009: Documentation Annotations
659                // ================================================================
660                "example" => {
661                    if let Some(ref mut builder) = current_symbol {
662                        let example_text = ann
663                            .directive
664                            .clone()
665                            .or_else(|| ann.value.clone().map(|v| v.trim_matches('"').to_string()))
666                            .unwrap_or_default();
667                        if !example_text.is_empty() {
668                            builder.documentation.examples.push(example_text);
669                        }
670                    }
671                }
672                "see" => {
673                    if let Some(ref mut builder) = current_symbol {
674                        if let Some(val) = &ann.value {
675                            builder
676                                .documentation
677                                .see_also
678                                .push(val.trim_matches('"').to_string());
679                        }
680                    }
681                }
682                "link" => {
683                    if let Some(ref mut builder) = current_symbol {
684                        if let Some(val) = &ann.value {
685                            builder
686                                .documentation
687                                .links
688                                .push(val.trim_matches('"').to_string());
689                        }
690                    }
691                }
692                "note" => {
693                    if let Some(ref mut builder) = current_symbol {
694                        let note_text = ann
695                            .directive
696                            .clone()
697                            .or_else(|| ann.value.clone().map(|v| v.trim_matches('"').to_string()))
698                            .unwrap_or_default();
699                        if !note_text.is_empty() {
700                            builder.documentation.notes.push(note_text);
701                        }
702                    }
703                }
704                "warning" => {
705                    if let Some(ref mut builder) = current_symbol {
706                        let warning_text = ann
707                            .directive
708                            .clone()
709                            .or_else(|| ann.value.clone().map(|v| v.trim_matches('"').to_string()))
710                            .unwrap_or_default();
711                        if !warning_text.is_empty() {
712                            builder.documentation.warnings.push(warning_text);
713                        }
714                    }
715                }
716
717                // ================================================================
718                // RFC-0009: Performance Annotations (memory, cached)
719                // ================================================================
720                "memory" => {
721                    if let Some(ref mut builder) = current_symbol {
722                        if let Some(val) = &ann.value {
723                            builder.performance.memory = Some(val.trim_matches('"').to_string());
724                        }
725                    }
726                }
727                "cached" => {
728                    if let Some(ref mut builder) = current_symbol {
729                        if let Some(val) = &ann.value {
730                            builder.performance.cached = Some(val.trim_matches('"').to_string());
731                        } else {
732                            builder.performance.cached = Some("true".to_string());
733                        }
734                    }
735                }
736
737                // ================================================================
738                // RFC-0009: File-Level Annotations
739                // ================================================================
740                "version" => {
741                    if let Some(val) = &ann.value {
742                        file_version = Some(val.trim_matches('"').to_string());
743                    }
744                }
745                "license" => {
746                    if let Some(val) = &ann.value {
747                        file_license = Some(val.trim_matches('"').to_string());
748                    }
749                }
750                "author" => {
751                    if let Some(val) = &ann.value {
752                        file_author = Some(val.trim_matches('"').to_string());
753                    }
754                }
755
756                _ => {}
757            }
758        }
759
760        // Save last symbol
761        if let Some(builder) = current_symbol {
762            let sym = builder.build(&file_path);
763            if !sym.calls.is_empty() {
764                calls.push((sym.name.clone(), sym.calls.clone()));
765            }
766            exports.push(sym.name.clone());
767            symbols.push(sym);
768        }
769
770        // Build call edges for earlier symbols
771        for sym in &symbols {
772            if !sym.calls.is_empty() {
773                calls.push((sym.name.clone(), sym.calls.clone()));
774            }
775        }
776
777        let file = FileEntry {
778            path: file_path,
779            lines,
780            language,
781            exports,
782            imports,
783            imported_by: Vec::new(), // RFC-0015: Populated during cache building
784            module: module_name,
785            summary: file_summary,
786            purpose: purpose.clone(),
787            owner: owner.clone(),
788            inline: inline_annotations.clone(),
789            domains,
790            layer,
791            stability: None,
792            ai_hints: ai_hints.clone(),
793            git: None,
794            annotations: std::collections::HashMap::new(), // RFC-0003: Populated during indexing
795            bridge: crate::cache::BridgeMetadata::default(), // RFC-0006: Populated during bridging
796            // RFC-0009: Extended file-level annotations
797            version: file_version,
798            since: file_since,
799            license: file_license,
800            author: file_author,
801            lifecycle: if file_lifecycle.is_empty() {
802                None
803            } else {
804                Some(file_lifecycle)
805            },
806            // RFC-0002: Populated during indexing with validation
807            refs: Vec::new(),
808            style: None,
809        };
810
811        Ok(ParseResult {
812            file,
813            symbols,
814            calls,
815            lock_level,
816            lock_directive,
817            ai_hints,
818            hacks,
819            inline_annotations,
820            purpose,
821            owner,
822        })
823    }
824
825    /// @acp:summary "Parse @acp: annotations from source comments (RFC-001)"
826    /// Extracts annotations with directive suffix support and multiline continuation.
827    pub fn parse_annotations(&self, content: &str) -> Vec<Annotation> {
828        let mut annotations = Vec::new();
829        let lines: Vec<&str> = content.lines().collect();
830        let mut i = 0;
831
832        while i < lines.len() {
833            let line = lines[i];
834            let line_1indexed = i + 1;
835
836            for cap in ANNOTATION_PATTERN.captures_iter(line) {
837                let name = cap.get(1).unwrap().as_str().to_string();
838                let value = cap.get(2).map(|m| m.as_str().trim().to_string());
839                let mut directive = cap.get(3).map(|m| m.as_str().trim().to_string());
840
841                // Check for multiline directive continuation
842                let mut j = i + 1;
843                while j < lines.len() {
844                    if let Some(cont_cap) = CONTINUATION_PATTERN.captures(lines[j]) {
845                        let continuation = cont_cap.get(1).unwrap().as_str().trim();
846                        if let Some(ref mut dir) = directive {
847                            dir.push(' ');
848                            dir.push_str(continuation);
849                        } else {
850                            directive = Some(continuation.to_string());
851                        }
852                        j += 1;
853                    } else {
854                        break;
855                    }
856                }
857
858                // Auto-generate directive if missing (per RFC-001 Q04 decision)
859                let (final_directive, auto_generated) = match directive {
860                    Some(d) if !d.is_empty() => (Some(d), false),
861                    _ => (self.default_directive(&name, value.as_deref()), true),
862                };
863
864                annotations.push(Annotation {
865                    name,
866                    value,
867                    directive: final_directive,
868                    auto_generated,
869                    line: line_1indexed,
870                });
871            }
872
873            i += 1;
874        }
875
876        annotations
877    }
878
879    /// @acp:summary "Generate default directive for annotation type (RFC-001 Q04)"
880    /// Returns auto-generated directive text based on annotation type and value.
881    fn default_directive(&self, name: &str, value: Option<&str>) -> Option<String> {
882        match name {
883            "lock" => match value {
884                Some("frozen") => Some("MUST NOT modify this code under any circumstances".into()),
885                Some("restricted") => {
886                    Some("Explain proposed changes and wait for explicit approval".into())
887                }
888                Some("approval-required") => {
889                    Some("Propose changes and request confirmation before applying".into())
890                }
891                Some("tests-required") => {
892                    Some("All changes must include corresponding tests".into())
893                }
894                Some("docs-required") => Some("All changes must update documentation".into()),
895                Some("review-required") => {
896                    Some("Changes require code review before merging".into())
897                }
898                Some("normal") | None => {
899                    Some("Safe to modify following project conventions".into())
900                }
901                Some("experimental") => {
902                    Some("Experimental code - changes welcome but may be unstable".into())
903                }
904                _ => None,
905            },
906            "ref" => value.map(|url| format!("Consult {} before making changes", url)),
907            "hack" => Some("Temporary workaround - check expiry before modifying".into()),
908            "deprecated" => Some("Do not use or extend - see replacement annotation".into()),
909            "todo" => Some("Pending work item - address before release".into()),
910            "fixme" => Some("Known issue requiring fix - prioritize resolution".into()),
911            "critical" => Some("Critical section - changes require extra review".into()),
912            "perf" => Some("Performance-sensitive code - benchmark any changes".into()),
913            "fn" | "function" => Some("Function implementation".into()),
914            "class" => Some("Class definition".into()),
915            "method" => Some("Method implementation".into()),
916            "purpose" => value.map(|v| v.trim_matches('"').to_string()),
917            _ => None,
918        }
919    }
920
921    // ========================================================================
922    // RFC-0003: Provenance Parsing Methods
923    // ========================================================================
924
925    /// Parse provenance annotations from comment lines (RFC-0003)
926    ///
927    /// Looks for @acp:source* annotations following the given start index.
928    /// Returns a ProvenanceMarker if any provenance annotations are found.
929    pub fn parse_provenance(&self, lines: &[&str], start_idx: usize) -> Option<ProvenanceMarker> {
930        let mut marker = ProvenanceMarker::default();
931        let mut found_any = false;
932
933        for line in lines.iter().skip(start_idx) {
934            let line = *line;
935
936            // Check for @acp:source
937            if let Some(cap) = SOURCE_PATTERN.captures(line) {
938                if let Ok(origin) = cap.get(1).unwrap().as_str().parse() {
939                    marker.source = origin;
940                    found_any = true;
941                }
942            }
943
944            // Check for @acp:source-confidence
945            if let Some(cap) = CONFIDENCE_PATTERN.captures(line) {
946                if let Ok(conf) = cap.get(1).unwrap().as_str().parse::<f64>() {
947                    // Clamp to valid range [0.0, 1.0]
948                    marker.confidence = Some(conf.clamp(0.0, 1.0));
949                    found_any = true;
950                }
951            }
952
953            // Check for @acp:source-reviewed
954            if let Some(cap) = REVIEWED_PATTERN.captures(line) {
955                marker.reviewed = Some(cap.get(1).unwrap().as_str() == "true");
956                found_any = true;
957            }
958
959            // Check for @acp:source-id
960            if let Some(cap) = ID_PATTERN.captures(line) {
961                marker.generation_id = Some(cap.get(1).unwrap().as_str().to_string());
962                found_any = true;
963            }
964
965            // Stop if we hit a non-provenance @acp annotation or non-comment line
966            let trimmed = line.trim();
967            let is_comment = trimmed.starts_with("//")
968                || trimmed.starts_with('*')
969                || trimmed.starts_with('#')
970                || trimmed.starts_with("/*");
971
972            if !is_comment {
973                break;
974            }
975
976            // If it's a comment with @acp: but not a provenance annotation, stop
977            if line.contains("@acp:") && !line.contains("@acp:source") {
978                break;
979            }
980        }
981
982        if found_any {
983            Some(marker)
984        } else {
985            None
986        }
987    }
988
989    /// Parse @acp: annotations with provenance support (RFC-0003)
990    ///
991    /// Returns annotations paired with their provenance metadata if present.
992    /// Provenance is detected from @acp:source* annotations following the main annotation.
993    pub fn parse_annotations_with_provenance(
994        &self,
995        content: &str,
996    ) -> Vec<AnnotationWithProvenance> {
997        let annotations = self.parse_annotations(content);
998        let lines: Vec<&str> = content.lines().collect();
999
1000        annotations
1001            .into_iter()
1002            .map(|ann| {
1003                // Look for provenance markers on lines following the annotation
1004                // Annotations are 1-indexed, so ann.line points to the actual line
1005                let provenance = if ann.line < lines.len() {
1006                    self.parse_provenance(&lines, ann.line)
1007                } else {
1008                    None
1009                };
1010
1011                AnnotationWithProvenance {
1012                    annotation: ann,
1013                    provenance,
1014                }
1015            })
1016            .collect()
1017    }
1018}
1019
1020impl Default for Parser {
1021    fn default() -> Self {
1022        Self::new()
1023    }
1024}
1025
1026/// @acp:summary "Parsed annotation from source (RFC-001 compliant)"
1027/// Supports directive extraction for self-documenting annotations.
1028#[derive(Debug, Clone, Serialize, Deserialize)]
1029pub struct Annotation {
1030    /// Annotation type (e.g., "lock", "ref", "hack", "fn", "class")
1031    pub name: String,
1032    /// Primary value after the annotation name
1033    pub value: Option<String>,
1034    /// Self-documenting directive text after ` - ` (RFC-001)
1035    pub directive: Option<String>,
1036    /// Whether directive was auto-generated from defaults (RFC-001)
1037    #[serde(default, skip_serializing_if = "is_false")]
1038    pub auto_generated: bool,
1039    /// Source line number (1-indexed)
1040    pub line: usize,
1041}
1042
1043fn is_false(b: &bool) -> bool {
1044    !*b
1045}
1046
1047/// Helper to build SymbolEntry from annotations
1048struct SymbolBuilder {
1049    name: String,
1050    qualified_name: String,
1051    line: usize,
1052    summary: Option<String>,
1053    purpose: Option<String>,
1054    calls: Vec<String>,
1055    symbol_type: SymbolType,
1056    // RFC-0009: Extended annotation accumulators
1057    behavioral: BehavioralAnnotations,
1058    lifecycle: LifecycleAnnotations,
1059    documentation: DocumentationAnnotations,
1060    performance: PerformanceAnnotations,
1061    // RFC-0008: Type annotation accumulator
1062    type_info: TypeInfo,
1063}
1064
1065impl SymbolBuilder {
1066    fn new(name: String, line: usize, file_path: &str) -> Self {
1067        let qualified_name = format!("{}:{}", file_path, name);
1068        Self {
1069            name,
1070            qualified_name,
1071            line,
1072            summary: None,
1073            purpose: None,
1074            calls: vec![],
1075            symbol_type: SymbolType::Function,
1076            // RFC-0009: Initialize with defaults
1077            behavioral: BehavioralAnnotations::default(),
1078            lifecycle: LifecycleAnnotations::default(),
1079            documentation: DocumentationAnnotations::default(),
1080            performance: PerformanceAnnotations::default(),
1081            // RFC-0008: Initialize with defaults
1082            type_info: TypeInfo::default(),
1083        }
1084    }
1085
1086    fn build(self, file_path: &str) -> SymbolEntry {
1087        SymbolEntry {
1088            name: self.name,
1089            qualified_name: self.qualified_name,
1090            symbol_type: self.symbol_type,
1091            file: file_path.to_string(),
1092            lines: [self.line, self.line + 10], // Approximate
1093            exported: true,
1094            signature: None,
1095            summary: self.summary,
1096            purpose: self.purpose,
1097            async_fn: self.behavioral.r#async, // RFC-0009: Use behavioral async flag
1098            visibility: Visibility::Public,
1099            calls: self.calls,
1100            called_by: vec![], // Populated later by indexer
1101            git: None,
1102            constraints: None,
1103            annotations: std::collections::HashMap::new(), // RFC-0003
1104            // RFC-0009: Extended annotation types (sparse serialization)
1105            behavioral: if self.behavioral.is_empty() {
1106                None
1107            } else {
1108                Some(self.behavioral)
1109            },
1110            lifecycle: if self.lifecycle.is_empty() {
1111                None
1112            } else {
1113                Some(self.lifecycle)
1114            },
1115            documentation: if self.documentation.is_empty() {
1116                None
1117            } else {
1118                Some(self.documentation)
1119            },
1120            performance: if self.performance.is_empty() {
1121                None
1122            } else {
1123                Some(self.performance)
1124            },
1125            // RFC-0008: Type annotation info (sparse serialization)
1126            type_info: if self.type_info.is_empty() {
1127                None
1128            } else {
1129                Some(self.type_info)
1130            },
1131        }
1132    }
1133}
1134
1135// ============================================================================
1136// RFC-0008: Type Annotation Tests
1137// ============================================================================
1138
1139#[cfg(test)]
1140mod type_annotation_tests {
1141    use super::*;
1142    use std::io::Write;
1143    use tempfile::NamedTempFile;
1144
1145    fn parse_test_file(content: &str) -> ParseResult {
1146        let mut file = NamedTempFile::with_suffix(".ts").unwrap();
1147        write!(file, "{}", content).unwrap();
1148        let parser = Parser::new();
1149        parser.parse(file.path()).unwrap()
1150    }
1151
1152    #[test]
1153    fn test_param_with_type() {
1154        let content = r#"
1155// @acp:fn "test" - Test function
1156// @acp:param {string} name - User name
1157// @acp:param {number} age - User age
1158"#;
1159        let result = parse_test_file(content);
1160        assert_eq!(result.symbols.len(), 1);
1161
1162        let sym = &result.symbols[0];
1163        let type_info = sym.type_info.as_ref().expect("Should have type_info");
1164        assert_eq!(type_info.params.len(), 2);
1165
1166        assert_eq!(type_info.params[0].name, "name");
1167        assert_eq!(type_info.params[0].r#type, Some("string".to_string()));
1168        assert_eq!(type_info.params[0].directive, Some("User name".to_string()));
1169
1170        assert_eq!(type_info.params[1].name, "age");
1171        assert_eq!(type_info.params[1].r#type, Some("number".to_string()));
1172    }
1173
1174    #[test]
1175    fn test_param_optional() {
1176        let content = r#"
1177// @acp:fn "test" - Test function
1178// @acp:param {string} [name] - Optional name
1179"#;
1180        let result = parse_test_file(content);
1181        let sym = &result.symbols[0];
1182        let type_info = sym.type_info.as_ref().expect("Should have type_info");
1183
1184        assert_eq!(type_info.params[0].name, "name");
1185        assert!(type_info.params[0].optional);
1186    }
1187
1188    #[test]
1189    fn test_param_with_default() {
1190        let content = r#"
1191// @acp:fn "test" - Test function
1192// @acp:param {number} [limit=10] - Limit with default
1193"#;
1194        let result = parse_test_file(content);
1195        let sym = &result.symbols[0];
1196        let type_info = sym.type_info.as_ref().expect("Should have type_info");
1197
1198        assert_eq!(type_info.params[0].name, "limit");
1199        assert!(type_info.params[0].optional);
1200        assert_eq!(type_info.params[0].default, Some("10".to_string()));
1201    }
1202
1203    #[test]
1204    fn test_param_without_type() {
1205        let content = r#"
1206// @acp:fn "test" - Test function
1207// @acp:param name - Just a name param
1208"#;
1209        let result = parse_test_file(content);
1210        let sym = &result.symbols[0];
1211        let type_info = sym.type_info.as_ref().expect("Should have type_info");
1212
1213        assert_eq!(type_info.params[0].name, "name");
1214        assert!(type_info.params[0].r#type.is_none());
1215    }
1216
1217    #[test]
1218    fn test_returns_with_type() {
1219        let content = r#"
1220// @acp:fn "test" - Test function
1221// @acp:returns {Promise<User>} - Returns user promise
1222"#;
1223        let result = parse_test_file(content);
1224        let sym = &result.symbols[0];
1225        let type_info = sym.type_info.as_ref().expect("Should have type_info");
1226
1227        let returns = type_info.returns.as_ref().expect("Should have returns");
1228        assert_eq!(returns.r#type, Some("Promise<User>".to_string()));
1229        assert_eq!(returns.directive, Some("Returns user promise".to_string()));
1230    }
1231
1232    #[test]
1233    fn test_returns_without_type() {
1234        let content = r#"
1235// @acp:fn "test" - Test function
1236// @acp:returns - Returns nothing special
1237"#;
1238        let result = parse_test_file(content);
1239        let sym = &result.symbols[0];
1240        let type_info = sym.type_info.as_ref().expect("Should have type_info");
1241
1242        let returns = type_info.returns.as_ref().expect("Should have returns");
1243        assert!(returns.r#type.is_none());
1244    }
1245
1246    #[test]
1247    fn test_template() {
1248        let content = r#"
1249// @acp:fn "test" - Test function
1250// @acp:template T - Type parameter
1251"#;
1252        let result = parse_test_file(content);
1253        let sym = &result.symbols[0];
1254        let type_info = sym.type_info.as_ref().expect("Should have type_info");
1255
1256        assert_eq!(type_info.type_params.len(), 1);
1257        assert_eq!(type_info.type_params[0].name, "T");
1258        assert!(type_info.type_params[0].constraint.is_none());
1259    }
1260
1261    #[test]
1262    fn test_template_with_constraint() {
1263        let content = r#"
1264// @acp:fn "test" - Test function
1265// @acp:template T extends BaseEntity - Entity type
1266"#;
1267        let result = parse_test_file(content);
1268        let sym = &result.symbols[0];
1269        let type_info = sym.type_info.as_ref().expect("Should have type_info");
1270
1271        assert_eq!(type_info.type_params[0].name, "T");
1272        assert_eq!(
1273            type_info.type_params[0].constraint,
1274            Some("BaseEntity".to_string())
1275        );
1276        assert_eq!(
1277            type_info.type_params[0].directive,
1278            Some("Entity type".to_string())
1279        );
1280    }
1281
1282    #[test]
1283    fn test_complex_type_expression() {
1284        let content = r#"
1285// @acp:fn "test" - Test function
1286// @acp:param {Map<string, User | null>} userMap - Complex type
1287// @acp:returns {Promise<Array<User>>} - Returns users
1288"#;
1289        let result = parse_test_file(content);
1290        let sym = &result.symbols[0];
1291        let type_info = sym.type_info.as_ref().expect("Should have type_info");
1292
1293        assert_eq!(
1294            type_info.params[0].r#type,
1295            Some("Map<string, User | null>".to_string())
1296        );
1297
1298        let returns = type_info.returns.as_ref().unwrap();
1299        assert_eq!(returns.r#type, Some("Promise<Array<User>>".to_string()));
1300    }
1301
1302    #[test]
1303    fn test_backward_compat_no_types() {
1304        // Ensure old-style annotations without types still work
1305        let content = r#"
1306// @acp:fn "test" - Test function
1307// @acp:param userId - User ID
1308// @acp:returns - User object or null
1309"#;
1310        let result = parse_test_file(content);
1311        let sym = &result.symbols[0];
1312        let type_info = sym.type_info.as_ref().expect("Should have type_info");
1313
1314        // Should still capture the param name and directive
1315        assert_eq!(type_info.params[0].name, "userId");
1316        assert!(type_info.params[0].r#type.is_none());
1317    }
1318
1319    #[test]
1320    fn test_type_source_is_acp() {
1321        let content = r#"
1322// @acp:fn "test" - Test function
1323// @acp:param {string} name - Name param
1324// @acp:returns {void} - Returns nothing
1325"#;
1326        let result = parse_test_file(content);
1327        let sym = &result.symbols[0];
1328        let type_info = sym.type_info.as_ref().expect("Should have type_info");
1329
1330        // When type is present, source should be Acp
1331        assert_eq!(type_info.params[0].type_source, Some(TypeSource::Acp));
1332        assert_eq!(
1333            type_info.returns.as_ref().unwrap().type_source,
1334            Some(TypeSource::Acp)
1335        );
1336    }
1337}