invar-tools 1.4.0__py3-none-any.whl → 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. invar/__init__.py +7 -1
  2. invar/core/entry_points.py +12 -10
  3. invar/core/formatter.py +21 -1
  4. invar/core/models.py +98 -0
  5. invar/core/patterns/__init__.py +53 -0
  6. invar/core/patterns/detector.py +249 -0
  7. invar/core/patterns/p0_exhaustive.py +207 -0
  8. invar/core/patterns/p0_literal.py +307 -0
  9. invar/core/patterns/p0_newtype.py +211 -0
  10. invar/core/patterns/p0_nonempty.py +307 -0
  11. invar/core/patterns/p0_validation.py +278 -0
  12. invar/core/patterns/registry.py +234 -0
  13. invar/core/patterns/types.py +167 -0
  14. invar/core/trivial_detection.py +189 -0
  15. invar/mcp/server.py +4 -0
  16. invar/shell/commands/guard.py +100 -8
  17. invar/shell/config.py +46 -0
  18. invar/shell/contract_coverage.py +358 -0
  19. invar/shell/guard_output.py +15 -0
  20. invar/shell/pattern_integration.py +234 -0
  21. invar/shell/testing.py +13 -2
  22. invar/templates/CLAUDE.md.template +18 -10
  23. invar/templates/config/CLAUDE.md.jinja +52 -30
  24. invar/templates/config/context.md.jinja +14 -0
  25. invar/templates/protocol/INVAR.md +1 -0
  26. invar/templates/skills/develop/SKILL.md.jinja +51 -1
  27. invar/templates/skills/review/SKILL.md.jinja +196 -31
  28. {invar_tools-1.4.0.dist-info → invar_tools-1.6.0.dist-info}/METADATA +12 -8
  29. {invar_tools-1.4.0.dist-info → invar_tools-1.6.0.dist-info}/RECORD +34 -22
  30. {invar_tools-1.4.0.dist-info → invar_tools-1.6.0.dist-info}/WHEEL +0 -0
  31. {invar_tools-1.4.0.dist-info → invar_tools-1.6.0.dist-info}/entry_points.txt +0 -0
  32. {invar_tools-1.4.0.dist-info → invar_tools-1.6.0.dist-info}/licenses/LICENSE +0 -0
  33. {invar_tools-1.4.0.dist-info → invar_tools-1.6.0.dist-info}/licenses/LICENSE-GPL +0 -0
  34. {invar_tools-1.4.0.dist-info → invar_tools-1.6.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,278 @@
1
+ """
2
+ Validation Pattern Detector (DX-61, P0).
3
+
4
+ Detects fail-fast validation patterns that could benefit from
5
+ error accumulation for better user experience.
6
+ """
7
+
8
+ import ast
9
+ from typing import ClassVar
10
+
11
+ from deal import post, pre
12
+
13
+ from invar.core.patterns.detector import BaseDetector
14
+ from invar.core.patterns.types import (
15
+ Confidence,
16
+ PatternID,
17
+ PatternSuggestion,
18
+ Priority,
19
+ )
20
+
21
+
22
+ class ValidationDetector(BaseDetector):
23
+ """
24
+ Detect fail-fast validation that returns early on first error.
25
+
26
+ These are candidates for error accumulation pattern.
27
+
28
+ Detection logic:
29
+ - Find functions with multiple early returns of error-like values
30
+ - Look for patterns like: if condition: return Failure/raise
31
+ - Suggest accumulating all errors before returning
32
+
33
+ >>> import ast
34
+ >>> detector = ValidationDetector()
35
+ >>> code = '''
36
+ ... def validate(data):
37
+ ... if "name" not in data:
38
+ ... return Failure("Missing name")
39
+ ... if "email" not in data:
40
+ ... return Failure("Missing email")
41
+ ... if "age" not in data:
42
+ ... return Failure("Missing age")
43
+ ... return Success(data)
44
+ ... '''
45
+ >>> tree = ast.parse(code)
46
+ >>> suggestions = detector.detect(tree, "test.py")
47
+ >>> len(suggestions) > 0
48
+ True
49
+ """
50
+
51
+ MIN_EARLY_RETURNS: ClassVar[int] = 3
52
+ ERROR_PATTERNS: ClassVar[set[str]] = {"Failure", "Err", "Error", "Left"}
53
+ VALIDATION_KEYWORDS: ClassVar[set[str]] = {"validate", "check", "verify", "parse"}
54
+
55
+ @property
56
+ @post(lambda result: result == PatternID.VALIDATION)
57
+ def pattern_id(self) -> PatternID:
58
+ """Unique identifier for this pattern."""
59
+ return PatternID.VALIDATION
60
+
61
+ @property
62
+ @post(lambda result: result == Priority.P0)
63
+ def priority(self) -> Priority:
64
+ """Priority tier."""
65
+ return Priority.P0
66
+
67
+ @property
68
+ @post(lambda result: len(result) > 0)
69
+ def description(self) -> str:
70
+ """Human-readable description."""
71
+ return "Use error accumulation instead of fail-fast validation"
72
+
73
+ @post(lambda result: all(isinstance(s, PatternSuggestion) for s in result))
74
+ def detect(self, tree: ast.AST, file_path: str) -> list[PatternSuggestion]:
75
+ """
76
+ Find functions with fail-fast validation patterns.
77
+
78
+ >>> import ast
79
+ >>> detector = ValidationDetector()
80
+ >>> code = '''
81
+ ... def check_config(cfg):
82
+ ... if not cfg.get("host"):
83
+ ... return Failure("No host")
84
+ ... if not cfg.get("port"):
85
+ ... return Failure("No port")
86
+ ... if not cfg.get("user"):
87
+ ... return Failure("No user")
88
+ ... return Success(cfg)
89
+ ... '''
90
+ >>> tree = ast.parse(code)
91
+ >>> suggestions = detector.detect(tree, "test.py")
92
+ >>> len(suggestions) > 0
93
+ True
94
+ """
95
+ suggestions = []
96
+
97
+ for node in ast.walk(tree):
98
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
99
+ suggestion = self._check_function(node, file_path)
100
+ if suggestion:
101
+ suggestions.append(suggestion)
102
+
103
+ return suggestions
104
+
105
+ @pre(lambda self, node, file_path: len(file_path) > 0)
106
+ def _check_function(
107
+ self, node: ast.FunctionDef | ast.AsyncFunctionDef, file_path: str
108
+ ) -> PatternSuggestion | None:
109
+ """
110
+ Check if function has fail-fast validation pattern.
111
+
112
+ >>> import ast
113
+ >>> detector = ValidationDetector()
114
+ >>> code = '''
115
+ ... def validate(x):
116
+ ... if not x.a: return Failure("a")
117
+ ... if not x.b: return Failure("b")
118
+ ... if not x.c: return Failure("c")
119
+ ... return Success(x)
120
+ ... '''
121
+ >>> tree = ast.parse(code)
122
+ >>> func = tree.body[0]
123
+ >>> suggestion = detector._check_function(func, "test.py")
124
+ >>> suggestion is not None
125
+ True
126
+ """
127
+ early_returns = self._count_early_error_returns(node)
128
+
129
+ if early_returns >= self.MIN_EARLY_RETURNS:
130
+ confidence = self._calculate_confidence(node, early_returns)
131
+
132
+ return self.make_suggestion(
133
+ pattern_id=self.pattern_id,
134
+ priority=self.priority,
135
+ file_path=file_path,
136
+ line=node.lineno,
137
+ message=f"{early_returns} early error returns - consider error accumulation",
138
+ current_code=self._format_function_preview(node),
139
+ suggested_pattern="Collect all errors, return list[Error]",
140
+ confidence=confidence,
141
+ reference_pattern="Pattern 2: Validation for Error Accumulation",
142
+ )
143
+
144
+ return None
145
+
146
+ @post(lambda result: result >= 0)
147
+ def _count_early_error_returns(
148
+ self, node: ast.FunctionDef | ast.AsyncFunctionDef
149
+ ) -> int:
150
+ """
151
+ Count early returns with error-like values inside if statements.
152
+
153
+ Only counts if statements at the top level of the function body,
154
+ not nested functions or lambdas.
155
+
156
+ >>> import ast
157
+ >>> detector = ValidationDetector()
158
+ >>> code = '''
159
+ ... def f(x):
160
+ ... if not x.a: return Failure("a")
161
+ ... if not x.b: return Failure("b")
162
+ ... return Success(x)
163
+ ... '''
164
+ >>> tree = ast.parse(code)
165
+ >>> func = tree.body[0]
166
+ >>> detector._count_early_error_returns(func)
167
+ 2
168
+ """
169
+ count = 0
170
+
171
+ # Only iterate direct children, not nested functions
172
+ for stmt in node.body:
173
+ count += self._count_if_error_returns(stmt)
174
+
175
+ return count
176
+
177
+ @post(lambda result: result >= 0)
178
+ def _count_if_error_returns(self, stmt: ast.stmt) -> int:
179
+ """
180
+ Recursively count if statements with error returns, avoiding nested functions.
181
+
182
+ >>> import ast
183
+ >>> detector = ValidationDetector()
184
+ >>> stmt = ast.parse("if x: return Failure('e')").body[0]
185
+ >>> detector._count_if_error_returns(stmt)
186
+ 1
187
+ """
188
+ count = 0
189
+
190
+ if isinstance(stmt, ast.If):
191
+ # Check if body has early error return
192
+ for body_stmt in stmt.body:
193
+ if isinstance(body_stmt, ast.Return) and self._is_error_return(body_stmt):
194
+ count += 1
195
+ break
196
+ # Recurse into else/elif but NOT into nested functions
197
+ for body_stmt in stmt.body:
198
+ if not isinstance(body_stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
199
+ count += self._count_if_error_returns(body_stmt)
200
+ for else_stmt in stmt.orelse:
201
+ if not isinstance(else_stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
202
+ count += self._count_if_error_returns(else_stmt)
203
+
204
+ return count
205
+
206
+ @post(lambda result: isinstance(result, bool))
207
+ def _is_error_return(self, node: ast.Return) -> bool:
208
+ """
209
+ Check if return value looks like an error.
210
+
211
+ >>> import ast
212
+ >>> detector = ValidationDetector()
213
+ >>> ret = ast.parse("return Failure('error')").body[0].value
214
+ >>> # This tests the return statement's value
215
+ >>> detector._is_error_return(ast.Return(value=ret))
216
+ True
217
+ """
218
+ if node.value is None:
219
+ return False
220
+
221
+ # Check for Failure(...), Err(...), etc.
222
+ if isinstance(node.value, ast.Call):
223
+ if isinstance(node.value.func, ast.Name):
224
+ return node.value.func.id in self.ERROR_PATTERNS
225
+ if isinstance(node.value.func, ast.Attribute):
226
+ return node.value.func.attr in self.ERROR_PATTERNS
227
+
228
+ return False
229
+
230
+ @pre(lambda self, node, early_returns: early_returns >= 0)
231
+ @post(lambda result: result in Confidence)
232
+ def _calculate_confidence(
233
+ self, node: ast.FunctionDef | ast.AsyncFunctionDef, early_returns: int
234
+ ) -> Confidence:
235
+ """
236
+ Calculate confidence based on function characteristics.
237
+
238
+ >>> import ast
239
+ >>> detector = ValidationDetector()
240
+ >>> func = ast.parse("def validate_config(x): pass").body[0]
241
+ >>> detector._calculate_confidence(func, 3)
242
+ <Confidence.HIGH: 'high'>
243
+ """
244
+ # High confidence if function name suggests validation
245
+ func_name = node.name.lower()
246
+ if any(kw in func_name for kw in self.VALIDATION_KEYWORDS):
247
+ return Confidence.HIGH
248
+
249
+ # High confidence if many early returns
250
+ if early_returns >= 5:
251
+ return Confidence.HIGH
252
+
253
+ # Medium confidence for moderate early returns
254
+ if early_returns >= 3:
255
+ return Confidence.MEDIUM
256
+
257
+ return Confidence.LOW
258
+
259
+ @post(lambda result: len(result) > 0 and "def " in result)
260
+ def _format_function_preview(
261
+ self, node: ast.FunctionDef | ast.AsyncFunctionDef
262
+ ) -> str:
263
+ """
264
+ Format function preview for display.
265
+
266
+ >>> import ast
267
+ >>> detector = ValidationDetector()
268
+ >>> func = ast.parse("def validate(data): pass").body[0]
269
+ >>> preview = detector._format_function_preview(func)
270
+ >>> "validate" in preview
271
+ True
272
+ """
273
+ prefix = "async def" if isinstance(node, ast.AsyncFunctionDef) else "def"
274
+ params = self.get_function_params(node)
275
+ param_str = ", ".join(name for name, _ in params[:3])
276
+ if len(params) > 3:
277
+ param_str += ", ..."
278
+ return f"{prefix} {node.name}({param_str})"
@@ -0,0 +1,234 @@
1
+ """
2
+ Pattern Detector Registry (DX-61).
3
+
4
+ Central registry for all pattern detectors. Provides unified API
5
+ for running detection across multiple patterns.
6
+ """
7
+
8
+ import ast
9
+ from functools import lru_cache
10
+
11
+ from deal import post, pre
12
+
13
+ from invar.core.patterns.detector import PatternDetector
14
+ from invar.core.patterns.p0_exhaustive import ExhaustiveMatchDetector
15
+ from invar.core.patterns.p0_literal import LiteralDetector
16
+ from invar.core.patterns.p0_newtype import NewTypeDetector
17
+ from invar.core.patterns.p0_nonempty import NonEmptyDetector
18
+ from invar.core.patterns.p0_validation import ValidationDetector
19
+ from invar.core.patterns.types import (
20
+ Confidence,
21
+ DetectionResult,
22
+ PatternID,
23
+ PatternSuggestion,
24
+ Priority,
25
+ )
26
+
27
+
28
+ class PatternRegistry:
29
+ """
30
+ Registry for pattern detectors.
31
+
32
+ Manages registration and execution of all pattern detectors.
33
+ Provides filtering by priority and confidence levels.
34
+
35
+ >>> registry = PatternRegistry()
36
+ >>> len(registry.detectors) > 0
37
+ True
38
+ >>> PatternID.NEWTYPE in [d.pattern_id for d in registry.detectors]
39
+ True
40
+ """
41
+
42
+ # @invar:allow missing_contract: __init__ takes only self, no inputs to validate
43
+ def __init__(self) -> None:
44
+ """Initialize with all P0 detectors."""
45
+ self._detectors: list[PatternDetector] = [
46
+ NewTypeDetector(),
47
+ ValidationDetector(),
48
+ NonEmptyDetector(),
49
+ LiteralDetector(),
50
+ ExhaustiveMatchDetector(),
51
+ ]
52
+
53
+ @property
54
+ @post(lambda result: len(result) > 0)
55
+ def detectors(self) -> list[PatternDetector]:
56
+ """Get all registered detectors."""
57
+ return self._detectors
58
+
59
+ @post(lambda result: result is not None)
60
+ def get_detectors_by_priority(self, priority: Priority) -> list[PatternDetector]:
61
+ """
62
+ Get detectors filtered by priority.
63
+
64
+ >>> registry = PatternRegistry()
65
+ >>> p0_detectors = registry.get_detectors_by_priority(Priority.P0)
66
+ >>> len(p0_detectors) >= 5
67
+ True
68
+ """
69
+ return [d for d in self._detectors if d.priority == priority]
70
+
71
+ @pre(lambda self, file_path, source, min_confidence=None, priority_filter=None: len(file_path) > 0)
72
+ @post(lambda result: result is not None)
73
+ def detect_file(
74
+ self,
75
+ file_path: str,
76
+ source: str,
77
+ min_confidence: Confidence = Confidence.LOW,
78
+ priority_filter: Priority | None = None,
79
+ ) -> DetectionResult:
80
+ """
81
+ Run all detectors on a file's source code.
82
+
83
+ Args:
84
+ file_path: Path to the Python file (for location reporting)
85
+ source: Source code to analyze
86
+ min_confidence: Minimum confidence level to include
87
+ priority_filter: Optional priority filter (None = all priorities)
88
+
89
+ Returns:
90
+ DetectionResult with all suggestions
91
+
92
+ >>> registry = PatternRegistry()
93
+ >>> code = '''
94
+ ... def process(user_id: str, order_id: str, product_id: str):
95
+ ... pass
96
+ ... '''
97
+ >>> result = registry.detect_file("test.py", source=code)
98
+ >>> result.has_suggestions
99
+ True
100
+ """
101
+ try:
102
+ tree = ast.parse(source)
103
+ except SyntaxError:
104
+ # Skip files with syntax errors
105
+ return DetectionResult(
106
+ file=file_path,
107
+ suggestions=[],
108
+ patterns_checked=[],
109
+ )
110
+
111
+ detectors = self._detectors
112
+ if priority_filter is not None:
113
+ detectors = self.get_detectors_by_priority(priority_filter)
114
+
115
+ all_suggestions: list[PatternSuggestion] = []
116
+ patterns_checked: list[PatternID] = []
117
+
118
+ for detector in detectors:
119
+ patterns_checked.append(detector.pattern_id)
120
+ suggestions = detector.detect(tree, file_path)
121
+ all_suggestions.extend(suggestions)
122
+
123
+ # Filter by confidence
124
+ result = DetectionResult(
125
+ file=file_path,
126
+ suggestions=all_suggestions,
127
+ patterns_checked=patterns_checked,
128
+ )
129
+
130
+ filtered_suggestions = result.filter_by_confidence(min_confidence)
131
+
132
+ return DetectionResult(
133
+ file=file_path,
134
+ suggestions=filtered_suggestions,
135
+ patterns_checked=patterns_checked,
136
+ )
137
+
138
+ @post(lambda result: result is not None)
139
+ def detect_sources(
140
+ self,
141
+ sources: list[tuple[str, str]],
142
+ min_confidence: Confidence = Confidence.LOW,
143
+ priority_filter: Priority | None = None,
144
+ ) -> list[DetectionResult]:
145
+ """
146
+ Run detection on multiple sources.
147
+
148
+ Args:
149
+ sources: List of (file_path, source_code) tuples
150
+
151
+ >>> registry = PatternRegistry()
152
+ >>> results = registry.detect_sources([])
153
+ >>> len(results)
154
+ 0
155
+ """
156
+ results = []
157
+ for file_path, source in sources:
158
+ result = self.detect_file(
159
+ file_path,
160
+ source,
161
+ min_confidence=min_confidence,
162
+ priority_filter=priority_filter,
163
+ )
164
+ results.append(result)
165
+ return results
166
+
167
+ @post(lambda result: result is not None)
168
+ def format_suggestions(
169
+ self, suggestions: list[PatternSuggestion], verbose: bool = False
170
+ ) -> str:
171
+ """
172
+ Format suggestions for display.
173
+
174
+ >>> from invar.core.patterns.types import PatternSuggestion, PatternID, Confidence, Priority, Location
175
+ >>> registry = PatternRegistry()
176
+ >>> suggestions = [
177
+ ... PatternSuggestion(
178
+ ... pattern_id=PatternID.NEWTYPE,
179
+ ... location=Location(file="test.py", line=10),
180
+ ... message="Test",
181
+ ... confidence=Confidence.HIGH,
182
+ ... priority=Priority.P0,
183
+ ... current_code="def f(a, b, c): pass",
184
+ ... suggested_pattern="NewType",
185
+ ... reference_file=".invar/examples/functional.py",
186
+ ... reference_pattern="Pattern 1",
187
+ ... )
188
+ ... ]
189
+ >>> output = registry.format_suggestions(suggestions)
190
+ >>> "SUGGEST" in output
191
+ True
192
+ """
193
+ if not suggestions:
194
+ return ""
195
+
196
+ if verbose:
197
+ return "\n\n".join(s.format_for_guard() for s in suggestions)
198
+ else:
199
+ # Compact format
200
+ lines = []
201
+ for s in suggestions:
202
+ lines.append(f"[{s.severity}] {s.location}: {s.message}")
203
+ return "\n".join(lines)
204
+
205
+
206
+ @lru_cache(maxsize=1)
207
+ @post(lambda result: result is not None)
208
+ def get_registry() -> PatternRegistry:
209
+ """
210
+ Get the global pattern registry (thread-safe via lru_cache).
211
+
212
+ >>> registry = get_registry()
213
+ >>> isinstance(registry, PatternRegistry)
214
+ True
215
+ """
216
+ return PatternRegistry()
217
+
218
+
219
+ @pre(lambda file_path, source, min_confidence=None: len(file_path) > 0)
220
+ @post(lambda result: result is not None)
221
+ def detect_patterns(
222
+ file_path: str,
223
+ source: str,
224
+ min_confidence: Confidence = Confidence.LOW,
225
+ ) -> DetectionResult:
226
+ """
227
+ Convenience function for pattern detection.
228
+
229
+ >>> code = "def f(a: str, b: str, c: str): pass"
230
+ >>> result = detect_patterns("test.py", source=code)
231
+ >>> isinstance(result, DetectionResult)
232
+ True
233
+ """
234
+ return get_registry().detect_file(file_path, source, min_confidence)
@@ -0,0 +1,167 @@
1
+ """
2
+ Pattern Detection Types (DX-61).
3
+
4
+ Core types for the functional pattern guidance system.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from typing import Literal
10
+
11
+ from deal import post, pre
12
+
13
+
14
+ class PatternID(str, Enum):
15
+ """Unique identifier for each pattern."""
16
+
17
+ # P0 - Core patterns
18
+ NEWTYPE = "newtype"
19
+ VALIDATION = "validation"
20
+ NONEMPTY = "nonempty"
21
+ LITERAL = "literal"
22
+ EXHAUSTIVE = "exhaustive"
23
+
24
+ # P1 - Extended patterns (future)
25
+ SMART_CONSTRUCTOR = "smart_constructor"
26
+ STRUCTURED_ERROR = "structured_error"
27
+
28
+
29
+ class Confidence(str, Enum):
30
+ """Confidence level for pattern suggestions."""
31
+
32
+ HIGH = "high" # Strong signal, very likely applicable
33
+ MEDIUM = "medium" # Moderate signal, likely applicable
34
+ LOW = "low" # Weak signal, possibly applicable
35
+
36
+
37
+ class Priority(str, Enum):
38
+ """Pattern priority tier."""
39
+
40
+ P0 = "P0" # Core patterns, always suggested
41
+ P1 = "P1" # Extended patterns, suggested when relevant
42
+
43
+
44
+ # Severity for Guard output
45
+ Severity = Literal["SUGGEST"] # Pattern suggestions are always SUGGEST level
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class Location:
50
+ """Source code location for a pattern opportunity."""
51
+
52
+ file: str
53
+ line: int
54
+ column: int = 0
55
+ end_line: int | None = None
56
+ end_column: int | None = None
57
+
58
+ @post(lambda result: ":" in result) # Contains file:line separator
59
+ def __str__(self) -> str:
60
+ """Format as file:line for display."""
61
+ return f"{self.file}:{self.line}"
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class PatternSuggestion:
66
+ """
67
+ A suggestion to apply a functional pattern.
68
+
69
+ >>> from invar.core.patterns.types import PatternSuggestion, PatternID, Confidence, Priority, Location
70
+ >>> suggestion = PatternSuggestion(
71
+ ... pattern_id=PatternID.NEWTYPE,
72
+ ... location=Location(file="src/api.py", line=42),
73
+ ... message="Consider using NewType for semantic clarity",
74
+ ... confidence=Confidence.HIGH,
75
+ ... priority=Priority.P0,
76
+ ... current_code="def process(user_id: str, order_id: str)",
77
+ ... suggested_pattern="NewType('UserId', str), NewType('OrderId', str)",
78
+ ... reference_file=".invar/examples/functional.py",
79
+ ... reference_pattern="Pattern 1: NewType for Semantic Clarity",
80
+ ... )
81
+ >>> suggestion.severity
82
+ 'SUGGEST'
83
+ >>> "NewType" in suggestion.message
84
+ True
85
+ """
86
+
87
+ pattern_id: PatternID
88
+ location: Location
89
+ message: str
90
+ confidence: Confidence
91
+ priority: Priority
92
+ current_code: str
93
+ suggested_pattern: str
94
+ reference_file: str
95
+ reference_pattern: str
96
+ severity: Severity = "SUGGEST"
97
+
98
+ @post(lambda result: "[SUGGEST]" in result and "Pattern:" in result)
99
+ def format_for_guard(self) -> str:
100
+ """
101
+ Format suggestion for Guard output.
102
+
103
+ >>> suggestion = PatternSuggestion(
104
+ ... pattern_id=PatternID.NEWTYPE,
105
+ ... location=Location(file="src/api.py", line=42),
106
+ ... message="3+ str params - consider NewType",
107
+ ... confidence=Confidence.HIGH,
108
+ ... priority=Priority.P0,
109
+ ... current_code="def f(a: str, b: str, c: str)",
110
+ ... suggested_pattern="NewType",
111
+ ... reference_file=".invar/examples/functional.py",
112
+ ... reference_pattern="Pattern 1",
113
+ ... )
114
+ >>> "SUGGEST" in suggestion.format_for_guard()
115
+ True
116
+ >>> "src/api.py:42" in suggestion.format_for_guard()
117
+ True
118
+ """
119
+ return (
120
+ f"[{self.severity}] {self.location}: {self.message}\n"
121
+ f" Pattern: {self.pattern_id.value} ({self.priority.value})\n"
122
+ f" Current: {self.current_code[:60]}{'...' if len(self.current_code) > 60 else ''}\n"
123
+ f" Suggest: {self.suggested_pattern}\n"
124
+ f" See: {self.reference_file} - {self.reference_pattern}"
125
+ )
126
+
127
+
128
+ @dataclass(frozen=True)
129
+ class DetectionResult:
130
+ """
131
+ Result of running pattern detection on a file.
132
+
133
+ >>> from invar.core.patterns.types import DetectionResult, PatternSuggestion, PatternID, Confidence, Priority, Location
134
+ >>> result = DetectionResult(
135
+ ... file="src/api.py",
136
+ ... suggestions=[],
137
+ ... patterns_checked=[PatternID.NEWTYPE, PatternID.LITERAL],
138
+ ... )
139
+ >>> len(result.suggestions)
140
+ 0
141
+ >>> PatternID.NEWTYPE in result.patterns_checked
142
+ True
143
+ """
144
+
145
+ file: str
146
+ suggestions: list[PatternSuggestion]
147
+ patterns_checked: list[PatternID]
148
+
149
+ @property
150
+ @post(lambda result: isinstance(result, bool))
151
+ def has_suggestions(self) -> bool:
152
+ """Check if any suggestions were found."""
153
+ return len(self.suggestions) > 0
154
+
155
+ @pre(lambda self, min_confidence: min_confidence in Confidence)
156
+ @post(lambda result: isinstance(result, list))
157
+ def filter_by_confidence(self, min_confidence: Confidence) -> list[PatternSuggestion]:
158
+ """
159
+ Filter suggestions by minimum confidence level.
160
+
161
+ >>> result = DetectionResult(file="test.py", suggestions=[], patterns_checked=[])
162
+ >>> result.filter_by_confidence(Confidence.HIGH)
163
+ []
164
+ """
165
+ confidence_order = {Confidence.HIGH: 2, Confidence.MEDIUM: 1, Confidence.LOW: 0}
166
+ min_level = confidence_order[min_confidence]
167
+ return [s for s in self.suggestions if confidence_order[s.confidence] >= min_level]