invar-tools 1.2.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 +10 -10
- invar/core/entry_points.py +105 -32
- invar/core/extraction.py +5 -6
- invar/core/format_specs.py +1 -2
- invar/core/formatter.py +6 -7
- invar/core/hypothesis_strategies.py +5 -7
- invar/core/inspect.py +1 -1
- invar/core/lambda_helpers.py +3 -3
- invar/core/models.py +7 -1
- invar/core/must_use.py +2 -1
- invar/core/parser.py +7 -4
- invar/core/postcondition_scope.py +128 -0
- invar/core/property_gen.py +8 -5
- invar/core/purity.py +3 -3
- invar/core/purity_heuristics.py +5 -9
- invar/core/references.py +8 -6
- invar/core/review_trigger.py +78 -6
- invar/core/rule_meta.py +8 -0
- invar/core/rules.py +18 -19
- invar/core/shell_analysis.py +5 -10
- invar/core/shell_architecture.py +2 -2
- invar/core/strategies.py +7 -14
- invar/core/suggestions.py +86 -0
- invar/core/sync_helpers.py +238 -0
- invar/core/tautology.py +102 -37
- invar/core/template_parser.py +467 -0
- invar/core/timeout_inference.py +4 -7
- invar/core/utils.py +13 -15
- invar/core/verification_routing.py +4 -7
- invar/mcp/server.py +100 -17
- invar/shell/commands/__init__.py +11 -0
- invar/shell/{cli.py → commands/guard.py} +94 -14
- invar/shell/{init_cmd.py → commands/init.py} +179 -27
- invar/shell/commands/merge.py +256 -0
- invar/shell/commands/sync_self.py +113 -0
- invar/shell/commands/template_sync.py +366 -0
- invar/shell/commands/update.py +48 -0
- invar/shell/config.py +12 -24
- invar/shell/coverage.py +351 -0
- invar/shell/guard_helpers.py +38 -17
- invar/shell/guard_output.py +7 -1
- invar/shell/property_tests.py +58 -22
- invar/shell/prove/__init__.py +9 -0
- invar/shell/{prove.py → prove/crosshair.py} +40 -33
- invar/shell/{prove_fallback.py → prove/hypothesis.py} +12 -4
- invar/shell/subprocess_env.py +393 -0
- invar/shell/template_engine.py +345 -0
- invar/shell/templates.py +19 -0
- invar/shell/testing.py +71 -20
- invar/templates/CLAUDE.md.template +38 -17
- invar/templates/aider.conf.yml.template +2 -2
- invar/templates/commands/{review.md → audit.md} +20 -82
- 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 +7 -4
- invar/templates/examples/README.md +2 -0
- invar/templates/examples/conftest.py +3 -0
- invar/templates/examples/contracts.py +5 -5
- invar/templates/examples/core_shell.py +11 -7
- invar/templates/examples/workflow.md +81 -0
- invar/templates/manifest.toml +137 -0
- invar/templates/{INVAR.md → protocol/INVAR.md} +10 -7
- 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.2.0.dist-info → invar_tools-1.3.0.dist-info}/METADATA +108 -118
- invar_tools-1.3.0.dist-info/RECORD +95 -0
- invar_tools-1.3.0.dist-info/entry_points.txt +2 -0
- invar/contracts.py +0 -152
- invar/decorators.py +0 -94
- invar/invariant.py +0 -58
- invar/resource.py +0 -99
- invar/shell/update_cmd.py +0 -193
- invar_tools-1.2.0.dist-info/RECORD +0 -77
- invar_tools-1.2.0.dist-info/entry_points.txt +0 -2
- /invar/shell/{mutate_cmd.py → commands/mutate.py} +0 -0
- /invar/shell/{perception.py → commands/perception.py} +0 -0
- /invar/shell/{test_cmd.py → commands/test.py} +0 -0
- /invar/shell/{prove_accept.py → prove/accept.py} +0 -0
- /invar/shell/{prove_cache.py → prove/cache.py} +0 -0
- {invar_tools-1.2.0.dist-info → invar_tools-1.3.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.2.0.dist-info → invar_tools-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.2.0.dist-info → invar_tools-1.3.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.2.0.dist-info → invar_tools-1.3.0.dist-info}/licenses/NOTICE +0 -0
invar/mcp/server.py
CHANGED
|
@@ -3,18 +3,54 @@ Invar MCP Server implementation.
|
|
|
3
3
|
|
|
4
4
|
Exposes invar guard, sig, and map as first-class MCP tools.
|
|
5
5
|
Part of DX-16: Agent Tool Enforcement.
|
|
6
|
+
DX-52: Added Phase 2 smart re-spawn for project Python compatibility.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
from __future__ import annotations
|
|
9
10
|
|
|
10
11
|
import json
|
|
12
|
+
import os
|
|
11
13
|
import subprocess
|
|
12
14
|
import sys
|
|
15
|
+
from pathlib import Path
|
|
13
16
|
from typing import Any
|
|
14
17
|
|
|
15
18
|
from mcp.server import Server
|
|
16
19
|
from mcp.types import TextContent, Tool
|
|
17
20
|
|
|
21
|
+
from invar.shell.subprocess_env import should_respawn
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# @invar:allow shell_result: Pure validation helper, no I/O, returns tuple not Result
|
|
25
|
+
# @shell_complexity: Security validation requires multiple checks
|
|
26
|
+
def _validate_path(path: str) -> tuple[bool, str]:
|
|
27
|
+
"""Validate path argument for safety.
|
|
28
|
+
|
|
29
|
+
Returns (is_valid, error_message).
|
|
30
|
+
Rejects paths that could be interpreted as shell commands or flags.
|
|
31
|
+
"""
|
|
32
|
+
if not path:
|
|
33
|
+
return True, "" # Empty path defaults to "." in handlers
|
|
34
|
+
|
|
35
|
+
# Reject if looks like a flag (starts with -)
|
|
36
|
+
if path.startswith("-"):
|
|
37
|
+
return False, f"Invalid path: cannot start with '-': {path}"
|
|
38
|
+
|
|
39
|
+
# Reject shell metacharacters that could cause issues
|
|
40
|
+
dangerous_chars = [";", "&", "|", "$", "`", "\n", "\r"]
|
|
41
|
+
for char in dangerous_chars:
|
|
42
|
+
if char in path:
|
|
43
|
+
return False, f"Invalid path: contains forbidden character: {char!r}"
|
|
44
|
+
|
|
45
|
+
# Try to resolve path - this catches malformed paths
|
|
46
|
+
try:
|
|
47
|
+
Path(path).resolve()
|
|
48
|
+
except (OSError, ValueError) as e:
|
|
49
|
+
return False, f"Invalid path: {e}"
|
|
50
|
+
|
|
51
|
+
return True, ""
|
|
52
|
+
|
|
53
|
+
|
|
18
54
|
# Strong instructions for agent behavior (DX-16 + DX-17 + DX-26)
|
|
19
55
|
INVAR_INSTRUCTIONS = """
|
|
20
56
|
## Invar Tool Usage (MANDATORY)
|
|
@@ -84,7 +120,7 @@ the MCP tools and may not follow the correct workflow.
|
|
|
84
120
|
|
|
85
121
|
|
|
86
122
|
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
87
|
-
# @invar:allow shell_result: MCP
|
|
123
|
+
# @invar:allow shell_result: MCP tool factory for guard command
|
|
88
124
|
def _get_guard_tool() -> Tool:
|
|
89
125
|
"""Define the invar_guard tool."""
|
|
90
126
|
return Tool(
|
|
@@ -100,13 +136,14 @@ def _get_guard_tool() -> Tool:
|
|
|
100
136
|
"path": {"type": "string", "description": "Project path (default: .)", "default": "."},
|
|
101
137
|
"changed": {"type": "boolean", "description": "Only verify git-changed files", "default": True},
|
|
102
138
|
"strict": {"type": "boolean", "description": "Treat warnings as errors", "default": False},
|
|
139
|
+
"coverage": {"type": "boolean", "description": "DX-37: Collect branch coverage from doctest + hypothesis", "default": False},
|
|
103
140
|
},
|
|
104
141
|
},
|
|
105
142
|
)
|
|
106
143
|
|
|
107
144
|
|
|
108
145
|
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
109
|
-
# @invar:allow shell_result: MCP
|
|
146
|
+
# @invar:allow shell_result: MCP tool factory for sig command
|
|
110
147
|
def _get_sig_tool() -> Tool:
|
|
111
148
|
"""Define the invar_sig tool."""
|
|
112
149
|
return Tool(
|
|
@@ -126,7 +163,7 @@ def _get_sig_tool() -> Tool:
|
|
|
126
163
|
|
|
127
164
|
|
|
128
165
|
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
129
|
-
# @invar:allow shell_result: MCP
|
|
166
|
+
# @invar:allow shell_result: MCP tool factory for map command
|
|
130
167
|
def _get_map_tool() -> Tool:
|
|
131
168
|
"""Define the invar_map tool."""
|
|
132
169
|
return Tool(
|
|
@@ -167,18 +204,25 @@ def create_server() -> Server:
|
|
|
167
204
|
|
|
168
205
|
|
|
169
206
|
# @shell_orchestration: MCP handler - subprocess is called inside
|
|
170
|
-
# @
|
|
207
|
+
# @shell_complexity: Guard command with multiple optional flags
|
|
208
|
+
# @invar:allow shell_result: MCP handler for guard tool
|
|
171
209
|
async def _run_guard(args: dict[str, Any]) -> list[TextContent]:
|
|
172
210
|
"""Run invar guard command."""
|
|
173
|
-
cmd = [sys.executable, "-m", "invar.shell.cli", "guard"]
|
|
174
|
-
|
|
175
211
|
path = args.get("path", ".")
|
|
212
|
+
is_valid, error = _validate_path(path)
|
|
213
|
+
if not is_valid:
|
|
214
|
+
return [TextContent(type="text", text=f"Error: {error}")]
|
|
215
|
+
|
|
216
|
+
cmd = [sys.executable, "-m", "invar.shell.commands.guard", "guard"]
|
|
176
217
|
cmd.append(path)
|
|
177
218
|
|
|
178
219
|
if args.get("changed", True):
|
|
179
220
|
cmd.append("--changed")
|
|
180
221
|
if args.get("strict", False):
|
|
181
222
|
cmd.append("--strict")
|
|
223
|
+
# DX-37: Optional coverage collection
|
|
224
|
+
if args.get("coverage", False):
|
|
225
|
+
cmd.append("--coverage")
|
|
182
226
|
|
|
183
227
|
# DX-26: TTY auto-detection - MCP runs in non-TTY, so agent JSON output is automatic
|
|
184
228
|
# No explicit flag needed
|
|
@@ -187,24 +231,33 @@ async def _run_guard(args: dict[str, Any]) -> list[TextContent]:
|
|
|
187
231
|
|
|
188
232
|
|
|
189
233
|
# @shell_orchestration: MCP handler - subprocess is called inside
|
|
190
|
-
# @invar:allow shell_result: MCP
|
|
234
|
+
# @invar:allow shell_result: MCP handler for sig tool
|
|
191
235
|
async def _run_sig(args: dict[str, Any]) -> list[TextContent]:
|
|
192
236
|
"""Run invar sig command."""
|
|
193
237
|
target = args.get("target", "")
|
|
194
238
|
if not target:
|
|
195
239
|
return [TextContent(type="text", text="Error: target is required")]
|
|
196
240
|
|
|
197
|
-
|
|
241
|
+
# Validate target (can be file path or file::symbol)
|
|
242
|
+
target_path = target.split("::")[0] if "::" in target else target
|
|
243
|
+
is_valid, error = _validate_path(target_path)
|
|
244
|
+
if not is_valid:
|
|
245
|
+
return [TextContent(type="text", text=f"Error: {error}")]
|
|
246
|
+
|
|
247
|
+
cmd = [sys.executable, "-m", "invar.shell.commands.guard", "sig", target, "--json"]
|
|
198
248
|
return await _execute_command(cmd)
|
|
199
249
|
|
|
200
250
|
|
|
201
251
|
# @shell_orchestration: MCP handler - subprocess is called inside
|
|
202
|
-
# @invar:allow shell_result: MCP
|
|
252
|
+
# @invar:allow shell_result: MCP handler for map tool
|
|
203
253
|
async def _run_map(args: dict[str, Any]) -> list[TextContent]:
|
|
204
254
|
"""Run invar map command."""
|
|
205
|
-
cmd = [sys.executable, "-m", "invar.shell.cli", "map"]
|
|
206
|
-
|
|
207
255
|
path = args.get("path", ".")
|
|
256
|
+
is_valid, error = _validate_path(path)
|
|
257
|
+
if not is_valid:
|
|
258
|
+
return [TextContent(type="text", text=f"Error: {error}")]
|
|
259
|
+
|
|
260
|
+
cmd = [sys.executable, "-m", "invar.shell.commands.guard", "map"]
|
|
208
261
|
cmd.append(path)
|
|
209
262
|
|
|
210
263
|
top = args.get("top", 10)
|
|
@@ -215,15 +268,20 @@ async def _run_map(args: dict[str, Any]) -> list[TextContent]:
|
|
|
215
268
|
|
|
216
269
|
|
|
217
270
|
# @shell_complexity: Command execution with error handling branches
|
|
218
|
-
# @invar:allow shell_result: MCP
|
|
219
|
-
async def _execute_command(cmd: list[str]) -> list[TextContent]:
|
|
220
|
-
"""Execute a command and return the result.
|
|
271
|
+
# @invar:allow shell_result: MCP subprocess wrapper utility
|
|
272
|
+
async def _execute_command(cmd: list[str], timeout: int = 600) -> list[TextContent]:
|
|
273
|
+
"""Execute a command and return the result.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
cmd: Command to execute
|
|
277
|
+
timeout: Maximum time in seconds (default: 600, accommodates full Guard cycle)
|
|
278
|
+
"""
|
|
221
279
|
try:
|
|
222
280
|
result = subprocess.run(
|
|
223
281
|
cmd,
|
|
224
282
|
capture_output=True,
|
|
225
283
|
text=True,
|
|
226
|
-
timeout=
|
|
284
|
+
timeout=timeout,
|
|
227
285
|
)
|
|
228
286
|
|
|
229
287
|
output = result.stdout
|
|
@@ -240,18 +298,43 @@ async def _execute_command(cmd: list[str]) -> list[TextContent]:
|
|
|
240
298
|
return [TextContent(type="text", text=output)]
|
|
241
299
|
|
|
242
300
|
except subprocess.TimeoutExpired:
|
|
243
|
-
return [TextContent(type="text", text="Error: Command timed out (
|
|
301
|
+
return [TextContent(type="text", text=f"Error: Command timed out ({timeout}s)")]
|
|
244
302
|
except Exception as e:
|
|
245
303
|
return [TextContent(type="text", text=f"Error: {e}")]
|
|
246
304
|
|
|
247
305
|
|
|
248
306
|
# @shell_orchestration: MCP server entry point - runs async server
|
|
249
307
|
def run_server() -> None:
|
|
250
|
-
"""Run the Invar MCP server.
|
|
308
|
+
"""Run the Invar MCP server.
|
|
309
|
+
|
|
310
|
+
DX-52 Phase 2: If project has invar installed, re-spawn with project Python
|
|
311
|
+
to ensure C extensions are compatible with project's Python version.
|
|
312
|
+
"""
|
|
251
313
|
import asyncio
|
|
252
314
|
|
|
253
315
|
from mcp.server.stdio import stdio_server
|
|
254
316
|
|
|
317
|
+
# DX-52 Phase 2: Smart re-spawn with project Python
|
|
318
|
+
cwd = Path.cwd()
|
|
319
|
+
do_respawn, project_python = should_respawn(cwd)
|
|
320
|
+
|
|
321
|
+
if do_respawn and project_python is not None:
|
|
322
|
+
# Re-spawn with project Python (has both invar AND project deps)
|
|
323
|
+
import subprocess
|
|
324
|
+
import sys
|
|
325
|
+
|
|
326
|
+
if os.name == "nt":
|
|
327
|
+
# Windows: execv doesn't replace process, use subprocess + exit
|
|
328
|
+
result = subprocess.call([str(project_python), "-m", "invar.mcp"])
|
|
329
|
+
sys.exit(result)
|
|
330
|
+
else:
|
|
331
|
+
# Unix: execv replaces current process, does not return
|
|
332
|
+
os.execv(
|
|
333
|
+
str(project_python),
|
|
334
|
+
[str(project_python), "-m", "invar.mcp"],
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Phase 1 fallback: Continue with uvx + PYTHONPATH injection
|
|
255
338
|
async def main():
|
|
256
339
|
server = create_server()
|
|
257
340
|
async with stdio_server() as (read_stream, write_stream):
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI commands for Invar.
|
|
3
|
+
|
|
4
|
+
This module contains Typer command implementations:
|
|
5
|
+
- guard: Main verification command
|
|
6
|
+
- init: Project initialization
|
|
7
|
+
- update: Update INVAR.md from template
|
|
8
|
+
- test: Run property tests
|
|
9
|
+
- mutate: Run mutation testing
|
|
10
|
+
- perception: Map and sig commands
|
|
11
|
+
"""
|
|
@@ -56,14 +56,19 @@ def _count_core_functions(file_info) -> tuple[int, int]:
|
|
|
56
56
|
return (total, with_contracts)
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
# @shell_complexity: Core orchestration - iterates files, handles failures, aggregates results
|
|
59
60
|
# @shell_complexity: Core orchestration - iterates files, handles failures, aggregates results
|
|
60
61
|
def _scan_and_check(
|
|
61
62
|
path: Path, config: RuleConfig, only_files: set[Path] | None = None
|
|
62
63
|
) -> Result[GuardReport, str]:
|
|
63
64
|
"""Scan project files and check against rules."""
|
|
65
|
+
from invar.core.entry_points import extract_escape_hatches
|
|
66
|
+
from invar.core.review_trigger import check_duplicate_escape_reasons
|
|
64
67
|
from invar.core.shell_architecture import check_complexity_debt
|
|
65
68
|
|
|
66
69
|
report = GuardReport(files_checked=0)
|
|
70
|
+
all_escapes: list[tuple[str, str, str]] = [] # DX-33: (file, rule, reason)
|
|
71
|
+
|
|
67
72
|
for file_result in scan_project(path, only_files):
|
|
68
73
|
if isinstance(file_result, Failure):
|
|
69
74
|
console.print(f"[yellow]Warning:[/yellow] {file_result.failure()}")
|
|
@@ -75,6 +80,10 @@ def _scan_and_check(
|
|
|
75
80
|
report.update_coverage(total, with_contracts)
|
|
76
81
|
for violation in check_all_rules(file_info, config):
|
|
77
82
|
report.add_violation(violation)
|
|
83
|
+
# DX-33: Collect escape hatches for cross-file analysis
|
|
84
|
+
if file_info.source:
|
|
85
|
+
for rule, reason in extract_escape_hatches(file_info.source):
|
|
86
|
+
all_escapes.append((file_info.path, rule, reason))
|
|
78
87
|
|
|
79
88
|
# DX-22: Check project-level complexity debt (Fix-or-Explain enforcement)
|
|
80
89
|
for debt_violation in check_complexity_debt(
|
|
@@ -82,6 +91,10 @@ def _scan_and_check(
|
|
|
82
91
|
):
|
|
83
92
|
report.add_violation(debt_violation)
|
|
84
93
|
|
|
94
|
+
# DX-33: Check for duplicate escape reasons across files
|
|
95
|
+
for escape_violation in check_duplicate_escape_reasons(all_escapes):
|
|
96
|
+
report.add_violation(escape_violation)
|
|
97
|
+
|
|
85
98
|
return Success(report)
|
|
86
99
|
|
|
87
100
|
|
|
@@ -117,6 +130,9 @@ def guard(
|
|
|
117
130
|
json_output: bool = typer.Option(
|
|
118
131
|
False, "--json", hidden=True, help="[Deprecated] Use TTY auto-detection instead"
|
|
119
132
|
),
|
|
133
|
+
coverage: bool = typer.Option(
|
|
134
|
+
False, "--coverage", help="DX-37: Collect branch coverage from doctest + hypothesis"
|
|
135
|
+
),
|
|
120
136
|
) -> None:
|
|
121
137
|
"""Check project against Invar architecture rules.
|
|
122
138
|
|
|
@@ -179,32 +195,76 @@ def guard(
|
|
|
179
195
|
# Run verification phases
|
|
180
196
|
static_exit_code = get_exit_code(report, strict)
|
|
181
197
|
doctest_passed, doctest_output = True, ""
|
|
182
|
-
crosshair_passed
|
|
183
|
-
|
|
198
|
+
crosshair_passed: bool = True
|
|
199
|
+
crosshair_output: dict = {}
|
|
200
|
+
property_passed: bool = True
|
|
201
|
+
property_output: dict = {}
|
|
202
|
+
# DX-37: Coverage data from doctest + hypothesis phases
|
|
203
|
+
doctest_coverage: dict | None = None
|
|
204
|
+
property_coverage: dict | None = None
|
|
205
|
+
|
|
206
|
+
# DX-37: Check coverage availability if requested
|
|
207
|
+
if coverage:
|
|
208
|
+
from invar.shell.coverage import check_coverage_available
|
|
209
|
+
cov_check = check_coverage_available()
|
|
210
|
+
if isinstance(cov_check, Failure):
|
|
211
|
+
console.print(f"[yellow]Warning:[/yellow] {cov_check.failure()}")
|
|
212
|
+
coverage = False # Disable coverage if not available
|
|
184
213
|
|
|
185
214
|
# DX-19: STANDARD runs all verification phases
|
|
186
215
|
if verification_level == VerificationLevel.STANDARD and static_exit_code == 0:
|
|
187
216
|
checked_files = collect_files_to_check(path, checked_files)
|
|
188
217
|
|
|
189
|
-
# Phase 1: Doctests
|
|
190
|
-
doctest_passed, doctest_output = run_doctests_phase(
|
|
218
|
+
# Phase 1: Doctests (DX-37: with optional coverage)
|
|
219
|
+
doctest_passed, doctest_output, doctest_coverage = run_doctests_phase(
|
|
220
|
+
checked_files, explain, timeout=config.timeout_doctest,
|
|
221
|
+
collect_coverage=coverage,
|
|
222
|
+
)
|
|
191
223
|
|
|
192
224
|
# Phase 2: CrossHair symbolic verification
|
|
225
|
+
# Note: CrossHair uses subprocess + symbolic execution, coverage not applicable
|
|
193
226
|
crosshair_passed, crosshair_output = run_crosshair_phase(
|
|
194
227
|
path, checked_files, doctest_passed, static_exit_code,
|
|
195
228
|
changed_mode=changed,
|
|
229
|
+
timeout=config.timeout_crosshair,
|
|
230
|
+
per_condition_timeout=config.timeout_crosshair_per_condition,
|
|
196
231
|
)
|
|
197
232
|
|
|
198
|
-
# Phase 3: Hypothesis property tests
|
|
199
|
-
property_passed, property_output = run_property_tests_phase(
|
|
200
|
-
checked_files, doctest_passed, static_exit_code
|
|
233
|
+
# Phase 3: Hypothesis property tests (DX-37: with optional coverage)
|
|
234
|
+
property_passed, property_output, property_coverage = run_property_tests_phase(
|
|
235
|
+
checked_files, doctest_passed, static_exit_code,
|
|
236
|
+
collect_coverage=coverage,
|
|
201
237
|
)
|
|
238
|
+
elif verification_level == VerificationLevel.STATIC:
|
|
239
|
+
# Static-only mode: explicitly mark verification as skipped
|
|
240
|
+
crosshair_output = {"status": "skipped", "reason": "static mode"}
|
|
241
|
+
property_output = {"status": "skipped", "reason": "static mode"}
|
|
242
|
+
elif static_exit_code != 0:
|
|
243
|
+
# Static failures: explicitly mark verification as skipped
|
|
244
|
+
crosshair_output = {"status": "skipped", "reason": "prior failures"}
|
|
245
|
+
property_output = {"status": "skipped", "reason": "prior failures"}
|
|
246
|
+
|
|
247
|
+
# DX-37: Merge coverage data from doctest + hypothesis
|
|
248
|
+
coverage_output: dict | None = None
|
|
249
|
+
if coverage and (doctest_coverage or property_coverage):
|
|
250
|
+
coverage_output = {
|
|
251
|
+
"enabled": True,
|
|
252
|
+
"phases_tracked": [],
|
|
253
|
+
"phases_excluded": ["crosshair"], # CrossHair uses symbolic execution
|
|
254
|
+
}
|
|
255
|
+
if doctest_coverage and doctest_coverage.get("collected"):
|
|
256
|
+
coverage_output["phases_tracked"].append("doctest")
|
|
257
|
+
if property_coverage and property_coverage.get("collected"):
|
|
258
|
+
coverage_output["phases_tracked"].append("hypothesis")
|
|
259
|
+
if "overall_branch_coverage" in property_coverage:
|
|
260
|
+
coverage_output["overall_branch_coverage"] = property_coverage["overall_branch_coverage"]
|
|
202
261
|
|
|
203
262
|
# DX-26: Unified output (agent JSON or human Rich)
|
|
204
263
|
if use_agent_output:
|
|
205
264
|
output_agent(
|
|
206
265
|
report, strict, doctest_passed, doctest_output, crosshair_output, level_name,
|
|
207
266
|
property_output=property_output,
|
|
267
|
+
coverage_data=coverage_output, # DX-37
|
|
208
268
|
)
|
|
209
269
|
else:
|
|
210
270
|
output_rich(report, config.strict_pure, changed, pedantic, explain, static)
|
|
@@ -214,6 +274,13 @@ def guard(
|
|
|
214
274
|
property_output=property_output,
|
|
215
275
|
strict=strict,
|
|
216
276
|
)
|
|
277
|
+
# DX-37: Show coverage info in human output
|
|
278
|
+
if coverage_output and coverage_output.get("phases_tracked"):
|
|
279
|
+
phases = coverage_output.get("phases_tracked", [])
|
|
280
|
+
overall = coverage_output.get("overall_branch_coverage", 0.0)
|
|
281
|
+
console.print(f"\n[bold]Coverage Analysis[/bold] ({' + '.join(phases)})")
|
|
282
|
+
console.print(f" Overall branch coverage: {overall}%")
|
|
283
|
+
console.print(" [dim]Note: CrossHair uses symbolic execution; coverage not applicable.[/dim]")
|
|
217
284
|
|
|
218
285
|
# Exit with combined status
|
|
219
286
|
all_passed = doctest_passed and crosshair_passed and property_passed
|
|
@@ -270,7 +337,7 @@ def map_command(
|
|
|
270
337
|
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
271
338
|
) -> None:
|
|
272
339
|
"""Generate symbol map with reference counts."""
|
|
273
|
-
from invar.shell.perception import run_map
|
|
340
|
+
from invar.shell.commands.perception import run_map
|
|
274
341
|
|
|
275
342
|
# Phase 9 P11: Auto-detect agent mode
|
|
276
343
|
use_json = json_output or _detect_agent_mode()
|
|
@@ -286,7 +353,7 @@ def sig_command(
|
|
|
286
353
|
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
287
354
|
) -> None:
|
|
288
355
|
"""Extract signatures from a file or symbol."""
|
|
289
|
-
from invar.shell.perception import run_sig
|
|
356
|
+
from invar.shell.commands.perception import run_sig
|
|
290
357
|
|
|
291
358
|
# Phase 9 P11: Auto-detect agent mode
|
|
292
359
|
use_json = json_output or _detect_agent_mode()
|
|
@@ -369,11 +436,12 @@ def rules(
|
|
|
369
436
|
console.print(f"\n[dim]{len(rules_list)} rules total. Use --json for full details.[/dim]")
|
|
370
437
|
|
|
371
438
|
|
|
372
|
-
# Import commands from
|
|
373
|
-
from invar.shell.
|
|
374
|
-
from invar.shell.
|
|
375
|
-
from invar.shell.
|
|
376
|
-
from invar.shell.
|
|
439
|
+
# DX-48b: Import commands from shell/commands/
|
|
440
|
+
from invar.shell.commands.init import init
|
|
441
|
+
from invar.shell.commands.mutate import mutate # DX-28
|
|
442
|
+
from invar.shell.commands.sync_self import sync_self # DX-49
|
|
443
|
+
from invar.shell.commands.test import test, verify
|
|
444
|
+
from invar.shell.commands.update import update
|
|
377
445
|
|
|
378
446
|
app.command()(init)
|
|
379
447
|
app.command()(update)
|
|
@@ -381,6 +449,18 @@ app.command()(test)
|
|
|
381
449
|
app.command()(verify)
|
|
382
450
|
app.command()(mutate) # DX-28: Mutation testing
|
|
383
451
|
|
|
452
|
+
# DX-56: Create dev subcommand group for developer commands
|
|
453
|
+
dev_app = typer.Typer(
|
|
454
|
+
name="dev",
|
|
455
|
+
help="Developer commands for Invar project development",
|
|
456
|
+
add_completion=False,
|
|
457
|
+
)
|
|
458
|
+
dev_app.command("sync")(sync_self) # DX-56: renamed from sync-self
|
|
459
|
+
app.add_typer(dev_app)
|
|
460
|
+
|
|
461
|
+
# DX-56: Keep sync-self as alias for backward compatibility (deprecated)
|
|
462
|
+
app.command("sync-self", hidden=True)(sync_self)
|
|
463
|
+
|
|
384
464
|
|
|
385
465
|
if __name__ == "__main__":
|
|
386
466
|
app()
|