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 CHANGED
@@ -160,7 +160,10 @@ def get_layer(file_info: FileInfo) -> CodeLayer:
160
160
  return CodeLayer.DEFAULT
161
161
 
162
162
 
163
- @pre(lambda layer, language="python": isinstance(layer, CodeLayer) and language in ("python", "typescript"))
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
- max_file_lines: int = Field(default=500, ge=1) # Phase 9 P1: Raised from 300
428
- max_function_lines: int = Field(default=50, ge=1)
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
- [(s.name, s.end_line - s.line + 1) for s in file_info.symbols if s.kind in (SymbolKind.FUNCTION, SymbolKind.METHOD)],
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
- max_lines = limits.max_file_lines
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(Violation(
140
- rule="file_size", severity=Severity.ERROR, file=file_info.path, line=None,
141
- message=f"File has {file_info.lines} lines (max: {max_lines} for {layer.value})",
142
- suggestion=_build_size_suggestion("Split into smaller modules.", extraction_hint, func_hint),
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(Violation(
149
- rule="file_size_warning", severity=Severity.WARNING, file=file_info.path, line=None,
150
- message=f"File has {file_info.lines} lines ({pct}% of {max_lines} limit)",
151
- suggestion=_build_size_suggestion("Consider splitting before reaching limit.", extraction_hint, func_hint),
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
- max_func_lines = limits.max_function_lines
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(symbol, file_info.source, "shell_result"):
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(symbol, file_info.source, "entry_point_too_thick"):
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
- find_result = _find_config_source(project_root)
376
- if isinstance(find_result, Failure):
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
- if source == "default":
381
- return Success(RuleConfig())
381
+ pyproject = project_root / "pyproject.toml"
382
+ if pyproject.exists():
383
+ sources_to_try.append((pyproject, "pyproject"))
382
384
 
383
- assert config_path is not None # source != "default" guarantees path exists
384
- result = _read_toml(config_path)
385
+ invar_toml = project_root / "invar.toml"
386
+ if invar_toml.exists():
387
+ sources_to_try.append((invar_toml, "invar"))
385
388
 
386
- if isinstance(result, Failure):
387
- return result
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
- data = result.unwrap()
390
- guard_config = extract_guard_section(data, source)
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
- # For pyproject.toml, if no [tool.invar.guard] section, use defaults
393
- if source == "pyproject" and not guard_config:
394
- return Success(RuleConfig())
402
+ if guard_config: # Found valid guard config
403
+ return Success(parse_guard_config(guard_config))
395
404
 
396
- return Success(parse_guard_config(guard_config))
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
@@ -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
- core_files = [f for f in checked_files if "core" in str(f)]
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
- core_files = [f for f in checked_files if "core" in str(f)]
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
 
@@ -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
- def _import_module_from_path(file_path: Path) -> object | None:
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
- module_name = file_path.stem
229
- spec = importlib.util.spec_from_file_location(module_name, file_path)
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
 
@@ -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": [r["file"] for r in cached_results if r.get("status") == "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.21
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=DP4nfQ5O58eg1NCsBdw9OsQrVY1nSvo9GIlRdV2QjkU,16758
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=XnFEDm4PSblGiCG14bfnhl-OcMmNsy7Slx7DAUbfaF4,22226
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=6N4AvhYPSUzd3YGXPIc8edF6Lp492W-cS8wwnHUJotI,18119
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=lpaFIe328ZISzim92TAxZHTT8jC4N0_TcQ7PV7u327w,16083
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=SB-3tS5TupYQP1jpEUNpQs6fWCwLWxck0akNsgVwGGM,10533
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=XDOoLvOrU_6OGYm2rTjWacdvKgpJ3NjUDetMwyT9kIE,16997
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.21.dist-info/METADATA,sha256=EauMUAZU2xrasvG2preE0ZC6zOL74-EstsdmrBUiq-s,28582
2782
- invar_tools-1.17.21.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
2783
- invar_tools-1.17.21.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
2784
- invar_tools-1.17.21.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
2785
- invar_tools-1.17.21.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
2786
- invar_tools-1.17.21.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
2787
- invar_tools-1.17.21.dist-info/RECORD,,
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,,