invar-tools 1.17.20__py3-none-any.whl → 1.17.22__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:
@@ -6,6 +6,8 @@ Shell module: handles user interaction and file I/O.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import os
10
+ import sys
9
11
  from pathlib import Path
10
12
 
11
13
  import typer
@@ -234,6 +236,19 @@ def guard(
234
236
  raise typer.Exit(1)
235
237
  path = pyproject_root
236
238
 
239
+ from invar.shell.subprocess_env import get_uvx_respawn_command
240
+
241
+ cmd = get_uvx_respawn_command(
242
+ project_root=path,
243
+ argv=sys.argv[1:],
244
+ tool_name=Path(sys.argv[0]).name,
245
+ invar_tools_version=__version__,
246
+ )
247
+ if cmd is not None:
248
+ env = os.environ.copy()
249
+ env["INVAR_UVX_RESPAWNED"] = "1"
250
+ os.execvpe(cmd[0], cmd, env)
251
+
237
252
  # Load and configure
238
253
  config_result = load_config(path)
239
254
  if isinstance(config_result, Failure):
@@ -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
  # ============================================================
@@ -11,6 +11,7 @@ This module provides three phases of dependency injection:
11
11
  from __future__ import annotations
12
12
 
13
13
  import os
14
+ import shutil
14
15
  import subprocess
15
16
  import sys
16
17
  from datetime import datetime, timedelta
@@ -24,6 +25,7 @@ __all__ = [
24
25
  "detect_project_python_with_invar",
25
26
  "detect_project_venv",
26
27
  "find_site_packages",
28
+ "get_uvx_respawn_command",
27
29
  "get_venv_python_version",
28
30
  "maybe_show_upgrade_prompt",
29
31
  "should_respawn",
@@ -200,6 +202,53 @@ def detect_project_python_with_invar(cwd: Path) -> Path | None:
200
202
  return None
201
203
 
202
204
 
205
+ def _detect_venv_python(venv: Path) -> Path | None:
206
+ python_path = venv / "bin" / "python"
207
+ if not python_path.exists():
208
+ python_path = venv / "Scripts" / "python.exe"
209
+ return python_path if python_path.exists() else None
210
+
211
+
212
+ def get_uvx_respawn_command(
213
+ project_root: Path,
214
+ argv: list[str],
215
+ tool_name: str,
216
+ invar_tools_version: str,
217
+ ) -> list[str] | None:
218
+ if os.environ.get("INVAR_UVX_RESPAWNED") == "1":
219
+ return None
220
+
221
+ venv = detect_project_venv(project_root)
222
+ if venv is None:
223
+ return None
224
+
225
+ venv_version = get_venv_python_version(venv)
226
+ if venv_version is None:
227
+ return None
228
+
229
+ current_version = (sys.version_info.major, sys.version_info.minor)
230
+ if venv_version == current_version:
231
+ return None
232
+
233
+ uvx_path = shutil.which("uvx")
234
+ if uvx_path is None:
235
+ return None
236
+
237
+ project_python = _detect_venv_python(venv)
238
+ if project_python is None:
239
+ return None
240
+
241
+ return [
242
+ uvx_path,
243
+ "--python",
244
+ str(project_python),
245
+ "--from",
246
+ f"invar-tools=={invar_tools_version}",
247
+ tool_name,
248
+ *argv,
249
+ ]
250
+
251
+
203
252
  @pre(lambda cwd: isinstance(cwd, Path))
204
253
  def should_respawn(cwd: Path) -> tuple[bool, Path | None]:
205
254
  """Check if MCP server should re-spawn with project Python.
invar/shell/testing.py CHANGED
@@ -194,7 +194,8 @@ def run_doctests_on_files(
194
194
  capture_output=True,
195
195
  text=True,
196
196
  timeout=timeout,
197
- env=build_subprocess_env(),
197
+ cwd=str(cwd) if cwd is not None else None,
198
+ env=build_subprocess_env(cwd=cwd),
198
199
  )
199
200
  # Pytest exit codes: 0=passed, 5=no tests collected (also OK)
200
201
  is_passed = result.returncode in (0, 5)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: invar-tools
3
- Version: 1.17.20
3
+ Version: 1.17.22
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
@@ -2673,15 +2673,15 @@ invar/shell/pi_tools.py,sha256=a3ACDDXykFV8fUB5UpBmgMvppwkmLvT1k_BWm0IY47k,4068
2673
2673
  invar/shell/property_tests.py,sha256=SB-3tS5TupYQP1jpEUNpQs6fWCwLWxck0akNsgVwGGM,10533
2674
2674
  invar/shell/py_refs.py,sha256=Vjz50lmt9prDBcBv4nkkODdiJ7_DKu5zO4UPZBjAfmM,4638
2675
2675
  invar/shell/skill_manager.py,sha256=Mr7Mh9rxPSKSAOTJCAM5ZHiG5nfUf6KQVCuD4LBNHSI,12440
2676
- invar/shell/subprocess_env.py,sha256=ToPqGw5bnfJIEk5pdvJBM6dslKxn6gqspDnhr1VRCDk,11554
2676
+ invar/shell/subprocess_env.py,sha256=hendEERSyAG4a8UFhYfPtOAlfspVRB03aVCYpj3uqk4,12745
2677
2677
  invar/shell/template_engine.py,sha256=eNKMz7R8g9Xp3_1TGx-QH137jf52E0u3KaVcnotu1Tg,12056
2678
2678
  invar/shell/templates.py,sha256=ilhGysbUcdkUFqPgv6ySVmKI3imS_cwYNCWTCdyb5cY,15407
2679
- invar/shell/testing.py,sha256=ig-LVe5k2-qvdz2H85mrY3_7_v440DJJX72Q3TVP5Xk,11472
2679
+ invar/shell/testing.py,sha256=mRk22-B3pkNWqPxQ_kjcnK8S6X9G6BQQd3bgdxS0zl8,11534
2680
2680
  invar/shell/ts_compiler.py,sha256=nA8brnOhThj9J_J3vAEGjDsM4NjbWQ_eX8Yf4pHPOgk,6672
2681
2681
  invar/shell/commands/__init__.py,sha256=MEkKwVyjI9DmkvBpJcuumXo2Pg_FFkfEr-Rr3nrAt7A,284
2682
2682
  invar/shell/commands/doc.py,sha256=SOLDoCXXGxx_JU0PKXlAIGEF36PzconHmmAtL-rM6D4,13819
2683
2683
  invar/shell/commands/feedback.py,sha256=lLxEeWW_71US_vlmorFrGXS8IARB9nbV6D0zruLs660,7640
2684
- invar/shell/commands/guard.py,sha256=ZeiK5FvRlTNHMLS6StIDKl-mDAdyAFGOovkRy0tG4eQ,25703
2684
+ invar/shell/commands/guard.py,sha256=iGqy6F41ojTYVk8EvGCbRMHDBEcS0tEu3kV0DRjdyqg,26105
2685
2685
  invar/shell/commands/hooks.py,sha256=W-SOnT4VQyUvXwipozkJwgEYfiOJGz7wksrbcdWegUg,2356
2686
2686
  invar/shell/commands/init.py,sha256=rtoPFsfq7xRZ6lfTipWT1OejNK5wfzqu1ncXi1kizU0,23634
2687
2687
  invar/shell/commands/merge.py,sha256=nuvKo8m32-OL-SCQlS4SLKmOZxQ3qj-1nGCx1Pgzifw,8183
@@ -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.20.dist-info/METADATA,sha256=_RDBs3-pAvqNBOLkNCMctnSLdDn-4Qs33RhgxIoEbok,28582
2782
- invar_tools-1.17.20.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
2783
- invar_tools-1.17.20.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
2784
- invar_tools-1.17.20.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
2785
- invar_tools-1.17.20.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
2786
- invar_tools-1.17.20.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
2787
- invar_tools-1.17.20.dist-info/RECORD,,
2781
+ invar_tools-1.17.22.dist-info/METADATA,sha256=LcLYpawRN5vHSOQWJkxa1MH8TIMhTsROH0XBK1J5PCQ,28582
2782
+ invar_tools-1.17.22.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
2783
+ invar_tools-1.17.22.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
2784
+ invar_tools-1.17.22.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
2785
+ invar_tools-1.17.22.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
2786
+ invar_tools-1.17.22.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
2787
+ invar_tools-1.17.22.dist-info/RECORD,,