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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Verification routing logic for smart tool selection.
|
|
3
|
+
|
|
4
|
+
DX-22: Automatically routes code to CrossHair or Hypothesis based on imports.
|
|
5
|
+
Core module: Pure logic, no I/O.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
from deal import post
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VerificationTool(Enum):
|
|
17
|
+
"""Verification tool selection."""
|
|
18
|
+
|
|
19
|
+
CROSSHAIR = "crosshair"
|
|
20
|
+
HYPOTHESIS = "hypothesis"
|
|
21
|
+
SKIP = "skip"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# C extensions that CrossHair cannot symbolically execute
|
|
25
|
+
# These libraries use native code that breaks symbolic execution
|
|
26
|
+
CROSSHAIR_INCOMPATIBLE_LIBS = frozenset(
|
|
27
|
+
[
|
|
28
|
+
# Scientific computing (C/Fortran extensions)
|
|
29
|
+
"numpy",
|
|
30
|
+
"pandas",
|
|
31
|
+
"scipy",
|
|
32
|
+
"sklearn",
|
|
33
|
+
"scikit-learn",
|
|
34
|
+
# Deep learning (CUDA/C++ backends)
|
|
35
|
+
"torch",
|
|
36
|
+
"tensorflow",
|
|
37
|
+
"keras",
|
|
38
|
+
"jax",
|
|
39
|
+
# Image processing (C extensions)
|
|
40
|
+
"cv2",
|
|
41
|
+
"PIL",
|
|
42
|
+
"pillow",
|
|
43
|
+
"skimage",
|
|
44
|
+
# Network I/O (non-deterministic)
|
|
45
|
+
"requests",
|
|
46
|
+
"aiohttp",
|
|
47
|
+
"httpx",
|
|
48
|
+
"urllib3",
|
|
49
|
+
# System calls (side effects)
|
|
50
|
+
"subprocess",
|
|
51
|
+
"multiprocessing",
|
|
52
|
+
# Database I/O
|
|
53
|
+
"sqlalchemy",
|
|
54
|
+
"psycopg2",
|
|
55
|
+
"pymongo",
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Regex pattern to detect imports
|
|
60
|
+
# Matches: import numpy, from numpy import, import numpy as np
|
|
61
|
+
_IMPORT_PATTERN = re.compile(
|
|
62
|
+
r"^\s*(?:import\s+(\w+)|from\s+(\w+)(?:\.\w+)*\s+import)",
|
|
63
|
+
re.MULTILINE,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# @invar:allow missing_contract: Boolean predicate, empty string returns False
|
|
68
|
+
def has_incompatible_imports(source: str) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Check if source contains imports incompatible with CrossHair.
|
|
71
|
+
|
|
72
|
+
DX-22: Detects C extension libraries that cannot be symbolically executed.
|
|
73
|
+
Used to route code directly to Hypothesis instead of wasting time on CrossHair.
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
>>> has_incompatible_imports("import numpy as np")
|
|
77
|
+
True
|
|
78
|
+
>>> has_incompatible_imports("from pandas import DataFrame")
|
|
79
|
+
True
|
|
80
|
+
>>> has_incompatible_imports("from pathlib import Path")
|
|
81
|
+
False
|
|
82
|
+
>>> has_incompatible_imports("import json")
|
|
83
|
+
False
|
|
84
|
+
>>> has_incompatible_imports("from sklearn.model_selection import train_test_split")
|
|
85
|
+
True
|
|
86
|
+
>>> has_incompatible_imports("import torch.nn as nn")
|
|
87
|
+
True
|
|
88
|
+
>>> has_incompatible_imports("")
|
|
89
|
+
False
|
|
90
|
+
"""
|
|
91
|
+
# Early return for empty/whitespace-only strings (avoids regex edge cases)
|
|
92
|
+
if not source or not source.strip():
|
|
93
|
+
return False
|
|
94
|
+
for match in _IMPORT_PATTERN.finditer(source):
|
|
95
|
+
lib = match.group(1) or match.group(2)
|
|
96
|
+
if lib and lib.lower() in CROSSHAIR_INCOMPATIBLE_LIBS:
|
|
97
|
+
return True
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@post(lambda result: all(lib in CROSSHAIR_INCOMPATIBLE_LIBS for lib in result))
|
|
102
|
+
def get_incompatible_imports(source: str) -> set[str]:
|
|
103
|
+
"""
|
|
104
|
+
Get the set of incompatible libraries imported in source.
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
>>> sorted(get_incompatible_imports("import numpy\\nfrom pandas import DataFrame"))
|
|
108
|
+
['numpy', 'pandas']
|
|
109
|
+
>>> get_incompatible_imports("import json")
|
|
110
|
+
set()
|
|
111
|
+
>>> sorted(get_incompatible_imports("import torch\\nimport tensorflow"))
|
|
112
|
+
['tensorflow', 'torch']
|
|
113
|
+
>>> get_incompatible_imports("")
|
|
114
|
+
set()
|
|
115
|
+
"""
|
|
116
|
+
# Early return for empty/whitespace-only strings (avoids regex edge cases)
|
|
117
|
+
if not source or not source.strip():
|
|
118
|
+
return set()
|
|
119
|
+
incompatible: set[str] = set()
|
|
120
|
+
for match in _IMPORT_PATTERN.finditer(source):
|
|
121
|
+
lib = match.group(1) or match.group(2)
|
|
122
|
+
if lib and lib.lower() in CROSSHAIR_INCOMPATIBLE_LIBS:
|
|
123
|
+
incompatible.add(lib.lower())
|
|
124
|
+
return incompatible
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@post(lambda result: result in VerificationTool) # Returns valid enum member
|
|
128
|
+
def select_verification_tool(source: str, has_contracts: bool) -> VerificationTool:
|
|
129
|
+
"""
|
|
130
|
+
Select the appropriate verification tool for a source file.
|
|
131
|
+
|
|
132
|
+
DX-22 Smart Routing:
|
|
133
|
+
- No contracts -> SKIP (nothing to verify)
|
|
134
|
+
- Has C extensions -> HYPOTHESIS (CrossHair will fail)
|
|
135
|
+
- Pure Python with contracts -> CROSSHAIR (can prove correctness)
|
|
136
|
+
|
|
137
|
+
Examples:
|
|
138
|
+
>>> select_verification_tool("def foo(): pass", has_contracts=False)
|
|
139
|
+
<VerificationTool.SKIP: 'skip'>
|
|
140
|
+
>>> select_verification_tool("import numpy\\n@pre(lambda x: x > 0)\\ndef foo(x): pass", has_contracts=True)
|
|
141
|
+
<VerificationTool.HYPOTHESIS: 'hypothesis'>
|
|
142
|
+
>>> select_verification_tool("@pre(lambda x: x > 0)\\ndef foo(x): pass", has_contracts=True)
|
|
143
|
+
<VerificationTool.CROSSHAIR: 'crosshair'>
|
|
144
|
+
>>> select_verification_tool("", has_contracts=False)
|
|
145
|
+
<VerificationTool.SKIP: 'skip'>
|
|
146
|
+
>>> select_verification_tool("", has_contracts=True)
|
|
147
|
+
<VerificationTool.CROSSHAIR: 'crosshair'>
|
|
148
|
+
"""
|
|
149
|
+
if not has_contracts:
|
|
150
|
+
return VerificationTool.SKIP
|
|
151
|
+
|
|
152
|
+
if has_incompatible_imports(source):
|
|
153
|
+
return VerificationTool.HYPOTHESIS
|
|
154
|
+
|
|
155
|
+
return VerificationTool.CROSSHAIR
|
invar/mcp/server.py
CHANGED
|
@@ -3,19 +3,55 @@ 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
|
|
|
18
|
-
|
|
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
|
+
|
|
54
|
+
# Strong instructions for agent behavior (DX-16 + DX-17 + DX-26)
|
|
19
55
|
INVAR_INSTRUCTIONS = """
|
|
20
56
|
## Invar Tool Usage (MANDATORY)
|
|
21
57
|
|
|
@@ -83,6 +119,8 @@ the MCP tools and may not follow the correct workflow.
|
|
|
83
119
|
"""
|
|
84
120
|
|
|
85
121
|
|
|
122
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
123
|
+
# @invar:allow shell_result: MCP tool factory for guard command
|
|
86
124
|
def _get_guard_tool() -> Tool:
|
|
87
125
|
"""Define the invar_guard tool."""
|
|
88
126
|
return Tool(
|
|
@@ -98,11 +136,14 @@ def _get_guard_tool() -> Tool:
|
|
|
98
136
|
"path": {"type": "string", "description": "Project path (default: .)", "default": "."},
|
|
99
137
|
"changed": {"type": "boolean", "description": "Only verify git-changed files", "default": True},
|
|
100
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},
|
|
101
140
|
},
|
|
102
141
|
},
|
|
103
142
|
)
|
|
104
143
|
|
|
105
144
|
|
|
145
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
146
|
+
# @invar:allow shell_result: MCP tool factory for sig command
|
|
106
147
|
def _get_sig_tool() -> Tool:
|
|
107
148
|
"""Define the invar_sig tool."""
|
|
108
149
|
return Tool(
|
|
@@ -121,6 +162,8 @@ def _get_sig_tool() -> Tool:
|
|
|
121
162
|
)
|
|
122
163
|
|
|
123
164
|
|
|
165
|
+
# @shell_orchestration: MCP tool factory - creates Tool objects
|
|
166
|
+
# @invar:allow shell_result: MCP tool factory for map command
|
|
124
167
|
def _get_map_tool() -> Tool:
|
|
125
168
|
"""Define the invar_map tool."""
|
|
126
169
|
return Tool(
|
|
@@ -139,6 +182,8 @@ def _get_map_tool() -> Tool:
|
|
|
139
182
|
)
|
|
140
183
|
|
|
141
184
|
|
|
185
|
+
# @shell_orchestration: MCP server setup - registers handlers with framework
|
|
186
|
+
# @invar:allow shell_result: MCP framework API returns Server
|
|
142
187
|
def create_server() -> Server:
|
|
143
188
|
"""Create and configure the Invar MCP server."""
|
|
144
189
|
server = Server(name="invar", version="0.1.0", instructions=INVAR_INSTRUCTIONS)
|
|
@@ -158,39 +203,61 @@ def create_server() -> Server:
|
|
|
158
203
|
return server
|
|
159
204
|
|
|
160
205
|
|
|
206
|
+
# @shell_orchestration: MCP handler - subprocess is called inside
|
|
207
|
+
# @shell_complexity: Guard command with multiple optional flags
|
|
208
|
+
# @invar:allow shell_result: MCP handler for guard tool
|
|
161
209
|
async def _run_guard(args: dict[str, Any]) -> list[TextContent]:
|
|
162
210
|
"""Run invar guard command."""
|
|
163
|
-
cmd = [sys.executable, "-m", "invar.shell.cli", "guard"]
|
|
164
|
-
|
|
165
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"]
|
|
166
217
|
cmd.append(path)
|
|
167
218
|
|
|
168
219
|
if args.get("changed", True):
|
|
169
220
|
cmd.append("--changed")
|
|
170
221
|
if args.get("strict", False):
|
|
171
222
|
cmd.append("--strict")
|
|
223
|
+
# DX-37: Optional coverage collection
|
|
224
|
+
if args.get("coverage", False):
|
|
225
|
+
cmd.append("--coverage")
|
|
172
226
|
|
|
173
|
-
#
|
|
174
|
-
|
|
227
|
+
# DX-26: TTY auto-detection - MCP runs in non-TTY, so agent JSON output is automatic
|
|
228
|
+
# No explicit flag needed
|
|
175
229
|
|
|
176
230
|
return await _execute_command(cmd)
|
|
177
231
|
|
|
178
232
|
|
|
233
|
+
# @shell_orchestration: MCP handler - subprocess is called inside
|
|
234
|
+
# @invar:allow shell_result: MCP handler for sig tool
|
|
179
235
|
async def _run_sig(args: dict[str, Any]) -> list[TextContent]:
|
|
180
236
|
"""Run invar sig command."""
|
|
181
237
|
target = args.get("target", "")
|
|
182
238
|
if not target:
|
|
183
239
|
return [TextContent(type="text", text="Error: target is required")]
|
|
184
240
|
|
|
185
|
-
|
|
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"]
|
|
186
248
|
return await _execute_command(cmd)
|
|
187
249
|
|
|
188
250
|
|
|
251
|
+
# @shell_orchestration: MCP handler - subprocess is called inside
|
|
252
|
+
# @invar:allow shell_result: MCP handler for map tool
|
|
189
253
|
async def _run_map(args: dict[str, Any]) -> list[TextContent]:
|
|
190
254
|
"""Run invar map command."""
|
|
191
|
-
cmd = [sys.executable, "-m", "invar.shell.cli", "map"]
|
|
192
|
-
|
|
193
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"]
|
|
194
261
|
cmd.append(path)
|
|
195
262
|
|
|
196
263
|
top = args.get("top", 10)
|
|
@@ -200,14 +267,21 @@ async def _run_map(args: dict[str, Any]) -> list[TextContent]:
|
|
|
200
267
|
return await _execute_command(cmd)
|
|
201
268
|
|
|
202
269
|
|
|
203
|
-
|
|
204
|
-
|
|
270
|
+
# @shell_complexity: Command execution with error handling branches
|
|
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
|
+
"""
|
|
205
279
|
try:
|
|
206
280
|
result = subprocess.run(
|
|
207
281
|
cmd,
|
|
208
282
|
capture_output=True,
|
|
209
283
|
text=True,
|
|
210
|
-
timeout=
|
|
284
|
+
timeout=timeout,
|
|
211
285
|
)
|
|
212
286
|
|
|
213
287
|
output = result.stdout
|
|
@@ -224,17 +298,43 @@ async def _execute_command(cmd: list[str]) -> list[TextContent]:
|
|
|
224
298
|
return [TextContent(type="text", text=output)]
|
|
225
299
|
|
|
226
300
|
except subprocess.TimeoutExpired:
|
|
227
|
-
return [TextContent(type="text", text="Error: Command timed out (
|
|
301
|
+
return [TextContent(type="text", text=f"Error: Command timed out ({timeout}s)")]
|
|
228
302
|
except Exception as e:
|
|
229
303
|
return [TextContent(type="text", text=f"Error: {e}")]
|
|
230
304
|
|
|
231
305
|
|
|
306
|
+
# @shell_orchestration: MCP server entry point - runs async server
|
|
232
307
|
def run_server() -> None:
|
|
233
|
-
"""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
|
+
"""
|
|
234
313
|
import asyncio
|
|
235
314
|
|
|
236
315
|
from mcp.server.stdio import stdio_server
|
|
237
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
|
|
238
338
|
async def main():
|
|
239
339
|
server = create_server()
|
|
240
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
|
+
"""
|