invar-tools 1.17.19__py3-none-any.whl → 1.17.20__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/shell/commands/guard.py +11 -2
- invar/shell/config.py +38 -5
- invar/shell/git.py +10 -11
- invar/shell/guard_helpers.py +69 -41
- invar/shell/property_tests.py +85 -38
- invar/shell/prove/crosshair.py +19 -12
- invar/shell/subprocess_env.py +9 -5
- invar/shell/testing.py +57 -30
- {invar_tools-1.17.19.dist-info → invar_tools-1.17.20.dist-info}/METADATA +3 -3
- {invar_tools-1.17.19.dist-info → invar_tools-1.17.20.dist-info}/RECORD +15 -15
- {invar_tools-1.17.19.dist-info → invar_tools-1.17.20.dist-info}/WHEEL +0 -0
- {invar_tools-1.17.19.dist-info → invar_tools-1.17.20.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.17.19.dist-info → invar_tools-1.17.20.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.17.19.dist-info → invar_tools-1.17.20.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.17.19.dist-info → invar_tools-1.17.20.dist-info}/licenses/NOTICE +0 -0
invar/shell/commands/guard.py
CHANGED
|
@@ -17,7 +17,7 @@ from invar import __version__
|
|
|
17
17
|
from invar.core.models import GuardReport, RuleConfig
|
|
18
18
|
from invar.core.rules import check_all_rules
|
|
19
19
|
from invar.core.utils import get_exit_code
|
|
20
|
-
from invar.shell.config import find_project_root, load_config
|
|
20
|
+
from invar.shell.config import find_project_root, find_pyproject_root, load_config
|
|
21
21
|
from invar.shell.fs import scan_project
|
|
22
22
|
from invar.shell.guard_output import output_agent, output_rich
|
|
23
23
|
|
|
@@ -225,7 +225,14 @@ def guard(
|
|
|
225
225
|
console.print(f"[red]Error:[/red] {path} is not a Python file")
|
|
226
226
|
raise typer.Exit(1)
|
|
227
227
|
single_file = path.resolve()
|
|
228
|
-
|
|
228
|
+
|
|
229
|
+
pyproject_root = find_pyproject_root(single_file if single_file else path)
|
|
230
|
+
if pyproject_root is None:
|
|
231
|
+
console.print(
|
|
232
|
+
"[red]Error:[/red] pyproject.toml not found (searched upward from the target path)"
|
|
233
|
+
)
|
|
234
|
+
raise typer.Exit(1)
|
|
235
|
+
path = pyproject_root
|
|
229
236
|
|
|
230
237
|
# Load and configure
|
|
231
238
|
config_result = load_config(path)
|
|
@@ -373,6 +380,7 @@ def guard(
|
|
|
373
380
|
|
|
374
381
|
# Phase 1: Doctests (DX-37: with optional coverage)
|
|
375
382
|
doctest_passed, doctest_output, doctest_coverage = run_doctests_phase(
|
|
383
|
+
path,
|
|
376
384
|
checked_files,
|
|
377
385
|
explain,
|
|
378
386
|
timeout=config.timeout_doctest,
|
|
@@ -393,6 +401,7 @@ def guard(
|
|
|
393
401
|
|
|
394
402
|
# Phase 3: Hypothesis property tests (DX-37: with optional coverage)
|
|
395
403
|
property_passed, property_output, property_coverage = run_property_tests_phase(
|
|
404
|
+
path,
|
|
396
405
|
checked_files,
|
|
397
406
|
doctest_passed,
|
|
398
407
|
static_exit_code,
|
invar/shell/config.py
CHANGED
|
@@ -39,11 +39,27 @@ class ModuleType(Enum):
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
# I/O libraries that indicate Shell module (for AST import checking)
|
|
42
|
-
_IO_LIBRARIES = frozenset(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
_IO_LIBRARIES = frozenset(
|
|
43
|
+
[
|
|
44
|
+
"os",
|
|
45
|
+
"sys",
|
|
46
|
+
"subprocess",
|
|
47
|
+
"pathlib",
|
|
48
|
+
"shutil",
|
|
49
|
+
"io",
|
|
50
|
+
"socket",
|
|
51
|
+
"requests",
|
|
52
|
+
"aiohttp",
|
|
53
|
+
"httpx",
|
|
54
|
+
"urllib",
|
|
55
|
+
"sqlite3",
|
|
56
|
+
"psycopg2",
|
|
57
|
+
"pymongo",
|
|
58
|
+
"sqlalchemy",
|
|
59
|
+
"typer",
|
|
60
|
+
"click",
|
|
61
|
+
]
|
|
62
|
+
)
|
|
47
63
|
|
|
48
64
|
# Contract decorator names
|
|
49
65
|
_CONTRACT_DECORATORS = frozenset(["pre", "post", "invariant"])
|
|
@@ -226,6 +242,7 @@ def auto_detect_module_type(source: str, file_path: str = "") -> ModuleType:
|
|
|
226
242
|
# Unknown: neither clear pattern
|
|
227
243
|
return ModuleType.UNKNOWN
|
|
228
244
|
|
|
245
|
+
|
|
229
246
|
if TYPE_CHECKING:
|
|
230
247
|
from pathlib import Path
|
|
231
248
|
|
|
@@ -268,6 +285,20 @@ def _find_config_source(project_root: Path) -> Result[tuple[Path | None, ConfigS
|
|
|
268
285
|
|
|
269
286
|
|
|
270
287
|
# @shell_complexity: Project root discovery requires checking multiple markers
|
|
288
|
+
def find_pyproject_root(start_path: "Path") -> "Path | None": # noqa: UP037
|
|
289
|
+
from pathlib import Path
|
|
290
|
+
|
|
291
|
+
current = Path(start_path).resolve()
|
|
292
|
+
if current.is_file():
|
|
293
|
+
current = current.parent
|
|
294
|
+
|
|
295
|
+
for parent in [current, *current.parents]:
|
|
296
|
+
if (parent / "pyproject.toml").exists():
|
|
297
|
+
return parent
|
|
298
|
+
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
|
|
271
302
|
def find_project_root(start_path: "Path") -> "Path": # noqa: UP037
|
|
272
303
|
"""
|
|
273
304
|
Find project root by walking up from start_path looking for config files.
|
|
@@ -492,6 +523,7 @@ def classify_file(
|
|
|
492
523
|
else:
|
|
493
524
|
# Log warning about config error, use defaults
|
|
494
525
|
import logging
|
|
526
|
+
|
|
495
527
|
logging.getLogger(__name__).debug(
|
|
496
528
|
"Pattern classification failed: %s, using defaults", pattern_result.failure()
|
|
497
529
|
)
|
|
@@ -503,6 +535,7 @@ def classify_file(
|
|
|
503
535
|
else:
|
|
504
536
|
# Log warning about config error, use defaults
|
|
505
537
|
import logging
|
|
538
|
+
|
|
506
539
|
logging.getLogger(__name__).debug(
|
|
507
540
|
"Path classification failed: %s, using defaults", path_result.failure()
|
|
508
541
|
)
|
invar/shell/git.py
CHANGED
|
@@ -7,13 +7,10 @@ Shell module: handles git I/O for changed file detection.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import subprocess
|
|
10
|
-
from
|
|
10
|
+
from pathlib import Path
|
|
11
11
|
|
|
12
12
|
from returns.result import Failure, Result, Success
|
|
13
13
|
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
|
|
17
14
|
|
|
18
15
|
def _run_git(args: list[str], cwd: Path) -> Result[str, str]:
|
|
19
16
|
"""Run a git command and return stdout."""
|
|
@@ -49,27 +46,29 @@ def get_changed_files(project_root: Path) -> Result[set[Path], str]:
|
|
|
49
46
|
>>> isinstance(result, (Success, Failure))
|
|
50
47
|
True
|
|
51
48
|
"""
|
|
52
|
-
# Verify git repo
|
|
53
49
|
check = _run_git(["rev-parse", "--git-dir"], project_root)
|
|
54
50
|
if isinstance(check, Failure):
|
|
55
51
|
return Failure(f"Not a git repository: {project_root}")
|
|
56
52
|
|
|
53
|
+
repo_root_result = _run_git(["rev-parse", "--show-toplevel"], project_root)
|
|
54
|
+
if isinstance(repo_root_result, Failure):
|
|
55
|
+
return Failure(repo_root_result.failure())
|
|
56
|
+
|
|
57
|
+
repo_root = Path(repo_root_result.unwrap().strip())
|
|
58
|
+
|
|
57
59
|
changed: set[Path] = set()
|
|
58
60
|
|
|
59
|
-
# Staged changes
|
|
60
61
|
staged = _run_git(["diff", "--cached", "--name-only"], project_root)
|
|
61
62
|
if isinstance(staged, Success):
|
|
62
|
-
changed.update(_parse_py_files(staged.unwrap(),
|
|
63
|
+
changed.update(_parse_py_files(staged.unwrap(), repo_root))
|
|
63
64
|
|
|
64
|
-
# Unstaged changes
|
|
65
65
|
unstaged = _run_git(["diff", "--name-only"], project_root)
|
|
66
66
|
if isinstance(unstaged, Success):
|
|
67
|
-
changed.update(_parse_py_files(unstaged.unwrap(),
|
|
67
|
+
changed.update(_parse_py_files(unstaged.unwrap(), repo_root))
|
|
68
68
|
|
|
69
|
-
# Untracked files
|
|
70
69
|
untracked = _run_git(["ls-files", "--others", "--exclude-standard"], project_root)
|
|
71
70
|
if isinstance(untracked, Success):
|
|
72
|
-
changed.update(_parse_py_files(untracked.unwrap(),
|
|
71
|
+
changed.update(_parse_py_files(untracked.unwrap(), repo_root))
|
|
73
72
|
|
|
74
73
|
return Success(changed)
|
|
75
74
|
|
invar/shell/guard_helpers.py
CHANGED
|
@@ -36,24 +36,36 @@ def handle_changed_mode(
|
|
|
36
36
|
if isinstance(changed_result, Failure):
|
|
37
37
|
return Failure(changed_result.failure())
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
all_files = changed_result.unwrap()
|
|
40
|
+
only_files = {p for p in all_files if p.is_relative_to(path)}
|
|
40
41
|
if not only_files:
|
|
41
|
-
return Failure("NO_CHANGES")
|
|
42
|
+
return Failure("NO_CHANGES")
|
|
42
43
|
|
|
43
44
|
return Success((only_files, list(only_files)))
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
# @shell_orchestration: Coordinates path classification and file collection
|
|
47
48
|
# @shell_complexity: File collection with path normalization
|
|
48
|
-
def collect_files_to_check(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
from invar.shell.config import get_path_classification
|
|
49
|
+
def collect_files_to_check(path: Path, checked_files: list[Path]) -> list[Path]:
|
|
50
|
+
"""Collect Python files for runtime phases, honoring exclude_paths."""
|
|
51
|
+
from invar.shell.config import get_exclude_paths, get_path_classification
|
|
52
|
+
from invar.shell.fs import _is_excluded
|
|
53
53
|
|
|
54
54
|
if checked_files:
|
|
55
55
|
return checked_files
|
|
56
56
|
|
|
57
|
+
exclude_result = get_exclude_paths(path)
|
|
58
|
+
exclude_patterns = exclude_result.unwrap() if isinstance(exclude_result, Success) else []
|
|
59
|
+
|
|
60
|
+
def _add_py_files_under(root: Path) -> None:
|
|
61
|
+
for py_file in root.rglob("*.py"):
|
|
62
|
+
try:
|
|
63
|
+
rel = str(py_file.relative_to(path))
|
|
64
|
+
except ValueError:
|
|
65
|
+
rel = str(py_file)
|
|
66
|
+
if not _is_excluded(rel, exclude_patterns):
|
|
67
|
+
result_files.append(py_file)
|
|
68
|
+
|
|
57
69
|
result_files: list[Path] = []
|
|
58
70
|
|
|
59
71
|
path_result = get_path_classification(path)
|
|
@@ -62,26 +74,33 @@ def collect_files_to_check(
|
|
|
62
74
|
else:
|
|
63
75
|
core_paths, shell_paths = ["src/core"], ["src/shell"]
|
|
64
76
|
|
|
65
|
-
# Scan core/shell paths
|
|
66
77
|
for core_path in core_paths:
|
|
67
78
|
full_path = path / core_path
|
|
68
79
|
if full_path.exists():
|
|
69
|
-
|
|
80
|
+
_add_py_files_under(full_path)
|
|
70
81
|
|
|
71
82
|
for shell_path in shell_paths:
|
|
72
83
|
full_path = path / shell_path
|
|
73
84
|
if full_path.exists():
|
|
74
|
-
|
|
85
|
+
_add_py_files_under(full_path)
|
|
75
86
|
|
|
76
|
-
# Fallback: scan path directly
|
|
77
87
|
if not result_files and path.exists():
|
|
78
|
-
|
|
88
|
+
_add_py_files_under(path)
|
|
89
|
+
|
|
90
|
+
seen: set[str] = set()
|
|
91
|
+
unique: list[Path] = []
|
|
92
|
+
for f in result_files:
|
|
93
|
+
key = str(f)
|
|
94
|
+
if key not in seen:
|
|
95
|
+
seen.add(key)
|
|
96
|
+
unique.append(f)
|
|
79
97
|
|
|
80
|
-
return
|
|
98
|
+
return unique
|
|
81
99
|
|
|
82
100
|
|
|
83
101
|
# @shell_orchestration: Coordinates doctest execution via testing module
|
|
84
102
|
def run_doctests_phase(
|
|
103
|
+
project_root: Path,
|
|
85
104
|
checked_files: list[Path],
|
|
86
105
|
explain: bool,
|
|
87
106
|
timeout: int = 60,
|
|
@@ -103,12 +122,20 @@ def run_doctests_phase(
|
|
|
103
122
|
return True, "", None
|
|
104
123
|
|
|
105
124
|
doctest_result = run_doctests_on_files(
|
|
106
|
-
checked_files,
|
|
125
|
+
checked_files,
|
|
126
|
+
verbose=explain,
|
|
127
|
+
timeout=timeout,
|
|
128
|
+
collect_coverage=collect_coverage,
|
|
129
|
+
cwd=project_root,
|
|
107
130
|
)
|
|
108
131
|
if isinstance(doctest_result, Success):
|
|
109
132
|
result_data = doctest_result.unwrap()
|
|
110
133
|
passed = result_data.get("status") in ("passed", "skipped")
|
|
111
|
-
|
|
134
|
+
stdout = result_data.get("stdout", "")
|
|
135
|
+
stderr = result_data.get("stderr", "")
|
|
136
|
+
output = stdout
|
|
137
|
+
if not passed and stderr:
|
|
138
|
+
output = f"{stdout}\n{stderr}" if stdout else stderr
|
|
112
139
|
# DX-37: Return coverage data if collected
|
|
113
140
|
coverage_data = {"collected": result_data.get("coverage_collected", False)}
|
|
114
141
|
return passed, output, coverage_data if collect_coverage else None
|
|
@@ -176,6 +203,7 @@ def run_crosshair_phase(
|
|
|
176
203
|
cache=cache,
|
|
177
204
|
timeout=timeout,
|
|
178
205
|
per_condition_timeout=per_condition_timeout,
|
|
206
|
+
project_root=path,
|
|
179
207
|
)
|
|
180
208
|
|
|
181
209
|
if isinstance(crosshair_result, Success):
|
|
@@ -230,26 +258,17 @@ def output_verification_status(
|
|
|
230
258
|
console.print(doctest_output)
|
|
231
259
|
|
|
232
260
|
# CrossHair results
|
|
233
|
-
_output_crosshair_status(
|
|
234
|
-
static_exit_code, doctest_passed, crosshair_output
|
|
235
|
-
)
|
|
261
|
+
_output_crosshair_status(static_exit_code, doctest_passed, crosshair_output)
|
|
236
262
|
|
|
237
263
|
# Property tests results
|
|
238
264
|
if property_output:
|
|
239
|
-
_output_property_tests_status(
|
|
240
|
-
static_exit_code, doctest_passed, property_output
|
|
241
|
-
)
|
|
265
|
+
_output_property_tests_status(static_exit_code, doctest_passed, property_output)
|
|
242
266
|
else:
|
|
243
267
|
console.print("[dim]⊘ Runtime tests skipped (static errors)[/dim]")
|
|
244
268
|
|
|
245
269
|
# DX-26: Combined conclusion after all phases
|
|
246
270
|
console.print("-" * 40)
|
|
247
|
-
all_passed =
|
|
248
|
-
static_exit_code == 0
|
|
249
|
-
and doctest_passed
|
|
250
|
-
and crosshair_passed
|
|
251
|
-
and property_passed
|
|
252
|
-
)
|
|
271
|
+
all_passed = static_exit_code == 0 and doctest_passed and crosshair_passed and property_passed
|
|
253
272
|
# In strict mode, warnings also cause failure (but exit code already reflects this)
|
|
254
273
|
status = "passed" if all_passed else "failed"
|
|
255
274
|
color = "green" if all_passed else "red"
|
|
@@ -259,6 +278,7 @@ def output_verification_status(
|
|
|
259
278
|
# @shell_orchestration: Coordinates shell module calls for property testing
|
|
260
279
|
# @shell_complexity: Property tests with result aggregation
|
|
261
280
|
def run_property_tests_phase(
|
|
281
|
+
project_root: Path,
|
|
262
282
|
checked_files: list[Path],
|
|
263
283
|
doctest_passed: bool,
|
|
264
284
|
static_exit_code: int,
|
|
@@ -290,7 +310,12 @@ def run_property_tests_phase(
|
|
|
290
310
|
if not core_files:
|
|
291
311
|
return True, {"status": "skipped", "reason": "no core files"}, None
|
|
292
312
|
|
|
293
|
-
result = run_property_tests_on_files(
|
|
313
|
+
result = run_property_tests_on_files(
|
|
314
|
+
core_files,
|
|
315
|
+
max_examples,
|
|
316
|
+
collect_coverage=collect_coverage,
|
|
317
|
+
project_root=project_root,
|
|
318
|
+
)
|
|
294
319
|
|
|
295
320
|
if isinstance(result, Success):
|
|
296
321
|
report, coverage_data = result.unwrap()
|
|
@@ -305,15 +330,19 @@ def run_property_tests_phase(
|
|
|
305
330
|
for r in report.results
|
|
306
331
|
if not r.passed
|
|
307
332
|
]
|
|
308
|
-
return
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
333
|
+
return (
|
|
334
|
+
report.all_passed(),
|
|
335
|
+
{
|
|
336
|
+
"status": "passed" if report.all_passed() else "failed",
|
|
337
|
+
"functions_tested": report.functions_tested,
|
|
338
|
+
"functions_passed": report.functions_passed,
|
|
339
|
+
"functions_failed": report.functions_failed,
|
|
340
|
+
"total_examples": report.total_examples,
|
|
341
|
+
"failures": failures, # DX-26: Structured failure info
|
|
342
|
+
"errors": report.errors,
|
|
343
|
+
},
|
|
344
|
+
coverage_data,
|
|
345
|
+
)
|
|
317
346
|
|
|
318
347
|
return False, {"status": "error", "error": result.failure()}, None
|
|
319
348
|
|
|
@@ -366,8 +395,8 @@ def _output_property_tests_status(
|
|
|
366
395
|
# Show reproduction command with seed
|
|
367
396
|
if seed:
|
|
368
397
|
console.print(
|
|
369
|
-
f
|
|
370
|
-
f
|
|
398
|
+
f' [dim]Reproduce: python -c "from hypothesis import reproduce_failure; '
|
|
399
|
+
f'import {func_name}" --seed={seed}[/dim]'
|
|
371
400
|
)
|
|
372
401
|
# Fallback for errors without structured failures
|
|
373
402
|
for error in property_output.get("errors", [])[:5]:
|
|
@@ -406,8 +435,7 @@ def _output_crosshair_status(
|
|
|
406
435
|
if workers > 1:
|
|
407
436
|
stats += f", {workers} workers"
|
|
408
437
|
console.print(
|
|
409
|
-
f"[green]✓ CrossHair verified[/green] "
|
|
410
|
-
f"[dim]({stats}, {time_sec:.1f}s)[/dim]"
|
|
438
|
+
f"[green]✓ CrossHair verified[/green] [dim]({stats}, {time_sec:.1f}s)[/dim]"
|
|
411
439
|
)
|
|
412
440
|
else:
|
|
413
441
|
console.print("[green]✓ CrossHair verified[/green]")
|
invar/shell/property_tests.py
CHANGED
|
@@ -9,28 +9,57 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import importlib.util
|
|
11
11
|
import sys
|
|
12
|
+
from contextlib import contextmanager, suppress
|
|
12
13
|
from typing import TYPE_CHECKING
|
|
13
14
|
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
14
18
|
from returns.result import Failure, Result, Success
|
|
15
19
|
from rich.console import Console
|
|
16
20
|
|
|
17
|
-
from invar.core.property_gen import
|
|
18
|
-
|
|
19
|
-
find_contracted_functions,
|
|
20
|
-
run_property_test,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
if TYPE_CHECKING:
|
|
24
|
-
from pathlib import Path
|
|
21
|
+
from invar.core.property_gen import PropertyTestReport, find_contracted_functions, run_property_test
|
|
22
|
+
from invar.shell.subprocess_env import detect_project_venv, find_site_packages
|
|
25
23
|
|
|
26
24
|
console = Console()
|
|
27
25
|
|
|
28
26
|
|
|
27
|
+
# @shell_orchestration: Temporarily inject venv site-packages for module imports
|
|
28
|
+
@contextmanager
|
|
29
|
+
def _inject_project_site_packages(project_root: Path):
|
|
30
|
+
venv = detect_project_venv(project_root)
|
|
31
|
+
site_packages = find_site_packages(venv) if venv is not None else None
|
|
32
|
+
|
|
33
|
+
if site_packages is None:
|
|
34
|
+
yield
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
src_dir = project_root / "src"
|
|
38
|
+
|
|
39
|
+
added: list[str] = []
|
|
40
|
+
if src_dir.exists():
|
|
41
|
+
src_dir_str = str(src_dir)
|
|
42
|
+
sys.path.insert(0, src_dir_str)
|
|
43
|
+
added.append(src_dir_str)
|
|
44
|
+
|
|
45
|
+
site_packages_str = str(site_packages)
|
|
46
|
+
sys.path.insert(0, site_packages_str)
|
|
47
|
+
added.append(site_packages_str)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
yield
|
|
51
|
+
finally:
|
|
52
|
+
for p in added:
|
|
53
|
+
with suppress(ValueError):
|
|
54
|
+
sys.path.remove(p)
|
|
55
|
+
|
|
56
|
+
|
|
29
57
|
# @shell_complexity: Property test orchestration with module import
|
|
30
58
|
def run_property_tests_on_file(
|
|
31
59
|
file_path: Path,
|
|
32
60
|
max_examples: int = 100,
|
|
33
61
|
verbose: bool = False,
|
|
62
|
+
project_root: Path | None = None,
|
|
34
63
|
) -> Result[PropertyTestReport, str]:
|
|
35
64
|
"""
|
|
36
65
|
Run property tests on all contracted functions in a file.
|
|
@@ -66,8 +95,10 @@ def run_property_tests_on_file(
|
|
|
66
95
|
if not contracted:
|
|
67
96
|
return Success(PropertyTestReport()) # No contracted functions, skip
|
|
68
97
|
|
|
69
|
-
|
|
70
|
-
|
|
98
|
+
root = project_root or file_path.parent
|
|
99
|
+
with _inject_project_site_packages(root):
|
|
100
|
+
module = _import_module_from_path(file_path)
|
|
101
|
+
|
|
71
102
|
if module is None:
|
|
72
103
|
return Failure(f"Could not import module: {file_path}")
|
|
73
104
|
|
|
@@ -105,6 +136,7 @@ def run_property_tests_on_files(
|
|
|
105
136
|
max_examples: int = 100,
|
|
106
137
|
verbose: bool = False,
|
|
107
138
|
collect_coverage: bool = False,
|
|
139
|
+
project_root: Path | None = None,
|
|
108
140
|
) -> Result[tuple[PropertyTestReport, dict | None], str]:
|
|
109
141
|
"""
|
|
110
142
|
Run property tests on multiple files.
|
|
@@ -122,9 +154,9 @@ def run_property_tests_on_files(
|
|
|
122
154
|
try:
|
|
123
155
|
import hypothesis # noqa: F401
|
|
124
156
|
except ImportError:
|
|
125
|
-
return Success(
|
|
126
|
-
errors=["Hypothesis not installed (pip install hypothesis)"]
|
|
127
|
-
)
|
|
157
|
+
return Success(
|
|
158
|
+
(PropertyTestReport(errors=["Hypothesis not installed (pip install hypothesis)"]), None)
|
|
159
|
+
)
|
|
128
160
|
|
|
129
161
|
combined_report = PropertyTestReport()
|
|
130
162
|
coverage_data = None
|
|
@@ -138,7 +170,9 @@ def run_property_tests_on_files(
|
|
|
138
170
|
source_dirs = list({f.parent for f in files})
|
|
139
171
|
with cov_ctx(source_dirs) as cov:
|
|
140
172
|
for file_path in files:
|
|
141
|
-
result = run_property_tests_on_file(
|
|
173
|
+
result = run_property_tests_on_file(
|
|
174
|
+
file_path, max_examples, verbose, project_root=project_root
|
|
175
|
+
)
|
|
142
176
|
_accumulate_report(combined_report, result)
|
|
143
177
|
|
|
144
178
|
# Extract coverage after all tests
|
|
@@ -151,11 +185,15 @@ def run_property_tests_on_files(
|
|
|
151
185
|
except ImportError:
|
|
152
186
|
# coverage not installed, run without it
|
|
153
187
|
for file_path in files:
|
|
154
|
-
result = run_property_tests_on_file(
|
|
188
|
+
result = run_property_tests_on_file(
|
|
189
|
+
file_path, max_examples, verbose, project_root=project_root
|
|
190
|
+
)
|
|
155
191
|
_accumulate_report(combined_report, result)
|
|
156
192
|
else:
|
|
157
193
|
for file_path in files:
|
|
158
|
-
result = run_property_tests_on_file(
|
|
194
|
+
result = run_property_tests_on_file(
|
|
195
|
+
file_path, max_examples, verbose, project_root=project_root
|
|
196
|
+
)
|
|
159
197
|
_accumulate_report(combined_report, result)
|
|
160
198
|
|
|
161
199
|
return Success((combined_report, coverage_data))
|
|
@@ -222,26 +260,29 @@ def format_property_test_report(
|
|
|
222
260
|
import json
|
|
223
261
|
|
|
224
262
|
if json_output:
|
|
225
|
-
return json.dumps(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
263
|
+
return json.dumps(
|
|
264
|
+
{
|
|
265
|
+
"functions_tested": report.functions_tested,
|
|
266
|
+
"functions_passed": report.functions_passed,
|
|
267
|
+
"functions_failed": report.functions_failed,
|
|
268
|
+
"functions_skipped": report.functions_skipped,
|
|
269
|
+
"total_examples": report.total_examples,
|
|
270
|
+
"all_passed": report.all_passed(),
|
|
271
|
+
"results": [
|
|
272
|
+
{
|
|
273
|
+
"function": r.function_name,
|
|
274
|
+
"passed": r.passed,
|
|
275
|
+
"examples": r.examples_run,
|
|
276
|
+
"error": r.error,
|
|
277
|
+
"file_path": r.file_path, # DX-26
|
|
278
|
+
"seed": r.seed, # DX-26
|
|
279
|
+
}
|
|
280
|
+
for r in report.results
|
|
281
|
+
],
|
|
282
|
+
"errors": report.errors,
|
|
283
|
+
},
|
|
284
|
+
indent=2,
|
|
285
|
+
)
|
|
245
286
|
|
|
246
287
|
# Human-readable format
|
|
247
288
|
lines = []
|
|
@@ -263,10 +304,16 @@ def format_property_test_report(
|
|
|
263
304
|
for result in report.results:
|
|
264
305
|
if not result.passed:
|
|
265
306
|
# DX-26: file::function format
|
|
266
|
-
location =
|
|
307
|
+
location = (
|
|
308
|
+
f"{result.file_path}::{result.function_name}"
|
|
309
|
+
if result.file_path
|
|
310
|
+
else result.function_name
|
|
311
|
+
)
|
|
267
312
|
lines.append(f" [red]✗[/red] {location}")
|
|
268
313
|
if result.error:
|
|
269
|
-
short_error =
|
|
314
|
+
short_error = (
|
|
315
|
+
result.error[:100] + "..." if len(result.error) > 100 else result.error
|
|
316
|
+
)
|
|
270
317
|
lines.append(f" {short_error}")
|
|
271
318
|
if result.seed:
|
|
272
319
|
lines.append(f" [dim]Seed: {result.seed}[/dim]")
|
invar/shell/prove/crosshair.py
CHANGED
|
@@ -12,7 +12,7 @@ import os
|
|
|
12
12
|
import subprocess
|
|
13
13
|
import sys
|
|
14
14
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
15
|
-
from pathlib import Path
|
|
15
|
+
from pathlib import Path
|
|
16
16
|
from typing import TYPE_CHECKING
|
|
17
17
|
|
|
18
18
|
from returns.result import Failure, Result, Success
|
|
@@ -82,10 +82,7 @@ def has_verifiable_contracts(source: str) -> bool:
|
|
|
82
82
|
if isinstance(func, ast.Name) and func.id in contract_decorators:
|
|
83
83
|
return True
|
|
84
84
|
# @deal.pre(...) or @deal.post(...)
|
|
85
|
-
if (
|
|
86
|
-
isinstance(func, ast.Attribute)
|
|
87
|
-
and func.attr in contract_decorators
|
|
88
|
-
):
|
|
85
|
+
if isinstance(func, ast.Attribute) and func.attr in contract_decorators:
|
|
89
86
|
return True
|
|
90
87
|
|
|
91
88
|
return False
|
|
@@ -102,6 +99,7 @@ def _verify_single_file(
|
|
|
102
99
|
max_iterations: int = 5,
|
|
103
100
|
timeout: int = 300,
|
|
104
101
|
per_condition_timeout: int = 30,
|
|
102
|
+
project_root: str | None = None,
|
|
105
103
|
) -> dict[str, Any]:
|
|
106
104
|
"""
|
|
107
105
|
Verify a single file with CrossHair.
|
|
@@ -133,13 +131,14 @@ def _verify_single_file(
|
|
|
133
131
|
]
|
|
134
132
|
|
|
135
133
|
try:
|
|
136
|
-
|
|
134
|
+
env_root = Path(project_root) if project_root else None
|
|
137
135
|
result = subprocess.run(
|
|
138
136
|
cmd,
|
|
139
137
|
capture_output=True,
|
|
140
138
|
text=True,
|
|
141
139
|
timeout=timeout,
|
|
142
|
-
|
|
140
|
+
cwd=project_root,
|
|
141
|
+
env=build_subprocess_env(cwd=env_root),
|
|
143
142
|
)
|
|
144
143
|
|
|
145
144
|
elapsed_ms = int((time.time() - start_time) * 1000)
|
|
@@ -222,6 +221,7 @@ def run_crosshair_parallel(
|
|
|
222
221
|
cache: ProveCache | None = None,
|
|
223
222
|
timeout: int = 300,
|
|
224
223
|
per_condition_timeout: int = 30,
|
|
224
|
+
project_root: Path | None = None,
|
|
225
225
|
) -> Result[dict, str]:
|
|
226
226
|
"""Run CrossHair on multiple files in parallel (DX-13).
|
|
227
227
|
|
|
@@ -331,7 +331,12 @@ def run_crosshair_parallel(
|
|
|
331
331
|
with ProcessPoolExecutor(max_workers=max_workers) as executor:
|
|
332
332
|
futures = {
|
|
333
333
|
executor.submit(
|
|
334
|
-
_verify_single_file,
|
|
334
|
+
_verify_single_file,
|
|
335
|
+
str(f.resolve()),
|
|
336
|
+
max_iterations,
|
|
337
|
+
timeout,
|
|
338
|
+
per_condition_timeout,
|
|
339
|
+
str(project_root) if project_root else None,
|
|
335
340
|
): f
|
|
336
341
|
for f in files_to_verify
|
|
337
342
|
}
|
|
@@ -355,7 +360,11 @@ def run_crosshair_parallel(
|
|
|
355
360
|
# Sequential execution (single file or max_workers=1)
|
|
356
361
|
for py_file in files_to_verify:
|
|
357
362
|
result = _verify_single_file(
|
|
358
|
-
str(py_file),
|
|
363
|
+
str(py_file.resolve()),
|
|
364
|
+
max_iterations,
|
|
365
|
+
timeout,
|
|
366
|
+
per_condition_timeout,
|
|
367
|
+
str(project_root) if project_root else None,
|
|
359
368
|
)
|
|
360
369
|
_process_verification_result(
|
|
361
370
|
result,
|
|
@@ -368,9 +377,7 @@ def run_crosshair_parallel(
|
|
|
368
377
|
total_time_ms += result.get("time_ms", 0)
|
|
369
378
|
|
|
370
379
|
# Determine overall status
|
|
371
|
-
status =
|
|
372
|
-
CrossHairStatus.VERIFIED if not failed_files else CrossHairStatus.COUNTEREXAMPLE
|
|
373
|
-
)
|
|
380
|
+
status = CrossHairStatus.VERIFIED if not failed_files else CrossHairStatus.COUNTEREXAMPLE
|
|
374
381
|
|
|
375
382
|
return Success(
|
|
376
383
|
{
|
invar/shell/subprocess_env.py
CHANGED
|
@@ -135,13 +135,17 @@ def build_subprocess_env(cwd: Path | None = None) -> dict[str, str]:
|
|
|
135
135
|
if site_packages is None:
|
|
136
136
|
return env
|
|
137
137
|
|
|
138
|
-
# Prepend to PYTHONPATH (project packages have priority)
|
|
139
138
|
current = env.get("PYTHONPATH", "")
|
|
140
139
|
separator = ";" if os.name == "nt" else ":"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
|
|
141
|
+
src_dir = project_root / "src"
|
|
142
|
+
prefix_parts: list[str] = []
|
|
143
|
+
if src_dir.exists():
|
|
144
|
+
prefix_parts.append(str(src_dir))
|
|
145
|
+
prefix_parts.append(str(site_packages))
|
|
146
|
+
|
|
147
|
+
prefix = separator.join(prefix_parts)
|
|
148
|
+
env["PYTHONPATH"] = f"{prefix}{separator}{current}" if current else prefix
|
|
145
149
|
|
|
146
150
|
return env
|
|
147
151
|
|
invar/shell/testing.py
CHANGED
|
@@ -119,6 +119,7 @@ def run_doctests_on_files(
|
|
|
119
119
|
verbose: bool = False,
|
|
120
120
|
timeout: int = 60,
|
|
121
121
|
collect_coverage: bool = False,
|
|
122
|
+
cwd: Path | None = None,
|
|
122
123
|
) -> Result[dict, str]:
|
|
123
124
|
"""
|
|
124
125
|
Run doctests on a list of Python files.
|
|
@@ -143,17 +144,16 @@ def run_doctests_on_files(
|
|
|
143
144
|
parts = f.parts
|
|
144
145
|
# Check for consecutive "templates/examples" or ".invar/examples"
|
|
145
146
|
for i in range(len(parts) - 1):
|
|
146
|
-
if (parts[i] == "templates" and parts[i + 1] == "examples") or
|
|
147
|
-
|
|
147
|
+
if (parts[i] == "templates" and parts[i + 1] == "examples") or (
|
|
148
|
+
parts[i] == ".invar" and parts[i + 1] == "examples"
|
|
149
|
+
):
|
|
148
150
|
return True
|
|
149
151
|
return False
|
|
150
152
|
|
|
151
153
|
py_files = [
|
|
152
|
-
f
|
|
153
|
-
|
|
154
|
-
and f.exists()
|
|
155
|
-
and f.name != "conftest.py"
|
|
156
|
-
and not is_excluded(f)
|
|
154
|
+
f
|
|
155
|
+
for f in files
|
|
156
|
+
if f.suffix == ".py" and f.exists() and f.name != "conftest.py" and not is_excluded(f)
|
|
157
157
|
]
|
|
158
158
|
if not py_files:
|
|
159
159
|
return Success({"status": "skipped", "reason": "no Python files", "files": []})
|
|
@@ -162,16 +162,26 @@ def run_doctests_on_files(
|
|
|
162
162
|
if collect_coverage:
|
|
163
163
|
# Use coverage run to wrap pytest
|
|
164
164
|
cmd = [
|
|
165
|
-
sys.executable,
|
|
165
|
+
sys.executable,
|
|
166
|
+
"-m",
|
|
167
|
+
"coverage",
|
|
168
|
+
"run",
|
|
166
169
|
"--branch", # Enable branch coverage
|
|
167
170
|
"--parallel-mode", # For merging with hypothesis later
|
|
168
|
-
"-m",
|
|
169
|
-
"
|
|
171
|
+
"-m",
|
|
172
|
+
"pytest",
|
|
173
|
+
"--doctest-modules",
|
|
174
|
+
"-x",
|
|
175
|
+
"--tb=short",
|
|
170
176
|
]
|
|
171
177
|
else:
|
|
172
178
|
cmd = [
|
|
173
|
-
sys.executable,
|
|
174
|
-
"
|
|
179
|
+
sys.executable,
|
|
180
|
+
"-m",
|
|
181
|
+
"pytest",
|
|
182
|
+
"--doctest-modules",
|
|
183
|
+
"-x",
|
|
184
|
+
"--tb=short",
|
|
175
185
|
]
|
|
176
186
|
cmd.extend(str(f) for f in py_files)
|
|
177
187
|
if verbose:
|
|
@@ -188,14 +198,16 @@ def run_doctests_on_files(
|
|
|
188
198
|
)
|
|
189
199
|
# Pytest exit codes: 0=passed, 5=no tests collected (also OK)
|
|
190
200
|
is_passed = result.returncode in (0, 5)
|
|
191
|
-
return Success(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
return Success(
|
|
202
|
+
{
|
|
203
|
+
"status": "passed" if is_passed else "failed",
|
|
204
|
+
"files": [str(f) for f in py_files],
|
|
205
|
+
"exit_code": result.returncode,
|
|
206
|
+
"stdout": result.stdout,
|
|
207
|
+
"stderr": result.stderr,
|
|
208
|
+
"coverage_collected": collect_coverage, # DX-37: Flag for caller
|
|
209
|
+
}
|
|
210
|
+
)
|
|
199
211
|
except subprocess.TimeoutExpired:
|
|
200
212
|
return Failure(f"Doctest timeout ({timeout}s)")
|
|
201
213
|
except Exception as e:
|
|
@@ -204,7 +216,11 @@ def run_doctests_on_files(
|
|
|
204
216
|
|
|
205
217
|
# @shell_complexity: Property test orchestration with subprocess
|
|
206
218
|
def run_test(
|
|
207
|
-
target: str,
|
|
219
|
+
target: str,
|
|
220
|
+
json_output: bool = False,
|
|
221
|
+
verbose: bool = False,
|
|
222
|
+
timeout: int = 300,
|
|
223
|
+
cwd: Path | None = None,
|
|
208
224
|
) -> Result[dict, str]:
|
|
209
225
|
"""
|
|
210
226
|
Run property-based tests using Hypothesis via deal.cases.
|
|
@@ -225,20 +241,25 @@ def run_test(
|
|
|
225
241
|
return Failure(f"Target must be a Python file: {target}")
|
|
226
242
|
|
|
227
243
|
cmd = [
|
|
228
|
-
sys.executable,
|
|
229
|
-
|
|
244
|
+
sys.executable,
|
|
245
|
+
"-m",
|
|
246
|
+
"pytest",
|
|
247
|
+
str(target_path),
|
|
248
|
+
"--doctest-modules",
|
|
249
|
+
"-x",
|
|
250
|
+
"--tb=short",
|
|
230
251
|
]
|
|
231
252
|
if verbose:
|
|
232
253
|
cmd.append("-v")
|
|
233
254
|
|
|
234
255
|
try:
|
|
235
|
-
# DX-52: Inject project venv site-packages for uvx compatibility
|
|
236
256
|
result = subprocess.run(
|
|
237
257
|
cmd,
|
|
238
258
|
capture_output=True,
|
|
239
259
|
text=True,
|
|
240
260
|
timeout=timeout,
|
|
241
|
-
|
|
261
|
+
cwd=str(cwd) if cwd is not None else None,
|
|
262
|
+
env=build_subprocess_env(cwd=cwd),
|
|
242
263
|
)
|
|
243
264
|
test_result = {
|
|
244
265
|
"status": "passed" if result.returncode == 0 else "failed",
|
|
@@ -274,6 +295,7 @@ def run_verify(
|
|
|
274
295
|
json_output: bool = False,
|
|
275
296
|
total_timeout: int = 300,
|
|
276
297
|
per_condition_timeout: int = 30,
|
|
298
|
+
cwd: Path | None = None,
|
|
277
299
|
) -> Result[dict, str]:
|
|
278
300
|
"""
|
|
279
301
|
Run symbolic verification using CrossHair.
|
|
@@ -302,23 +324,28 @@ def run_verify(
|
|
|
302
324
|
return Failure(f"Target must be a Python file: {target}")
|
|
303
325
|
|
|
304
326
|
cmd = [
|
|
305
|
-
sys.executable,
|
|
306
|
-
|
|
327
|
+
sys.executable,
|
|
328
|
+
"-m",
|
|
329
|
+
"crosshair",
|
|
330
|
+
"check",
|
|
331
|
+
str(target_path),
|
|
332
|
+
f"--per_condition_timeout={per_condition_timeout}",
|
|
307
333
|
]
|
|
308
334
|
|
|
309
335
|
try:
|
|
310
|
-
# DX-52: Inject project venv site-packages for uvx compatibility
|
|
311
336
|
result = subprocess.run(
|
|
312
337
|
cmd,
|
|
313
338
|
capture_output=True,
|
|
314
339
|
text=True,
|
|
315
340
|
timeout=total_timeout,
|
|
316
|
-
|
|
341
|
+
cwd=str(cwd) if cwd is not None else None,
|
|
342
|
+
env=build_subprocess_env(cwd=cwd),
|
|
317
343
|
)
|
|
318
344
|
|
|
319
345
|
# CrossHair format: "file:line: error: Err when calling func(...)"
|
|
320
346
|
counterexamples = [
|
|
321
|
-
line.strip()
|
|
347
|
+
line.strip()
|
|
348
|
+
for line in result.stdout.split("\n")
|
|
322
349
|
if ": error:" in line.lower() or "counterexample" in line.lower()
|
|
323
350
|
]
|
|
324
351
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: invar-tools
|
|
3
|
-
Version: 1.17.
|
|
3
|
+
Version: 1.17.20
|
|
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
|
|
@@ -23,13 +23,14 @@ Classifier: Typing :: Typed
|
|
|
23
23
|
Requires-Python: >=3.11
|
|
24
24
|
Requires-Dist: crosshair-tool>=0.0.60
|
|
25
25
|
Requires-Dist: hypothesis>=6.0
|
|
26
|
-
Requires-Dist: invar-runtime>=1.0
|
|
26
|
+
Requires-Dist: invar-runtime>=1.3.0
|
|
27
27
|
Requires-Dist: jedi>=0.19
|
|
28
28
|
Requires-Dist: jinja2>=3.0
|
|
29
29
|
Requires-Dist: markdown-it-py>=3.0
|
|
30
30
|
Requires-Dist: mcp>=1.0
|
|
31
31
|
Requires-Dist: pre-commit>=3.0
|
|
32
32
|
Requires-Dist: pydantic>=2.0
|
|
33
|
+
Requires-Dist: pytest>=7.0
|
|
33
34
|
Requires-Dist: questionary>=2.0
|
|
34
35
|
Requires-Dist: returns>=0.20
|
|
35
36
|
Requires-Dist: rich>=13.0
|
|
@@ -38,7 +39,6 @@ Provides-Extra: dev
|
|
|
38
39
|
Requires-Dist: coverage[toml]>=7.0; extra == 'dev'
|
|
39
40
|
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
40
41
|
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
41
|
-
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
42
42
|
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
43
43
|
Description-Content-Type: text/markdown
|
|
44
44
|
|
|
@@ -2657,31 +2657,31 @@ 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=6N4AvhYPSUzd3YGXPIc8edF6Lp492W-cS8wwnHUJotI,18119
|
|
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
|
-
invar/shell/git.py,sha256=
|
|
2666
|
-
invar/shell/guard_helpers.py,sha256=
|
|
2665
|
+
invar/shell/git.py,sha256=R-ynlYa65xtCdnNjHeu42uPyrqoo9KZDzl7BZUW0oWU,2866
|
|
2666
|
+
invar/shell/guard_helpers.py,sha256=lpaFIe328ZISzim92TAxZHTT8jC4N0_TcQ7PV7u327w,16083
|
|
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=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=
|
|
2676
|
+
invar/shell/subprocess_env.py,sha256=ToPqGw5bnfJIEk5pdvJBM6dslKxn6gqspDnhr1VRCDk,11554
|
|
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=
|
|
2679
|
+
invar/shell/testing.py,sha256=ig-LVe5k2-qvdz2H85mrY3_7_v440DJJX72Q3TVP5Xk,11472
|
|
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=
|
|
2684
|
+
invar/shell/commands/guard.py,sha256=ZeiK5FvRlTNHMLS6StIDKl-mDAdyAFGOovkRy0tG4eQ,25703
|
|
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=
|
|
2699
|
+
invar/shell/prove/crosshair.py,sha256=XDOoLvOrU_6OGYm2rTjWacdvKgpJ3NjUDetMwyT9kIE,16997
|
|
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.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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|