invar-tools 1.17.26__py3-none-any.whl → 1.17.27__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.
- invar/core/contracts.py +38 -6
- invar/core/doc_edit.py +22 -9
- invar/core/doc_parser.py +17 -11
- invar/core/entry_points.py +48 -42
- invar/core/extraction.py +25 -9
- invar/core/format_specs.py +7 -2
- invar/core/format_strategies.py +6 -4
- invar/core/formatter.py +9 -6
- invar/core/hypothesis_strategies.py +24 -5
- invar/core/lambda_helpers.py +2 -2
- invar/core/parser.py +40 -21
- invar/core/patterns/detector.py +25 -6
- invar/core/patterns/p0_exhaustive.py +5 -5
- invar/core/patterns/p0_literal.py +8 -8
- invar/core/patterns/p0_newtype.py +2 -2
- invar/core/patterns/p0_nonempty.py +12 -7
- invar/core/patterns/p0_validation.py +4 -8
- invar/core/patterns/registry.py +12 -2
- invar/core/property_gen.py +47 -23
- invar/core/shell_analysis.py +70 -66
- invar/core/strategies.py +14 -3
- invar/core/suggestions.py +12 -4
- invar/core/tautology.py +33 -10
- invar/core/template_parser.py +23 -15
- invar/core/ts_parsers.py +6 -2
- invar/core/ts_sig_parser.py +18 -10
- invar/core/utils.py +20 -9
- invar/templates/protocol/python/tools.md +3 -0
- {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/METADATA +1 -1
- {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/RECORD +35 -35
- {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/WHEEL +0 -0
- {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.17.26.dist-info → invar_tools-1.17.27.dist-info}/licenses/NOTICE +0 -0
invar/core/strategies.py
CHANGED
|
@@ -111,7 +111,10 @@ PATTERNS: list[tuple[str, Callable[[re.Match, str], dict[str, Any] | None]]] = [
|
|
|
111
111
|
),
|
|
112
112
|
(
|
|
113
113
|
r"(-?[\d.]+(?:e[+-]?\d+)?)\s*<=\s*(\w+)\s*<=\s*(-?[\d.]+(?:e[+-]?\d+)?)",
|
|
114
|
-
lambda m, p: {
|
|
114
|
+
lambda m, p: {
|
|
115
|
+
"min_value": _parse_number(m.group(1)),
|
|
116
|
+
"max_value": _parse_number(m.group(3)),
|
|
117
|
+
}
|
|
115
118
|
if m.group(2) == p
|
|
116
119
|
else None,
|
|
117
120
|
),
|
|
@@ -140,7 +143,11 @@ PATTERNS: list[tuple[str, Callable[[re.Match, str], dict[str, Any] | None]]] = [
|
|
|
140
143
|
]
|
|
141
144
|
|
|
142
145
|
|
|
143
|
-
@pre(
|
|
146
|
+
@pre(
|
|
147
|
+
lambda pre_source, param_name, param_type=None: isinstance(pre_source, str)
|
|
148
|
+
and len(param_name) > 0
|
|
149
|
+
and (param_type is None or isinstance(param_type, type))
|
|
150
|
+
) # Param must be named
|
|
144
151
|
@post(lambda result: isinstance(result.constraints, dict)) # Returns valid hint
|
|
145
152
|
def infer_from_lambda(
|
|
146
153
|
pre_source: str,
|
|
@@ -193,7 +200,11 @@ def infer_from_lambda(
|
|
|
193
200
|
)
|
|
194
201
|
|
|
195
202
|
|
|
196
|
-
@pre(
|
|
203
|
+
@pre(
|
|
204
|
+
lambda pre_sources, param_name, param_type=None: isinstance(pre_sources, list)
|
|
205
|
+
and len(param_name) > 0
|
|
206
|
+
and (param_type is None or isinstance(param_type, type))
|
|
207
|
+
) # Param must be named
|
|
197
208
|
@post(lambda result: isinstance(result.constraints, dict)) # Returns valid hint
|
|
198
209
|
def infer_from_multiple(
|
|
199
210
|
pre_sources: list[str],
|
invar/core/suggestions.py
CHANGED
|
@@ -35,7 +35,7 @@ CONSTRAINT_PATTERNS: dict[str, list[str]] = {
|
|
|
35
35
|
# Return-type-aware @post patterns for redundant_type_contract suggestions
|
|
36
36
|
RETURN_TYPE_POST_PATTERNS: dict[str, str] = {
|
|
37
37
|
"list[Violation]": '@post(lambda result: all(v.rule == "RULE_NAME" for v in result))',
|
|
38
|
-
"list":
|
|
38
|
+
"list": "@post(lambda result: all(<predicate> for item in result))",
|
|
39
39
|
"dict": "@post(lambda result: all(isinstance(k, <type>) for k in result))",
|
|
40
40
|
"set": "@post(lambda result: all(<predicate> for item in result))",
|
|
41
41
|
"int": "@post(lambda result: result >= 0)",
|
|
@@ -331,11 +331,18 @@ _VIOLATION_PREFIXES = {
|
|
|
331
331
|
"missing_contract": ("Add: ", "Add: "),
|
|
332
332
|
"empty_contract": ("Replace with: ", "Replace with: "),
|
|
333
333
|
"redundant_type_contract": ("Replace with business logic: ", "Replace with: "),
|
|
334
|
-
"semantic_tautology": (
|
|
334
|
+
"semantic_tautology": (
|
|
335
|
+
"Replace tautology with meaningful constraint: ",
|
|
336
|
+
"Replace tautology with: ",
|
|
337
|
+
),
|
|
335
338
|
}
|
|
336
339
|
|
|
337
340
|
|
|
338
|
-
@pre(
|
|
341
|
+
@pre(
|
|
342
|
+
lambda prefix, suggestion, patterns: bool(prefix)
|
|
343
|
+
and bool(suggestion)
|
|
344
|
+
and isinstance(patterns, str)
|
|
345
|
+
)
|
|
339
346
|
@post(lambda result: isinstance(result, str) and len(result) > 0)
|
|
340
347
|
def _format_with_patterns(prefix: str, suggestion: str, patterns: str) -> str:
|
|
341
348
|
"""Format suggestion with optional patterns.
|
|
@@ -350,7 +357,8 @@ def _format_with_patterns(prefix: str, suggestion: str, patterns: str) -> str:
|
|
|
350
357
|
|
|
351
358
|
|
|
352
359
|
@pre(
|
|
353
|
-
lambda symbol, violation_type:
|
|
360
|
+
lambda symbol, violation_type: symbol is not None
|
|
361
|
+
and violation_type
|
|
354
362
|
in ("missing_contract", "empty_contract", "redundant_type_contract", "semantic_tautology", "")
|
|
355
363
|
)
|
|
356
364
|
def format_suggestion_for_violation(symbol: Symbol, violation_type: str) -> str:
|
invar/core/tautology.py
CHANGED
|
@@ -61,7 +61,13 @@ def is_semantic_tautology(expression: str) -> tuple[bool, str]:
|
|
|
61
61
|
|
|
62
62
|
# DX-38 Tier 1: Check for no-parameter lambda
|
|
63
63
|
args = lambda_node.args
|
|
64
|
-
if
|
|
64
|
+
if (
|
|
65
|
+
not args.args
|
|
66
|
+
and not args.posonlyargs
|
|
67
|
+
and not args.kwonlyargs
|
|
68
|
+
and not args.vararg
|
|
69
|
+
and not args.kwarg
|
|
70
|
+
):
|
|
65
71
|
return (True, "contract has no parameters (doesn't validate function inputs)")
|
|
66
72
|
|
|
67
73
|
return _check_tautology_patterns(lambda_node.body)
|
|
@@ -92,9 +98,14 @@ def _check_comparison_patterns(node: ast.expr) -> tuple[bool, str] | None:
|
|
|
92
98
|
# Length non-negative pattern
|
|
93
99
|
if len(node.comparators) == 1:
|
|
94
100
|
left, op, right = node.left, node.ops[0], node.comparators[0]
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
isinstance(
|
|
101
|
+
if (
|
|
102
|
+
isinstance(left, ast.Call)
|
|
103
|
+
and isinstance(left.func, ast.Name)
|
|
104
|
+
and left.func.id == "len"
|
|
105
|
+
and isinstance(op, ast.GtE)
|
|
106
|
+
and isinstance(right, ast.Constant)
|
|
107
|
+
and right.value == 0
|
|
108
|
+
):
|
|
98
109
|
arg = ast.unparse(left.args[0]) if left.args else "x"
|
|
99
110
|
return (True, f"len({arg}) >= 0 is always True for any sequence")
|
|
100
111
|
return None
|
|
@@ -103,8 +114,12 @@ def _check_comparison_patterns(node: ast.expr) -> tuple[bool, str] | None:
|
|
|
103
114
|
@pre(lambda node: isinstance(node, ast.expr))
|
|
104
115
|
def _check_isinstance_object(node: ast.expr) -> tuple[bool, str] | None:
|
|
105
116
|
"""Check for isinstance(x, object) pattern."""
|
|
106
|
-
if (
|
|
107
|
-
|
|
117
|
+
if (
|
|
118
|
+
isinstance(node, ast.Call)
|
|
119
|
+
and isinstance(node.func, ast.Name)
|
|
120
|
+
and node.func.id == "isinstance"
|
|
121
|
+
and len(node.args) == 2
|
|
122
|
+
):
|
|
108
123
|
type_arg = node.args[1]
|
|
109
124
|
if isinstance(type_arg, ast.Name) and type_arg.id == "object":
|
|
110
125
|
return (True, f"isinstance({ast.unparse(node.args[0])}, object) is always True")
|
|
@@ -139,7 +154,7 @@ def _check_boolop_patterns(node: ast.expr) -> tuple[bool, str] | None:
|
|
|
139
154
|
return None
|
|
140
155
|
|
|
141
156
|
|
|
142
|
-
@pre(lambda node: isinstance(node, ast.expr) and hasattr(node,
|
|
157
|
+
@pre(lambda node: isinstance(node, ast.expr) and hasattr(node, "__class__"))
|
|
143
158
|
@post(lambda result: isinstance(result, tuple) and len(result) == 2)
|
|
144
159
|
def _check_tautology_patterns(node: ast.expr) -> tuple[bool, str]:
|
|
145
160
|
"""Check for common tautology patterns in AST node.
|
|
@@ -157,15 +172,23 @@ def _check_tautology_patterns(node: ast.expr) -> tuple[bool, str]:
|
|
|
157
172
|
>>> _check_tautology_patterns(ast.Constant(value=False))
|
|
158
173
|
(True, 'contract always returns False (contradiction - will always fail)')
|
|
159
174
|
"""
|
|
160
|
-
for checker in [
|
|
161
|
-
|
|
175
|
+
for checker in [
|
|
176
|
+
_check_literal_patterns,
|
|
177
|
+
_check_comparison_patterns,
|
|
178
|
+
_check_isinstance_object,
|
|
179
|
+
_check_boolop_patterns,
|
|
180
|
+
]:
|
|
162
181
|
result = checker(node)
|
|
163
182
|
if result:
|
|
164
183
|
return result
|
|
165
184
|
return (False, "")
|
|
166
185
|
|
|
167
186
|
|
|
168
|
-
@pre(
|
|
187
|
+
@pre(
|
|
188
|
+
lambda file_info, config: file_info is not None
|
|
189
|
+
and len(file_info.path) > 0
|
|
190
|
+
and config is not None
|
|
191
|
+
)
|
|
169
192
|
def check_semantic_tautology(file_info: FileInfo, config: RuleConfig) -> list[Violation]:
|
|
170
193
|
"""Check for semantic tautology contracts. Core files only.
|
|
171
194
|
|
invar/core/template_parser.py
CHANGED
|
@@ -73,9 +73,7 @@ class ParsedFile:
|
|
|
73
73
|
# Patterns for region markers
|
|
74
74
|
# <!--invar:managed version="5.0"-->
|
|
75
75
|
# <!--/invar:managed-->
|
|
76
|
-
REGION_START_PATTERN = re.compile(
|
|
77
|
-
r'<!--invar:(\w+)(?:\s+version=["\']([^"\']+)["\'])?-->'
|
|
78
|
-
)
|
|
76
|
+
REGION_START_PATTERN = re.compile(r'<!--invar:(\w+)(?:\s+version=["\']([^"\']+)["\'])?-->')
|
|
79
77
|
REGION_END_PATTERN = re.compile(r"<!--/invar:(\w+)-->")
|
|
80
78
|
|
|
81
79
|
|
|
@@ -157,10 +155,15 @@ def parse_invar_regions(content: str) -> ParsedFile:
|
|
|
157
155
|
return ParsedFile(regions=regions, before=before, after=after, raw=content)
|
|
158
156
|
|
|
159
157
|
|
|
160
|
-
@pre(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)
|
|
158
|
+
@pre(
|
|
159
|
+
lambda parsed, updates: isinstance(updates, dict)
|
|
160
|
+
and all(k == v.name for k, v in parsed.regions.items())
|
|
161
|
+
) # Keys must match names
|
|
162
|
+
@ensure(
|
|
163
|
+
lambda parsed, updates, result: (
|
|
164
|
+
not parsed.has_regions or all(f"<!--invar:{r}" in result for r in parsed.regions)
|
|
165
|
+
)
|
|
166
|
+
) # Checks start tag prefix (version attribute may follow)
|
|
164
167
|
def reconstruct_file(parsed: ParsedFile, updates: dict[str, str]) -> str:
|
|
165
168
|
"""Reconstruct file content with updated regions.
|
|
166
169
|
|
|
@@ -359,11 +362,16 @@ def detect_claude_md_state(content: str) -> ClaudeMdState:
|
|
|
359
362
|
project_complete = has_project_open and has_project_close
|
|
360
363
|
|
|
361
364
|
# All markers present
|
|
362
|
-
any_marker = any(
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
365
|
+
any_marker = any(
|
|
366
|
+
[
|
|
367
|
+
has_managed_open,
|
|
368
|
+
has_managed_close,
|
|
369
|
+
has_user_open,
|
|
370
|
+
has_user_close,
|
|
371
|
+
has_project_open,
|
|
372
|
+
has_project_close,
|
|
373
|
+
]
|
|
374
|
+
)
|
|
367
375
|
|
|
368
376
|
if not any_marker:
|
|
369
377
|
return ClaudeMdState(state="missing")
|
|
@@ -424,13 +432,13 @@ def strip_invar_markers(content: str) -> str:
|
|
|
424
432
|
"""
|
|
425
433
|
# Remove all <!--invar:xxx--> and <!--/invar:xxx--> markers
|
|
426
434
|
# Also handle version attribute
|
|
427
|
-
cleaned = re.sub(r
|
|
435
|
+
cleaned = re.sub(r"<!--/?invar:\w+[^>]*-->", "", content)
|
|
428
436
|
# Clean up excessive blank lines
|
|
429
|
-
cleaned = re.sub(r
|
|
437
|
+
cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
|
|
430
438
|
return cleaned.strip()
|
|
431
439
|
|
|
432
440
|
|
|
433
|
-
@pre(lambda content, merge_date: len(content) > 0)
|
|
441
|
+
@pre(lambda content, merge_date="": len(content) > 0 and isinstance(merge_date, str))
|
|
434
442
|
@post(lambda result: "MERGED CONTENT" in result)
|
|
435
443
|
def format_preserved_content(content: str, merge_date: str = "") -> str:
|
|
436
444
|
"""Format preserved content with review markers.
|
invar/core/ts_parsers.py
CHANGED
|
@@ -115,7 +115,9 @@ def parse_tsc_output(output: str) -> list[TSViolation]:
|
|
|
115
115
|
return violations
|
|
116
116
|
|
|
117
117
|
|
|
118
|
-
@pre(
|
|
118
|
+
@pre(
|
|
119
|
+
lambda output, base_path="": output is not None and isinstance(base_path, str)
|
|
120
|
+
) # Accepts any string including empty
|
|
119
121
|
@post(lambda result: all(v.source == "eslint" for v in result))
|
|
120
122
|
def parse_eslint_json(output: str, base_path: str = "") -> list[TSViolation]:
|
|
121
123
|
"""Parse ESLint JSON output into violations list.
|
|
@@ -194,7 +196,9 @@ def parse_eslint_json(output: str, base_path: str = "") -> list[TSViolation]:
|
|
|
194
196
|
return violations
|
|
195
197
|
|
|
196
198
|
|
|
197
|
-
@pre(
|
|
199
|
+
@pre(
|
|
200
|
+
lambda output, base_path="": output is not None and isinstance(base_path, str)
|
|
201
|
+
) # Accepts any string including empty
|
|
198
202
|
@post(lambda result: all(v.source == "vitest" for v in result))
|
|
199
203
|
def parse_vitest_json(output: str, base_path: str = "") -> list[TSViolation]:
|
|
200
204
|
"""Parse Vitest JSON output into violations list.
|
invar/core/ts_sig_parser.py
CHANGED
|
@@ -188,7 +188,9 @@ def extract_ts_signatures(source: str) -> list[TSSymbol]:
|
|
|
188
188
|
# Find actual class line (skip decorators)
|
|
189
189
|
match_text = match.group(0)
|
|
190
190
|
class_keyword_pos = match_text.find("class ")
|
|
191
|
-
actual_start =
|
|
191
|
+
actual_start = (
|
|
192
|
+
match.start() + class_keyword_pos if class_keyword_pos >= 0 else match.start()
|
|
193
|
+
)
|
|
192
194
|
line = get_line_number(actual_start)
|
|
193
195
|
line_content = lines[line - 1].strip() if line <= len(lines) else ""
|
|
194
196
|
signature = line_content.rstrip("{").rstrip()
|
|
@@ -241,11 +243,13 @@ def extract_ts_signatures(source: str) -> list[TSSymbol]:
|
|
|
241
243
|
return symbols
|
|
242
244
|
|
|
243
245
|
|
|
244
|
-
@pre(
|
|
246
|
+
@pre(
|
|
247
|
+
lambda symbols, file_path="": isinstance(symbols, list)
|
|
248
|
+
and isinstance(file_path, str)
|
|
249
|
+
and all(s.line > 0 for s in symbols)
|
|
250
|
+
) # All symbols have valid line numbers
|
|
245
251
|
@post(lambda result: "file" in result and "symbols" in result)
|
|
246
|
-
def format_ts_signatures_json(
|
|
247
|
-
symbols: list[TSSymbol], file_path: str = ""
|
|
248
|
-
) -> dict:
|
|
252
|
+
def format_ts_signatures_json(symbols: list[TSSymbol], file_path: str = "") -> dict:
|
|
249
253
|
"""Format TypeScript symbols as JSON output.
|
|
250
254
|
|
|
251
255
|
Args:
|
|
@@ -277,11 +281,13 @@ def format_ts_signatures_json(
|
|
|
277
281
|
}
|
|
278
282
|
|
|
279
283
|
|
|
280
|
-
@pre(
|
|
284
|
+
@pre(
|
|
285
|
+
lambda symbols, file_path="": isinstance(symbols, list)
|
|
286
|
+
and isinstance(file_path, str)
|
|
287
|
+
and all(s.line > 0 for s in symbols)
|
|
288
|
+
) # All symbols have valid line numbers
|
|
281
289
|
@post(lambda result: len(result) > 0) # Always produces output (at least header)
|
|
282
|
-
def format_ts_signatures_text(
|
|
283
|
-
symbols: list[TSSymbol], file_path: str = ""
|
|
284
|
-
) -> str:
|
|
290
|
+
def format_ts_signatures_text(symbols: list[TSSymbol], file_path: str = "") -> str:
|
|
285
291
|
"""Format TypeScript symbols as human-readable text.
|
|
286
292
|
|
|
287
293
|
Args:
|
|
@@ -303,7 +309,9 @@ def format_ts_signatures_text(
|
|
|
303
309
|
lines.append(f" {symbol.signature}")
|
|
304
310
|
if symbol.docstring:
|
|
305
311
|
# Truncate long docstrings
|
|
306
|
-
doc =
|
|
312
|
+
doc = (
|
|
313
|
+
symbol.docstring[:100] + "..." if len(symbol.docstring) > 100 else symbol.docstring
|
|
314
|
+
)
|
|
307
315
|
lines.append(f" /** {doc} */")
|
|
308
316
|
lines.append("")
|
|
309
317
|
|
invar/core/utils.py
CHANGED
|
@@ -15,7 +15,11 @@ from deal import post, pre
|
|
|
15
15
|
from invar.core.models import GuardReport, RuleConfig, RuleExclusion
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
@pre(
|
|
18
|
+
@pre(
|
|
19
|
+
lambda report, strict: report.files_checked >= 0
|
|
20
|
+
and report.errors >= 0
|
|
21
|
+
and isinstance(strict, bool)
|
|
22
|
+
)
|
|
19
23
|
@post(lambda result: result in (0, 1))
|
|
20
24
|
def get_exit_code(report: GuardReport, strict: bool) -> int:
|
|
21
25
|
"""
|
|
@@ -43,6 +47,10 @@ def get_exit_code(report: GuardReport, strict: bool) -> int:
|
|
|
43
47
|
doctest_passed=True,
|
|
44
48
|
crosshair_passed=True,
|
|
45
49
|
property_passed=True: report.files_checked >= 0
|
|
50
|
+
and isinstance(strict, bool)
|
|
51
|
+
and isinstance(doctest_passed, bool)
|
|
52
|
+
and isinstance(crosshair_passed, bool)
|
|
53
|
+
and isinstance(property_passed, bool)
|
|
46
54
|
)
|
|
47
55
|
@post(lambda result: result in ("passed", "failed"))
|
|
48
56
|
def get_combined_status(
|
|
@@ -89,7 +97,10 @@ def get_combined_status(
|
|
|
89
97
|
return "passed"
|
|
90
98
|
|
|
91
99
|
|
|
92
|
-
@pre(
|
|
100
|
+
@pre(
|
|
101
|
+
lambda data, source: isinstance(data, dict)
|
|
102
|
+
and source in ("pyproject", "invar", "invar_dir", "default")
|
|
103
|
+
)
|
|
93
104
|
@post(lambda result: isinstance(result, dict))
|
|
94
105
|
def extract_guard_section(data: dict[str, Any], source: str) -> dict[str, Any]:
|
|
95
106
|
"""
|
|
@@ -121,7 +132,7 @@ def extract_guard_section(data: dict[str, Any], source: str) -> dict[str, Any]:
|
|
|
121
132
|
return result if isinstance(result, dict) else {}
|
|
122
133
|
|
|
123
134
|
|
|
124
|
-
@pre(lambda config, key: len(key) > 0)
|
|
135
|
+
@pre(lambda config, key: isinstance(config, dict) and len(key) > 0)
|
|
125
136
|
@post(lambda result: result is None or isinstance(result, bool))
|
|
126
137
|
def _get_bool(config: dict[str, Any], key: str) -> bool | None:
|
|
127
138
|
"""
|
|
@@ -138,7 +149,7 @@ def _get_bool(config: dict[str, Any], key: str) -> bool | None:
|
|
|
138
149
|
return None
|
|
139
150
|
|
|
140
151
|
|
|
141
|
-
@pre(lambda config, key: len(key) > 0)
|
|
152
|
+
@pre(lambda config, key: isinstance(config, dict) and len(key) > 0)
|
|
142
153
|
@post(lambda result: result is None or isinstance(result, int))
|
|
143
154
|
def _get_int(config: dict[str, Any], key: str) -> int | None:
|
|
144
155
|
"""
|
|
@@ -155,7 +166,7 @@ def _get_int(config: dict[str, Any], key: str) -> int | None:
|
|
|
155
166
|
return None
|
|
156
167
|
|
|
157
168
|
|
|
158
|
-
@pre(lambda config, key: len(key) > 0)
|
|
169
|
+
@pre(lambda config, key: isinstance(config, dict) and len(key) > 0)
|
|
159
170
|
@post(lambda result: result is None or isinstance(result, float))
|
|
160
171
|
def _get_float(config: dict[str, Any], key: str) -> float | None:
|
|
161
172
|
"""
|
|
@@ -174,7 +185,7 @@ def _get_float(config: dict[str, Any], key: str) -> float | None:
|
|
|
174
185
|
return None
|
|
175
186
|
|
|
176
187
|
|
|
177
|
-
@pre(lambda config, key: len(key) > 0)
|
|
188
|
+
@pre(lambda config, key: isinstance(config, dict) and len(key) > 0)
|
|
178
189
|
@post(lambda result: result is None or isinstance(result, list))
|
|
179
190
|
def _get_str_list(config: dict[str, Any], key: str) -> list[str] | None:
|
|
180
191
|
"""
|
|
@@ -301,7 +312,7 @@ def parse_guard_config(guard_config: dict[str, Any]) -> RuleConfig:
|
|
|
301
312
|
return RuleConfig()
|
|
302
313
|
|
|
303
314
|
|
|
304
|
-
@pre(lambda file_path, patterns: len(file_path) > 0)
|
|
315
|
+
@pre(lambda file_path, patterns: len(file_path) > 0 and isinstance(patterns, list))
|
|
305
316
|
def matches_pattern(file_path: str, patterns: list[str]) -> bool:
|
|
306
317
|
"""
|
|
307
318
|
Check if a file path matches any of the glob patterns.
|
|
@@ -331,7 +342,7 @@ def matches_pattern(file_path: str, patterns: list[str]) -> bool:
|
|
|
331
342
|
return False
|
|
332
343
|
|
|
333
344
|
|
|
334
|
-
@pre(lambda file_path, prefixes: len(file_path) > 0)
|
|
345
|
+
@pre(lambda file_path, prefixes: len(file_path) > 0 and isinstance(prefixes, list))
|
|
335
346
|
def matches_path_prefix(file_path: str, prefixes: list[str]) -> bool:
|
|
336
347
|
"""
|
|
337
348
|
Check if file_path starts with any of the given prefixes.
|
|
@@ -401,7 +412,7 @@ def match_glob_pattern(file_path: str, pattern: str) -> bool:
|
|
|
401
412
|
return False
|
|
402
413
|
|
|
403
414
|
|
|
404
|
-
@pre(lambda file_path, config: len(file_path) > 0)
|
|
415
|
+
@pre(lambda file_path, config: len(file_path) > 0 and isinstance(config, RuleConfig))
|
|
405
416
|
def get_excluded_rules(file_path: str, config: RuleConfig) -> set[str]:
|
|
406
417
|
"""
|
|
407
418
|
Get the set of rules to exclude for a given file path.
|
|
@@ -23,5 +23,8 @@ core_paths = ["src/myapp/core"] # Default: ["src/core", "core"]
|
|
|
23
23
|
shell_paths = ["src/myapp/shell"] # Default: ["src/shell", "shell"]
|
|
24
24
|
max_file_lines = 500 # Default: 500 (warning at 80%)
|
|
25
25
|
max_function_lines = 50 # Default: 50
|
|
26
|
+
timeout_doctest = 60 # Default: 60s
|
|
27
|
+
timeout_crosshair = 300 # Default: 300s
|
|
28
|
+
timeout_hypothesis = 300 # Default: 300s
|
|
26
29
|
# Doctest lines are excluded from size calculations
|
|
27
30
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: invar-tools
|
|
3
|
-
Version: 1.17.
|
|
3
|
+
Version: 1.17.27
|
|
4
4
|
Summary: AI-native software engineering tools with design-by-contract verification
|
|
5
5
|
Project-URL: Homepage, https://github.com/tefx/invar
|
|
6
6
|
Project-URL: Documentation, https://github.com/tefx/invar#readme
|
|
@@ -1,52 +1,52 @@
|
|
|
1
1
|
invar/__init__.py,sha256=E-Mg9DG6qFzjP8D5TEkmy2ponvR99Yfn96RVJggAr64,1682
|
|
2
2
|
invar/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
invar/core/__init__.py,sha256=01TgQ2bqTFV4VFdksfqXYPa2WUqo-DpUWUkEcIUXFb4,218
|
|
4
|
-
invar/core/contracts.py,sha256=
|
|
5
|
-
invar/core/doc_edit.py,sha256=
|
|
6
|
-
invar/core/doc_parser.py,sha256=
|
|
7
|
-
invar/core/entry_points.py,sha256=
|
|
8
|
-
invar/core/extraction.py,sha256=
|
|
4
|
+
invar/core/contracts.py,sha256=xLCI3rMLmI2W1sTjWeVPx98v6eLSHfJPi2FBfjO_HhA,20586
|
|
5
|
+
invar/core/doc_edit.py,sha256=ZIfdMeKfqLDwA0rOn19WdgmuKgLXyZWnr12fJU5ABvs,6638
|
|
6
|
+
invar/core/doc_parser.py,sha256=LJ3ljIgJP-RpiGLRAGWyNJXB14Y4IOMwFAYrGVAx5eg,18294
|
|
7
|
+
invar/core/entry_points.py,sha256=WXfOFDhpOUl52rrL0EwBIUMr__9TPD7JfyUEMV4oDfk,12413
|
|
8
|
+
invar/core/extraction.py,sha256=hpzW7qsWkVlLNXOmPp1TB_fuP65D1q1JrkmcvQ5TbFo,6387
|
|
9
9
|
invar/core/feedback.py,sha256=JhQf32y_Qutza8D2b5qX2U4fM--vtR3sBdV22KrVNY0,3246
|
|
10
|
-
invar/core/format_specs.py,sha256=
|
|
11
|
-
invar/core/format_strategies.py,sha256=
|
|
12
|
-
invar/core/formatter.py,sha256=
|
|
13
|
-
invar/core/hypothesis_strategies.py,sha256=
|
|
10
|
+
invar/core/format_specs.py,sha256=cd9dyiux4nCYQzT6r3u3atwR8oEuaXizxyhR42Y-xZ8,5638
|
|
11
|
+
invar/core/format_strategies.py,sha256=qfybRDhFiyxFWruEAypph5J_nZLpRLO6sWAZLJIaq9o,5887
|
|
12
|
+
invar/core/formatter.py,sha256=m0jAbSjY5vTg_l7OW4qECOABtBFXLra9LD0CsGQMpLs,11498
|
|
13
|
+
invar/core/hypothesis_strategies.py,sha256=zP_QTpXuFXnznNTzMQdkFhqNExSu8TVNDimfMh-novg,16856
|
|
14
14
|
invar/core/inspect.py,sha256=l1knohwpLRHSNySPUjyeBHJusnU0vYiQGj4dMVgQZIo,4381
|
|
15
|
-
invar/core/lambda_helpers.py,sha256=
|
|
15
|
+
invar/core/lambda_helpers.py,sha256=cemVKI-9iUZrHYfNMiqDGv8vyUwnUQJu55WYSc_uDoU,6457
|
|
16
16
|
invar/core/language.py,sha256=aGUcrq--eQtAjb5bYE40eFmDhRs_EbBNGQ1sBYgTdt0,2637
|
|
17
17
|
invar/core/models.py,sha256=WLHCd9M8anw5mALTMboTjxt5PcXO7eJajiYA8b6FGHk,16881
|
|
18
18
|
invar/core/must_use.py,sha256=7HnnbT53lb4dOT-1mL64pz0JbQYytuw4eejNVe7iWKY,5496
|
|
19
|
-
invar/core/parser.py,sha256=
|
|
19
|
+
invar/core/parser.py,sha256=GJdgAn9Ys8xKmLtbEp6n-vgkxAm3WS5ihuLJQ60YUPY,9379
|
|
20
20
|
invar/core/postcondition_scope.py,sha256=ykjVNqZZ1zItBmI7ebgmLW5vFGE-vpaLRTvSgWaJMgM,5245
|
|
21
|
-
invar/core/property_gen.py,sha256=
|
|
21
|
+
invar/core/property_gen.py,sha256=XOGejiVqiEpGrghsagicL2WaFPnBYsy1yxWTcPPDi8g,14557
|
|
22
22
|
invar/core/purity.py,sha256=dt5dFy5V8Ch93iBJF5OuKUr1jjfimfY3oHLQD8KmLHw,12036
|
|
23
23
|
invar/core/purity_heuristics.py,sha256=vsgphC1XPIFtsoLB0xvp--AyaJHqlh83LyKXYda4pWc,4546
|
|
24
24
|
invar/core/references.py,sha256=64yGIdj9vL72Y4uUhJsi9pztZkuMnLN-7OcOziyxYMo,6339
|
|
25
25
|
invar/core/review_trigger.py,sha256=4GGHUmgbVsQJAob4OO6A8G7KrLcNMwNOuqHiT6Jc7cs,14085
|
|
26
26
|
invar/core/rule_meta.py,sha256=il_KUTjSlW1MOVgLguuLDS9wEdyqUe3CDvUx4gQjACo,10180
|
|
27
27
|
invar/core/rules.py,sha256=y_NIpO8GAcw8WNPZjbJniu9FU7ofgfn3kZ_keexTqA8,23481
|
|
28
|
-
invar/core/shell_analysis.py,sha256=
|
|
28
|
+
invar/core/shell_analysis.py,sha256=o1ogUvxzBlaupha88n17BUJwaV5vdcwD8UxpeSTyaKQ,6806
|
|
29
29
|
invar/core/shell_architecture.py,sha256=98EVdBFIs8tO-i9jKuzdmv7fLB4PKnyI-vKh5lxnB98,6538
|
|
30
|
-
invar/core/strategies.py,sha256=
|
|
31
|
-
invar/core/suggestions.py,sha256=
|
|
30
|
+
invar/core/strategies.py,sha256=XXCoiK2FVmlIvDoED6aEEvbqKZyvKy78f4r5kELuPyQ,9041
|
|
31
|
+
invar/core/suggestions.py,sha256=ghN0YrXO_F21qyP8bHecQ9xKEIWZPXZ21VjBzLQnCig,15223
|
|
32
32
|
invar/core/sync_helpers.py,sha256=rVfRFECO16Ntc9b9A7LxIV_0XfRJbRXaagVT-jJBYqI,8635
|
|
33
|
-
invar/core/tautology.py,sha256=
|
|
33
|
+
invar/core/tautology.py,sha256=IUC7H3OxWYlxofcl3I4ANgqaWQz5PL1WUzmJ9nzybMs,9463
|
|
34
34
|
invar/core/template_helpers.py,sha256=E1UT7ct0DaUFlfHr9oTBvW4xfxAiS81rbmZHSucPw4c,881
|
|
35
|
-
invar/core/template_parser.py,sha256=
|
|
35
|
+
invar/core/template_parser.py,sha256=S-7M67JuUthhP_zoP8oAdPg6L-fFvO3pD8cS7KUVbes,14879
|
|
36
36
|
invar/core/timeout_inference.py,sha256=BS2fJGmwOrLpYZUku4qrizgNDSIXVLFBslW-6sRAvpc,3451
|
|
37
37
|
invar/core/trivial_detection.py,sha256=KYP8jJb7QDeusAxFdX5NAML_H0NL5wLgMeBWDQmNqfU,6086
|
|
38
|
-
invar/core/ts_parsers.py,sha256=
|
|
39
|
-
invar/core/ts_sig_parser.py,sha256=
|
|
40
|
-
invar/core/utils.py,sha256=
|
|
38
|
+
invar/core/ts_parsers.py,sha256=DTGig4qLNGBnRIM7FXAi2bYumjLf3iFK-VkfP9F7204,8817
|
|
39
|
+
invar/core/ts_sig_parser.py,sha256=ZB8PaxrN4ADVYd7ArPbpEJlnRIy79aPjyxjkJIKwmI8,10125
|
|
40
|
+
invar/core/utils.py,sha256=HMGB2w5B-4XhBHoOLaQkQ882a4vZFJ9deCy1qpgLrRc,14930
|
|
41
41
|
invar/core/verification_routing.py,sha256=_jXi1txFCcUdnB3-Yavtuyk8N-XhEO_Vu_051Vuz27Y,5020
|
|
42
42
|
invar/core/patterns/__init__.py,sha256=79a3ucN0BI54RnIOe49lngKASpADygs1hll9ROCrP6s,1429
|
|
43
|
-
invar/core/patterns/detector.py,sha256=
|
|
44
|
-
invar/core/patterns/p0_exhaustive.py,sha256=
|
|
45
|
-
invar/core/patterns/p0_literal.py,sha256=
|
|
46
|
-
invar/core/patterns/p0_newtype.py,sha256=
|
|
47
|
-
invar/core/patterns/p0_nonempty.py,sha256=
|
|
48
|
-
invar/core/patterns/p0_validation.py,sha256=
|
|
49
|
-
invar/core/patterns/registry.py,sha256=
|
|
43
|
+
invar/core/patterns/detector.py,sha256=mwUDJ0Ha930BnRYF-ydbNCIF6ndQFwkHn055X1xy-OE,9107
|
|
44
|
+
invar/core/patterns/p0_exhaustive.py,sha256=ykbObko9bC3BQaaCNVhUk1nt29CvqOcAjXQa_ZAkFho,7229
|
|
45
|
+
invar/core/patterns/p0_literal.py,sha256=jp-3uVKwTGn5XgX_kz_wYRrtHi27oIrLvwT_MpXjY7A,10951
|
|
46
|
+
invar/core/patterns/p0_newtype.py,sha256=iKlErz697lXrTCFkMKU8yn4FiTAIkP8fCTTs6kJ_6BA,7846
|
|
47
|
+
invar/core/patterns/p0_nonempty.py,sha256=Ipe5eIKBBXN6w-a3h0bawigw5lEMrxUF_9CYPV9YDuU,11151
|
|
48
|
+
invar/core/patterns/p0_validation.py,sha256=Ri_L9StWCpL2C_akn0QQudsRhrU51gEUtR3PHBhZBjA,9833
|
|
49
|
+
invar/core/patterns/registry.py,sha256=4SPMgP9uutNTvvys8pUru41ibLNhR1mGHtQHxwKNhFw,7794
|
|
50
50
|
invar/core/patterns/types.py,sha256=ULAlWuAdmO6CFcEDjTrWBfzNTBsnomAl2d25tR11ihU,5506
|
|
51
51
|
invar/mcp/__init__.py,sha256=n3S7QwMjSMqOMT8cI2jf9E0yZPjKmBOJyIYhq4WZ8TQ,226
|
|
52
52
|
invar/mcp/__main__.py,sha256=ZcIT2U6xUyGOWucl4jq422BDE3lRLjqyxb9pFylRBdk,219
|
|
@@ -2745,7 +2745,7 @@ invar/templates/protocol/INVAR.md.jinja,sha256=t2ZIQZJvzDTJMrRw_ijUo6ScZmeNK0-nV
|
|
|
2745
2745
|
invar/templates/protocol/python/architecture-examples.md,sha256=O96LH9WFpk7G9MrhSbifLS5pyibTIDG-_EGFF7g3V4M,1175
|
|
2746
2746
|
invar/templates/protocol/python/contracts-syntax.md,sha256=Q6supTQ3tChVrlN7xhcdb3Q8VGIESxQLA-mQvrNIZmo,1162
|
|
2747
2747
|
invar/templates/protocol/python/markers.md,sha256=fzltCKbdPVz_vCuJFiQ9pbRPztvpMJpSf_4aFHcXFLM,1223
|
|
2748
|
-
invar/templates/protocol/python/tools.md,sha256=
|
|
2748
|
+
invar/templates/protocol/python/tools.md,sha256=qdKRRJok_ZU1z0B8SV9huhRED8qFRGaICmuWNZkwrCM,1201
|
|
2749
2749
|
invar/templates/protocol/python/troubleshooting.md,sha256=-JHLUOxvfQeSrLpqKrxUXQ5UrkW44AHFr3LGHwxnw7w,1081
|
|
2750
2750
|
invar/templates/protocol/typescript/architecture-examples.md,sha256=Dej-DI6OqRVsbzjukjOqdO8WEz0aT-1iwYqrah2B_xk,1454
|
|
2751
2751
|
invar/templates/protocol/typescript/contracts-syntax.md,sha256=yKyM6WhyF5p-bt-RqD5SI4ZZudE7bLLFTAMzVSa74QE,1610
|
|
@@ -2778,10 +2778,10 @@ invar/templates/skills/invar-reflect/template.md,sha256=Rr5hvbllvmd8jSLf_0ZjyKt6
|
|
|
2778
2778
|
invar/templates/skills/investigate/SKILL.md.jinja,sha256=cp6TBEixBYh1rLeeHOR1yqEnFqv1NZYePORMnavLkQI,3231
|
|
2779
2779
|
invar/templates/skills/propose/SKILL.md.jinja,sha256=6BuKiCqO1AEu3VtzMHy1QWGqr_xqG9eJlhbsKT4jev4,3463
|
|
2780
2780
|
invar/templates/skills/review/SKILL.md.jinja,sha256=ET5mbdSe_eKgJbi2LbgFC-z1aviKcHOBw7J5Q28fr4U,14105
|
|
2781
|
-
invar_tools-1.17.
|
|
2782
|
-
invar_tools-1.17.
|
|
2783
|
-
invar_tools-1.17.
|
|
2784
|
-
invar_tools-1.17.
|
|
2785
|
-
invar_tools-1.17.
|
|
2786
|
-
invar_tools-1.17.
|
|
2787
|
-
invar_tools-1.17.
|
|
2781
|
+
invar_tools-1.17.27.dist-info/METADATA,sha256=lA_4PuEF0Dloh5LYtlMf_0vNvyicErt7xJYEeoioQZA,28582
|
|
2782
|
+
invar_tools-1.17.27.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
2783
|
+
invar_tools-1.17.27.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
|
|
2784
|
+
invar_tools-1.17.27.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
|
|
2785
|
+
invar_tools-1.17.27.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
|
|
2786
|
+
invar_tools-1.17.27.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
|
|
2787
|
+
invar_tools-1.17.27.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|