invar-tools 1.0.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. invar/__init__.py +1 -0
  2. invar/core/contracts.py +80 -10
  3. invar/core/entry_points.py +367 -0
  4. invar/core/extraction.py +5 -6
  5. invar/core/format_specs.py +195 -0
  6. invar/core/format_strategies.py +197 -0
  7. invar/core/formatter.py +32 -10
  8. invar/core/hypothesis_strategies.py +50 -10
  9. invar/core/inspect.py +1 -1
  10. invar/core/lambda_helpers.py +3 -2
  11. invar/core/models.py +30 -18
  12. invar/core/must_use.py +2 -1
  13. invar/core/parser.py +13 -6
  14. invar/core/postcondition_scope.py +128 -0
  15. invar/core/property_gen.py +86 -42
  16. invar/core/purity.py +13 -7
  17. invar/core/purity_heuristics.py +5 -9
  18. invar/core/references.py +8 -6
  19. invar/core/review_trigger.py +370 -0
  20. invar/core/rule_meta.py +69 -2
  21. invar/core/rules.py +91 -28
  22. invar/core/shell_analysis.py +247 -0
  23. invar/core/shell_architecture.py +171 -0
  24. invar/core/strategies.py +7 -14
  25. invar/core/suggestions.py +92 -0
  26. invar/core/sync_helpers.py +238 -0
  27. invar/core/tautology.py +103 -37
  28. invar/core/template_parser.py +467 -0
  29. invar/core/timeout_inference.py +4 -7
  30. invar/core/utils.py +63 -18
  31. invar/core/verification_routing.py +155 -0
  32. invar/mcp/server.py +113 -13
  33. invar/shell/commands/__init__.py +11 -0
  34. invar/shell/{cli.py → commands/guard.py} +152 -44
  35. invar/shell/{init_cmd.py → commands/init.py} +200 -28
  36. invar/shell/commands/merge.py +256 -0
  37. invar/shell/commands/mutate.py +184 -0
  38. invar/shell/{perception.py → commands/perception.py} +2 -0
  39. invar/shell/commands/sync_self.py +113 -0
  40. invar/shell/commands/template_sync.py +366 -0
  41. invar/shell/{test_cmd.py → commands/test.py} +3 -1
  42. invar/shell/commands/update.py +48 -0
  43. invar/shell/config.py +247 -10
  44. invar/shell/coverage.py +351 -0
  45. invar/shell/fs.py +5 -2
  46. invar/shell/git.py +2 -0
  47. invar/shell/guard_helpers.py +116 -20
  48. invar/shell/guard_output.py +106 -24
  49. invar/shell/mcp_config.py +3 -0
  50. invar/shell/mutation.py +314 -0
  51. invar/shell/property_tests.py +75 -24
  52. invar/shell/prove/__init__.py +9 -0
  53. invar/shell/prove/accept.py +113 -0
  54. invar/shell/{prove.py → prove/crosshair.py} +69 -30
  55. invar/shell/prove/hypothesis.py +293 -0
  56. invar/shell/subprocess_env.py +393 -0
  57. invar/shell/template_engine.py +345 -0
  58. invar/shell/templates.py +53 -0
  59. invar/shell/testing.py +77 -37
  60. invar/templates/CLAUDE.md.template +86 -9
  61. invar/templates/aider.conf.yml.template +16 -14
  62. invar/templates/commands/audit.md +138 -0
  63. invar/templates/commands/guard.md +77 -0
  64. invar/templates/config/CLAUDE.md.jinja +206 -0
  65. invar/templates/config/context.md.jinja +92 -0
  66. invar/templates/config/pre-commit.yaml.jinja +44 -0
  67. invar/templates/context.md.template +33 -0
  68. invar/templates/cursorrules.template +25 -13
  69. invar/templates/examples/README.md +2 -0
  70. invar/templates/examples/conftest.py +3 -0
  71. invar/templates/examples/contracts.py +4 -2
  72. invar/templates/examples/core_shell.py +10 -4
  73. invar/templates/examples/workflow.md +81 -0
  74. invar/templates/manifest.toml +137 -0
  75. invar/templates/protocol/INVAR.md +210 -0
  76. invar/templates/skills/develop/SKILL.md.jinja +318 -0
  77. invar/templates/skills/investigate/SKILL.md.jinja +106 -0
  78. invar/templates/skills/propose/SKILL.md.jinja +104 -0
  79. invar/templates/skills/review/SKILL.md.jinja +125 -0
  80. invar_tools-1.3.0.dist-info/METADATA +377 -0
  81. invar_tools-1.3.0.dist-info/RECORD +95 -0
  82. invar_tools-1.3.0.dist-info/entry_points.txt +2 -0
  83. invar_tools-1.3.0.dist-info/licenses/LICENSE +190 -0
  84. invar_tools-1.3.0.dist-info/licenses/LICENSE-GPL +674 -0
  85. invar_tools-1.3.0.dist-info/licenses/NOTICE +63 -0
  86. invar/contracts.py +0 -152
  87. invar/decorators.py +0 -94
  88. invar/invariant.py +0 -57
  89. invar/resource.py +0 -99
  90. invar/shell/prove_fallback.py +0 -183
  91. invar/shell/update_cmd.py +0 -191
  92. invar/templates/INVAR.md +0 -134
  93. invar_tools-1.0.0.dist-info/METADATA +0 -321
  94. invar_tools-1.0.0.dist-info/RECORD +0 -64
  95. invar_tools-1.0.0.dist-info/entry_points.txt +0 -2
  96. invar_tools-1.0.0.dist-info/licenses/LICENSE +0 -21
  97. /invar/shell/{prove_cache.py → prove/cache.py} +0 -0
  98. {invar_tools-1.0.0.dist-info → invar_tools-1.3.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,63 @@
1
+ Invar
2
+ Copyright 2025 Invar Contributors
3
+
4
+ This product includes software developed by the Invar project
5
+ (https://github.com/Tefx/Invar).
6
+
7
+ ================================================================================
8
+
9
+ This project uses the following third-party libraries:
10
+
11
+ --------------------------------------------------------------------------------
12
+ deal - Design by Contract library
13
+ License: LGPL-3.0-or-later
14
+ https://github.com/life4/deal
15
+ --------------------------------------------------------------------------------
16
+
17
+ --------------------------------------------------------------------------------
18
+ hypothesis - Property-based testing library
19
+ License: MPL-2.0
20
+ https://github.com/HypothesisWorks/hypothesis
21
+ --------------------------------------------------------------------------------
22
+
23
+ --------------------------------------------------------------------------------
24
+ crosshair-tool - Symbolic execution analysis
25
+ License: MIT
26
+ https://github.com/pschanely/CrossHair
27
+ --------------------------------------------------------------------------------
28
+
29
+ --------------------------------------------------------------------------------
30
+ typer - CLI framework
31
+ License: MIT
32
+ https://github.com/tiangolo/typer
33
+ --------------------------------------------------------------------------------
34
+
35
+ --------------------------------------------------------------------------------
36
+ rich - Terminal formatting library
37
+ License: MIT
38
+ https://github.com/Textualize/rich
39
+ --------------------------------------------------------------------------------
40
+
41
+ --------------------------------------------------------------------------------
42
+ pydantic - Data validation library
43
+ License: MIT
44
+ https://github.com/pydantic/pydantic
45
+ --------------------------------------------------------------------------------
46
+
47
+ --------------------------------------------------------------------------------
48
+ returns - Functional programming primitives
49
+ License: BSD-2-Clause
50
+ https://github.com/dry-python/returns
51
+ --------------------------------------------------------------------------------
52
+
53
+ --------------------------------------------------------------------------------
54
+ mcp - Model Context Protocol SDK
55
+ License: MIT
56
+ https://github.com/modelcontextprotocol/python-sdk
57
+ --------------------------------------------------------------------------------
58
+
59
+ --------------------------------------------------------------------------------
60
+ pre-commit - Git hooks framework
61
+ License: MIT
62
+ https://github.com/pre-commit/pre-commit
63
+ --------------------------------------------------------------------------------
invar/contracts.py DELETED
@@ -1,152 +0,0 @@
1
- """
2
- Composable contracts for Invar.
3
-
4
- Provides Contract class with &, |, ~ operators for combining conditions,
5
- and a standard library of common predicates. Works with deal decorators.
6
-
7
- Inspired by Idris' dependent types.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- from dataclasses import dataclass
13
- from typing import TYPE_CHECKING, Any
14
-
15
- import deal
16
-
17
- if TYPE_CHECKING:
18
- from collections.abc import Callable
19
-
20
-
21
- @dataclass
22
- class Contract:
23
- """
24
- Composable contract with &, |, ~ operators.
25
-
26
- Contracts encapsulate predicates that can be combined and reused.
27
- Works with deal.pre for runtime checking.
28
-
29
- Examples:
30
- >>> NonEmpty = Contract(lambda x: len(x) > 0, "non-empty")
31
- >>> Sorted = Contract(lambda x: list(x) == sorted(x), "sorted")
32
- >>> combined = NonEmpty & Sorted
33
- >>> combined.check([1, 2, 3])
34
- True
35
- >>> combined.check([])
36
- False
37
- >>> combined.check([3, 1, 2])
38
- False
39
- >>> (NonEmpty | Sorted).check([]) # Empty but sorted
40
- True
41
- >>> (~NonEmpty).check([]) # NOT non-empty
42
- True
43
- """
44
-
45
- predicate: Callable[[Any], bool]
46
- description: str
47
-
48
- def check(self, value: Any) -> bool:
49
- """Check if value satisfies the contract."""
50
- return self.predicate(value)
51
-
52
- def __and__(self, other: Contract) -> Contract:
53
- """Combine contracts with AND."""
54
- return Contract(
55
- predicate=lambda x: self.check(x) and other.check(x),
56
- description=f"({self.description} AND {other.description})",
57
- )
58
-
59
- def __or__(self, other: Contract) -> Contract:
60
- """Combine contracts with OR."""
61
- return Contract(
62
- predicate=lambda x: self.check(x) or other.check(x),
63
- description=f"({self.description} OR {other.description})",
64
- )
65
-
66
- def __invert__(self) -> Contract:
67
- """Negate the contract."""
68
- return Contract(
69
- predicate=lambda x: not self.check(x),
70
- description=f"NOT({self.description})",
71
- )
72
-
73
- def __call__(self, *args: Any, **kwargs: Any) -> bool:
74
- """Allow using as deal.pre predicate directly."""
75
- value = args[0] if args else next(iter(kwargs.values()))
76
- return self.check(value)
77
-
78
- def __repr__(self) -> str:
79
- return f"Contract({self.description!r})"
80
-
81
-
82
- def pre(*contracts: Contract) -> Callable[[Callable], Callable]:
83
- """
84
- Decorator accepting Contract objects for preconditions.
85
-
86
- Works with deal.pre under the hood.
87
-
88
- Examples:
89
- >>> from invar.contracts import pre, NonEmpty
90
- >>> @pre(NonEmpty)
91
- ... def first(xs): return xs[0]
92
- >>> first([1, 2, 3])
93
- 1
94
- """
95
-
96
- def combined(*args: Any, **kwargs: Any) -> bool:
97
- value = args[0] if args else next(iter(kwargs.values()))
98
- return all(c.check(value) for c in contracts)
99
-
100
- return deal.pre(combined)
101
-
102
-
103
- def post(*contracts: Contract) -> Callable[[Callable], Callable]:
104
- """
105
- Decorator accepting Contract objects for postconditions.
106
-
107
- Works with deal.post under the hood.
108
-
109
- Examples:
110
- >>> from invar.contracts import post, NonEmpty
111
- >>> @post(NonEmpty)
112
- ... def get_list(): return [1]
113
- >>> get_list()
114
- [1]
115
- """
116
-
117
- def combined(result: Any) -> bool:
118
- return all(c.check(result) for c in contracts)
119
-
120
- return deal.post(combined)
121
-
122
-
123
- # =============================================================================
124
- # Standard Library of Contracts
125
- # =============================================================================
126
-
127
- # --- Collections ---
128
- NonEmpty: Contract = Contract(lambda x: len(x) > 0, "non-empty")
129
- Sorted: Contract = Contract(lambda x: list(x) == sorted(x), "sorted")
130
- Unique: Contract = Contract(lambda x: len(x) == len(set(x)), "unique")
131
- SortedNonEmpty: Contract = NonEmpty & Sorted
132
-
133
- # --- Numbers ---
134
- Positive: Contract = Contract(lambda x: x > 0, "positive")
135
- NonNegative: Contract = Contract(lambda x: x >= 0, "non-negative")
136
- Negative: Contract = Contract(lambda x: x < 0, "negative")
137
-
138
-
139
- def InRange(lo: float, hi: float) -> Contract:
140
- """Create a contract checking value is in [lo, hi]."""
141
- return Contract(lambda x: lo <= x <= hi, f"[{lo},{hi}]")
142
-
143
-
144
- Percentage: Contract = InRange(0, 100)
145
-
146
- # --- Strings ---
147
- NonBlank: Contract = Contract(lambda s: bool(s and s.strip()), "non-blank")
148
-
149
- # --- Lists with elements ---
150
- AllPositive: Contract = Contract(lambda xs: all(x > 0 for x in xs), "all positive")
151
- AllNonNegative: Contract = Contract(lambda xs: all(x >= 0 for x in xs), "all non-negative")
152
- NoNone: Contract = Contract(lambda xs: None not in xs, "no None")
invar/decorators.py DELETED
@@ -1,94 +0,0 @@
1
- """
2
- Invar contract decorators.
3
-
4
- Provides decorators that extend deal's contract system for additional
5
- static analysis by Guard.
6
-
7
- DX-12-B: Added @strategy for custom Hypothesis strategies.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- from collections.abc import Callable
13
- from typing import Any, TypeVar
14
-
15
- F = TypeVar("F", bound=Callable)
16
-
17
-
18
- def must_use(reason: str | None = None) -> Callable[[F], F]:
19
- """
20
- Mark a function's return value as must-use.
21
-
22
- Guard will warn if calls to this function discard the return value.
23
- Inspired by Move's lack of drop ability and Rust's #[must_use].
24
-
25
- Args:
26
- reason: Explanation of why the return value must be used.
27
-
28
- Returns:
29
- A decorator that marks the function.
30
-
31
- >>> @must_use("Error must be handled")
32
- ... def may_fail() -> int:
33
- ... return 42
34
- >>> may_fail.__invar_must_use__
35
- 'Error must be handled'
36
-
37
- >>> @must_use()
38
- ... def important() -> str:
39
- ... return "result"
40
- >>> important.__invar_must_use__
41
- 'Return value must be used'
42
- """
43
-
44
- def decorator(func: F) -> F:
45
- func.__invar_must_use__ = reason or "Return value must be used" # type: ignore[attr-defined]
46
- return func
47
-
48
- return decorator
49
-
50
-
51
- def strategy(**param_strategies: Any) -> Callable[[F], F]:
52
- """
53
- Specify custom Hypothesis strategies for function parameters.
54
-
55
- DX-12-B: Escape hatch when automatic strategy inference fails or
56
- when you need precise control over generated values.
57
-
58
- Args:
59
- **param_strategies: Mapping of parameter names to Hypothesis strategies.
60
-
61
- Returns:
62
- A decorator that attaches strategies to the function.
63
-
64
- Examples:
65
- >>> from hypothesis import strategies as st
66
- >>> @strategy(x=st.floats(min_value=1e-10, max_value=1e10))
67
- ... def sqrt(x: float) -> float:
68
- ... return x ** 0.5
69
- >>> hasattr(sqrt, '__invar_strategies__')
70
- True
71
- >>> 'x' in sqrt.__invar_strategies__
72
- True
73
-
74
- >>> # NumPy array with specific shape
75
- >>> @strategy(arr="arrays(dtype=float64, shape=(10,))")
76
- ... def normalize(arr):
77
- ... return arr / arr.sum()
78
- >>> 'arr' in normalize.__invar_strategies__
79
- True
80
-
81
- Note:
82
- Strategies can be either:
83
- - Hypothesis strategy objects (e.g., st.floats())
84
- - String representations (e.g., "floats(min_value=0)")
85
-
86
- String representations are useful when you don't want to import
87
- Hypothesis at module load time.
88
- """
89
-
90
- def decorator(func: F) -> F:
91
- func.__invar_strategies__ = param_strategies # type: ignore[attr-defined]
92
- return func
93
-
94
- return decorator
invar/invariant.py DELETED
@@ -1,57 +0,0 @@
1
- """
2
- Loop invariant support for Invar.
3
-
4
- Provides runtime-checked loop invariants inspired by Dafny.
5
- Checking controlled by INVAR_CHECK environment variable (default: ON).
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import os
11
-
12
-
13
- class InvariantViolation(Exception):
14
- """Raised when a loop invariant is violated."""
15
-
16
- pass
17
-
18
-
19
- # Read once at module load time - effectively a constant
20
- _INVAR_CHECK = os.environ.get("INVAR_CHECK", "1") == "1"
21
-
22
-
23
- def invariant(condition: bool, message: str = "") -> None:
24
- """
25
- Assert loop invariant. Checked at runtime when INVAR_CHECK=1.
26
-
27
- Place at the START of loop body to check condition each iteration.
28
- Invariants document what must remain true throughout loop execution.
29
-
30
- Args:
31
- condition: Boolean condition that must hold
32
- message: Optional message describing the invariant
33
-
34
- Raises:
35
- InvariantViolation: When condition is False and INVAR_CHECK=1
36
-
37
- Examples:
38
- >>> invariant(True) # OK
39
-
40
- >>> invariant(True, "x is positive") # OK with message
41
-
42
- >>> try:
43
- ... invariant(False, "x must be positive")
44
- ... except InvariantViolation as e:
45
- ... print(str(e))
46
- Loop invariant violated: x must be positive
47
-
48
- Typical usage in a binary search:
49
-
50
- while lo < hi:
51
- invariant(0 <= lo <= hi <= len(arr))
52
- invariant(target not in arr[:lo]) # Already searched
53
- ...
54
- """
55
- if _INVAR_CHECK and not condition:
56
- msg = f"Loop invariant violated: {message}" if message else "Loop invariant violated"
57
- raise InvariantViolation(msg)
invar/resource.py DELETED
@@ -1,99 +0,0 @@
1
- """
2
- Resource management decorators for Invar.
3
-
4
- Provides @must_close for marking classes that require explicit cleanup.
5
- Inspired by Move language's resource semantics.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from typing import Any, TypeVar
11
-
12
- T = TypeVar("T")
13
-
14
-
15
- class ResourceWarning(UserWarning):
16
- """Warning raised when a resource may not be properly closed."""
17
-
18
- pass
19
-
20
-
21
- class MustCloseViolation(Exception):
22
- """Raised when a @must_close resource is not properly managed."""
23
-
24
- pass
25
-
26
-
27
- def must_close(cls: type[T]) -> type[T]:
28
- """
29
- Mark a class as a resource that must be explicitly closed.
30
-
31
- The decorated class should have a `close()` method. The decorator:
32
- 1. Adds __invar_must_close__ marker for Guard detection
33
- 2. Adds context manager protocol if not present
34
-
35
- Examples:
36
- >>> @must_close
37
- ... class TempFile:
38
- ... def __init__(self, path: str):
39
- ... self.path = path
40
- ... self.closed = False
41
- ... def write(self, data: str) -> None:
42
- ... if self.closed:
43
- ... raise ValueError("File is closed")
44
- ... def close(self) -> None:
45
- ... self.closed = True
46
-
47
- >>> # Preferred: use as context manager
48
- >>> with TempFile("test.txt") as f:
49
- ... f.write("hello")
50
- >>> f.closed
51
- True
52
-
53
- >>> # Also works: explicit close
54
- >>> f2 = TempFile("test2.txt")
55
- >>> f2.write("world")
56
- >>> f2.close()
57
- >>> f2.closed
58
- True
59
- """
60
- # Mark for Guard detection
61
- cls.__invar_must_close__ = True # type: ignore[attr-defined]
62
-
63
- # Add context manager protocol if not present
64
- if not hasattr(cls, "__enter__"):
65
-
66
- def __enter__(self: Any) -> Any:
67
- return self
68
-
69
- cls.__enter__ = __enter__ # type: ignore[attr-defined]
70
-
71
- if not hasattr(cls, "__exit__"):
72
-
73
- def __exit__(self: Any, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
74
- if hasattr(self, "close") and callable(self.close):
75
- self.close()
76
-
77
- cls.__exit__ = __exit__ # type: ignore[attr-defined]
78
-
79
- return cls
80
-
81
-
82
- def is_must_close(cls_or_obj: Any) -> bool:
83
- """
84
- Check if a class or instance is marked with @must_close.
85
-
86
- >>> @must_close
87
- ... class Resource:
88
- ... def close(self): pass
89
- >>> is_must_close(Resource)
90
- True
91
- >>> is_must_close(Resource())
92
- True
93
- >>> class Plain: pass
94
- >>> is_must_close(Plain)
95
- False
96
- """
97
- if isinstance(cls_or_obj, type):
98
- return getattr(cls_or_obj, "__invar_must_close__", False)
99
- return getattr(type(cls_or_obj), "__invar_must_close__", False)
@@ -1,183 +0,0 @@
1
- """
2
- Hypothesis fallback for proof verification.
3
-
4
- DX-12: Provides Hypothesis as automatic fallback when CrossHair
5
- is unavailable, times out, or skips files.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import subprocess
11
- import sys
12
- from pathlib import Path
13
-
14
- from returns.result import Failure, Result, Success
15
-
16
-
17
- def run_hypothesis_fallback(
18
- files: list[Path],
19
- max_examples: int = 100,
20
- ) -> Result[dict, str]:
21
- """
22
- Run Hypothesis property tests as fallback when CrossHair skips/times out.
23
-
24
- DX-12: Uses inferred strategies from type hints and @pre contracts.
25
-
26
- Args:
27
- files: List of Python file paths to test
28
- max_examples: Maximum examples per test
29
-
30
- Returns:
31
- Success with test results or Failure with error message
32
- """
33
- # Import CrossHairStatus here to avoid circular import
34
- from invar.shell.prove import CrossHairStatus
35
-
36
- # Check if hypothesis is available
37
- try:
38
- import hypothesis # noqa: F401
39
- except ImportError:
40
- return Success(
41
- {
42
- "status": CrossHairStatus.SKIPPED,
43
- "reason": "Hypothesis not installed (pip install hypothesis)",
44
- "files": [],
45
- "tool": "hypothesis",
46
- }
47
- )
48
-
49
- if not files:
50
- return Success(
51
- {
52
- "status": CrossHairStatus.SKIPPED,
53
- "reason": "no files",
54
- "files": [],
55
- "tool": "hypothesis",
56
- }
57
- )
58
-
59
- # Filter to Python files only
60
- py_files = [f for f in files if f.suffix == ".py" and f.exists()]
61
- if not py_files:
62
- return Success(
63
- {
64
- "status": CrossHairStatus.SKIPPED,
65
- "reason": "no Python files",
66
- "files": [],
67
- "tool": "hypothesis",
68
- }
69
- )
70
-
71
- # Use pytest with hypothesis
72
- cmd = [
73
- sys.executable,
74
- "-m",
75
- "pytest",
76
- "--hypothesis-show-statistics",
77
- "--hypothesis-seed=0", # Reproducible
78
- "-x", # Stop on first failure
79
- "--tb=short",
80
- ]
81
- cmd.extend(str(f) for f in py_files)
82
-
83
- try:
84
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
85
- # Pytest exit codes: 0=passed, 5=no tests collected
86
- is_passed = result.returncode in (0, 5)
87
- return Success(
88
- {
89
- "status": "passed" if is_passed else "failed",
90
- "files": [str(f) for f in py_files],
91
- "exit_code": result.returncode,
92
- "stdout": result.stdout,
93
- "stderr": result.stderr,
94
- "tool": "hypothesis",
95
- "note": "Fallback from CrossHair",
96
- }
97
- )
98
- except subprocess.TimeoutExpired:
99
- return Failure("Hypothesis timeout (300s)")
100
- except Exception as e:
101
- return Failure(f"Hypothesis error: {e}")
102
-
103
-
104
- def run_prove_with_fallback(
105
- files: list[Path],
106
- crosshair_timeout: int = 10,
107
- hypothesis_max_examples: int = 100,
108
- use_cache: bool = True,
109
- cache_dir: Path | None = None,
110
- ) -> Result[dict, str]:
111
- """
112
- Run proof verification with automatic Hypothesis fallback.
113
-
114
- DX-12 + DX-13: Tries CrossHair first with optimizations, falls back to Hypothesis.
115
-
116
- Args:
117
- files: List of Python file paths to verify
118
- crosshair_timeout: Ignored (kept for backwards compatibility)
119
- hypothesis_max_examples: Maximum Hypothesis examples
120
- use_cache: Whether to use verification cache (DX-13)
121
- cache_dir: Cache directory (default: .invar/cache/prove)
122
-
123
- Returns:
124
- Success with verification results or Failure with error message
125
- """
126
- # Import here to avoid circular import
127
- from invar.shell.prove import CrossHairStatus, run_crosshair_parallel
128
- from invar.shell.prove_cache import ProveCache
129
-
130
- # DX-13: Initialize cache
131
- cache = None
132
- if use_cache:
133
- if cache_dir is None:
134
- cache_dir = Path(".invar/cache/prove")
135
- cache = ProveCache(cache_dir=cache_dir)
136
-
137
- # DX-13: Use parallel CrossHair with caching
138
- crosshair_result = run_crosshair_parallel(
139
- files,
140
- max_iterations=5, # Fast mode
141
- max_workers=None, # Auto-detect
142
- cache=cache,
143
- )
144
-
145
- if isinstance(crosshair_result, Failure):
146
- # CrossHair failed, try Hypothesis
147
- return run_hypothesis_fallback(files, max_examples=hypothesis_max_examples)
148
-
149
- result_data = crosshair_result.unwrap()
150
- status = result_data.get("status", "")
151
-
152
- # Check if we need fallback
153
- needs_fallback = (
154
- status == CrossHairStatus.SKIPPED
155
- or status == CrossHairStatus.TIMEOUT
156
- or "not installed" in result_data.get("reason", "")
157
- )
158
-
159
- if needs_fallback:
160
- # Run Hypothesis as fallback
161
- hypothesis_result = run_hypothesis_fallback(
162
- files, max_examples=hypothesis_max_examples
163
- )
164
-
165
- if isinstance(hypothesis_result, Success):
166
- hyp_data = hypothesis_result.unwrap()
167
- # Merge results
168
- return Success(
169
- {
170
- "status": hyp_data.get("status", "unknown"),
171
- "primary_tool": "hypothesis",
172
- "crosshair_status": status,
173
- "crosshair_reason": result_data.get("reason", ""),
174
- "hypothesis_result": hyp_data,
175
- "files": [str(f) for f in files],
176
- "note": "CrossHair skipped/unavailable, used Hypothesis fallback",
177
- }
178
- )
179
- return hypothesis_result
180
-
181
- # CrossHair succeeded (verified or found counterexample)
182
- result_data["primary_tool"] = "crosshair"
183
- return Success(result_data)