invar-tools 1.0.0__py3-none-any.whl → 1.2.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.
- invar/core/contracts.py +75 -5
- invar/core/entry_points.py +294 -0
- invar/core/format_specs.py +196 -0
- invar/core/format_strategies.py +197 -0
- invar/core/formatter.py +27 -4
- invar/core/hypothesis_strategies.py +47 -5
- invar/core/lambda_helpers.py +1 -0
- invar/core/models.py +23 -17
- invar/core/parser.py +6 -2
- invar/core/property_gen.py +81 -40
- invar/core/purity.py +10 -4
- invar/core/review_trigger.py +298 -0
- invar/core/rule_meta.py +61 -2
- invar/core/rules.py +83 -19
- invar/core/shell_analysis.py +252 -0
- invar/core/shell_architecture.py +171 -0
- invar/core/suggestions.py +6 -0
- invar/core/tautology.py +1 -0
- invar/core/utils.py +51 -4
- invar/core/verification_routing.py +158 -0
- invar/invariant.py +1 -0
- invar/mcp/server.py +20 -3
- invar/shell/cli.py +59 -31
- invar/shell/config.py +259 -10
- invar/shell/fs.py +5 -2
- invar/shell/git.py +2 -0
- invar/shell/guard_helpers.py +78 -3
- invar/shell/guard_output.py +100 -24
- invar/shell/init_cmd.py +27 -7
- invar/shell/mcp_config.py +3 -0
- invar/shell/mutate_cmd.py +184 -0
- invar/shell/mutation.py +314 -0
- invar/shell/perception.py +2 -0
- invar/shell/property_tests.py +17 -2
- invar/shell/prove.py +35 -3
- invar/shell/prove_accept.py +113 -0
- invar/shell/prove_fallback.py +148 -46
- invar/shell/templates.py +34 -0
- invar/shell/test_cmd.py +3 -1
- invar/shell/testing.py +6 -17
- invar/shell/update_cmd.py +2 -0
- invar/templates/CLAUDE.md.template +65 -9
- invar/templates/INVAR.md +96 -23
- invar/templates/aider.conf.yml.template +16 -14
- invar/templates/commands/review.md +200 -0
- invar/templates/cursorrules.template +22 -13
- invar/templates/examples/contracts.py +3 -1
- invar/templates/examples/core_shell.py +3 -1
- {invar_tools-1.0.0.dist-info → invar_tools-1.2.0.dist-info}/METADATA +81 -15
- invar_tools-1.2.0.dist-info/RECORD +77 -0
- invar_tools-1.2.0.dist-info/licenses/LICENSE +190 -0
- invar_tools-1.2.0.dist-info/licenses/LICENSE-GPL +674 -0
- invar_tools-1.2.0.dist-info/licenses/NOTICE +63 -0
- invar_tools-1.0.0.dist-info/RECORD +0 -64
- invar_tools-1.0.0.dist-info/licenses/LICENSE +0 -21
- {invar_tools-1.0.0.dist-info → invar_tools-1.2.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.0.0.dist-info → invar_tools-1.2.0.dist-info}/entry_points.txt +0 -0
invar/shell/guard_helpers.py
CHANGED
|
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
|
|
|
19
19
|
console = Console()
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
# @shell_complexity: Git changed mode with file collection
|
|
22
23
|
def handle_changed_mode(
|
|
23
24
|
path: Path,
|
|
24
25
|
) -> Result[tuple[set[Path], list[Path]], str]:
|
|
@@ -42,6 +43,8 @@ def handle_changed_mode(
|
|
|
42
43
|
return Success((only_files, list(only_files)))
|
|
43
44
|
|
|
44
45
|
|
|
46
|
+
# @shell_orchestration: Coordinates path classification and file collection
|
|
47
|
+
# @shell_complexity: File collection with path normalization
|
|
45
48
|
def collect_files_to_check(
|
|
46
49
|
path: Path, checked_files: list[Path]
|
|
47
50
|
) -> list[Path]:
|
|
@@ -77,6 +80,7 @@ def collect_files_to_check(
|
|
|
77
80
|
return result_files
|
|
78
81
|
|
|
79
82
|
|
|
83
|
+
# @shell_orchestration: Coordinates doctest execution via testing module
|
|
80
84
|
def run_doctests_phase(
|
|
81
85
|
checked_files: list[Path], explain: bool
|
|
82
86
|
) -> tuple[bool, str]:
|
|
@@ -99,6 +103,8 @@ def run_doctests_phase(
|
|
|
99
103
|
return False, doctest_result.failure()
|
|
100
104
|
|
|
101
105
|
|
|
106
|
+
# @shell_orchestration: Coordinates CrossHair verification via prove module
|
|
107
|
+
# @shell_complexity: CrossHair phase with conditional execution
|
|
102
108
|
def run_crosshair_phase(
|
|
103
109
|
path: Path,
|
|
104
110
|
checked_files: list[Path],
|
|
@@ -161,6 +167,7 @@ def run_crosshair_phase(
|
|
|
161
167
|
return False, {"status": "error", "error": crosshair_result.failure()}
|
|
162
168
|
|
|
163
169
|
|
|
170
|
+
# @shell_complexity: Status output with multiple phases
|
|
164
171
|
def output_verification_status(
|
|
165
172
|
verification_level: VerificationLevel,
|
|
166
173
|
static_exit_code: int,
|
|
@@ -169,17 +176,30 @@ def output_verification_status(
|
|
|
169
176
|
crosshair_output: dict,
|
|
170
177
|
explain: bool,
|
|
171
178
|
property_output: dict | None = None,
|
|
179
|
+
strict: bool = False,
|
|
172
180
|
) -> None:
|
|
173
181
|
"""Output verification status for human-readable mode.
|
|
174
182
|
|
|
175
183
|
DX-19: Simplified - STANDARD runs all phases (doctests + CrossHair + Hypothesis).
|
|
184
|
+
DX-26: Shows combined conclusion after all phase results.
|
|
176
185
|
"""
|
|
177
186
|
from invar.shell.testing import VerificationLevel
|
|
178
187
|
|
|
179
|
-
# STATIC mode: no runtime tests to report
|
|
188
|
+
# STATIC mode: no runtime tests to report (conclusion shown by output_rich)
|
|
180
189
|
if verification_level == VerificationLevel.STATIC:
|
|
181
190
|
return
|
|
182
191
|
|
|
192
|
+
# DX-26: Extract passed status from phase outputs
|
|
193
|
+
crosshair_passed = True
|
|
194
|
+
if crosshair_output:
|
|
195
|
+
crosshair_status = crosshair_output.get("status", "verified")
|
|
196
|
+
crosshair_passed = crosshair_status in ("verified", "skipped")
|
|
197
|
+
|
|
198
|
+
property_passed = True
|
|
199
|
+
if property_output:
|
|
200
|
+
property_status = property_output.get("status", "passed")
|
|
201
|
+
property_passed = property_status in ("passed", "skipped")
|
|
202
|
+
|
|
183
203
|
# STANDARD mode: report all test results
|
|
184
204
|
if static_exit_code == 0:
|
|
185
205
|
# Doctest results
|
|
@@ -203,7 +223,22 @@ def output_verification_status(
|
|
|
203
223
|
else:
|
|
204
224
|
console.print("[dim]⊘ Runtime tests skipped (static errors)[/dim]")
|
|
205
225
|
|
|
226
|
+
# DX-26: Combined conclusion after all phases
|
|
227
|
+
console.print("-" * 40)
|
|
228
|
+
all_passed = (
|
|
229
|
+
static_exit_code == 0
|
|
230
|
+
and doctest_passed
|
|
231
|
+
and crosshair_passed
|
|
232
|
+
and property_passed
|
|
233
|
+
)
|
|
234
|
+
# In strict mode, warnings also cause failure (but exit code already reflects this)
|
|
235
|
+
status = "passed" if all_passed else "failed"
|
|
236
|
+
color = "green" if all_passed else "red"
|
|
237
|
+
console.print(f"[{color}]Guard {status}.[/{color}]")
|
|
238
|
+
|
|
206
239
|
|
|
240
|
+
# @shell_orchestration: Coordinates shell module calls for property testing
|
|
241
|
+
# @shell_complexity: Property tests with result aggregation
|
|
207
242
|
def run_property_tests_phase(
|
|
208
243
|
checked_files: list[Path],
|
|
209
244
|
doctest_passed: bool,
|
|
@@ -238,24 +273,40 @@ def run_property_tests_phase(
|
|
|
238
273
|
|
|
239
274
|
if isinstance(result, Success):
|
|
240
275
|
report = result.unwrap()
|
|
276
|
+
# DX-26: Build structured failures array for actionable output
|
|
277
|
+
failures = [
|
|
278
|
+
{
|
|
279
|
+
"function": r.function_name,
|
|
280
|
+
"file_path": r.file_path,
|
|
281
|
+
"error": r.error,
|
|
282
|
+
"seed": r.seed,
|
|
283
|
+
}
|
|
284
|
+
for r in report.results
|
|
285
|
+
if not r.passed
|
|
286
|
+
]
|
|
241
287
|
return report.all_passed(), {
|
|
242
288
|
"status": "passed" if report.all_passed() else "failed",
|
|
243
289
|
"functions_tested": report.functions_tested,
|
|
244
290
|
"functions_passed": report.functions_passed,
|
|
245
291
|
"functions_failed": report.functions_failed,
|
|
246
292
|
"total_examples": report.total_examples,
|
|
293
|
+
"failures": failures, # DX-26: Structured failure info
|
|
247
294
|
"errors": report.errors,
|
|
248
295
|
}
|
|
249
296
|
|
|
250
297
|
return False, {"status": "error", "error": result.failure()}
|
|
251
298
|
|
|
252
299
|
|
|
300
|
+
# @shell_complexity: Property test status formatting
|
|
253
301
|
def _output_property_tests_status(
|
|
254
302
|
static_exit_code: int,
|
|
255
303
|
doctest_passed: bool,
|
|
256
304
|
property_output: dict,
|
|
257
305
|
) -> None:
|
|
258
|
-
"""Output property tests status (DX-08).
|
|
306
|
+
"""Output property tests status (DX-08, DX-26).
|
|
307
|
+
|
|
308
|
+
DX-26: Show file::function format and reproduction command for failures.
|
|
309
|
+
"""
|
|
259
310
|
if static_exit_code != 0 or not doctest_passed:
|
|
260
311
|
console.print("[dim]⊘ Property tests skipped (prior failures)[/dim]")
|
|
261
312
|
return
|
|
@@ -275,12 +326,36 @@ def _output_property_tests_status(
|
|
|
275
326
|
elif status == "failed":
|
|
276
327
|
failed = property_output.get("functions_failed", 0)
|
|
277
328
|
console.print(f"[red]✗ Property tests failed ({failed} functions)[/red]")
|
|
329
|
+
# DX-26: Show actionable failure info
|
|
330
|
+
for failure in property_output.get("failures", [])[:5]:
|
|
331
|
+
file_path = failure.get("file_path", "")
|
|
332
|
+
func_name = failure.get("function", "unknown")
|
|
333
|
+
seed = failure.get("seed")
|
|
334
|
+
error = failure.get("error", "")
|
|
335
|
+
|
|
336
|
+
# Show file::function format
|
|
337
|
+
location = f"{file_path}::{func_name}" if file_path else func_name
|
|
338
|
+
console.print(f" [red]✗[/red] {location}")
|
|
339
|
+
|
|
340
|
+
# Show truncated error
|
|
341
|
+
if error:
|
|
342
|
+
short_error = error[:100] + "..." if len(error) > 100 else error
|
|
343
|
+
console.print(f" {short_error}")
|
|
344
|
+
|
|
345
|
+
# Show reproduction command with seed
|
|
346
|
+
if seed:
|
|
347
|
+
console.print(
|
|
348
|
+
f" [dim]Reproduce: python -c \"from hypothesis import reproduce_failure; "
|
|
349
|
+
f"import {func_name}\" --seed={seed}[/dim]"
|
|
350
|
+
)
|
|
351
|
+
# Fallback for errors without structured failures
|
|
278
352
|
for error in property_output.get("errors", [])[:5]:
|
|
279
|
-
console.print(f" {error}")
|
|
353
|
+
console.print(f" [yellow]![/yellow] {error}")
|
|
280
354
|
else:
|
|
281
355
|
console.print(f"[yellow]! Property tests: {status}[/yellow]")
|
|
282
356
|
|
|
283
357
|
|
|
358
|
+
# @shell_complexity: CrossHair status formatting
|
|
284
359
|
def _output_crosshair_status(
|
|
285
360
|
static_exit_code: int,
|
|
286
361
|
doctest_passed: bool,
|
invar/shell/guard_output.py
CHANGED
|
@@ -3,18 +3,77 @@ Guard output formatters.
|
|
|
3
3
|
|
|
4
4
|
Shell module: handles output formatting for guard command.
|
|
5
5
|
Extracted from cli.py to reduce file size.
|
|
6
|
+
|
|
7
|
+
DX-22: Added verification routing statistics for de-duplication.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
from __future__ import annotations
|
|
9
11
|
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
10
14
|
from rich.console import Console
|
|
11
15
|
|
|
12
16
|
from invar.core.formatter import format_guard_agent
|
|
13
17
|
from invar.core.models import GuardReport, Severity
|
|
18
|
+
from invar.core.utils import get_combined_status
|
|
14
19
|
|
|
15
20
|
console = Console()
|
|
16
21
|
|
|
17
22
|
|
|
23
|
+
@dataclass
|
|
24
|
+
class VerificationStats:
|
|
25
|
+
"""
|
|
26
|
+
DX-22: De-duplicated verification statistics.
|
|
27
|
+
|
|
28
|
+
Tracks separate counts for CrossHair (proof) vs Hypothesis (testing)
|
|
29
|
+
to avoid misleading double-counting.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
crosshair_proven: int = 0
|
|
33
|
+
hypothesis_tested: int = 0
|
|
34
|
+
doctests_passed: int = 0
|
|
35
|
+
routed_to_hypothesis: int = 0 # Files routed due to C extensions
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def total_verified(self) -> int:
|
|
39
|
+
"""Total unique functions verified (no double-counting)."""
|
|
40
|
+
return self.crosshair_proven + self.hypothesis_tested
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def proof_coverage_pct(self) -> float:
|
|
44
|
+
"""Percentage of verifiable code proven by CrossHair."""
|
|
45
|
+
total = self.crosshair_proven + self.hypothesis_tested
|
|
46
|
+
if total == 0:
|
|
47
|
+
return 0.0
|
|
48
|
+
return (self.crosshair_proven / total) * 100
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# @shell_orchestration: Rich markup formatting tightly coupled to shell output
|
|
52
|
+
# @shell_complexity: Conditional formatting for each stat category
|
|
53
|
+
def format_verification_stats(stats: VerificationStats) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Format verification statistics for display.
|
|
56
|
+
|
|
57
|
+
DX-22: Shows de-duplicated counts distinguishing proof from testing.
|
|
58
|
+
"""
|
|
59
|
+
lines = []
|
|
60
|
+
lines.append("Verification breakdown:")
|
|
61
|
+
if stats.crosshair_proven > 0:
|
|
62
|
+
lines.append(f" ✓ Proven (CrossHair): {stats.crosshair_proven} functions")
|
|
63
|
+
if stats.hypothesis_tested > 0:
|
|
64
|
+
lines.append(f" ✓ Tested (Hypothesis): {stats.hypothesis_tested} functions")
|
|
65
|
+
if stats.routed_to_hypothesis > 0:
|
|
66
|
+
lines.append(
|
|
67
|
+
f" [dim](C-extension routing: {stats.routed_to_hypothesis} files)[/dim]"
|
|
68
|
+
)
|
|
69
|
+
if stats.doctests_passed > 0:
|
|
70
|
+
lines.append(f" ✓ Doctests: {stats.doctests_passed} passed")
|
|
71
|
+
if stats.total_verified > 0:
|
|
72
|
+
lines.append(f" Proof coverage: {stats.proof_coverage_pct:.0f}%")
|
|
73
|
+
return "\n".join(lines)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# @shell_complexity: Context display with line range extraction
|
|
18
77
|
def show_file_context(file_path: str) -> None:
|
|
19
78
|
"""
|
|
20
79
|
Show INSPECT section for a file (Phase 9.2 P14).
|
|
@@ -47,12 +106,14 @@ def show_file_context(file_path: str) -> None:
|
|
|
47
106
|
pass # Silently ignore errors in context display
|
|
48
107
|
|
|
49
108
|
|
|
109
|
+
# @shell_complexity: Rich formatting with conditional sections
|
|
50
110
|
def output_rich(
|
|
51
111
|
report: GuardReport,
|
|
52
112
|
strict_pure: bool = False,
|
|
53
113
|
changed_mode: bool = False,
|
|
54
114
|
pedantic_mode: bool = False,
|
|
55
115
|
explain_mode: bool = False,
|
|
116
|
+
static_mode: bool = False,
|
|
56
117
|
) -> None:
|
|
57
118
|
"""Output report using Rich formatting."""
|
|
58
119
|
console.print("\n[bold]Invar Guard Report[/bold]")
|
|
@@ -60,6 +121,7 @@ def output_rich(
|
|
|
60
121
|
mode_info = [
|
|
61
122
|
m
|
|
62
123
|
for m, c in [
|
|
124
|
+
("static", static_mode),
|
|
63
125
|
("strict-pure", strict_pure),
|
|
64
126
|
("changed-only", changed_mode),
|
|
65
127
|
("pedantic", pedantic_mode),
|
|
@@ -174,51 +236,62 @@ def output_rich(
|
|
|
174
236
|
"[dim]💡 Fix warnings in files you modified to improve code health.[/dim]"
|
|
175
237
|
)
|
|
176
238
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def output_json(report: GuardReport) -> None:
|
|
187
|
-
"""Output report as JSON."""
|
|
188
|
-
import json
|
|
189
|
-
|
|
190
|
-
output = {
|
|
191
|
-
"files_checked": report.files_checked,
|
|
192
|
-
"errors": report.errors,
|
|
193
|
-
"warnings": report.warnings,
|
|
194
|
-
"infos": report.infos,
|
|
195
|
-
"passed": report.passed,
|
|
196
|
-
"violations": [v.model_dump() for v in report.violations],
|
|
197
|
-
}
|
|
198
|
-
console.print(json.dumps(output, indent=2))
|
|
239
|
+
# DX-26: Show static-only conclusion for --static mode
|
|
240
|
+
# Full mode shows conclusion after all phases in output_verification_status()
|
|
241
|
+
if static_mode:
|
|
242
|
+
console.print(
|
|
243
|
+
f"\n[{'green' if report.passed else 'red'}]Guard {'passed' if report.passed else 'failed'}.[/]"
|
|
244
|
+
)
|
|
245
|
+
console.print(
|
|
246
|
+
"\n[dim]Note: --static mode skips runtime tests (doctests, CrossHair, Hypothesis).[/dim]"
|
|
247
|
+
)
|
|
199
248
|
|
|
200
249
|
|
|
250
|
+
# @shell_complexity: JSON output assembly with multiple sections
|
|
201
251
|
def output_agent(
|
|
202
252
|
report: GuardReport,
|
|
253
|
+
strict: bool = False,
|
|
203
254
|
doctest_passed: bool = True,
|
|
204
255
|
doctest_output: str = "",
|
|
205
256
|
crosshair_output: dict | None = None,
|
|
206
257
|
verification_level: str = "standard",
|
|
207
258
|
property_output: dict | None = None, # DX-08
|
|
259
|
+
routing_stats: dict | None = None, # DX-22
|
|
208
260
|
) -> None:
|
|
209
|
-
"""Output report in Agent-optimized JSON format (Phase 8.2 + DX-06 + DX-08 + DX-09).
|
|
261
|
+
"""Output report in Agent-optimized JSON format (Phase 8.2 + DX-06 + DX-08 + DX-09 + DX-22 + DX-26).
|
|
210
262
|
|
|
211
263
|
Args:
|
|
212
264
|
report: Guard analysis report
|
|
265
|
+
strict: Whether warnings are treated as errors
|
|
213
266
|
doctest_passed: Whether doctests passed
|
|
214
267
|
doctest_output: Doctest stdout (only if failed)
|
|
215
268
|
crosshair_output: CrossHair results dict
|
|
216
269
|
verification_level: Current level (static/standard)
|
|
217
270
|
property_output: Property test results dict (DX-08)
|
|
271
|
+
routing_stats: Smart routing statistics (DX-22)
|
|
272
|
+
|
|
273
|
+
DX-22: Adds routing stats showing CrossHair vs Hypothesis distribution.
|
|
274
|
+
DX-26: status now reflects ALL test phases, not just static analysis.
|
|
218
275
|
"""
|
|
219
276
|
import json
|
|
220
277
|
|
|
221
|
-
|
|
278
|
+
# DX-26: Extract passed status from phase outputs
|
|
279
|
+
crosshair_passed = True
|
|
280
|
+
if crosshair_output:
|
|
281
|
+
crosshair_status = crosshair_output.get("status", "verified")
|
|
282
|
+
crosshair_passed = crosshair_status in ("verified", "skipped")
|
|
283
|
+
|
|
284
|
+
property_passed = True
|
|
285
|
+
if property_output:
|
|
286
|
+
property_status = property_output.get("status", "passed")
|
|
287
|
+
property_passed = property_status in ("passed", "skipped")
|
|
288
|
+
|
|
289
|
+
# DX-26: Calculate combined status including all test phases
|
|
290
|
+
combined_status = get_combined_status(
|
|
291
|
+
report, strict, doctest_passed, crosshair_passed, property_passed
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
output = format_guard_agent(report, combined_status=combined_status)
|
|
222
295
|
# DX-09: Add verification level for Agent transparency
|
|
223
296
|
output["verification_level"] = verification_level
|
|
224
297
|
# DX-06: Add doctest results to agent output
|
|
@@ -232,4 +305,7 @@ def output_agent(
|
|
|
232
305
|
# DX-08: Add property test results if available
|
|
233
306
|
if property_output:
|
|
234
307
|
output["property_tests"] = property_output
|
|
308
|
+
# DX-22: Add smart routing statistics if available
|
|
309
|
+
if routing_stats:
|
|
310
|
+
output["routing"] = routing_stats
|
|
235
311
|
console.print(json.dumps(output, indent=2))
|
invar/shell/init_cmd.py
CHANGED
|
@@ -24,6 +24,7 @@ from invar.shell.mcp_config import (
|
|
|
24
24
|
from invar.shell.templates import (
|
|
25
25
|
add_config,
|
|
26
26
|
add_invar_reference,
|
|
27
|
+
copy_commands_directory,
|
|
27
28
|
copy_examples_directory,
|
|
28
29
|
copy_template,
|
|
29
30
|
create_agent_config,
|
|
@@ -35,6 +36,7 @@ from invar.shell.templates import (
|
|
|
35
36
|
console = Console()
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
# @shell_complexity: Claude init with config file detection
|
|
38
40
|
def run_claude_init(path: Path) -> bool:
|
|
39
41
|
"""
|
|
40
42
|
Run 'claude /init' to generate intelligent CLAUDE.md.
|
|
@@ -95,16 +97,27 @@ def append_invar_reference_to_claude_md(path: Path) -> bool:
|
|
|
95
97
|
|
|
96
98
|
## Invar Protocol
|
|
97
99
|
|
|
98
|
-
> **Protocol:** Follow [INVAR.md](./INVAR.md)
|
|
100
|
+
> **Protocol:** Follow [INVAR.md](./INVAR.md) — includes Check-In, USBV workflow, and Task Completion.
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
### Check-In
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
Your first message MUST display:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
✓ Check-In: guard PASS | top: <entry1>, <entry2>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Execute `invar guard --changed` and `invar map --top 10`, then show this one-line summary.
|
|
111
|
+
|
|
112
|
+
### Final
|
|
113
|
+
|
|
114
|
+
Your last message MUST display:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
✓ Final: guard PASS | 0 errors, 2 warnings
|
|
105
118
|
```
|
|
106
119
|
|
|
107
|
-
|
|
120
|
+
Execute `invar guard` and show this one-line summary.
|
|
108
121
|
"""
|
|
109
122
|
|
|
110
123
|
claude_md.write_text(content + invar_reference)
|
|
@@ -112,6 +125,7 @@ invar map --top 10 # or: invar_map(top=10)
|
|
|
112
125
|
return True
|
|
113
126
|
|
|
114
127
|
|
|
128
|
+
# @shell_complexity: MCP config with method selection and validation
|
|
115
129
|
def configure_mcp_with_method(
|
|
116
130
|
path: Path, mcp_method: str | None
|
|
117
131
|
) -> None:
|
|
@@ -163,6 +177,7 @@ def show_available_mcp_methods() -> None:
|
|
|
163
177
|
console.print(f" {marker} {method.method.value}: {method.description}")
|
|
164
178
|
|
|
165
179
|
|
|
180
|
+
# @shell_complexity: Project init with config detection and template setup
|
|
166
181
|
def init(
|
|
167
182
|
path: Path = typer.Argument(Path(), help="Project root directory"),
|
|
168
183
|
claude: bool = typer.Option(
|
|
@@ -187,7 +202,9 @@ def init(
|
|
|
187
202
|
Initialize Invar configuration in a project.
|
|
188
203
|
|
|
189
204
|
Works with or without pyproject.toml:
|
|
190
|
-
|
|
205
|
+
|
|
206
|
+
\b
|
|
207
|
+
- If pyproject.toml exists: adds tool.invar section
|
|
191
208
|
- Otherwise: creates invar.toml
|
|
192
209
|
|
|
193
210
|
Use --claude to run 'claude /init' first (recommended for Claude Code users).
|
|
@@ -256,6 +273,9 @@ def init(
|
|
|
256
273
|
# Create full template with workflow enforcement (DX-17)
|
|
257
274
|
create_agent_config(path, agent, console)
|
|
258
275
|
|
|
276
|
+
# Copy Claude commands (DX-32: /review skill with Mode Detection)
|
|
277
|
+
copy_commands_directory(path, console)
|
|
278
|
+
|
|
259
279
|
# Configure MCP server (DX-16, DX-21B)
|
|
260
280
|
configure_mcp_with_method(path, mcp_method)
|
|
261
281
|
|
invar/shell/mcp_config.py
CHANGED
|
@@ -100,6 +100,7 @@ def detect_available_methods() -> list[McpExecConfig]:
|
|
|
100
100
|
return methods
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
# @shell_orchestration: MCP method selection helper
|
|
103
104
|
def get_recommended_method() -> McpExecConfig:
|
|
104
105
|
"""
|
|
105
106
|
Get the recommended MCP execution method.
|
|
@@ -115,6 +116,7 @@ def get_recommended_method() -> McpExecConfig:
|
|
|
115
116
|
return methods[0]
|
|
116
117
|
|
|
117
118
|
|
|
119
|
+
# @shell_orchestration: MCP method lookup helper
|
|
118
120
|
def get_method_by_name(name: str) -> McpExecConfig | None:
|
|
119
121
|
"""
|
|
120
122
|
Get a specific MCP method by name.
|
|
@@ -139,6 +141,7 @@ def get_method_by_name(name: str) -> McpExecConfig | None:
|
|
|
139
141
|
return None
|
|
140
142
|
|
|
141
143
|
|
|
144
|
+
# @shell_orchestration: MCP configuration generator
|
|
142
145
|
def generate_mcp_json(config: McpExecConfig | None = None) -> dict[str, Any]:
|
|
143
146
|
"""
|
|
144
147
|
Generate .mcp.json content for the given configuration.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mutation testing command for Invar CLI.
|
|
3
|
+
|
|
4
|
+
DX-28: `invar mutate` wraps mutmut to detect undertested code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json as json_lib
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from returns.result import Failure
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from invar.shell.mutation import (
|
|
17
|
+
MutationResult,
|
|
18
|
+
check_mutmut_installed,
|
|
19
|
+
get_surviving_mutants,
|
|
20
|
+
run_mutation_test,
|
|
21
|
+
show_mutant,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# @shell:entry - CLI command entry point
|
|
28
|
+
# @invar:allow entry_point_too_thick: CLI orchestration with multiple output modes
|
|
29
|
+
def mutate(
|
|
30
|
+
target: Path = typer.Argument(
|
|
31
|
+
Path(),
|
|
32
|
+
help="File or directory to mutate",
|
|
33
|
+
exists=True,
|
|
34
|
+
),
|
|
35
|
+
tests: Path = typer.Option(
|
|
36
|
+
None,
|
|
37
|
+
"--tests",
|
|
38
|
+
"-t",
|
|
39
|
+
help="Test directory (auto-detected if not specified)",
|
|
40
|
+
),
|
|
41
|
+
timeout: int = typer.Option(
|
|
42
|
+
300,
|
|
43
|
+
"--timeout",
|
|
44
|
+
help="Maximum time in seconds",
|
|
45
|
+
),
|
|
46
|
+
show_survivors: bool = typer.Option(
|
|
47
|
+
False,
|
|
48
|
+
"--survivors",
|
|
49
|
+
"-s",
|
|
50
|
+
help="Show surviving mutants",
|
|
51
|
+
),
|
|
52
|
+
json_output: bool = typer.Option(
|
|
53
|
+
False,
|
|
54
|
+
"--json",
|
|
55
|
+
help="Output as JSON",
|
|
56
|
+
),
|
|
57
|
+
) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Run mutation testing to find undertested code.
|
|
60
|
+
|
|
61
|
+
DX-28: Uses mutmut to automatically mutate code (e.g., `in` → `not in`)
|
|
62
|
+
and check if tests catch the mutations. Surviving mutants indicate
|
|
63
|
+
weak test coverage.
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
|
|
67
|
+
invar mutate src/myapp/core/parser.py
|
|
68
|
+
|
|
69
|
+
invar mutate src/myapp --tests tests/ --timeout 600
|
|
70
|
+
|
|
71
|
+
invar mutate --survivors # Show surviving mutants from last run
|
|
72
|
+
"""
|
|
73
|
+
# Check if mutmut is installed
|
|
74
|
+
install_check = check_mutmut_installed()
|
|
75
|
+
if isinstance(install_check, Failure):
|
|
76
|
+
if json_output:
|
|
77
|
+
console.print(json_lib.dumps({"error": install_check.failure()}))
|
|
78
|
+
else:
|
|
79
|
+
console.print(f"[red]Error:[/red] {install_check.failure()}")
|
|
80
|
+
console.print("\n[dim]Install with: pip install mutmut[/dim]")
|
|
81
|
+
raise typer.Exit(1)
|
|
82
|
+
|
|
83
|
+
# If just showing survivors from last run
|
|
84
|
+
if show_survivors:
|
|
85
|
+
result = get_surviving_mutants(target)
|
|
86
|
+
if isinstance(result, Failure):
|
|
87
|
+
if json_output:
|
|
88
|
+
console.print(json_lib.dumps({"error": result.failure()}))
|
|
89
|
+
else:
|
|
90
|
+
console.print(f"[red]Error:[/red] {result.failure()}")
|
|
91
|
+
raise typer.Exit(1)
|
|
92
|
+
|
|
93
|
+
survivors = result.unwrap()
|
|
94
|
+
if json_output:
|
|
95
|
+
console.print(json_lib.dumps({"survivors": survivors}))
|
|
96
|
+
else:
|
|
97
|
+
if survivors:
|
|
98
|
+
console.print(f"[yellow]Surviving mutants ({len(survivors)}):[/yellow]")
|
|
99
|
+
for s in survivors:
|
|
100
|
+
console.print(f" {s}")
|
|
101
|
+
else:
|
|
102
|
+
console.print("[green]No surviving mutants![/green]")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# Run mutation testing
|
|
106
|
+
if not json_output:
|
|
107
|
+
console.print(f"[bold]Running mutation testing on {target}...[/bold]")
|
|
108
|
+
console.print("[dim]This may take a while.[/dim]\n")
|
|
109
|
+
|
|
110
|
+
result = run_mutation_test(target, tests, timeout)
|
|
111
|
+
|
|
112
|
+
if isinstance(result, Failure):
|
|
113
|
+
if json_output:
|
|
114
|
+
console.print(json_lib.dumps({"error": result.failure()}))
|
|
115
|
+
else:
|
|
116
|
+
console.print(f"[red]Error:[/red] {result.failure()}")
|
|
117
|
+
raise typer.Exit(1)
|
|
118
|
+
|
|
119
|
+
mutation_result = result.unwrap()
|
|
120
|
+
_display_mutation_result(mutation_result, json_output)
|
|
121
|
+
|
|
122
|
+
# Exit with error if mutation score is too low
|
|
123
|
+
if not mutation_result.passed:
|
|
124
|
+
raise typer.Exit(1)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# @shell_complexity: Result display with dual output modes (JSON/human)
|
|
128
|
+
def _display_mutation_result(result: MutationResult, json_output: bool) -> None:
|
|
129
|
+
"""Display mutation testing results."""
|
|
130
|
+
if json_output:
|
|
131
|
+
data = {
|
|
132
|
+
"total": result.total,
|
|
133
|
+
"killed": result.killed,
|
|
134
|
+
"survived": result.survived,
|
|
135
|
+
"timeout": result.timeout,
|
|
136
|
+
"score": round(result.score, 1),
|
|
137
|
+
"passed": result.passed,
|
|
138
|
+
"errors": result.errors,
|
|
139
|
+
"survivors": result.survivors,
|
|
140
|
+
}
|
|
141
|
+
console.print(json_lib.dumps(data, indent=2))
|
|
142
|
+
else:
|
|
143
|
+
# Human-readable output
|
|
144
|
+
score_color = "green" if result.passed else "red"
|
|
145
|
+
|
|
146
|
+
console.print("\n[bold]Mutation Testing Results[/bold]")
|
|
147
|
+
console.print(f" Total mutants: {result.total}")
|
|
148
|
+
console.print(f" [green]Killed:[/green] {result.killed}")
|
|
149
|
+
console.print(f" [red]Survived:[/red] {result.survived}")
|
|
150
|
+
if result.timeout > 0:
|
|
151
|
+
console.print(f" [yellow]Timeout:[/yellow] {result.timeout}")
|
|
152
|
+
|
|
153
|
+
console.print(
|
|
154
|
+
f"\n [{score_color}]Mutation Score: {result.score:.1f}%[/{score_color}]"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if result.passed:
|
|
158
|
+
console.print("\n[green]✓ Mutation testing passed (≥80% killed)[/green]")
|
|
159
|
+
else:
|
|
160
|
+
console.print("\n[red]✗ Mutation testing failed (<80% killed)[/red]")
|
|
161
|
+
console.print("[dim]Run with --survivors to see surviving mutants[/dim]")
|
|
162
|
+
|
|
163
|
+
if result.errors:
|
|
164
|
+
console.print("\n[yellow]Errors:[/yellow]")
|
|
165
|
+
for err in result.errors:
|
|
166
|
+
console.print(f" {err}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# @shell:entry - CLI command for showing mutant details
|
|
170
|
+
def mutant_show(
|
|
171
|
+
mutant_id: int = typer.Argument(..., help="Mutant ID to show"),
|
|
172
|
+
) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Show the diff for a specific mutant.
|
|
175
|
+
|
|
176
|
+
Use after `invar mutate --survivors` to investigate surviving mutants.
|
|
177
|
+
"""
|
|
178
|
+
result = show_mutant(mutant_id)
|
|
179
|
+
|
|
180
|
+
if isinstance(result, Failure):
|
|
181
|
+
console.print(f"[red]Error:[/red] {result.failure()}")
|
|
182
|
+
raise typer.Exit(1)
|
|
183
|
+
|
|
184
|
+
console.print(result.unwrap())
|