Skip to main content

tldr_core/patterns/
signals.rs

1//! Pattern signals - Accumulated signals from single AST walk
2//!
3//! This module defines the PatternSignals struct that collects all pattern
4//! signals during a single AST traversal, avoiding multiple passes.
5
6use crate::types::Evidence;
7use std::collections::{HashMap, HashSet};
8
9/// Accumulated signals from a single AST walk
10#[derive(Debug, Clone, Default)]
11pub struct PatternSignals {
12    /// Soft delete signals
13    pub soft_delete: SoftDeleteSignals,
14    /// Error handling signals
15    pub error_handling: ErrorHandlingSignals,
16    /// Naming convention signals
17    pub naming: NamingSignals,
18    /// Resource management signals
19    pub resource_management: ResourceManagementSignals,
20    /// Validation signals
21    pub validation: ValidationSignals,
22    /// Test idiom signals
23    pub test_idioms: TestIdiomSignals,
24    /// Import pattern signals
25    pub import_patterns: ImportPatternSignals,
26    /// Type coverage signals
27    pub type_coverage: TypeCoverageSignals,
28    /// API convention signals
29    pub api_conventions: ApiConventionSignals,
30    /// Async pattern signals
31    pub async_patterns: AsyncPatternSignals,
32    /// Language-specific extension signals not covered by standard schema.
33    /// Key format: "category.field_name" (e.g., "pattern_matching.arm_count").
34    pub extensions: HashMap<String, Vec<Evidence>>,
35}
36
37impl PatternSignals {
38    /// Merge signals from another PatternSignals instance
39    pub fn merge(&mut self, other: &PatternSignals) {
40        self.soft_delete.merge(&other.soft_delete);
41        self.error_handling.merge(&other.error_handling);
42        self.naming.merge(&other.naming);
43        self.resource_management.merge(&other.resource_management);
44        self.validation.merge(&other.validation);
45        self.test_idioms.merge(&other.test_idioms);
46        self.import_patterns.merge(&other.import_patterns);
47        self.type_coverage.merge(&other.type_coverage);
48        self.api_conventions.merge(&other.api_conventions);
49        self.async_patterns.merge(&other.async_patterns);
50        for (key, values) in &other.extensions {
51            self.extensions
52                .entry(key.clone())
53                .or_default()
54                .extend(values.clone());
55        }
56    }
57}
58
59// =============================================================================
60// Soft Delete Signals
61// =============================================================================
62
63/// Signals related to soft delete patterns detected during AST traversal.
64#[derive(Debug, Clone, Default)]
65pub struct SoftDeleteSignals {
66    /// Fields named is_deleted (weight: +0.4)
67    pub is_deleted_fields: Vec<Evidence>,
68    /// Fields named deleted_at (weight: +0.4)
69    pub deleted_at_fields: Vec<Evidence>,
70    /// Query filters on delete fields (weight: +0.2)
71    pub delete_query_filters: Vec<Evidence>,
72    /// ORM paranoid annotations (weight: +0.3)
73    pub paranoid_annotations: Vec<Evidence>,
74}
75
76impl SoftDeleteSignals {
77    /// Merge signals from another `SoftDeleteSignals` instance into this one.
78    pub fn merge(&mut self, other: &SoftDeleteSignals) {
79        self.is_deleted_fields
80            .extend(other.is_deleted_fields.clone());
81        self.deleted_at_fields
82            .extend(other.deleted_at_fields.clone());
83        self.delete_query_filters
84            .extend(other.delete_query_filters.clone());
85        self.paranoid_annotations
86            .extend(other.paranoid_annotations.clone());
87    }
88
89    /// Returns true if any soft delete signals have been collected.
90    pub fn has_signals(&self) -> bool {
91        !self.is_deleted_fields.is_empty()
92            || !self.deleted_at_fields.is_empty()
93            || !self.delete_query_filters.is_empty()
94            || !self.paranoid_annotations.is_empty()
95    }
96
97    /// Calculate confidence score (0.0-1.0) based on accumulated soft delete signals.
98    pub fn calculate_confidence(&self) -> f64 {
99        let mut confidence: f64 = 0.0;
100        if !self.is_deleted_fields.is_empty() {
101            confidence += 0.4;
102        }
103        if !self.deleted_at_fields.is_empty() {
104            confidence += 0.4;
105        }
106        if !self.delete_query_filters.is_empty() {
107            confidence += 0.2;
108        }
109        if !self.paranoid_annotations.is_empty() {
110            confidence += 0.3;
111        }
112        confidence.min(1.0)
113    }
114}
115
116// =============================================================================
117// Error Handling Signals
118// =============================================================================
119
120/// Signals related to error handling patterns detected during AST traversal.
121#[derive(Debug, Clone, Default)]
122pub struct ErrorHandlingSignals {
123    /// Python try/except blocks (weight: +0.3)
124    pub try_except_blocks: Vec<Evidence>,
125    /// Python custom Exception classes (weight: +0.4)
126    pub custom_exceptions: Vec<(String, Evidence)>,
127    /// Rust Result<T, E> return types (weight: +0.4)
128    pub result_types: Vec<Evidence>,
129    /// Rust ? operator usage (weight: +0.3)
130    pub question_mark_ops: Vec<Evidence>,
131    /// Go if err != nil pattern (weight: +0.4)
132    pub err_nil_checks: Vec<Evidence>,
133    /// TypeScript try/catch blocks (weight: +0.3)
134    pub try_catch_blocks: Vec<Evidence>,
135    /// Custom error enum definitions
136    pub error_enums: Vec<(String, Evidence)>,
137}
138
139impl ErrorHandlingSignals {
140    /// Merge signals from another `ErrorHandlingSignals` instance into this one.
141    pub fn merge(&mut self, other: &ErrorHandlingSignals) {
142        self.try_except_blocks
143            .extend(other.try_except_blocks.clone());
144        self.custom_exceptions
145            .extend(other.custom_exceptions.clone());
146        self.result_types.extend(other.result_types.clone());
147        self.question_mark_ops
148            .extend(other.question_mark_ops.clone());
149        self.err_nil_checks.extend(other.err_nil_checks.clone());
150        self.try_catch_blocks.extend(other.try_catch_blocks.clone());
151        self.error_enums.extend(other.error_enums.clone());
152    }
153
154    /// Returns true if any error handling signals have been collected.
155    pub fn has_signals(&self) -> bool {
156        !self.try_except_blocks.is_empty()
157            || !self.custom_exceptions.is_empty()
158            || !self.result_types.is_empty()
159            || !self.question_mark_ops.is_empty()
160            || !self.err_nil_checks.is_empty()
161            || !self.try_catch_blocks.is_empty()
162            || !self.error_enums.is_empty()
163    }
164
165    /// Calculate confidence score (0.0-1.0) based on accumulated error handling signals.
166    pub fn calculate_confidence(&self) -> f64 {
167        let mut confidence: f64 = 0.0;
168        if !self.try_except_blocks.is_empty() || !self.try_catch_blocks.is_empty() {
169            confidence += 0.3;
170        }
171        if !self.custom_exceptions.is_empty() || !self.error_enums.is_empty() {
172            confidence += 0.4;
173        }
174        if !self.result_types.is_empty() {
175            confidence += 0.4;
176        }
177        if !self.question_mark_ops.is_empty() {
178            confidence += 0.3;
179        }
180        if !self.err_nil_checks.is_empty() {
181            confidence += 0.4;
182        }
183        confidence.min(1.0)
184    }
185}
186
187// =============================================================================
188// Naming Signals
189// =============================================================================
190
191/// Signals related to naming convention patterns detected during AST traversal.
192#[derive(Debug, Clone, Default)]
193pub struct NamingSignals {
194    /// Function names with their detected case
195    pub function_names: Vec<(String, NamingCase, String)>, // (name, case, file)
196    /// Class names with their detected case
197    pub class_names: Vec<(String, NamingCase, String)>,
198    /// Constant names with their detected case
199    pub constant_names: Vec<(String, NamingCase, String)>,
200    /// Private member prefix detection
201    pub private_prefixes: HashMap<String, usize>, // prefix -> count
202}
203
204/// Detected naming case convention for an identifier.
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
206pub enum NamingCase {
207    /// Lowercase with underscores: `my_function`
208    SnakeCase,
209    /// Lowercase first, then capitalized words: `myFunction`
210    CamelCase,
211    /// Capitalized words without separators: `MyClass`
212    PascalCase,
213    /// All uppercase with underscores: `MAX_VALUE`
214    UpperSnakeCase,
215    /// Could not determine naming convention
216    Unknown,
217}
218
219impl NamingSignals {
220    /// Merge signals from another `NamingSignals` instance into this one.
221    pub fn merge(&mut self, other: &NamingSignals) {
222        self.function_names.extend(other.function_names.clone());
223        self.class_names.extend(other.class_names.clone());
224        self.constant_names.extend(other.constant_names.clone());
225        for (prefix, count) in &other.private_prefixes {
226            *self.private_prefixes.entry(prefix.clone()).or_insert(0) += count;
227        }
228    }
229
230    /// Returns true if any naming signals have been collected.
231    pub fn has_signals(&self) -> bool {
232        !self.function_names.is_empty()
233            || !self.class_names.is_empty()
234            || !self.constant_names.is_empty()
235    }
236}
237
238/// Detect naming case from an identifier
239pub fn detect_naming_case(name: &str) -> NamingCase {
240    if name.is_empty() {
241        return NamingCase::Unknown;
242    }
243
244    // Skip dunder methods and single char names
245    if name.starts_with("__") && name.ends_with("__") {
246        return NamingCase::Unknown;
247    }
248    if name.len() == 1 {
249        return NamingCase::Unknown;
250    }
251
252    // UPPER_SNAKE_CASE: all uppercase with underscores
253    if name
254        .chars()
255        .all(|c| c.is_ascii_uppercase() || c == '_' || c.is_ascii_digit())
256    {
257        return NamingCase::UpperSnakeCase;
258    }
259
260    // snake_case: lowercase with underscores
261    if name
262        .chars()
263        .all(|c| c.is_ascii_lowercase() || c == '_' || c.is_ascii_digit())
264    {
265        return NamingCase::SnakeCase;
266    }
267
268    // PascalCase: starts with uppercase, no underscores (except at start for private)
269    let check_name = name.trim_start_matches('_');
270    if !check_name.is_empty() {
271        let first = check_name.chars().next().unwrap();
272        if first.is_ascii_uppercase() && !check_name.contains('_') {
273            return NamingCase::PascalCase;
274        }
275
276        // camelCase: starts with lowercase, no underscores, has uppercase
277        if first.is_ascii_lowercase()
278            && !check_name.contains('_')
279            && check_name.chars().any(|c| c.is_ascii_uppercase())
280        {
281            return NamingCase::CamelCase;
282        }
283    }
284
285    NamingCase::Unknown
286}
287
288// =============================================================================
289// Resource Management Signals
290// =============================================================================
291
292/// Signals related to resource management patterns detected during AST traversal.
293#[derive(Debug, Clone, Default)]
294pub struct ResourceManagementSignals {
295    /// Python context managers (with statements)
296    pub context_managers: Vec<Evidence>,
297    /// Python __enter__/__exit__ methods
298    pub enter_exit_methods: Vec<Evidence>,
299    /// Go defer statements
300    pub defer_statements: Vec<Evidence>,
301    /// Rust Drop trait implementations
302    pub drop_impls: Vec<Evidence>,
303    /// TypeScript/JS try/finally blocks
304    pub try_finally_blocks: Vec<Evidence>,
305    /// Explicit close calls
306    pub close_calls: Vec<Evidence>,
307}
308
309impl ResourceManagementSignals {
310    /// Merge signals from another `ResourceManagementSignals` instance into this one.
311    pub fn merge(&mut self, other: &ResourceManagementSignals) {
312        self.context_managers.extend(other.context_managers.clone());
313        self.enter_exit_methods
314            .extend(other.enter_exit_methods.clone());
315        self.defer_statements.extend(other.defer_statements.clone());
316        self.drop_impls.extend(other.drop_impls.clone());
317        self.try_finally_blocks
318            .extend(other.try_finally_blocks.clone());
319        self.close_calls.extend(other.close_calls.clone());
320    }
321
322    /// Returns true if any resource management signals have been collected.
323    pub fn has_signals(&self) -> bool {
324        !self.context_managers.is_empty()
325            || !self.enter_exit_methods.is_empty()
326            || !self.defer_statements.is_empty()
327            || !self.drop_impls.is_empty()
328            || !self.try_finally_blocks.is_empty()
329            || !self.close_calls.is_empty()
330    }
331
332    /// Calculate confidence score (0.0-1.0) based on accumulated resource management signals.
333    pub fn calculate_confidence(&self) -> f64 {
334        let mut confidence: f64 = 0.0;
335        if !self.context_managers.is_empty() {
336            confidence += 0.4;
337        }
338        if !self.enter_exit_methods.is_empty() {
339            confidence += 0.3;
340        }
341        if !self.defer_statements.is_empty() {
342            confidence += 0.4;
343        }
344        if !self.drop_impls.is_empty() {
345            confidence += 0.4;
346        }
347        if !self.try_finally_blocks.is_empty() {
348            confidence += 0.3;
349        }
350        if !self.close_calls.is_empty() {
351            confidence += 0.2;
352        }
353        confidence.min(1.0)
354    }
355}
356
357// =============================================================================
358// Validation Signals
359// =============================================================================
360
361/// Signals related to input validation patterns detected during AST traversal.
362#[derive(Debug, Clone, Default)]
363pub struct ValidationSignals {
364    /// Pydantic BaseModel inheritance (weight: +0.5)
365    pub pydantic_models: Vec<Evidence>,
366    /// Zod schema definitions (weight: +0.5)
367    pub zod_schemas: Vec<Evidence>,
368    /// Guard clauses at function start (weight: +0.3)
369    pub guard_clauses: Vec<Evidence>,
370    /// Assert statements (weight: +0.2)
371    pub assert_statements: Vec<Evidence>,
372    /// Type validation (isinstance, typeof) (weight: +0.2)
373    pub type_checks: Vec<Evidence>,
374    /// Marshmallow/Cerberus/other validators
375    pub other_validators: Vec<(String, Evidence)>,
376}
377
378impl ValidationSignals {
379    /// Merge signals from another `ValidationSignals` instance into this one.
380    pub fn merge(&mut self, other: &ValidationSignals) {
381        self.pydantic_models.extend(other.pydantic_models.clone());
382        self.zod_schemas.extend(other.zod_schemas.clone());
383        self.guard_clauses.extend(other.guard_clauses.clone());
384        self.assert_statements
385            .extend(other.assert_statements.clone());
386        self.type_checks.extend(other.type_checks.clone());
387        self.other_validators.extend(other.other_validators.clone());
388    }
389
390    /// Returns true if any validation signals have been collected.
391    pub fn has_signals(&self) -> bool {
392        !self.pydantic_models.is_empty()
393            || !self.zod_schemas.is_empty()
394            || !self.guard_clauses.is_empty()
395            || !self.assert_statements.is_empty()
396            || !self.type_checks.is_empty()
397            || !self.other_validators.is_empty()
398    }
399
400    /// Calculate confidence score (0.0-1.0) based on accumulated validation signals.
401    pub fn calculate_confidence(&self) -> f64 {
402        let mut confidence: f64 = 0.0;
403        if !self.pydantic_models.is_empty() {
404            confidence += 0.5;
405        }
406        if !self.zod_schemas.is_empty() {
407            confidence += 0.5;
408        }
409        if !self.guard_clauses.is_empty() {
410            confidence += 0.3;
411        }
412        if !self.assert_statements.is_empty() {
413            confidence += 0.2;
414        }
415        if !self.type_checks.is_empty() {
416            confidence += 0.2;
417        }
418        if !self.other_validators.is_empty() {
419            confidence += 0.3;
420        }
421        confidence.min(1.0)
422    }
423}
424
425// =============================================================================
426// Test Idiom Signals
427// =============================================================================
428
429/// Signals related to test idiom patterns detected during AST traversal.
430#[derive(Debug, Clone, Default)]
431pub struct TestIdiomSignals {
432    /// pytest fixtures (weight: +0.4)
433    pub pytest_fixtures: Vec<Evidence>,
434    /// mock.patch usage (weight: +0.3)
435    pub mock_patches: Vec<Evidence>,
436    /// Jest describe/it blocks (weight: +0.4)
437    pub jest_blocks: Vec<Evidence>,
438    /// Go table-driven tests (weight: +0.4)
439    pub go_table_tests: Vec<Evidence>,
440    /// Arrange-Act-Assert structure indicators
441    pub aaa_patterns: Vec<Evidence>,
442    /// Test function count
443    pub test_function_count: usize,
444    /// Detected framework name
445    pub detected_framework: Option<String>,
446}
447
448impl TestIdiomSignals {
449    /// Merge signals from another `TestIdiomSignals` instance into this one.
450    pub fn merge(&mut self, other: &TestIdiomSignals) {
451        self.pytest_fixtures.extend(other.pytest_fixtures.clone());
452        self.mock_patches.extend(other.mock_patches.clone());
453        self.jest_blocks.extend(other.jest_blocks.clone());
454        self.go_table_tests.extend(other.go_table_tests.clone());
455        self.aaa_patterns.extend(other.aaa_patterns.clone());
456        self.test_function_count += other.test_function_count;
457        if self.detected_framework.is_none() {
458            self.detected_framework = other.detected_framework.clone();
459        }
460    }
461
462    /// Returns true if any test idiom signals have been collected.
463    pub fn has_signals(&self) -> bool {
464        !self.pytest_fixtures.is_empty()
465            || !self.mock_patches.is_empty()
466            || !self.jest_blocks.is_empty()
467            || !self.go_table_tests.is_empty()
468            || !self.aaa_patterns.is_empty()
469            || self.test_function_count > 0
470    }
471
472    /// Calculate confidence score (0.0-1.0) based on accumulated test idiom signals.
473    pub fn calculate_confidence(&self) -> f64 {
474        let mut confidence: f64 = 0.0;
475        if !self.pytest_fixtures.is_empty() {
476            confidence += 0.4;
477        }
478        if !self.mock_patches.is_empty() {
479            confidence += 0.3;
480        }
481        if !self.jest_blocks.is_empty() {
482            confidence += 0.4;
483        }
484        if !self.go_table_tests.is_empty() {
485            confidence += 0.4;
486        }
487        if !self.aaa_patterns.is_empty() {
488            confidence += 0.3;
489        }
490        confidence.min(1.0)
491    }
492}
493
494// =============================================================================
495// Import Pattern Signals
496// =============================================================================
497
498/// Signals related to import organization patterns detected during AST traversal.
499#[derive(Debug, Clone, Default)]
500pub struct ImportPatternSignals {
501    /// Absolute imports
502    pub absolute_imports: Vec<(String, String)>, // (module, file)
503    /// Relative imports
504    pub relative_imports: Vec<(String, String)>,
505    /// Star imports (from x import *)
506    pub star_imports: Vec<Evidence>,
507    /// Import aliases
508    pub aliases: HashMap<String, String>, // module -> alias
509    /// Import groupings detected (file -> groups)
510    pub groupings: Vec<ImportGrouping>,
511}
512
513/// Grouping of imports detected in a single file, organized by origin.
514#[derive(Debug, Clone)]
515pub struct ImportGrouping {
516    /// File path where the imports were found.
517    pub file: String,
518    /// Standard library imports.
519    pub stdlib_imports: Vec<String>,
520    /// Third-party (external dependency) imports.
521    pub third_party_imports: Vec<String>,
522    /// Local (project-internal) imports.
523    pub local_imports: Vec<String>,
524}
525
526impl ImportPatternSignals {
527    /// Merge signals from another `ImportPatternSignals` instance into this one.
528    pub fn merge(&mut self, other: &ImportPatternSignals) {
529        self.absolute_imports.extend(other.absolute_imports.clone());
530        self.relative_imports.extend(other.relative_imports.clone());
531        self.star_imports.extend(other.star_imports.clone());
532        self.aliases.extend(other.aliases.clone());
533        self.groupings.extend(other.groupings.clone());
534    }
535
536    /// Returns true if any import pattern signals have been collected.
537    pub fn has_signals(&self) -> bool {
538        !self.absolute_imports.is_empty()
539            || !self.relative_imports.is_empty()
540            || !self.star_imports.is_empty()
541    }
542}
543
544// =============================================================================
545// Type Coverage Signals
546// =============================================================================
547
548/// Signals related to type annotation coverage detected during AST traversal.
549#[derive(Debug, Clone, Default)]
550pub struct TypeCoverageSignals {
551    /// Typed function parameters
552    pub typed_params: usize,
553    /// Untyped function parameters
554    pub untyped_params: usize,
555    /// Typed return types
556    pub typed_returns: usize,
557    /// Untyped return types
558    pub untyped_returns: usize,
559    /// Typed variables
560    pub typed_variables: usize,
561    /// Untyped variables
562    pub untyped_variables: usize,
563    /// TypeVar/Generic usage
564    pub generic_usage: Vec<Evidence>,
565    /// Common generic patterns found
566    pub generic_patterns: HashSet<String>,
567}
568
569impl TypeCoverageSignals {
570    /// Merge signals from another `TypeCoverageSignals` instance into this one.
571    pub fn merge(&mut self, other: &TypeCoverageSignals) {
572        self.typed_params += other.typed_params;
573        self.untyped_params += other.untyped_params;
574        self.typed_returns += other.typed_returns;
575        self.untyped_returns += other.untyped_returns;
576        self.typed_variables += other.typed_variables;
577        self.untyped_variables += other.untyped_variables;
578        self.generic_usage.extend(other.generic_usage.clone());
579        self.generic_patterns.extend(other.generic_patterns.clone());
580    }
581
582    /// Returns true if any type coverage signals have been collected.
583    pub fn has_signals(&self) -> bool {
584        self.typed_params > 0
585            || self.typed_returns > 0
586            || self.typed_variables > 0
587            || self.untyped_params > 0
588            || self.untyped_returns > 0
589    }
590
591    /// Calculate the ratio of typed function signatures to total (params + returns).
592    pub fn calculate_function_coverage(&self) -> f64 {
593        let total =
594            self.typed_params + self.untyped_params + self.typed_returns + self.untyped_returns;
595        if total == 0 {
596            return 0.0;
597        }
598        (self.typed_params + self.typed_returns) as f64 / total as f64
599    }
600
601    /// Calculate the ratio of typed variables to total variables.
602    pub fn calculate_variable_coverage(&self) -> f64 {
603        let total = self.typed_variables + self.untyped_variables;
604        if total == 0 {
605            return 0.0;
606        }
607        self.typed_variables as f64 / total as f64
608    }
609
610    /// Calculate the overall ratio of all typed items to total items.
611    pub fn calculate_overall_coverage(&self) -> f64 {
612        let total_typed = self.typed_params + self.typed_returns + self.typed_variables;
613        let total_untyped = self.untyped_params + self.untyped_returns + self.untyped_variables;
614        let total = total_typed + total_untyped;
615        if total == 0 {
616            return 0.0;
617        }
618        total_typed as f64 / total as f64
619    }
620}
621
622// =============================================================================
623// API Convention Signals
624// =============================================================================
625
626/// Signals related to API convention patterns detected during AST traversal.
627#[derive(Debug, Clone, Default)]
628pub struct ApiConventionSignals {
629    /// FastAPI decorators
630    pub fastapi_decorators: Vec<Evidence>,
631    /// Flask route decorators
632    pub flask_decorators: Vec<Evidence>,
633    /// Express route handlers
634    pub express_routes: Vec<Evidence>,
635    /// RESTful naming patterns
636    pub restful_patterns: Vec<Evidence>,
637    /// ORM model definitions
638    pub orm_models: Vec<(String, Evidence)>, // (orm_name, evidence)
639    /// GraphQL definitions
640    pub graphql_defs: Vec<Evidence>,
641}
642
643impl ApiConventionSignals {
644    /// Merge signals from another `ApiConventionSignals` instance into this one.
645    pub fn merge(&mut self, other: &ApiConventionSignals) {
646        self.fastapi_decorators
647            .extend(other.fastapi_decorators.clone());
648        self.flask_decorators.extend(other.flask_decorators.clone());
649        self.express_routes.extend(other.express_routes.clone());
650        self.restful_patterns.extend(other.restful_patterns.clone());
651        self.orm_models.extend(other.orm_models.clone());
652        self.graphql_defs.extend(other.graphql_defs.clone());
653    }
654
655    /// Returns true if any API convention signals have been collected.
656    pub fn has_signals(&self) -> bool {
657        !self.fastapi_decorators.is_empty()
658            || !self.flask_decorators.is_empty()
659            || !self.express_routes.is_empty()
660            || !self.restful_patterns.is_empty()
661            || !self.orm_models.is_empty()
662            || !self.graphql_defs.is_empty()
663    }
664
665    /// Calculate confidence score (0.0-1.0) based on accumulated API convention signals.
666    pub fn calculate_confidence(&self) -> f64 {
667        let mut confidence: f64 = 0.0;
668        if !self.fastapi_decorators.is_empty() {
669            confidence += 0.5;
670        }
671        if !self.flask_decorators.is_empty() {
672            confidence += 0.5;
673        }
674        if !self.express_routes.is_empty() {
675            confidence += 0.5;
676        }
677        if !self.restful_patterns.is_empty() {
678            confidence += 0.3;
679        }
680        if !self.orm_models.is_empty() {
681            confidence += 0.4;
682        }
683        if !self.graphql_defs.is_empty() {
684            confidence += 0.4;
685        }
686        confidence.min(1.0)
687    }
688
689    /// Detect the primary web framework based on collected signals.
690    pub fn detect_framework(&self) -> Option<String> {
691        if !self.fastapi_decorators.is_empty() {
692            return Some("fastapi".to_string());
693        }
694        if !self.flask_decorators.is_empty() {
695            return Some("flask".to_string());
696        }
697        if !self.express_routes.is_empty() {
698            return Some("express".to_string());
699        }
700        None
701    }
702
703    /// Detect the ORM framework based on collected model signals.
704    pub fn detect_orm(&self) -> Option<String> {
705        self.orm_models.first().map(|(orm, _)| orm.clone())
706    }
707}
708
709// =============================================================================
710// Async Pattern Signals
711// =============================================================================
712
713/// Signals related to async/concurrency patterns detected during AST traversal.
714#[derive(Debug, Clone, Default)]
715pub struct AsyncPatternSignals {
716    /// async/await keywords
717    pub async_await: Vec<Evidence>,
718    /// Go goroutines (go keyword)
719    pub goroutines: Vec<Evidence>,
720    /// Tokio runtime usage
721    pub tokio_usage: Vec<Evidence>,
722    /// Sync primitives (mutex, channel, semaphore)
723    pub sync_primitives: Vec<(String, Evidence)>,
724    /// Thread spawn patterns
725    pub thread_spawns: Vec<Evidence>,
726}
727
728impl AsyncPatternSignals {
729    /// Merge signals from another `AsyncPatternSignals` instance into this one.
730    pub fn merge(&mut self, other: &AsyncPatternSignals) {
731        self.async_await.extend(other.async_await.clone());
732        self.goroutines.extend(other.goroutines.clone());
733        self.tokio_usage.extend(other.tokio_usage.clone());
734        self.sync_primitives.extend(other.sync_primitives.clone());
735        self.thread_spawns.extend(other.thread_spawns.clone());
736    }
737
738    /// Returns true if any async pattern signals have been collected.
739    pub fn has_signals(&self) -> bool {
740        !self.async_await.is_empty()
741            || !self.goroutines.is_empty()
742            || !self.tokio_usage.is_empty()
743            || !self.sync_primitives.is_empty()
744            || !self.thread_spawns.is_empty()
745    }
746
747    /// Calculate confidence score (0.0-1.0) based on accumulated async pattern signals.
748    pub fn calculate_confidence(&self) -> f64 {
749        let mut confidence: f64 = 0.0;
750        if !self.async_await.is_empty() {
751            confidence += 0.4;
752        }
753        if !self.goroutines.is_empty() {
754            confidence += 0.5;
755        }
756        if !self.tokio_usage.is_empty() {
757            confidence += 0.5;
758        }
759        if !self.sync_primitives.is_empty() {
760            confidence += 0.3;
761        }
762        if !self.thread_spawns.is_empty() {
763            confidence += 0.3;
764        }
765        confidence.min(1.0)
766    }
767}