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.
Files changed (64) hide show
  1. invar/__init__.py +68 -0
  2. invar/contracts.py +152 -0
  3. invar/core/__init__.py +8 -0
  4. invar/core/contracts.py +375 -0
  5. invar/core/extraction.py +172 -0
  6. invar/core/formatter.py +281 -0
  7. invar/core/hypothesis_strategies.py +454 -0
  8. invar/core/inspect.py +154 -0
  9. invar/core/lambda_helpers.py +190 -0
  10. invar/core/models.py +289 -0
  11. invar/core/must_use.py +172 -0
  12. invar/core/parser.py +276 -0
  13. invar/core/property_gen.py +383 -0
  14. invar/core/purity.py +369 -0
  15. invar/core/purity_heuristics.py +184 -0
  16. invar/core/references.py +180 -0
  17. invar/core/rule_meta.py +203 -0
  18. invar/core/rules.py +435 -0
  19. invar/core/strategies.py +267 -0
  20. invar/core/suggestions.py +324 -0
  21. invar/core/tautology.py +137 -0
  22. invar/core/timeout_inference.py +114 -0
  23. invar/core/utils.py +364 -0
  24. invar/decorators.py +94 -0
  25. invar/invariant.py +57 -0
  26. invar/mcp/__init__.py +10 -0
  27. invar/mcp/__main__.py +13 -0
  28. invar/mcp/server.py +251 -0
  29. invar/py.typed +0 -0
  30. invar/resource.py +99 -0
  31. invar/shell/__init__.py +8 -0
  32. invar/shell/cli.py +358 -0
  33. invar/shell/config.py +248 -0
  34. invar/shell/fs.py +112 -0
  35. invar/shell/git.py +85 -0
  36. invar/shell/guard_helpers.py +324 -0
  37. invar/shell/guard_output.py +235 -0
  38. invar/shell/init_cmd.py +289 -0
  39. invar/shell/mcp_config.py +171 -0
  40. invar/shell/perception.py +125 -0
  41. invar/shell/property_tests.py +227 -0
  42. invar/shell/prove.py +460 -0
  43. invar/shell/prove_cache.py +133 -0
  44. invar/shell/prove_fallback.py +183 -0
  45. invar/shell/templates.py +443 -0
  46. invar/shell/test_cmd.py +117 -0
  47. invar/shell/testing.py +297 -0
  48. invar/shell/update_cmd.py +191 -0
  49. invar/templates/CLAUDE.md.template +58 -0
  50. invar/templates/INVAR.md +134 -0
  51. invar/templates/__init__.py +1 -0
  52. invar/templates/aider.conf.yml.template +29 -0
  53. invar/templates/context.md.template +51 -0
  54. invar/templates/cursorrules.template +28 -0
  55. invar/templates/examples/README.md +21 -0
  56. invar/templates/examples/contracts.py +111 -0
  57. invar/templates/examples/core_shell.py +121 -0
  58. invar/templates/pre-commit-config.yaml.template +44 -0
  59. invar/templates/proposal.md.template +93 -0
  60. invar_tools-1.0.0.dist-info/METADATA +321 -0
  61. invar_tools-1.0.0.dist-info/RECORD +64 -0
  62. invar_tools-1.0.0.dist-info/WHEEL +4 -0
  63. invar_tools-1.0.0.dist-info/entry_points.txt +2 -0
  64. 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)
@@ -0,0 +1,8 @@
1
+ """
2
+ Shell module: I/O operations.
3
+
4
+ This module contains:
5
+ - cli.py: Typer commands (invar guard, invar map, etc.)
6
+ - fs.py: File system operations
7
+ - config.py: Configuration loading from pyproject.toml
8
+ """