Skip to main content

tldr_core/patterns/
constraints.rs

1//! LLM constraint generation from detected patterns
2//!
3//! Generates natural language rules/constraints for LLM code generation
4//! based on detected codebase patterns.
5
6use crate::types::{
7    Constraint, SoftDeletePattern, ErrorHandlingPattern, NamingPattern,
8    ResourceManagementPattern, ValidationPattern, TestIdiomPattern,
9    ImportPattern, TypeCoveragePattern, ApiConventionPattern, AsyncPattern,
10    NamingConvention, ImportStyle, ImportGrouping,
11};
12
13/// Collection of detected code patterns for constraint generation and analysis.
14pub struct DetectedPatterns<'a> {
15    /// Detected soft-delete conventions.
16    pub soft_delete: &'a Option<SoftDeletePattern>,
17    /// Detected error handling conventions.
18    pub error_handling: &'a Option<ErrorHandlingPattern>,
19    /// Detected naming conventions.
20    pub naming: &'a Option<NamingPattern>,
21    /// Detected resource lifecycle/cleanup conventions.
22    pub resource_management: &'a Option<ResourceManagementPattern>,
23    /// Detected validation conventions.
24    pub validation: &'a Option<ValidationPattern>,
25    /// Detected testing idioms.
26    pub test_idioms: &'a Option<TestIdiomPattern>,
27    /// Detected import organization/style conventions.
28    pub import_patterns: &'a Option<ImportPattern>,
29    /// Detected type coverage conventions.
30    pub type_coverage: &'a Option<TypeCoveragePattern>,
31    /// Detected API design conventions.
32    pub api_conventions: &'a Option<ApiConventionPattern>,
33    /// Detected async/concurrency conventions.
34    pub async_patterns: &'a Option<AsyncPattern>,
35}
36
37/// Generate LLM constraints from detected patterns
38pub fn generate_constraints(patterns: &DetectedPatterns<'_>) -> Vec<Constraint> {
39    let mut constraints = Vec::new();
40
41    add_soft_delete_constraints(patterns.soft_delete, &mut constraints);
42    add_error_handling_constraints(patterns.error_handling, &mut constraints);
43    add_naming_constraints(patterns.naming, &mut constraints);
44    add_resource_management_constraints(patterns.resource_management, &mut constraints);
45    add_validation_constraints(patterns.validation, &mut constraints);
46    add_test_constraints(patterns.test_idioms, &mut constraints);
47    add_import_constraints(patterns.import_patterns, &mut constraints);
48    add_type_constraints(patterns.type_coverage, &mut constraints);
49    add_api_constraints(patterns.api_conventions, &mut constraints);
50    add_async_constraints(patterns.async_patterns, &mut constraints);
51
52    // Sort by priority
53    constraints.sort_by_key(|c| c.priority);
54
55    constraints
56}
57
58fn add_soft_delete_constraints(
59    soft_delete: &Option<SoftDeletePattern>,
60    constraints: &mut Vec<Constraint>,
61) {
62    let Some(pattern) = soft_delete else {
63        return;
64    };
65    if !pattern.detected || pattern.confidence < 0.4 {
66        return;
67    }
68    constraints.push(Constraint::new(
69        "soft_delete",
70        format!(
71            "Use soft delete pattern with {} fields instead of hard DELETE",
72            pattern.column_names.join(", ")
73        ),
74        pattern.confidence,
75        1,
76    ));
77}
78
79fn add_error_handling_constraints(
80    error_handling: &Option<ErrorHandlingPattern>,
81    constraints: &mut Vec<Constraint>,
82) {
83    let Some(pattern) = error_handling else {
84        return;
85    };
86    if pattern.confidence < 0.3 {
87        return;
88    }
89    if pattern.patterns.contains(&"result_type".to_string()) {
90        constraints.push(Constraint::new(
91            "error_handling",
92            "Use Result<T, E> return types for fallible operations",
93            pattern.confidence,
94            1,
95        ));
96    }
97    if pattern.patterns.contains(&"try_catch".to_string()) {
98        constraints.push(Constraint::new(
99            "error_handling",
100            "Wrap error-prone operations in try/catch blocks with specific exception handling",
101            pattern.confidence,
102            2,
103        ));
104    }
105    if pattern.patterns.contains(&"custom_errors".to_string()) && !pattern.exception_types.is_empty() {
106        constraints.push(Constraint::new(
107            "error_handling",
108            format!(
109                "Use existing custom error types: {}",
110                pattern.exception_types.join(", ")
111            ),
112            pattern.confidence,
113            2,
114        ));
115    }
116}
117
118fn add_naming_constraints(naming: &Option<NamingPattern>, constraints: &mut Vec<Constraint>) {
119    let Some(pattern) = naming else {
120        return;
121    };
122    if pattern.consistency_score < 0.5 {
123        return;
124    }
125    constraints.push(Constraint::new(
126        "naming",
127        format!(
128            "Function names: {}, Class names: {}, Constants: {}",
129            convention_to_string(&pattern.functions),
130            convention_to_string(&pattern.classes),
131            convention_to_string(&pattern.constants),
132        ),
133        pattern.consistency_score,
134        1,
135    ));
136    if let Some(ref prefix) = pattern.private_prefix {
137        constraints.push(Constraint::new(
138            "naming",
139            format!("Use '{}' prefix for private members", prefix),
140            pattern.consistency_score,
141            2,
142        ));
143    }
144}
145
146fn add_resource_management_constraints(
147    resource_management: &Option<ResourceManagementPattern>,
148    constraints: &mut Vec<Constraint>,
149) {
150    let Some(pattern) = resource_management else {
151        return;
152    };
153    if pattern.confidence < 0.4 {
154        return;
155    }
156    for p in &pattern.patterns {
157        let rule = match p.as_str() {
158            "context_manager" => "Use 'with' statements (context managers) for resource management",
159            "defer" => "Use 'defer' to ensure cleanup runs even on error",
160            "raii" => "Implement Drop trait for types that manage external resources",
161            "finally" => "Use try/finally for explicit resource cleanup",
162            _ => continue,
163        };
164        constraints.push(Constraint::new(
165            "resource_management",
166            rule,
167            pattern.confidence,
168            1,
169        ));
170    }
171}
172
173fn add_validation_constraints(
174    validation: &Option<ValidationPattern>,
175    constraints: &mut Vec<Constraint>,
176) {
177    let Some(pattern) = validation else {
178        return;
179    };
180    if pattern.confidence < 0.3 {
181        return;
182    }
183    if !pattern.frameworks.is_empty() {
184        constraints.push(Constraint::new(
185            "validation",
186            format!("Use {} for input validation", pattern.frameworks.join(" or ")),
187            pattern.confidence,
188            1,
189        ));
190    }
191    if pattern.patterns.contains(&"guard_clauses".to_string()) {
192        constraints.push(Constraint::new(
193            "validation",
194            "Validate inputs at function start with guard clauses",
195            pattern.confidence,
196            2,
197        ));
198    }
199}
200
201fn add_test_constraints(test_idioms: &Option<TestIdiomPattern>, constraints: &mut Vec<Constraint>) {
202    let Some(pattern) = test_idioms else {
203        return;
204    };
205    if pattern.confidence < 0.3 {
206        return;
207    }
208    if let Some(ref framework) = pattern.framework {
209        constraints.push(Constraint::new(
210            "testing",
211            format!("Use {} testing framework", framework),
212            pattern.confidence,
213            1,
214        ));
215    }
216    if pattern.fixture_usage {
217        constraints.push(Constraint::new(
218            "testing",
219            "Use fixtures for test setup/teardown",
220            pattern.confidence,
221            2,
222        ));
223    }
224    if pattern.mock_usage {
225        constraints.push(Constraint::new(
226            "testing",
227            "Use mocking for external dependencies in tests",
228            pattern.confidence,
229            2,
230        ));
231    }
232}
233
234fn add_import_constraints(import_patterns: &Option<ImportPattern>, constraints: &mut Vec<Constraint>) {
235    let Some(pattern) = import_patterns else {
236        return;
237    };
238    match pattern.absolute_vs_relative {
239        ImportStyle::Absolute => constraints.push(Constraint::new(
240            "imports",
241            "Prefer absolute imports over relative imports",
242            1.0,
243            2,
244        )),
245        ImportStyle::Relative => constraints.push(Constraint::new(
246            "imports",
247            "Prefer relative imports for local modules",
248            1.0,
249            2,
250        )),
251        ImportStyle::Mixed => {}
252    }
253    match pattern.grouping_style {
254        ImportGrouping::StdlibFirst => constraints.push(Constraint::new(
255            "imports",
256            "Group imports: stdlib first, then third-party, then local",
257            1.0,
258            3,
259        )),
260        ImportGrouping::ThirdPartyFirst => constraints.push(Constraint::new(
261            "imports",
262            "Group imports: third-party first, then stdlib, then local",
263            1.0,
264            3,
265        )),
266        _ => {}
267    }
268}
269
270fn add_type_constraints(type_coverage: &Option<TypeCoveragePattern>, constraints: &mut Vec<Constraint>) {
271    let Some(pattern) = type_coverage else {
272        return;
273    };
274    if pattern.coverage_overall >= 0.5 {
275        constraints.push(Constraint::new(
276            "types",
277            "Add type annotations to function parameters and return values",
278            pattern.coverage_overall,
279            1,
280        ));
281    }
282    if pattern.typevar_usage {
283        constraints.push(Constraint::new(
284            "types",
285            "Use generics/TypeVar for reusable type-safe functions",
286            pattern.coverage_overall,
287            2,
288        ));
289    }
290}
291
292fn add_api_constraints(api_conventions: &Option<ApiConventionPattern>, constraints: &mut Vec<Constraint>) {
293    let Some(pattern) = api_conventions else {
294        return;
295    };
296    if pattern.confidence < 0.4 {
297        return;
298    }
299    if let Some(ref framework) = pattern.framework {
300        constraints.push(Constraint::new(
301            "api",
302            format!("Follow {} patterns for API endpoints", framework),
303            pattern.confidence,
304            1,
305        ));
306    }
307    if pattern.patterns.contains(&"rest_crud".to_string()) {
308        constraints.push(Constraint::new(
309            "api",
310            "Follow REST conventions: GET for read, POST for create, PUT for update, DELETE for delete",
311            pattern.confidence,
312            1,
313        ));
314    }
315    if let Some(ref orm) = pattern.orm_usage {
316        constraints.push(Constraint::new(
317            "api",
318            format!("Use {} for database operations", orm),
319            pattern.confidence,
320            2,
321        ));
322    }
323}
324
325fn add_async_constraints(async_patterns: &Option<AsyncPattern>, constraints: &mut Vec<Constraint>) {
326    let Some(pattern) = async_patterns else {
327        return;
328    };
329    if pattern.concurrency_confidence < 0.3 {
330        return;
331    }
332    if pattern.patterns.contains(&"async_await".to_string()) {
333        constraints.push(Constraint::new(
334            "async",
335            "Use async/await for asynchronous operations",
336            pattern.concurrency_confidence,
337            1,
338        ));
339    }
340    if pattern.patterns.contains(&"goroutines".to_string()) {
341        constraints.push(Constraint::new(
342            "async",
343            "Use goroutines for concurrent operations",
344            pattern.concurrency_confidence,
345            1,
346        ));
347    }
348    if !pattern.sync_primitives.is_empty() {
349        constraints.push(Constraint::new(
350            "async",
351            format!(
352                "Use {} for thread synchronization",
353                pattern.sync_primitives.join(", ")
354            ),
355            pattern.concurrency_confidence,
356            2,
357        ));
358    }
359}
360
361fn convention_to_string(conv: &NamingConvention) -> &'static str {
362    match conv {
363        NamingConvention::SnakeCase => "snake_case",
364        NamingConvention::CamelCase => "camelCase",
365        NamingConvention::PascalCase => "PascalCase",
366        NamingConvention::UpperSnakeCase => "UPPER_SNAKE_CASE",
367        NamingConvention::Mixed => "mixed",
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn test_soft_delete_constraint() {
377        let soft_delete = Some(SoftDeletePattern {
378            detected: true,
379            confidence: 0.8,
380            column_names: vec!["is_deleted".to_string(), "deleted_at".to_string()],
381            evidence: vec![],
382        });
383
384        let constraints = generate_constraints(&DetectedPatterns {
385            soft_delete: &soft_delete,
386            error_handling: &None,
387            naming: &None,
388            resource_management: &None,
389            validation: &None,
390            test_idioms: &None,
391            import_patterns: &None,
392            type_coverage: &None,
393            api_conventions: &None,
394            async_patterns: &None,
395        });
396
397        assert!(!constraints.is_empty());
398        assert!(constraints[0].rule.contains("soft delete"));
399        assert!(constraints[0].rule.contains("is_deleted"));
400    }
401
402    #[test]
403    fn test_naming_constraint() {
404        let naming = Some(NamingPattern {
405            functions: NamingConvention::SnakeCase,
406            classes: NamingConvention::PascalCase,
407            constants: NamingConvention::UpperSnakeCase,
408            private_prefix: Some("_".to_string()),
409            consistency_score: 0.9,
410            violations: vec![],
411        });
412
413        let constraints = generate_constraints(&DetectedPatterns {
414            soft_delete: &None,
415            error_handling: &None,
416            naming: &naming,
417            resource_management: &None,
418            validation: &None,
419            test_idioms: &None,
420            import_patterns: &None,
421            type_coverage: &None,
422            api_conventions: &None,
423            async_patterns: &None,
424        });
425
426        assert!(constraints.len() >= 2);
427        assert!(constraints.iter().any(|c| c.rule.contains("snake_case")));
428        assert!(constraints.iter().any(|c| c.rule.contains("_")));
429    }
430
431    #[test]
432    fn test_no_constraints_below_threshold() {
433        let soft_delete = Some(SoftDeletePattern {
434            detected: true,
435            confidence: 0.2, // Below threshold
436            column_names: vec!["is_deleted".to_string()],
437            evidence: vec![],
438        });
439
440        let constraints = generate_constraints(&DetectedPatterns {
441            soft_delete: &soft_delete,
442            error_handling: &None,
443            naming: &None,
444            resource_management: &None,
445            validation: &None,
446            test_idioms: &None,
447            import_patterns: &None,
448            type_coverage: &None,
449            api_conventions: &None,
450            async_patterns: &None,
451        });
452
453        assert!(constraints.is_empty());
454    }
455}