invar-tools 1.17.21__py3-none-any.whl → 1.17.24__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/models.py +7 -3
- invar/core/rules.py +50 -15
- invar/shell/config.py +26 -16
- invar/shell/guard_helpers.py +36 -2
- invar/shell/property_tests.py +45 -4
- invar/shell/prove/crosshair.py +128 -1
- {invar_tools-1.17.21.dist-info → invar_tools-1.17.24.dist-info}/METADATA +1 -1
- {invar_tools-1.17.21.dist-info → invar_tools-1.17.24.dist-info}/RECORD +13 -13
- {invar_tools-1.17.21.dist-info → invar_tools-1.17.24.dist-info}/WHEEL +0 -0
- {invar_tools-1.17.21.dist-info → invar_tools-1.17.24.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.17.21.dist-info → invar_tools-1.17.24.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.17.21.dist-info → invar_tools-1.17.24.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.17.21.dist-info → invar_tools-1.17.24.dist-info}/licenses/NOTICE +0 -0
invar/core/models.py
CHANGED
|
@@ -160,7 +160,10 @@ def get_layer(file_info: FileInfo) -> CodeLayer:
|
|
|
160
160
|
return CodeLayer.DEFAULT
|
|
161
161
|
|
|
162
162
|
|
|
163
|
-
@pre(
|
|
163
|
+
@pre(
|
|
164
|
+
lambda layer, language="python": isinstance(layer, CodeLayer)
|
|
165
|
+
and language in ("python", "typescript")
|
|
166
|
+
)
|
|
164
167
|
@post(lambda result: result.max_file_lines > 0 and result.max_function_lines > 0)
|
|
165
168
|
def get_limits(layer: CodeLayer, language: str = "python") -> LayerLimits:
|
|
166
169
|
"""
|
|
@@ -424,8 +427,9 @@ class RuleConfig(BaseModel):
|
|
|
424
427
|
"""
|
|
425
428
|
|
|
426
429
|
# MINOR-6: Added ge=1 constraints for numeric fields
|
|
427
|
-
|
|
428
|
-
|
|
430
|
+
# BUG-55: These override layer-based limits when set to non-default values
|
|
431
|
+
max_file_lines: int = Field(default=500, ge=1) # Override all layers if != 500
|
|
432
|
+
max_function_lines: int = Field(default=50, ge=1) # Override all layers if != 50
|
|
429
433
|
entry_max_lines: int = Field(default=15, ge=1) # DX-23: Entry point max lines
|
|
430
434
|
shell_max_branches: int = Field(default=3, ge=1) # DX-22: Shell function max branches
|
|
431
435
|
shell_complexity_debt_limit: int = Field(default=5, ge=0) # DX-22: 0 = no limit
|
invar/core/rules.py
CHANGED
|
@@ -71,7 +71,11 @@ def _build_size_suggestion(base: str, extraction_hint: str, func_hint: str) -> s
|
|
|
71
71
|
def _get_func_hint(file_info: FileInfo) -> str:
|
|
72
72
|
"""Get top 5 largest functions as hint string."""
|
|
73
73
|
funcs = sorted(
|
|
74
|
-
[
|
|
74
|
+
[
|
|
75
|
+
(s.name, s.end_line - s.line + 1)
|
|
76
|
+
for s in file_info.symbols
|
|
77
|
+
if s.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD)
|
|
78
|
+
],
|
|
75
79
|
key=lambda x: -x[1],
|
|
76
80
|
)[:5]
|
|
77
81
|
return f" Functions: {', '.join(f'{n}({sz}L)' for n, sz in funcs)}" if funcs else ""
|
|
@@ -104,6 +108,7 @@ def check_file_size(file_info: FileInfo, config: RuleConfig) -> list[Violation]:
|
|
|
104
108
|
Check if file exceeds maximum line count or warning threshold.
|
|
105
109
|
|
|
106
110
|
LX-10: Uses layer-based limits (Core/Shell/Tests/Default).
|
|
111
|
+
BUG-55: Config override - if max_file_lines is set to non-default, use it.
|
|
107
112
|
P18: Shows function groups in size warnings to help agents decide what to extract.
|
|
108
113
|
P25: Shows extractable groups with dependencies for warnings.
|
|
109
114
|
|
|
@@ -121,6 +126,10 @@ def check_file_size(file_info: FileInfo, config: RuleConfig) -> list[Violation]:
|
|
|
121
126
|
>>> # Core layer: 500 lines max (strict)
|
|
122
127
|
>>> len(check_file_size(FileInfo(path="core/calc.py", lines=550, is_core=True), RuleConfig()))
|
|
123
128
|
1
|
|
129
|
+
>>> # BUG-55: Config override allows larger files (no error at 550 with max 600)
|
|
130
|
+
>>> vs = check_file_size(FileInfo(path="core/calc.py", lines=550, is_core=True), RuleConfig(max_file_lines=600, size_warning_threshold=0))
|
|
131
|
+
>>> any(v.rule == "file_size" for v in vs)
|
|
132
|
+
False
|
|
124
133
|
"""
|
|
125
134
|
# Check for escape hatch
|
|
126
135
|
if _has_file_escape(file_info, "file_size"):
|
|
@@ -133,23 +142,38 @@ def check_file_size(file_info: FileInfo, config: RuleConfig) -> list[Violation]:
|
|
|
133
142
|
# LX-10: Get layer-based limits
|
|
134
143
|
layer = get_layer(file_info)
|
|
135
144
|
limits = get_limits(layer)
|
|
136
|
-
|
|
145
|
+
# BUG-55: Allow config override if user sets non-default value
|
|
146
|
+
max_lines = config.max_file_lines if config.max_file_lines != 500 else limits.max_file_lines
|
|
137
147
|
|
|
138
148
|
if file_info.lines > max_lines:
|
|
139
|
-
violations.append(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
violations.append(
|
|
150
|
+
Violation(
|
|
151
|
+
rule="file_size",
|
|
152
|
+
severity=Severity.ERROR,
|
|
153
|
+
file=file_info.path,
|
|
154
|
+
line=None,
|
|
155
|
+
message=f"File has {file_info.lines} lines (max: {max_lines} for {layer.value})",
|
|
156
|
+
suggestion=_build_size_suggestion(
|
|
157
|
+
"Split into smaller modules.", extraction_hint, func_hint
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
144
161
|
elif config.size_warning_threshold > 0:
|
|
145
162
|
threshold = int(max_lines * config.size_warning_threshold)
|
|
146
163
|
if file_info.lines >= threshold:
|
|
147
164
|
pct = int(file_info.lines / max_lines * 100)
|
|
148
|
-
violations.append(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
165
|
+
violations.append(
|
|
166
|
+
Violation(
|
|
167
|
+
rule="file_size_warning",
|
|
168
|
+
severity=Severity.WARNING,
|
|
169
|
+
file=file_info.path,
|
|
170
|
+
line=None,
|
|
171
|
+
message=f"File has {file_info.lines} lines ({pct}% of {max_lines} limit)",
|
|
172
|
+
suggestion=_build_size_suggestion(
|
|
173
|
+
"Consider splitting before reaching limit.", extraction_hint, func_hint
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
)
|
|
153
177
|
return violations
|
|
154
178
|
|
|
155
179
|
|
|
@@ -159,6 +183,7 @@ def check_function_size(file_info: FileInfo, config: RuleConfig) -> list[Violati
|
|
|
159
183
|
Check if any function exceeds maximum line count.
|
|
160
184
|
|
|
161
185
|
LX-10: Uses layer-based limits (Core/Shell/Tests/Default).
|
|
186
|
+
BUG-55: Config override - if max_function_lines is set to non-default, use it.
|
|
162
187
|
DX-22: Always uses code_lines (excluding docstring) and excludes doctest lines.
|
|
163
188
|
|
|
164
189
|
Examples:
|
|
@@ -177,13 +202,19 @@ def check_function_size(file_info: FileInfo, config: RuleConfig) -> list[Violati
|
|
|
177
202
|
>>> info3 = FileInfo(path="core/calc.py", lines=100, symbols=[sym3], is_core=True)
|
|
178
203
|
>>> len(check_function_size(info3, RuleConfig()))
|
|
179
204
|
1
|
|
205
|
+
>>> # BUG-55: Config override allows larger functions
|
|
206
|
+
>>> len(check_function_size(info3, RuleConfig(max_function_lines=70)))
|
|
207
|
+
0
|
|
180
208
|
"""
|
|
181
209
|
violations: list[Violation] = []
|
|
182
210
|
|
|
183
211
|
# LX-10: Get layer-based limits
|
|
184
212
|
layer = get_layer(file_info)
|
|
185
213
|
limits = get_limits(layer)
|
|
186
|
-
|
|
214
|
+
# BUG-55: Allow config override if user sets non-default value
|
|
215
|
+
max_func_lines = (
|
|
216
|
+
config.max_function_lines if config.max_function_lines != 50 else limits.max_function_lines
|
|
217
|
+
)
|
|
187
218
|
|
|
188
219
|
for symbol in file_info.symbols:
|
|
189
220
|
if symbol.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD):
|
|
@@ -392,7 +423,9 @@ def check_shell_result(file_info: FileInfo, config: RuleConfig) -> list[Violatio
|
|
|
392
423
|
):
|
|
393
424
|
continue
|
|
394
425
|
# DX-23: Skip entry points; DX-22: Skip if @invar:allow marker
|
|
395
|
-
if is_entry_point(symbol, file_info.source) or has_allow_marker(
|
|
426
|
+
if is_entry_point(symbol, file_info.source) or has_allow_marker(
|
|
427
|
+
symbol, file_info.source, "shell_result"
|
|
428
|
+
):
|
|
396
429
|
continue
|
|
397
430
|
if "Result[" not in symbol.signature:
|
|
398
431
|
violations.append(
|
|
@@ -435,7 +468,9 @@ def check_entry_point_thin(file_info: FileInfo, config: RuleConfig) -> list[Viol
|
|
|
435
468
|
continue
|
|
436
469
|
|
|
437
470
|
# Only check entry points; DX-22: Skip if @invar:allow marker
|
|
438
|
-
if not is_entry_point(symbol, file_info.source) or has_allow_marker(
|
|
471
|
+
if not is_entry_point(symbol, file_info.source) or has_allow_marker(
|
|
472
|
+
symbol, file_info.source, "entry_point_too_thick"
|
|
473
|
+
):
|
|
439
474
|
continue
|
|
440
475
|
lines = get_symbol_lines(symbol)
|
|
441
476
|
if lines > max_lines:
|
invar/shell/config.py
CHANGED
|
@@ -366,34 +366,44 @@ def load_config(project_root: Path) -> Result[RuleConfig, str]:
|
|
|
366
366
|
3. .invar/config.toml [guard]
|
|
367
367
|
4. Built-in defaults
|
|
368
368
|
|
|
369
|
+
If pyproject.toml exists but has no [tool.invar.guard] section,
|
|
370
|
+
continues to check other sources (fallback behavior).
|
|
371
|
+
|
|
369
372
|
Args:
|
|
370
373
|
project_root: Path to project root directory
|
|
371
374
|
|
|
372
375
|
Returns:
|
|
373
376
|
Result containing RuleConfig or error message
|
|
374
377
|
"""
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return find_result
|
|
378
|
-
config_path, source = find_result.unwrap()
|
|
378
|
+
# Try each config source in priority order
|
|
379
|
+
sources_to_try: list[tuple[Path, ConfigSource]] = []
|
|
379
380
|
|
|
380
|
-
|
|
381
|
-
|
|
381
|
+
pyproject = project_root / "pyproject.toml"
|
|
382
|
+
if pyproject.exists():
|
|
383
|
+
sources_to_try.append((pyproject, "pyproject"))
|
|
382
384
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
+
invar_toml = project_root / "invar.toml"
|
|
386
|
+
if invar_toml.exists():
|
|
387
|
+
sources_to_try.append((invar_toml, "invar"))
|
|
385
388
|
|
|
386
|
-
|
|
387
|
-
|
|
389
|
+
invar_config = project_root / ".invar" / "config.toml"
|
|
390
|
+
if invar_config.exists():
|
|
391
|
+
sources_to_try.append((invar_config, "invar_dir"))
|
|
388
392
|
|
|
389
|
-
|
|
390
|
-
|
|
393
|
+
# Try each source, fallback if no guard config found
|
|
394
|
+
for config_path, source in sources_to_try:
|
|
395
|
+
result = _read_toml(config_path)
|
|
396
|
+
if isinstance(result, Failure):
|
|
397
|
+
continue # Skip unreadable files
|
|
398
|
+
|
|
399
|
+
data = result.unwrap()
|
|
400
|
+
guard_config = extract_guard_section(data, source)
|
|
391
401
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return Success(RuleConfig())
|
|
402
|
+
if guard_config: # Found valid guard config
|
|
403
|
+
return Success(parse_guard_config(guard_config))
|
|
395
404
|
|
|
396
|
-
|
|
405
|
+
# No config found in any source, use defaults
|
|
406
|
+
return Success(RuleConfig())
|
|
397
407
|
|
|
398
408
|
|
|
399
409
|
# Default paths for Core/Shell classification
|
invar/shell/guard_helpers.py
CHANGED
|
@@ -178,7 +178,24 @@ def run_crosshair_phase(
|
|
|
178
178
|
return True, {"status": "skipped", "reason": "no files to verify"}
|
|
179
179
|
|
|
180
180
|
# Only verify Core files (pure logic)
|
|
181
|
-
|
|
181
|
+
# BUG-57: Use config-based core detection instead of hardcoded "core" in path
|
|
182
|
+
from invar.core.utils import matches_path_prefix
|
|
183
|
+
from invar.shell.config import get_path_classification
|
|
184
|
+
|
|
185
|
+
path_result = get_path_classification(path)
|
|
186
|
+
if isinstance(path_result, Success):
|
|
187
|
+
core_paths, _ = path_result.unwrap()
|
|
188
|
+
else:
|
|
189
|
+
core_paths = ["src/core", "core"]
|
|
190
|
+
|
|
191
|
+
def is_core_file(f: Path) -> bool:
|
|
192
|
+
try:
|
|
193
|
+
rel = str(f.relative_to(path))
|
|
194
|
+
except ValueError:
|
|
195
|
+
rel = str(f)
|
|
196
|
+
return matches_path_prefix(rel, core_paths)
|
|
197
|
+
|
|
198
|
+
core_files = [f for f in checked_files if is_core_file(f)]
|
|
182
199
|
if not core_files:
|
|
183
200
|
return True, {"status": "skipped", "reason": "no core files found"}
|
|
184
201
|
|
|
@@ -306,7 +323,24 @@ def run_property_tests_phase(
|
|
|
306
323
|
return True, {"status": "skipped", "reason": "no files"}, None
|
|
307
324
|
|
|
308
325
|
# Only test Core files (with contracts)
|
|
309
|
-
|
|
326
|
+
# BUG-57: Use config-based core detection instead of hardcoded "core" in path
|
|
327
|
+
from invar.core.utils import matches_path_prefix
|
|
328
|
+
from invar.shell.config import get_path_classification
|
|
329
|
+
|
|
330
|
+
path_result = get_path_classification(project_root)
|
|
331
|
+
if isinstance(path_result, Success):
|
|
332
|
+
core_paths, _ = path_result.unwrap()
|
|
333
|
+
else:
|
|
334
|
+
core_paths = ["src/core", "core"]
|
|
335
|
+
|
|
336
|
+
def is_core_file(f: Path) -> bool:
|
|
337
|
+
try:
|
|
338
|
+
rel = str(f.relative_to(project_root))
|
|
339
|
+
except ValueError:
|
|
340
|
+
rel = str(f)
|
|
341
|
+
return matches_path_prefix(rel, core_paths)
|
|
342
|
+
|
|
343
|
+
core_files = [f for f in checked_files if is_core_file(f)]
|
|
310
344
|
if not core_files:
|
|
311
345
|
return True, {"status": "skipped", "reason": "no core files"}, None
|
|
312
346
|
|
invar/shell/property_tests.py
CHANGED
|
@@ -97,7 +97,7 @@ def run_property_tests_on_file(
|
|
|
97
97
|
|
|
98
98
|
root = project_root or file_path.parent
|
|
99
99
|
with _inject_project_site_packages(root):
|
|
100
|
-
module = _import_module_from_path(file_path)
|
|
100
|
+
module = _import_module_from_path(file_path, project_root=root)
|
|
101
101
|
|
|
102
102
|
if module is None:
|
|
103
103
|
return Failure(f"Could not import module: {file_path}")
|
|
@@ -218,15 +218,56 @@ def _accumulate_report(
|
|
|
218
218
|
combined_report.errors.extend(file_report.errors)
|
|
219
219
|
|
|
220
220
|
|
|
221
|
-
|
|
221
|
+
# @shell_complexity: BUG-57 fix requires package hierarchy setup for relative imports
|
|
222
|
+
def _import_module_from_path(file_path: Path, project_root: Path | None = None) -> object | None:
|
|
222
223
|
"""
|
|
223
224
|
Import a Python module from a file path.
|
|
224
225
|
|
|
226
|
+
BUG-57: Properly handles relative imports by setting up package context.
|
|
227
|
+
|
|
225
228
|
Returns None if import fails.
|
|
226
229
|
"""
|
|
227
230
|
try:
|
|
228
|
-
|
|
229
|
-
|
|
231
|
+
# Calculate the full module name from project root
|
|
232
|
+
if project_root and file_path.is_relative_to(project_root):
|
|
233
|
+
# Convert path to module name: my_package/main.py -> my_package.main
|
|
234
|
+
relative = file_path.relative_to(project_root)
|
|
235
|
+
parts = list(relative.with_suffix("").parts)
|
|
236
|
+
module_name = ".".join(parts)
|
|
237
|
+
else:
|
|
238
|
+
module_name = file_path.stem
|
|
239
|
+
|
|
240
|
+
# Ensure project root is in sys.path for relative imports
|
|
241
|
+
if project_root:
|
|
242
|
+
root_str = str(project_root)
|
|
243
|
+
if root_str not in sys.path:
|
|
244
|
+
sys.path.insert(0, root_str)
|
|
245
|
+
|
|
246
|
+
# For packages with relative imports, we need to set up parent packages first
|
|
247
|
+
if "." in module_name:
|
|
248
|
+
# Import parent packages first
|
|
249
|
+
parts = module_name.split(".")
|
|
250
|
+
for i in range(1, len(parts)):
|
|
251
|
+
parent_name = ".".join(parts[:i])
|
|
252
|
+
if parent_name not in sys.modules:
|
|
253
|
+
parent_path = project_root / "/".join(parts[:i]) if project_root else None
|
|
254
|
+
if parent_path and (parent_path / "__init__.py").exists():
|
|
255
|
+
parent_spec = importlib.util.spec_from_file_location(
|
|
256
|
+
parent_name,
|
|
257
|
+
parent_path / "__init__.py",
|
|
258
|
+
submodule_search_locations=[str(parent_path)],
|
|
259
|
+
)
|
|
260
|
+
if parent_spec and parent_spec.loader:
|
|
261
|
+
parent_module = importlib.util.module_from_spec(parent_spec)
|
|
262
|
+
sys.modules[parent_name] = parent_module
|
|
263
|
+
parent_spec.loader.exec_module(parent_module)
|
|
264
|
+
|
|
265
|
+
# Now import the target module
|
|
266
|
+
spec = importlib.util.spec_from_file_location(
|
|
267
|
+
module_name,
|
|
268
|
+
file_path,
|
|
269
|
+
submodule_search_locations=[str(file_path.parent)],
|
|
270
|
+
)
|
|
230
271
|
if spec is None or spec.loader is None:
|
|
231
272
|
return None
|
|
232
273
|
|
invar/shell/prove/crosshair.py
CHANGED
|
@@ -88,6 +88,88 @@ def has_verifiable_contracts(source: str) -> bool:
|
|
|
88
88
|
return False
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
# BUG-56: Detect Literal types that CrossHair cannot handle
|
|
92
|
+
# @shell_orchestration: Pre-filter for CrossHair verification
|
|
93
|
+
# @shell_complexity: AST traversal for type annotation analysis
|
|
94
|
+
def has_literal_in_contracted_functions(source: str) -> bool:
|
|
95
|
+
"""Check if any contracted function uses Literal types in parameters.
|
|
96
|
+
|
|
97
|
+
CrossHair cannot symbolically execute Literal types and silently skips them.
|
|
98
|
+
This function detects such cases for pre-filtering and warning.
|
|
99
|
+
"""
|
|
100
|
+
# Fast path: no Literal import
|
|
101
|
+
if "Literal" not in source:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
# Fast path: no contracts
|
|
105
|
+
if "@pre" not in source and "@post" not in source:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
import ast
|
|
110
|
+
|
|
111
|
+
tree = ast.parse(source)
|
|
112
|
+
except SyntaxError:
|
|
113
|
+
return False # Can't analyze, don't skip
|
|
114
|
+
|
|
115
|
+
contract_decorators = {"pre", "post"}
|
|
116
|
+
|
|
117
|
+
def has_contract(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
118
|
+
for dec in node.decorator_list:
|
|
119
|
+
if isinstance(dec, ast.Call):
|
|
120
|
+
func = dec.func
|
|
121
|
+
if isinstance(func, ast.Name) and func.id in contract_decorators:
|
|
122
|
+
return True
|
|
123
|
+
if isinstance(func, ast.Attribute) and func.attr in contract_decorators:
|
|
124
|
+
return True
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
def annotation_uses_literal(annotation: ast.expr | None) -> bool:
|
|
128
|
+
"""Check if annotation contains Literal type."""
|
|
129
|
+
if annotation is None:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
# Direct Literal["..."]
|
|
133
|
+
if isinstance(annotation, ast.Subscript):
|
|
134
|
+
value = annotation.value
|
|
135
|
+
if isinstance(value, ast.Name) and value.id == "Literal":
|
|
136
|
+
return True
|
|
137
|
+
if isinstance(value, ast.Attribute) and value.attr == "Literal":
|
|
138
|
+
return True
|
|
139
|
+
# Check nested (e.g., Optional[Literal["..."]])
|
|
140
|
+
if annotation_uses_literal(annotation.slice):
|
|
141
|
+
return True
|
|
142
|
+
if annotation_uses_literal(value):
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
# Union types
|
|
146
|
+
if isinstance(annotation, ast.BinOp): # X | Y syntax
|
|
147
|
+
return annotation_uses_literal(annotation.left) or annotation_uses_literal(
|
|
148
|
+
annotation.right
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Tuple of types (for Subscript.slice)
|
|
152
|
+
if isinstance(annotation, ast.Tuple):
|
|
153
|
+
return any(annotation_uses_literal(elt) for elt in annotation.elts)
|
|
154
|
+
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
for node in ast.walk(tree):
|
|
158
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
159
|
+
if has_contract(node):
|
|
160
|
+
# Check all parameter annotations
|
|
161
|
+
for arg in node.args.args + node.args.posonlyargs + node.args.kwonlyargs:
|
|
162
|
+
if annotation_uses_literal(arg.annotation):
|
|
163
|
+
return True
|
|
164
|
+
# Check *args and **kwargs
|
|
165
|
+
if node.args.vararg and annotation_uses_literal(node.args.vararg.annotation):
|
|
166
|
+
return True
|
|
167
|
+
if node.args.kwarg and annotation_uses_literal(node.args.kwarg.annotation):
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
|
|
91
173
|
# ============================================================
|
|
92
174
|
# DX-13: Single File Verification (for parallel execution)
|
|
93
175
|
# ============================================================
|
|
@@ -156,6 +238,23 @@ def _verify_single_file(
|
|
|
156
238
|
# symbolically execute C extensions like ast.parse()
|
|
157
239
|
# Check both stdout and stderr for error patterns
|
|
158
240
|
output = result.stdout + "\n" + result.stderr
|
|
241
|
+
|
|
242
|
+
# BUG-56: Detect Literal type errors (CrossHair limitation)
|
|
243
|
+
literal_errors = [
|
|
244
|
+
"Cannot instantiate typing.Literal",
|
|
245
|
+
"CrosshairUnsupported",
|
|
246
|
+
]
|
|
247
|
+
is_literal_error = any(err in output for err in literal_errors)
|
|
248
|
+
|
|
249
|
+
if is_literal_error:
|
|
250
|
+
return {
|
|
251
|
+
"file": file_path,
|
|
252
|
+
"status": CrossHairStatus.SKIPPED,
|
|
253
|
+
"time_ms": elapsed_ms,
|
|
254
|
+
"reason": "Literal type not supported (tested by Hypothesis)",
|
|
255
|
+
"stdout": output,
|
|
256
|
+
}
|
|
257
|
+
|
|
159
258
|
execution_errors = [
|
|
160
259
|
"TypeError:",
|
|
161
260
|
"AttributeError:",
|
|
@@ -298,6 +397,17 @@ def run_crosshair_parallel(
|
|
|
298
397
|
}
|
|
299
398
|
)
|
|
300
399
|
continue
|
|
400
|
+
|
|
401
|
+
# BUG-56: Check for Literal types that CrossHair cannot handle
|
|
402
|
+
if has_literal_in_contracted_functions(source):
|
|
403
|
+
cached_results.append(
|
|
404
|
+
{
|
|
405
|
+
"file": str(py_file),
|
|
406
|
+
"status": CrossHairStatus.SKIPPED,
|
|
407
|
+
"reason": "Literal type not supported (tested by Hypothesis)",
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
continue
|
|
301
411
|
except OSError:
|
|
302
412
|
pass # Include file anyway
|
|
303
413
|
|
|
@@ -324,6 +434,7 @@ def run_crosshair_parallel(
|
|
|
324
434
|
verified_files: list[str] = []
|
|
325
435
|
failed_files: list[str] = []
|
|
326
436
|
all_counterexamples: list[str] = []
|
|
437
|
+
skipped_at_runtime: list[dict] = [] # BUG-56: Track runtime skips with reasons
|
|
327
438
|
total_time_ms = 0
|
|
328
439
|
|
|
329
440
|
if max_workers > 1 and len(files_to_verify) > 1:
|
|
@@ -351,6 +462,7 @@ def run_crosshair_parallel(
|
|
|
351
462
|
verified_files,
|
|
352
463
|
failed_files,
|
|
353
464
|
all_counterexamples,
|
|
465
|
+
skipped_at_runtime,
|
|
354
466
|
cache,
|
|
355
467
|
)
|
|
356
468
|
total_time_ms += result.get("time_ms", 0)
|
|
@@ -372,6 +484,7 @@ def run_crosshair_parallel(
|
|
|
372
484
|
verified_files,
|
|
373
485
|
failed_files,
|
|
374
486
|
all_counterexamples,
|
|
487
|
+
skipped_at_runtime,
|
|
375
488
|
cache,
|
|
376
489
|
)
|
|
377
490
|
total_time_ms += result.get("time_ms", 0)
|
|
@@ -379,13 +492,18 @@ def run_crosshair_parallel(
|
|
|
379
492
|
# Determine overall status
|
|
380
493
|
status = CrossHairStatus.VERIFIED if not failed_files else CrossHairStatus.COUNTEREXAMPLE
|
|
381
494
|
|
|
495
|
+
# BUG-56: Combine pre-filtered skipped and runtime skipped
|
|
496
|
+
all_skipped = [r["file"] for r in cached_results if r.get("status") == "skipped"]
|
|
497
|
+
all_skipped.extend([r["file"] for r in skipped_at_runtime])
|
|
498
|
+
|
|
382
499
|
return Success(
|
|
383
500
|
{
|
|
384
501
|
"status": status,
|
|
385
502
|
"verified": verified_files,
|
|
386
503
|
"failed": failed_files,
|
|
387
504
|
"cached": [r["file"] for r in cached_results if r.get("status") == "cached"],
|
|
388
|
-
"skipped":
|
|
505
|
+
"skipped": all_skipped,
|
|
506
|
+
"skipped_reasons": skipped_at_runtime, # BUG-56: Include reasons
|
|
389
507
|
"counterexamples": all_counterexamples,
|
|
390
508
|
"files": [str(f) for f in py_files],
|
|
391
509
|
"files_verified": len(files_to_verify),
|
|
@@ -404,6 +522,7 @@ def _process_verification_result(
|
|
|
404
522
|
verified_files: list[str],
|
|
405
523
|
failed_files: list[str],
|
|
406
524
|
all_counterexamples: list[str],
|
|
525
|
+
skipped_at_runtime: list[dict], # BUG-56: Track runtime skips
|
|
407
526
|
cache: ProveCache | None,
|
|
408
527
|
) -> None:
|
|
409
528
|
"""Process a single verification result."""
|
|
@@ -425,6 +544,14 @@ def _process_verification_result(
|
|
|
425
544
|
failed_files.append(f"{file_path} (timeout)")
|
|
426
545
|
elif status == CrossHairStatus.ERROR:
|
|
427
546
|
failed_files.append(f"{file_path} ({result.get('error', 'unknown error')})")
|
|
547
|
+
elif status == CrossHairStatus.SKIPPED:
|
|
548
|
+
# BUG-56: Track skipped files with reasons (e.g., Literal type)
|
|
549
|
+
skipped_at_runtime.append(
|
|
550
|
+
{
|
|
551
|
+
"file": str(file_path),
|
|
552
|
+
"reason": result.get("reason", "unsupported"),
|
|
553
|
+
}
|
|
554
|
+
)
|
|
428
555
|
|
|
429
556
|
|
|
430
557
|
# ============================================================
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: invar-tools
|
|
3
|
-
Version: 1.17.
|
|
3
|
+
Version: 1.17.24
|
|
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
|
|
@@ -14,7 +14,7 @@ invar/core/hypothesis_strategies.py,sha256=_MfjG7KxkmJvuPsczr_1JayR_YmiDzU2jJ8fQ
|
|
|
14
14
|
invar/core/inspect.py,sha256=l1knohwpLRHSNySPUjyeBHJusnU0vYiQGj4dMVgQZIo,4381
|
|
15
15
|
invar/core/lambda_helpers.py,sha256=Ap1y7N0wpgCgPHwrs2pd7zD9Qq4Ptfd2iTliprXIkME,6457
|
|
16
16
|
invar/core/language.py,sha256=aGUcrq--eQtAjb5bYE40eFmDhRs_EbBNGQ1sBYgTdt0,2637
|
|
17
|
-
invar/core/models.py,sha256=
|
|
17
|
+
invar/core/models.py,sha256=WLHCd9M8anw5mALTMboTjxt5PcXO7eJajiYA8b6FGHk,16881
|
|
18
18
|
invar/core/must_use.py,sha256=7HnnbT53lb4dOT-1mL64pz0JbQYytuw4eejNVe7iWKY,5496
|
|
19
19
|
invar/core/parser.py,sha256=ucVpGziVzUvbkXT1n_SgOrYdStDEcNBqLuRGqK3_M5g,9205
|
|
20
20
|
invar/core/postcondition_scope.py,sha256=ykjVNqZZ1zItBmI7ebgmLW5vFGE-vpaLRTvSgWaJMgM,5245
|
|
@@ -24,7 +24,7 @@ invar/core/purity_heuristics.py,sha256=vsgphC1XPIFtsoLB0xvp--AyaJHqlh83LyKXYda4p
|
|
|
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
|
-
invar/core/rules.py,sha256=
|
|
27
|
+
invar/core/rules.py,sha256=y_NIpO8GAcw8WNPZjbJniu9FU7ofgfn3kZ_keexTqA8,23481
|
|
28
28
|
invar/core/shell_analysis.py,sha256=i2A9SMqBI3Rb4Ai0QNTM7awIkSJIY6yZJVWS72lv0bY,6457
|
|
29
29
|
invar/core/shell_architecture.py,sha256=98EVdBFIs8tO-i9jKuzdmv7fLB4PKnyI-vKh5lxnB98,6538
|
|
30
30
|
invar/core/strategies.py,sha256=2DPl0z2p_CBNd4RlSbZzTeAy6Dq6cpCiBCB2p5qHHkk,8798
|
|
@@ -2657,20 +2657,20 @@ invar/node_tools/quick-check/cli.js,sha256=dwV3hdJleFQga2cKUn3PPfQDvvujhzKdjQcIv
|
|
|
2657
2657
|
invar/node_tools/ts-analyzer/cli.js,sha256=SvZ6HyjmobpP8NAZqXFiy8BwH_t5Hb17Ytar_18udaQ,4092887
|
|
2658
2658
|
invar/shell/__init__.py,sha256=FFw1mNbh_97PeKPcHIqQpQ7mw-JoIvyLM1yOdxLw5uk,204
|
|
2659
2659
|
invar/shell/claude_hooks.py,sha256=hV4DfG3cVng32f0Rxoo070tliVlYFC5v9slIWEbAD7E,18899
|
|
2660
|
-
invar/shell/config.py,sha256=
|
|
2660
|
+
invar/shell/config.py,sha256=Q8HI_bYz3mwKTAuG4JYjiXt0kXCdQdWZ0hZn3xy9r-M,18570
|
|
2661
2661
|
invar/shell/contract_coverage.py,sha256=81OQkQqUVYUKytG5aiJyRK62gwh9UzbSG926vkvFTc8,12088
|
|
2662
2662
|
invar/shell/coverage.py,sha256=m01o898IFIdBztEBQLwwL1Vt5PWrpUntO4lv4nWEkls,11344
|
|
2663
2663
|
invar/shell/doc_tools.py,sha256=16gvo_ay9-_EK6lX16WkiRGg4OfTAKK_i0ucQkE7lbI,15149
|
|
2664
2664
|
invar/shell/fs.py,sha256=ctqU-EX0NnKC4txudRCRpbWxWSgBZTInXMeOUnl3IM0,6196
|
|
2665
2665
|
invar/shell/git.py,sha256=R-ynlYa65xtCdnNjHeu42uPyrqoo9KZDzl7BZUW0oWU,2866
|
|
2666
|
-
invar/shell/guard_helpers.py,sha256=
|
|
2666
|
+
invar/shell/guard_helpers.py,sha256=IWiQEhDXnvN7QizGFrTNgfkSRN3ZVRF66gj-CeTjuJE,17261
|
|
2667
2667
|
invar/shell/guard_output.py,sha256=v3gG5P-_47nIFo8eAMKwdA_hLf2KZ0cQ-45Z6JjKp4w,12520
|
|
2668
2668
|
invar/shell/mcp_config.py,sha256=-hC7Y5BGuVs285b6gBARk7ZyzVxHwPgXSyt_GoN0jfs,4580
|
|
2669
2669
|
invar/shell/mutation.py,sha256=Lfyk2b8j8-hxAq-iwAgQeOhr7Ci6c5tRF1TXe3CxQCs,8914
|
|
2670
2670
|
invar/shell/pattern_integration.py,sha256=pRcjfq3NvMW_tvQCnaXZnD1k5AVEWK8CYOE2jN6VTro,7842
|
|
2671
2671
|
invar/shell/pi_hooks.py,sha256=ulZc1sP8mTRJTBsjwFHQzUgg-h8ajRIMp7iF1Y4UUtw,6885
|
|
2672
2672
|
invar/shell/pi_tools.py,sha256=a3ACDDXykFV8fUB5UpBmgMvppwkmLvT1k_BWm0IY47k,4068
|
|
2673
|
-
invar/shell/property_tests.py,sha256=
|
|
2673
|
+
invar/shell/property_tests.py,sha256=qt0CP5RH9Md2ZZV64ziNsjQ_-x0onCYtZwbQfqw9gbY,12586
|
|
2674
2674
|
invar/shell/py_refs.py,sha256=Vjz50lmt9prDBcBv4nkkODdiJ7_DKu5zO4UPZBjAfmM,4638
|
|
2675
2675
|
invar/shell/skill_manager.py,sha256=Mr7Mh9rxPSKSAOTJCAM5ZHiG5nfUf6KQVCuD4LBNHSI,12440
|
|
2676
2676
|
invar/shell/subprocess_env.py,sha256=hendEERSyAG4a8UFhYfPtOAlfspVRB03aVCYpj3uqk4,12745
|
|
@@ -2696,7 +2696,7 @@ invar/shell/commands/update.py,sha256=-NNQQScEb_0bV1B8YFxTjpUECk9c8dGAiNVRc64nWh
|
|
|
2696
2696
|
invar/shell/prove/__init__.py,sha256=ZqlbmyMFJf6yAle8634jFuPRv8wNvHps8loMlOJyf8A,240
|
|
2697
2697
|
invar/shell/prove/accept.py,sha256=cnY_6jzU1EBnpLF8-zWUWcXiSXtCwxPsXEYXsSVPG38,3717
|
|
2698
2698
|
invar/shell/prove/cache.py,sha256=jbNdrvfLjvK7S0iqugErqeabb4YIbQuwIlcSRyCKbcg,4105
|
|
2699
|
-
invar/shell/prove/crosshair.py,sha256=
|
|
2699
|
+
invar/shell/prove/crosshair.py,sha256=w0p7aT_TscHpAOKKZtlbta46O9EUWRfvQ2qG0OMXjCE,21970
|
|
2700
2700
|
invar/shell/prove/guard_ts.py,sha256=8InEWFd6oqFxBUEaAdlqOvPi43p0VZKxJIrAaF0eReU,37936
|
|
2701
2701
|
invar/shell/prove/hypothesis.py,sha256=QUclOOUg_VB6wbmHw8O2EPiL5qBOeBRqQeM04AVuLw0,9880
|
|
2702
2702
|
invar/templates/CLAUDE.md.template,sha256=eaGU3SyRO_NEifw5b26k3srgQH4jyeujjCJ-HbM36_w,4913
|
|
@@ -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.24.dist-info/METADATA,sha256=1X_j-_QQoFhmxpzH-VwKO8QgQNWp_oDyV8qxZJFoXdY,28582
|
|
2782
|
+
invar_tools-1.17.24.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
2783
|
+
invar_tools-1.17.24.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
|
|
2784
|
+
invar_tools-1.17.24.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
|
|
2785
|
+
invar_tools-1.17.24.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
|
|
2786
|
+
invar_tools-1.17.24.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
|
|
2787
|
+
invar_tools-1.17.24.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|