invar-tools 1.0.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 +68 -0
- invar/contracts.py +152 -0
- invar/core/__init__.py +8 -0
- invar/core/contracts.py +375 -0
- invar/core/extraction.py +172 -0
- invar/core/formatter.py +281 -0
- invar/core/hypothesis_strategies.py +454 -0
- invar/core/inspect.py +154 -0
- invar/core/lambda_helpers.py +190 -0
- invar/core/models.py +289 -0
- invar/core/must_use.py +172 -0
- invar/core/parser.py +276 -0
- invar/core/property_gen.py +383 -0
- invar/core/purity.py +369 -0
- invar/core/purity_heuristics.py +184 -0
- invar/core/references.py +180 -0
- invar/core/rule_meta.py +203 -0
- invar/core/rules.py +435 -0
- invar/core/strategies.py +267 -0
- invar/core/suggestions.py +324 -0
- invar/core/tautology.py +137 -0
- invar/core/timeout_inference.py +114 -0
- invar/core/utils.py +364 -0
- invar/decorators.py +94 -0
- invar/invariant.py +57 -0
- invar/mcp/__init__.py +10 -0
- invar/mcp/__main__.py +13 -0
- invar/mcp/server.py +251 -0
- invar/py.typed +0 -0
- invar/resource.py +99 -0
- invar/shell/__init__.py +8 -0
- invar/shell/cli.py +358 -0
- invar/shell/config.py +248 -0
- invar/shell/fs.py +112 -0
- invar/shell/git.py +85 -0
- invar/shell/guard_helpers.py +324 -0
- invar/shell/guard_output.py +235 -0
- invar/shell/init_cmd.py +289 -0
- invar/shell/mcp_config.py +171 -0
- invar/shell/perception.py +125 -0
- invar/shell/property_tests.py +227 -0
- invar/shell/prove.py +460 -0
- invar/shell/prove_cache.py +133 -0
- invar/shell/prove_fallback.py +183 -0
- invar/shell/templates.py +443 -0
- invar/shell/test_cmd.py +117 -0
- invar/shell/testing.py +297 -0
- invar/shell/update_cmd.py +191 -0
- invar/templates/CLAUDE.md.template +58 -0
- invar/templates/INVAR.md +134 -0
- invar/templates/__init__.py +1 -0
- invar/templates/aider.conf.yml.template +29 -0
- invar/templates/context.md.template +51 -0
- invar/templates/cursorrules.template +28 -0
- invar/templates/examples/README.md +21 -0
- invar/templates/examples/contracts.py +111 -0
- invar/templates/examples/core_shell.py +121 -0
- invar/templates/pre-commit-config.yaml.template +44 -0
- invar/templates/proposal.md.template +93 -0
- invar_tools-1.0.0.dist-info/METADATA +321 -0
- invar_tools-1.0.0.dist-info/RECORD +64 -0
- invar_tools-1.0.0.dist-info/WHEEL +4 -0
- invar_tools-1.0.0.dist-info/entry_points.txt +2 -0
- invar_tools-1.0.0.dist-info/licenses/LICENSE +21 -0
invar/mcp/server.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Invar MCP Server implementation.
|
|
3
|
+
|
|
4
|
+
Exposes invar guard, sig, and map as first-class MCP tools.
|
|
5
|
+
Part of DX-16: Agent Tool Enforcement.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from mcp.server import Server
|
|
16
|
+
from mcp.types import TextContent, Tool
|
|
17
|
+
|
|
18
|
+
# Strong instructions for agent behavior (DX-16 + DX-17)
|
|
19
|
+
INVAR_INSTRUCTIONS = """
|
|
20
|
+
## Invar Tool Usage (MANDATORY)
|
|
21
|
+
|
|
22
|
+
This project uses Invar for all code verification and analysis.
|
|
23
|
+
The following rules are MANDATORY, not suggestions.
|
|
24
|
+
|
|
25
|
+
### Session Start (REQUIRED)
|
|
26
|
+
|
|
27
|
+
Before writing ANY code, you MUST execute:
|
|
28
|
+
|
|
29
|
+
1. `invar_guard(changed=true)` — Check existing violations
|
|
30
|
+
2. `invar_map(top=10)` — Understand code structure
|
|
31
|
+
|
|
32
|
+
Then read `.invar/examples/` and `.invar/context.md` for project context.
|
|
33
|
+
|
|
34
|
+
**Skipping Session Start → Non-compliant code → Task failure.**
|
|
35
|
+
|
|
36
|
+
### Tool Substitution Rules (ENFORCED)
|
|
37
|
+
|
|
38
|
+
| Task | ❌ NEVER Use | ✅ ALWAYS Use |
|
|
39
|
+
|------|-------------|---------------|
|
|
40
|
+
| Verify code quality | `Bash("pytest ...")` | `invar_guard` |
|
|
41
|
+
| Symbolic verification | `Bash("crosshair ...")` | `invar_guard` (included by default) |
|
|
42
|
+
| Understand file structure | `Read` entire .py file | `invar_sig` |
|
|
43
|
+
| Find entry points | `Grep` for "def " | `invar_map` |
|
|
44
|
+
|
|
45
|
+
### Common Mistakes to AVOID
|
|
46
|
+
|
|
47
|
+
❌ `Bash("python -m pytest file.py")` - Use invar_guard instead
|
|
48
|
+
❌ `Bash("pytest --doctest-modules ...")` - invar_guard includes doctests
|
|
49
|
+
❌ `Bash("crosshair check ...")` - invar_guard includes CrossHair by default
|
|
50
|
+
❌ `Read("src/foo.py")` just to see signatures - Use invar_sig instead
|
|
51
|
+
❌ `Grep` for function definitions - Use invar_map instead
|
|
52
|
+
❌ `Bash("invar guard ...")` - Use invar_guard MCP tool instead
|
|
53
|
+
|
|
54
|
+
### Task Completion
|
|
55
|
+
|
|
56
|
+
A task is complete ONLY when:
|
|
57
|
+
- Session Start executed (invar_guard + invar_map)
|
|
58
|
+
- Final `invar_guard` passed
|
|
59
|
+
- User requirement satisfied
|
|
60
|
+
|
|
61
|
+
### Why This Matters
|
|
62
|
+
|
|
63
|
+
1. **invar_guard** = Smart Guard (static + doctests + CrossHair + Hypothesis)
|
|
64
|
+
2. **invar_sig** shows @pre/@post contracts that Read misses
|
|
65
|
+
3. **invar_map** includes reference counts for importance ranking
|
|
66
|
+
|
|
67
|
+
### Correct Usage Examples
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
# Session Start (REQUIRED before any code)
|
|
71
|
+
invar_guard(changed=true)
|
|
72
|
+
invar_map(top=10)
|
|
73
|
+
|
|
74
|
+
# Verify code after changes (full verification by default)
|
|
75
|
+
invar_guard(changed=true)
|
|
76
|
+
|
|
77
|
+
# Understand a file's structure
|
|
78
|
+
invar_sig(target="src/invar/core/parser.py")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
IMPORTANT: Using Bash commands for Invar operations bypasses
|
|
82
|
+
the MCP tools and may not follow the correct workflow.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _get_guard_tool() -> Tool:
|
|
87
|
+
"""Define the invar_guard tool."""
|
|
88
|
+
return Tool(
|
|
89
|
+
name="invar_guard",
|
|
90
|
+
description=(
|
|
91
|
+
"Smart Guard: Verify code quality with static analysis + doctests. "
|
|
92
|
+
"Use this INSTEAD of Bash('pytest ...') or Bash('crosshair ...'). "
|
|
93
|
+
"Default runs static + doctests + CrossHair + Hypothesis."
|
|
94
|
+
),
|
|
95
|
+
inputSchema={
|
|
96
|
+
"type": "object",
|
|
97
|
+
"properties": {
|
|
98
|
+
"path": {"type": "string", "description": "Project path (default: .)", "default": "."},
|
|
99
|
+
"changed": {"type": "boolean", "description": "Only verify git-changed files", "default": True},
|
|
100
|
+
"strict": {"type": "boolean", "description": "Treat warnings as errors", "default": False},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _get_sig_tool() -> Tool:
|
|
107
|
+
"""Define the invar_sig tool."""
|
|
108
|
+
return Tool(
|
|
109
|
+
name="invar_sig",
|
|
110
|
+
description=(
|
|
111
|
+
"Show function signatures and contracts (@pre/@post). "
|
|
112
|
+
"Use this INSTEAD of Read('file.py') when you want to understand structure."
|
|
113
|
+
),
|
|
114
|
+
inputSchema={
|
|
115
|
+
"type": "object",
|
|
116
|
+
"properties": {
|
|
117
|
+
"target": {"type": "string", "description": "File or file::symbol path"},
|
|
118
|
+
},
|
|
119
|
+
"required": ["target"],
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _get_map_tool() -> Tool:
|
|
125
|
+
"""Define the invar_map tool."""
|
|
126
|
+
return Tool(
|
|
127
|
+
name="invar_map",
|
|
128
|
+
description=(
|
|
129
|
+
"Symbol map with reference counts. "
|
|
130
|
+
"Use this INSTEAD of Grep for 'def ' to find functions."
|
|
131
|
+
),
|
|
132
|
+
inputSchema={
|
|
133
|
+
"type": "object",
|
|
134
|
+
"properties": {
|
|
135
|
+
"path": {"type": "string", "description": "Project path", "default": "."},
|
|
136
|
+
"top": {"type": "integer", "description": "Show top N symbols", "default": 10},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def create_server() -> Server:
|
|
143
|
+
"""Create and configure the Invar MCP server."""
|
|
144
|
+
server = Server(name="invar", version="0.1.0", instructions=INVAR_INSTRUCTIONS)
|
|
145
|
+
|
|
146
|
+
@server.list_tools()
|
|
147
|
+
async def list_tools() -> list[Tool]:
|
|
148
|
+
return [_get_guard_tool(), _get_sig_tool(), _get_map_tool()]
|
|
149
|
+
|
|
150
|
+
@server.call_tool()
|
|
151
|
+
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
|
|
152
|
+
handlers = {"invar_guard": _run_guard, "invar_sig": _run_sig, "invar_map": _run_map}
|
|
153
|
+
handler = handlers.get(name)
|
|
154
|
+
if handler:
|
|
155
|
+
return await handler(arguments)
|
|
156
|
+
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
157
|
+
|
|
158
|
+
return server
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def _run_guard(args: dict[str, Any]) -> list[TextContent]:
|
|
162
|
+
"""Run invar guard command."""
|
|
163
|
+
cmd = [sys.executable, "-m", "invar.shell.cli", "guard"]
|
|
164
|
+
|
|
165
|
+
path = args.get("path", ".")
|
|
166
|
+
cmd.append(path)
|
|
167
|
+
|
|
168
|
+
if args.get("changed", True):
|
|
169
|
+
cmd.append("--changed")
|
|
170
|
+
if args.get("strict", False):
|
|
171
|
+
cmd.append("--strict")
|
|
172
|
+
|
|
173
|
+
# Always use JSON output for agent consumption
|
|
174
|
+
cmd.append("--json")
|
|
175
|
+
|
|
176
|
+
return await _execute_command(cmd)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def _run_sig(args: dict[str, Any]) -> list[TextContent]:
|
|
180
|
+
"""Run invar sig command."""
|
|
181
|
+
target = args.get("target", "")
|
|
182
|
+
if not target:
|
|
183
|
+
return [TextContent(type="text", text="Error: target is required")]
|
|
184
|
+
|
|
185
|
+
cmd = [sys.executable, "-m", "invar.shell.cli", "sig", target, "--json"]
|
|
186
|
+
return await _execute_command(cmd)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async def _run_map(args: dict[str, Any]) -> list[TextContent]:
|
|
190
|
+
"""Run invar map command."""
|
|
191
|
+
cmd = [sys.executable, "-m", "invar.shell.cli", "map"]
|
|
192
|
+
|
|
193
|
+
path = args.get("path", ".")
|
|
194
|
+
cmd.append(path)
|
|
195
|
+
|
|
196
|
+
top = args.get("top", 10)
|
|
197
|
+
cmd.extend(["--top", str(top)])
|
|
198
|
+
|
|
199
|
+
cmd.append("--json")
|
|
200
|
+
return await _execute_command(cmd)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def _execute_command(cmd: list[str]) -> list[TextContent]:
|
|
204
|
+
"""Execute a command and return the result."""
|
|
205
|
+
try:
|
|
206
|
+
result = subprocess.run(
|
|
207
|
+
cmd,
|
|
208
|
+
capture_output=True,
|
|
209
|
+
text=True,
|
|
210
|
+
timeout=120,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
output = result.stdout
|
|
214
|
+
if result.stderr:
|
|
215
|
+
output += f"\n\nStderr:\n{result.stderr}"
|
|
216
|
+
|
|
217
|
+
# Try to parse as JSON for better formatting
|
|
218
|
+
try:
|
|
219
|
+
parsed = json.loads(result.stdout)
|
|
220
|
+
output = json.dumps(parsed, indent=2)
|
|
221
|
+
except json.JSONDecodeError:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
return [TextContent(type="text", text=output)]
|
|
225
|
+
|
|
226
|
+
except subprocess.TimeoutExpired:
|
|
227
|
+
return [TextContent(type="text", text="Error: Command timed out (120s)")]
|
|
228
|
+
except Exception as e:
|
|
229
|
+
return [TextContent(type="text", text=f"Error: {e}")]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def run_server() -> None:
|
|
233
|
+
"""Run the Invar MCP server."""
|
|
234
|
+
import asyncio
|
|
235
|
+
|
|
236
|
+
from mcp.server.stdio import stdio_server
|
|
237
|
+
|
|
238
|
+
async def main():
|
|
239
|
+
server = create_server()
|
|
240
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
241
|
+
await server.run(
|
|
242
|
+
read_stream,
|
|
243
|
+
write_stream,
|
|
244
|
+
server.create_initialization_options(),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
asyncio.run(main())
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
run_server()
|
invar/py.typed
ADDED
|
File without changes
|
invar/resource.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resource management decorators for Invar.
|
|
3
|
+
|
|
4
|
+
Provides @must_close for marking classes that require explicit cleanup.
|
|
5
|
+
Inspired by Move language's resource semantics.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, TypeVar
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ResourceWarning(UserWarning):
|
|
16
|
+
"""Warning raised when a resource may not be properly closed."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MustCloseViolation(Exception):
|
|
22
|
+
"""Raised when a @must_close resource is not properly managed."""
|
|
23
|
+
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def must_close(cls: type[T]) -> type[T]:
|
|
28
|
+
"""
|
|
29
|
+
Mark a class as a resource that must be explicitly closed.
|
|
30
|
+
|
|
31
|
+
The decorated class should have a `close()` method. The decorator:
|
|
32
|
+
1. Adds __invar_must_close__ marker for Guard detection
|
|
33
|
+
2. Adds context manager protocol if not present
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
>>> @must_close
|
|
37
|
+
... class TempFile:
|
|
38
|
+
... def __init__(self, path: str):
|
|
39
|
+
... self.path = path
|
|
40
|
+
... self.closed = False
|
|
41
|
+
... def write(self, data: str) -> None:
|
|
42
|
+
... if self.closed:
|
|
43
|
+
... raise ValueError("File is closed")
|
|
44
|
+
... def close(self) -> None:
|
|
45
|
+
... self.closed = True
|
|
46
|
+
|
|
47
|
+
>>> # Preferred: use as context manager
|
|
48
|
+
>>> with TempFile("test.txt") as f:
|
|
49
|
+
... f.write("hello")
|
|
50
|
+
>>> f.closed
|
|
51
|
+
True
|
|
52
|
+
|
|
53
|
+
>>> # Also works: explicit close
|
|
54
|
+
>>> f2 = TempFile("test2.txt")
|
|
55
|
+
>>> f2.write("world")
|
|
56
|
+
>>> f2.close()
|
|
57
|
+
>>> f2.closed
|
|
58
|
+
True
|
|
59
|
+
"""
|
|
60
|
+
# Mark for Guard detection
|
|
61
|
+
cls.__invar_must_close__ = True # type: ignore[attr-defined]
|
|
62
|
+
|
|
63
|
+
# Add context manager protocol if not present
|
|
64
|
+
if not hasattr(cls, "__enter__"):
|
|
65
|
+
|
|
66
|
+
def __enter__(self: Any) -> Any:
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
cls.__enter__ = __enter__ # type: ignore[attr-defined]
|
|
70
|
+
|
|
71
|
+
if not hasattr(cls, "__exit__"):
|
|
72
|
+
|
|
73
|
+
def __exit__(self: Any, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
74
|
+
if hasattr(self, "close") and callable(self.close):
|
|
75
|
+
self.close()
|
|
76
|
+
|
|
77
|
+
cls.__exit__ = __exit__ # type: ignore[attr-defined]
|
|
78
|
+
|
|
79
|
+
return cls
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_must_close(cls_or_obj: Any) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Check if a class or instance is marked with @must_close.
|
|
85
|
+
|
|
86
|
+
>>> @must_close
|
|
87
|
+
... class Resource:
|
|
88
|
+
... def close(self): pass
|
|
89
|
+
>>> is_must_close(Resource)
|
|
90
|
+
True
|
|
91
|
+
>>> is_must_close(Resource())
|
|
92
|
+
True
|
|
93
|
+
>>> class Plain: pass
|
|
94
|
+
>>> is_must_close(Plain)
|
|
95
|
+
False
|
|
96
|
+
"""
|
|
97
|
+
if isinstance(cls_or_obj, type):
|
|
98
|
+
return getattr(cls_or_obj, "__invar_must_close__", False)
|
|
99
|
+
return getattr(type(cls_or_obj), "__invar_must_close__", False)
|