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.
- invar/__init__.py +1 -0
- invar/core/contracts.py +80 -10
- invar/core/entry_points.py +367 -0
- invar/core/extraction.py +5 -6
- invar/core/format_specs.py +195 -0
- invar/core/format_strategies.py +197 -0
- invar/core/formatter.py +32 -10
- invar/core/hypothesis_strategies.py +50 -10
- invar/core/inspect.py +1 -1
- invar/core/lambda_helpers.py +3 -2
- invar/core/models.py +30 -18
- invar/core/must_use.py +2 -1
- invar/core/parser.py +13 -6
- invar/core/postcondition_scope.py +128 -0
- invar/core/property_gen.py +86 -42
- invar/core/purity.py +13 -7
- invar/core/purity_heuristics.py +5 -9
- invar/core/references.py +8 -6
- invar/core/review_trigger.py +370 -0
- invar/core/rule_meta.py +69 -2
- invar/core/rules.py +91 -28
- invar/core/shell_analysis.py +247 -0
- invar/core/shell_architecture.py +171 -0
- invar/core/strategies.py +7 -14
- invar/core/suggestions.py +92 -0
- invar/core/sync_helpers.py +238 -0
- invar/core/tautology.py +103 -37
- invar/core/template_parser.py +467 -0
- invar/core/timeout_inference.py +4 -7
- invar/core/utils.py +63 -18
- invar/core/verification_routing.py +155 -0
- invar/mcp/server.py +113 -13
- invar/shell/commands/__init__.py +11 -0
- invar/shell/{cli.py → commands/guard.py} +152 -44
- invar/shell/{init_cmd.py → commands/init.py} +200 -28
- invar/shell/commands/merge.py +256 -0
- invar/shell/commands/mutate.py +184 -0
- invar/shell/{perception.py → commands/perception.py} +2 -0
- invar/shell/commands/sync_self.py +113 -0
- invar/shell/commands/template_sync.py +366 -0
- invar/shell/{test_cmd.py → commands/test.py} +3 -1
- invar/shell/commands/update.py +48 -0
- invar/shell/config.py +247 -10
- invar/shell/coverage.py +351 -0
- invar/shell/fs.py +5 -2
- invar/shell/git.py +2 -0
- invar/shell/guard_helpers.py +116 -20
- invar/shell/guard_output.py +106 -24
- invar/shell/mcp_config.py +3 -0
- invar/shell/mutation.py +314 -0
- invar/shell/property_tests.py +75 -24
- invar/shell/prove/__init__.py +9 -0
- invar/shell/prove/accept.py +113 -0
- invar/shell/{prove.py → prove/crosshair.py} +69 -30
- invar/shell/prove/hypothesis.py +293 -0
- invar/shell/subprocess_env.py +393 -0
- invar/shell/template_engine.py +345 -0
- invar/shell/templates.py +53 -0
- invar/shell/testing.py +77 -37
- invar/templates/CLAUDE.md.template +86 -9
- invar/templates/aider.conf.yml.template +16 -14
- invar/templates/commands/audit.md +138 -0
- invar/templates/commands/guard.md +77 -0
- invar/templates/config/CLAUDE.md.jinja +206 -0
- invar/templates/config/context.md.jinja +92 -0
- invar/templates/config/pre-commit.yaml.jinja +44 -0
- invar/templates/context.md.template +33 -0
- invar/templates/cursorrules.template +25 -13
- invar/templates/examples/README.md +2 -0
- invar/templates/examples/conftest.py +3 -0
- invar/templates/examples/contracts.py +4 -2
- invar/templates/examples/core_shell.py +10 -4
- invar/templates/examples/workflow.md +81 -0
- invar/templates/manifest.toml +137 -0
- invar/templates/protocol/INVAR.md +210 -0
- invar/templates/skills/develop/SKILL.md.jinja +318 -0
- invar/templates/skills/investigate/SKILL.md.jinja +106 -0
- invar/templates/skills/propose/SKILL.md.jinja +104 -0
- invar/templates/skills/review/SKILL.md.jinja +125 -0
- invar_tools-1.3.0.dist-info/METADATA +377 -0
- invar_tools-1.3.0.dist-info/RECORD +95 -0
- invar_tools-1.3.0.dist-info/entry_points.txt +2 -0
- invar_tools-1.3.0.dist-info/licenses/LICENSE +190 -0
- invar_tools-1.3.0.dist-info/licenses/LICENSE-GPL +674 -0
- invar_tools-1.3.0.dist-info/licenses/NOTICE +63 -0
- invar/contracts.py +0 -152
- invar/decorators.py +0 -94
- invar/invariant.py +0 -57
- invar/resource.py +0 -99
- invar/shell/prove_fallback.py +0 -183
- invar/shell/update_cmd.py +0 -191
- invar/templates/INVAR.md +0 -134
- invar_tools-1.0.0.dist-info/METADATA +0 -321
- invar_tools-1.0.0.dist-info/RECORD +0 -64
- invar_tools-1.0.0.dist-info/entry_points.txt +0 -2
- invar_tools-1.0.0.dist-info/licenses/LICENSE +0 -21
- /invar/shell/{prove_cache.py → prove/cache.py} +0 -0
- {invar_tools-1.0.0.dist-info → invar_tools-1.3.0.dist-info}/WHEEL +0 -0
invar/shell/git.py
CHANGED
|
@@ -28,6 +28,7 @@ def _run_git(args: list[str], cwd: Path) -> Result[str, str]:
|
|
|
28
28
|
return Failure(f"Git error: {e}")
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
# @shell_orchestration: Helper for git output parsing, tightly coupled to Shell
|
|
31
32
|
def _parse_py_files(output: str, project_root: Path) -> set[Path]:
|
|
32
33
|
"""Parse git output and return Python file paths."""
|
|
33
34
|
files: set[Path] = set()
|
|
@@ -37,6 +38,7 @@ def _parse_py_files(output: str, project_root: Path) -> set[Path]:
|
|
|
37
38
|
return files
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
# @shell_complexity: Git operations require multiple subprocess calls with error handling
|
|
40
42
|
def get_changed_files(project_root: Path) -> Result[set[Path], str]:
|
|
41
43
|
"""
|
|
42
44
|
Get Python files modified according to git (staged, unstaged, untracked).
|
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,34 +80,52 @@ 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
|
-
checked_files: list[Path],
|
|
82
|
-
|
|
85
|
+
checked_files: list[Path],
|
|
86
|
+
explain: bool,
|
|
87
|
+
timeout: int = 60,
|
|
88
|
+
collect_coverage: bool = False,
|
|
89
|
+
) -> tuple[bool, str, dict | None]:
|
|
83
90
|
"""Run doctests on collected files.
|
|
84
91
|
|
|
85
|
-
|
|
92
|
+
Args:
|
|
93
|
+
checked_files: Files to run doctests on
|
|
94
|
+
explain: Show verbose output
|
|
95
|
+
timeout: Maximum time in seconds (default: 60, from RuleConfig.timeout_doctest)
|
|
96
|
+
collect_coverage: DX-37: If True, collect branch coverage data
|
|
97
|
+
|
|
98
|
+
Returns (passed, output, coverage_data).
|
|
86
99
|
"""
|
|
87
100
|
from invar.shell.testing import run_doctests_on_files
|
|
88
101
|
|
|
89
102
|
if not checked_files:
|
|
90
|
-
return True, ""
|
|
103
|
+
return True, "", None
|
|
91
104
|
|
|
92
|
-
doctest_result = run_doctests_on_files(
|
|
105
|
+
doctest_result = run_doctests_on_files(
|
|
106
|
+
checked_files, verbose=explain, timeout=timeout, collect_coverage=collect_coverage
|
|
107
|
+
)
|
|
93
108
|
if isinstance(doctest_result, Success):
|
|
94
109
|
result_data = doctest_result.unwrap()
|
|
95
110
|
passed = result_data.get("status") in ("passed", "skipped")
|
|
96
111
|
output = result_data.get("stdout", "")
|
|
97
|
-
|
|
112
|
+
# DX-37: Return coverage data if collected
|
|
113
|
+
coverage_data = {"collected": result_data.get("coverage_collected", False)}
|
|
114
|
+
return passed, output, coverage_data if collect_coverage else None
|
|
98
115
|
|
|
99
|
-
return False, doctest_result.failure()
|
|
116
|
+
return False, doctest_result.failure(), None
|
|
100
117
|
|
|
101
118
|
|
|
119
|
+
# @shell_orchestration: Coordinates CrossHair verification via prove module
|
|
120
|
+
# @shell_complexity: CrossHair phase with conditional execution
|
|
102
121
|
def run_crosshair_phase(
|
|
103
122
|
path: Path,
|
|
104
123
|
checked_files: list[Path],
|
|
105
124
|
doctest_passed: bool,
|
|
106
125
|
static_exit_code: int,
|
|
107
126
|
changed_mode: bool = False,
|
|
127
|
+
timeout: int = 300,
|
|
128
|
+
per_condition_timeout: int = 30,
|
|
108
129
|
) -> tuple[bool, dict]:
|
|
109
130
|
"""Run CrossHair verification phase.
|
|
110
131
|
|
|
@@ -114,10 +135,12 @@ def run_crosshair_phase(
|
|
|
114
135
|
doctest_passed: Whether doctests passed
|
|
115
136
|
static_exit_code: Exit code from static analysis
|
|
116
137
|
changed_mode: If True, only verify git-changed files (--changed flag)
|
|
138
|
+
timeout: Max time per file in seconds (default: 300)
|
|
139
|
+
per_condition_timeout: Max time per contract in seconds (default: 30)
|
|
117
140
|
|
|
118
141
|
Returns (passed, output_dict).
|
|
119
142
|
"""
|
|
120
|
-
from invar.shell.
|
|
143
|
+
from invar.shell.prove.cache import ProveCache
|
|
121
144
|
from invar.shell.testing import get_files_to_prove, run_crosshair_parallel
|
|
122
145
|
|
|
123
146
|
# Skip if prior failures
|
|
@@ -151,6 +174,8 @@ def run_crosshair_phase(
|
|
|
151
174
|
max_iterations=5,
|
|
152
175
|
max_workers=None,
|
|
153
176
|
cache=cache,
|
|
177
|
+
timeout=timeout,
|
|
178
|
+
per_condition_timeout=per_condition_timeout,
|
|
154
179
|
)
|
|
155
180
|
|
|
156
181
|
if isinstance(crosshair_result, Success):
|
|
@@ -161,6 +186,7 @@ def run_crosshair_phase(
|
|
|
161
186
|
return False, {"status": "error", "error": crosshair_result.failure()}
|
|
162
187
|
|
|
163
188
|
|
|
189
|
+
# @shell_complexity: Status output with multiple phases
|
|
164
190
|
def output_verification_status(
|
|
165
191
|
verification_level: VerificationLevel,
|
|
166
192
|
static_exit_code: int,
|
|
@@ -169,17 +195,30 @@ def output_verification_status(
|
|
|
169
195
|
crosshair_output: dict,
|
|
170
196
|
explain: bool,
|
|
171
197
|
property_output: dict | None = None,
|
|
198
|
+
strict: bool = False,
|
|
172
199
|
) -> None:
|
|
173
200
|
"""Output verification status for human-readable mode.
|
|
174
201
|
|
|
175
202
|
DX-19: Simplified - STANDARD runs all phases (doctests + CrossHair + Hypothesis).
|
|
203
|
+
DX-26: Shows combined conclusion after all phase results.
|
|
176
204
|
"""
|
|
177
205
|
from invar.shell.testing import VerificationLevel
|
|
178
206
|
|
|
179
|
-
# STATIC mode: no runtime tests to report
|
|
207
|
+
# STATIC mode: no runtime tests to report (conclusion shown by output_rich)
|
|
180
208
|
if verification_level == VerificationLevel.STATIC:
|
|
181
209
|
return
|
|
182
210
|
|
|
211
|
+
# DX-26: Extract passed status from phase outputs
|
|
212
|
+
crosshair_passed = True
|
|
213
|
+
if crosshair_output:
|
|
214
|
+
crosshair_status = crosshair_output.get("status", "verified")
|
|
215
|
+
crosshair_passed = crosshair_status in ("verified", "skipped")
|
|
216
|
+
|
|
217
|
+
property_passed = True
|
|
218
|
+
if property_output:
|
|
219
|
+
property_status = property_output.get("status", "passed")
|
|
220
|
+
property_passed = property_status in ("passed", "skipped")
|
|
221
|
+
|
|
183
222
|
# STANDARD mode: report all test results
|
|
184
223
|
if static_exit_code == 0:
|
|
185
224
|
# Doctest results
|
|
@@ -203,13 +242,29 @@ def output_verification_status(
|
|
|
203
242
|
else:
|
|
204
243
|
console.print("[dim]⊘ Runtime tests skipped (static errors)[/dim]")
|
|
205
244
|
|
|
245
|
+
# DX-26: Combined conclusion after all phases
|
|
246
|
+
console.print("-" * 40)
|
|
247
|
+
all_passed = (
|
|
248
|
+
static_exit_code == 0
|
|
249
|
+
and doctest_passed
|
|
250
|
+
and crosshair_passed
|
|
251
|
+
and property_passed
|
|
252
|
+
)
|
|
253
|
+
# In strict mode, warnings also cause failure (but exit code already reflects this)
|
|
254
|
+
status = "passed" if all_passed else "failed"
|
|
255
|
+
color = "green" if all_passed else "red"
|
|
256
|
+
console.print(f"[{color}]Guard {status}.[/{color}]")
|
|
257
|
+
|
|
206
258
|
|
|
259
|
+
# @shell_orchestration: Coordinates shell module calls for property testing
|
|
260
|
+
# @shell_complexity: Property tests with result aggregation
|
|
207
261
|
def run_property_tests_phase(
|
|
208
262
|
checked_files: list[Path],
|
|
209
263
|
doctest_passed: bool,
|
|
210
264
|
static_exit_code: int,
|
|
211
265
|
max_examples: int = 100,
|
|
212
|
-
|
|
266
|
+
collect_coverage: bool = False,
|
|
267
|
+
) -> tuple[bool, dict, dict | None]:
|
|
213
268
|
"""Run property tests phase (DX-08).
|
|
214
269
|
|
|
215
270
|
Args:
|
|
@@ -217,45 +272,62 @@ def run_property_tests_phase(
|
|
|
217
272
|
doctest_passed: Whether doctests passed
|
|
218
273
|
static_exit_code: Exit code from static analysis
|
|
219
274
|
max_examples: Maximum Hypothesis examples per function
|
|
275
|
+
collect_coverage: DX-37: If True, collect branch coverage data
|
|
220
276
|
|
|
221
|
-
Returns (passed, output_dict).
|
|
277
|
+
Returns (passed, output_dict, coverage_data).
|
|
222
278
|
"""
|
|
223
279
|
from invar.shell.property_tests import run_property_tests_on_files
|
|
224
280
|
|
|
225
281
|
# Skip if prior failures
|
|
226
282
|
if not doctest_passed or static_exit_code != 0:
|
|
227
|
-
return True, {"status": "skipped", "reason": "prior failures"}
|
|
283
|
+
return True, {"status": "skipped", "reason": "prior failures"}, None
|
|
228
284
|
|
|
229
285
|
if not checked_files:
|
|
230
|
-
return True, {"status": "skipped", "reason": "no files"}
|
|
286
|
+
return True, {"status": "skipped", "reason": "no files"}, None
|
|
231
287
|
|
|
232
288
|
# Only test Core files (with contracts)
|
|
233
289
|
core_files = [f for f in checked_files if "core" in str(f)]
|
|
234
290
|
if not core_files:
|
|
235
|
-
return True, {"status": "skipped", "reason": "no core files"}
|
|
291
|
+
return True, {"status": "skipped", "reason": "no core files"}, None
|
|
236
292
|
|
|
237
|
-
result = run_property_tests_on_files(core_files, max_examples)
|
|
293
|
+
result = run_property_tests_on_files(core_files, max_examples, collect_coverage=collect_coverage)
|
|
238
294
|
|
|
239
295
|
if isinstance(result, Success):
|
|
240
|
-
report = result.unwrap()
|
|
296
|
+
report, coverage_data = result.unwrap()
|
|
297
|
+
# DX-26: Build structured failures array for actionable output
|
|
298
|
+
failures = [
|
|
299
|
+
{
|
|
300
|
+
"function": r.function_name,
|
|
301
|
+
"file_path": r.file_path,
|
|
302
|
+
"error": r.error,
|
|
303
|
+
"seed": r.seed,
|
|
304
|
+
}
|
|
305
|
+
for r in report.results
|
|
306
|
+
if not r.passed
|
|
307
|
+
]
|
|
241
308
|
return report.all_passed(), {
|
|
242
309
|
"status": "passed" if report.all_passed() else "failed",
|
|
243
310
|
"functions_tested": report.functions_tested,
|
|
244
311
|
"functions_passed": report.functions_passed,
|
|
245
312
|
"functions_failed": report.functions_failed,
|
|
246
313
|
"total_examples": report.total_examples,
|
|
314
|
+
"failures": failures, # DX-26: Structured failure info
|
|
247
315
|
"errors": report.errors,
|
|
248
|
-
}
|
|
316
|
+
}, coverage_data
|
|
249
317
|
|
|
250
|
-
return False, {"status": "error", "error": result.failure()}
|
|
318
|
+
return False, {"status": "error", "error": result.failure()}, None
|
|
251
319
|
|
|
252
320
|
|
|
321
|
+
# @shell_complexity: Property test status formatting
|
|
253
322
|
def _output_property_tests_status(
|
|
254
323
|
static_exit_code: int,
|
|
255
324
|
doctest_passed: bool,
|
|
256
325
|
property_output: dict,
|
|
257
326
|
) -> None:
|
|
258
|
-
"""Output property tests status (DX-08).
|
|
327
|
+
"""Output property tests status (DX-08, DX-26).
|
|
328
|
+
|
|
329
|
+
DX-26: Show file::function format and reproduction command for failures.
|
|
330
|
+
"""
|
|
259
331
|
if static_exit_code != 0 or not doctest_passed:
|
|
260
332
|
console.print("[dim]⊘ Property tests skipped (prior failures)[/dim]")
|
|
261
333
|
return
|
|
@@ -275,12 +347,36 @@ def _output_property_tests_status(
|
|
|
275
347
|
elif status == "failed":
|
|
276
348
|
failed = property_output.get("functions_failed", 0)
|
|
277
349
|
console.print(f"[red]✗ Property tests failed ({failed} functions)[/red]")
|
|
350
|
+
# DX-26: Show actionable failure info
|
|
351
|
+
for failure in property_output.get("failures", [])[:5]:
|
|
352
|
+
file_path = failure.get("file_path", "")
|
|
353
|
+
func_name = failure.get("function", "unknown")
|
|
354
|
+
seed = failure.get("seed")
|
|
355
|
+
error = failure.get("error", "")
|
|
356
|
+
|
|
357
|
+
# Show file::function format
|
|
358
|
+
location = f"{file_path}::{func_name}" if file_path else func_name
|
|
359
|
+
console.print(f" [red]✗[/red] {location}")
|
|
360
|
+
|
|
361
|
+
# Show truncated error
|
|
362
|
+
if error:
|
|
363
|
+
short_error = error[:100] + "..." if len(error) > 100 else error
|
|
364
|
+
console.print(f" {short_error}")
|
|
365
|
+
|
|
366
|
+
# Show reproduction command with seed
|
|
367
|
+
if seed:
|
|
368
|
+
console.print(
|
|
369
|
+
f" [dim]Reproduce: python -c \"from hypothesis import reproduce_failure; "
|
|
370
|
+
f"import {func_name}\" --seed={seed}[/dim]"
|
|
371
|
+
)
|
|
372
|
+
# Fallback for errors without structured failures
|
|
278
373
|
for error in property_output.get("errors", [])[:5]:
|
|
279
|
-
console.print(f" {error}")
|
|
374
|
+
console.print(f" [yellow]![/yellow] {error}")
|
|
280
375
|
else:
|
|
281
376
|
console.print(f"[yellow]! Property tests: {status}[/yellow]")
|
|
282
377
|
|
|
283
378
|
|
|
379
|
+
# @shell_complexity: CrossHair status formatting
|
|
284
380
|
def _output_crosshair_status(
|
|
285
381
|
static_exit_code: int,
|
|
286
382
|
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,65 @@ 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
|
|
260
|
+
coverage_data: dict | None = None, # DX-37
|
|
208
261
|
) -> None:
|
|
209
|
-
"""Output report in Agent-optimized JSON format (Phase 8.2 + DX-06 + DX-08 + DX-09).
|
|
262
|
+
"""Output report in Agent-optimized JSON format (Phase 8.2 + DX-06 + DX-08 + DX-09 + DX-22 + DX-26 + DX-37).
|
|
210
263
|
|
|
211
264
|
Args:
|
|
212
265
|
report: Guard analysis report
|
|
266
|
+
strict: Whether warnings are treated as errors
|
|
213
267
|
doctest_passed: Whether doctests passed
|
|
214
268
|
doctest_output: Doctest stdout (only if failed)
|
|
215
269
|
crosshair_output: CrossHair results dict
|
|
216
270
|
verification_level: Current level (static/standard)
|
|
217
271
|
property_output: Property test results dict (DX-08)
|
|
272
|
+
routing_stats: Smart routing statistics (DX-22)
|
|
273
|
+
coverage_data: DX-37: Branch coverage data from doctest + hypothesis
|
|
274
|
+
|
|
275
|
+
DX-22: Adds routing stats showing CrossHair vs Hypothesis distribution.
|
|
276
|
+
DX-26: status now reflects ALL test phases, not just static analysis.
|
|
277
|
+
DX-37: Adds optional coverage data from doctest + hypothesis phases.
|
|
218
278
|
"""
|
|
219
279
|
import json
|
|
220
280
|
|
|
221
|
-
|
|
281
|
+
# DX-26: Extract passed status from phase outputs
|
|
282
|
+
crosshair_passed = True
|
|
283
|
+
if crosshair_output:
|
|
284
|
+
crosshair_status = crosshair_output.get("status", "verified")
|
|
285
|
+
crosshair_passed = crosshair_status in ("verified", "skipped")
|
|
286
|
+
|
|
287
|
+
property_passed = True
|
|
288
|
+
if property_output:
|
|
289
|
+
property_status = property_output.get("status", "passed")
|
|
290
|
+
property_passed = property_status in ("passed", "skipped")
|
|
291
|
+
|
|
292
|
+
# DX-26: Calculate combined status including all test phases
|
|
293
|
+
combined_status = get_combined_status(
|
|
294
|
+
report, strict, doctest_passed, crosshair_passed, property_passed
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
output = format_guard_agent(report, combined_status=combined_status)
|
|
222
298
|
# DX-09: Add verification level for Agent transparency
|
|
223
299
|
output["verification_level"] = verification_level
|
|
224
300
|
# DX-06: Add doctest results to agent output
|
|
@@ -232,4 +308,10 @@ def output_agent(
|
|
|
232
308
|
# DX-08: Add property test results if available
|
|
233
309
|
if property_output:
|
|
234
310
|
output["property_tests"] = property_output
|
|
311
|
+
# DX-22: Add smart routing statistics if available
|
|
312
|
+
if routing_stats:
|
|
313
|
+
output["routing"] = routing_stats
|
|
314
|
+
# DX-37: Add coverage data if collected
|
|
315
|
+
if coverage_data:
|
|
316
|
+
output["coverage"] = coverage_data
|
|
235
317
|
console.print(json.dumps(output, indent=2))
|
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.
|