invar-tools 1.7.0__py3-none-any.whl → 1.7.1__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/core/utils.py +3 -1
- invar/shell/claude_hooks.py +90 -0
- invar/shell/commands/init.py +303 -313
- invar/shell/commands/uninstall.py +145 -7
- invar/shell/contract_coverage.py +4 -1
- invar/shell/templates.py +35 -29
- invar/templates/config/pre-commit.yaml.jinja +2 -0
- {invar_tools-1.7.0.dist-info → invar_tools-1.7.1.dist-info}/METADATA +55 -45
- {invar_tools-1.7.0.dist-info → invar_tools-1.7.1.dist-info}/RECORD +14 -15
- invar/templates/pre-commit-config.yaml.template +0 -46
- {invar_tools-1.7.0.dist-info → invar_tools-1.7.1.dist-info}/WHEEL +0 -0
- {invar_tools-1.7.0.dist-info → invar_tools-1.7.1.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.7.0.dist-info → invar_tools-1.7.1.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.7.0.dist-info → invar_tools-1.7.1.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.7.0.dist-info → invar_tools-1.7.1.dist-info}/licenses/NOTICE +0 -0
|
@@ -15,6 +15,8 @@ from pathlib import Path
|
|
|
15
15
|
import typer
|
|
16
16
|
from rich.console import Console
|
|
17
17
|
|
|
18
|
+
from invar.shell.claude_hooks import is_invar_hook
|
|
19
|
+
|
|
18
20
|
console = Console()
|
|
19
21
|
|
|
20
22
|
|
|
@@ -52,9 +54,42 @@ def has_invar_hook_marker(path: Path) -> bool:
|
|
|
52
54
|
return False
|
|
53
55
|
|
|
54
56
|
|
|
57
|
+
# @shell_orchestration: Regex patterns tightly coupled to file removal logic
|
|
58
|
+
def _is_empty_user_region(content: str) -> bool:
|
|
59
|
+
"""Check if user region only contains template comments (no real user content)."""
|
|
60
|
+
# Extract user region content
|
|
61
|
+
match = re.search(r"<!--invar:user-->(.*?)<!--/invar:user-->", content, flags=re.DOTALL)
|
|
62
|
+
if not match:
|
|
63
|
+
return True # No user region = empty
|
|
64
|
+
|
|
65
|
+
user_content = match.group(1)
|
|
66
|
+
|
|
67
|
+
# Remove all HTML/markdown comments
|
|
68
|
+
cleaned = re.sub(r"<!--.*?-->", "", user_content, flags=re.DOTALL)
|
|
69
|
+
|
|
70
|
+
# Remove invar-generated merge markers and headers
|
|
71
|
+
invar_patterns = [
|
|
72
|
+
r"## Claude Analysis \(Preserved\)\s*",
|
|
73
|
+
r"## My Custom Rules\s*",
|
|
74
|
+
r"- Rule \d+:.*\n?", # Template rules
|
|
75
|
+
]
|
|
76
|
+
for pattern in invar_patterns:
|
|
77
|
+
cleaned = re.sub(pattern, "", cleaned)
|
|
78
|
+
|
|
79
|
+
# Remove whitespace
|
|
80
|
+
cleaned = re.sub(r"\s+", " ", cleaned).strip()
|
|
81
|
+
|
|
82
|
+
# If only whitespace or empty after removing comments and invar content, it's "empty"
|
|
83
|
+
return len(cleaned) == 0
|
|
84
|
+
|
|
85
|
+
|
|
55
86
|
# @shell_orchestration: Regex patterns tightly coupled to file removal logic
|
|
56
87
|
def remove_invar_regions(content: str) -> str:
|
|
57
|
-
"""Remove <!--invar:xxx-->...<!--/invar:xxx--> regions
|
|
88
|
+
"""Remove <!--invar:xxx-->...<!--/invar:xxx--> regions.
|
|
89
|
+
|
|
90
|
+
User region is also removed if it only contains template comments.
|
|
91
|
+
Merge markers are always cleaned from user region.
|
|
92
|
+
"""
|
|
58
93
|
patterns = [
|
|
59
94
|
# HTML-style regions (CLAUDE.md)
|
|
60
95
|
(r"<!--invar:critical-->.*?<!--/invar:critical-->\n?", ""),
|
|
@@ -63,8 +98,34 @@ def remove_invar_regions(content: str) -> str:
|
|
|
63
98
|
# Comment-style regions (.pre-commit-config.yaml)
|
|
64
99
|
(r"# invar:begin\n.*?# invar:end\n?", ""),
|
|
65
100
|
]
|
|
101
|
+
|
|
102
|
+
# Also remove empty user region (only has template comments)
|
|
103
|
+
if _is_empty_user_region(content):
|
|
104
|
+
patterns.append((r"<!--invar:user-->.*?<!--/invar:user-->\n?", ""))
|
|
105
|
+
else:
|
|
106
|
+
# User region has real content - just remove the markers but keep content
|
|
107
|
+
patterns.append((r"<!--invar:user-->\n?", ""))
|
|
108
|
+
patterns.append((r"<!--/invar:user-->\n?", ""))
|
|
109
|
+
# Also clean invar-generated merge markers from user content
|
|
110
|
+
patterns.extend([
|
|
111
|
+
(r"<!-- =+ -->\n?", ""),
|
|
112
|
+
(r"<!-- MERGED CONTENT.*?-->\n?", ""),
|
|
113
|
+
(r"<!-- Original source:.*?-->\n?", ""),
|
|
114
|
+
(r"<!-- Merge date:.*?-->\n?", ""),
|
|
115
|
+
(r"<!-- END MERGED CONTENT -->\n?", ""),
|
|
116
|
+
(r"<!-- =+ -->\n?", ""),
|
|
117
|
+
(r"## Claude Analysis \(Preserved\)\n*", ""),
|
|
118
|
+
])
|
|
119
|
+
|
|
66
120
|
for pattern, replacement in patterns:
|
|
67
121
|
content = re.sub(pattern, replacement, content, flags=re.DOTALL)
|
|
122
|
+
|
|
123
|
+
# Clean up trailing footer if nothing else left
|
|
124
|
+
content = re.sub(r"\n*---\n+\*Generated by.*?\*\s*$", "", content, flags=re.DOTALL)
|
|
125
|
+
|
|
126
|
+
# Clean up multiple blank lines
|
|
127
|
+
content = re.sub(r"\n{3,}", "\n\n", content)
|
|
128
|
+
|
|
68
129
|
return content.strip()
|
|
69
130
|
|
|
70
131
|
|
|
@@ -84,6 +145,59 @@ def remove_mcp_invar_entry(path: Path) -> tuple[bool, str]:
|
|
|
84
145
|
return False, ""
|
|
85
146
|
|
|
86
147
|
|
|
148
|
+
# @shell_complexity: JSON parsing with conditional cleanup logic
|
|
149
|
+
def remove_hooks_from_settings(path: Path) -> tuple[bool, str]:
|
|
150
|
+
"""Remove Invar hooks from .claude/settings.local.json.
|
|
151
|
+
|
|
152
|
+
Uses merge strategy:
|
|
153
|
+
- Only removes Invar hooks (identified by .claude/hooks/ path)
|
|
154
|
+
- Preserves user's existing hooks
|
|
155
|
+
- Cleans up empty hook types and hooks section
|
|
156
|
+
"""
|
|
157
|
+
settings_path = path / ".claude" / "settings.local.json"
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
if not settings_path.exists():
|
|
161
|
+
return False, ""
|
|
162
|
+
content = settings_path.read_text()
|
|
163
|
+
data = json.loads(content)
|
|
164
|
+
|
|
165
|
+
if "hooks" not in data:
|
|
166
|
+
return False, content
|
|
167
|
+
|
|
168
|
+
existing_hooks = data["hooks"]
|
|
169
|
+
modified = False
|
|
170
|
+
|
|
171
|
+
# Filter out Invar hooks from each hook type
|
|
172
|
+
for hook_type in list(existing_hooks.keys()):
|
|
173
|
+
hook_list = existing_hooks[hook_type]
|
|
174
|
+
if isinstance(hook_list, list):
|
|
175
|
+
# Keep only non-Invar hooks
|
|
176
|
+
filtered = [h for h in hook_list if not is_invar_hook(h)]
|
|
177
|
+
if len(filtered) != len(hook_list):
|
|
178
|
+
modified = True
|
|
179
|
+
if filtered:
|
|
180
|
+
existing_hooks[hook_type] = filtered
|
|
181
|
+
else:
|
|
182
|
+
# No hooks left for this type, remove the key
|
|
183
|
+
del existing_hooks[hook_type]
|
|
184
|
+
|
|
185
|
+
# If no hooks left, remove the hooks section entirely
|
|
186
|
+
if not existing_hooks:
|
|
187
|
+
del data["hooks"]
|
|
188
|
+
|
|
189
|
+
if not modified:
|
|
190
|
+
return False, content
|
|
191
|
+
|
|
192
|
+
# If nothing left in data, indicate file can be deleted
|
|
193
|
+
if not data:
|
|
194
|
+
return True, ""
|
|
195
|
+
|
|
196
|
+
return True, json.dumps(data, indent=2)
|
|
197
|
+
except (OSError, json.JSONDecodeError):
|
|
198
|
+
return False, ""
|
|
199
|
+
|
|
200
|
+
|
|
87
201
|
# @shell_complexity: Multi-file type detection requires comprehensive branching
|
|
88
202
|
def collect_removal_targets(path: Path) -> dict:
|
|
89
203
|
"""Collect files and directories to remove/modify."""
|
|
@@ -146,16 +260,20 @@ def collect_removal_targets(path: Path) -> dict:
|
|
|
146
260
|
(f".claude/hooks/{hook_file.name}", "hook, has invar marker")
|
|
147
261
|
)
|
|
148
262
|
|
|
149
|
-
# CLAUDE.md -
|
|
263
|
+
# CLAUDE.md - delete if empty user region, otherwise modify
|
|
150
264
|
claude_md = path / "CLAUDE.md"
|
|
151
265
|
if claude_md.exists():
|
|
152
266
|
content = claude_md.read_text()
|
|
153
267
|
if "<!--invar:" in content:
|
|
154
|
-
# Check if
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
("CLAUDE.md",
|
|
158
|
-
|
|
268
|
+
# Check if user region has real content
|
|
269
|
+
if _is_empty_user_region(content):
|
|
270
|
+
# Will be empty after cleanup - delete
|
|
271
|
+
targets["delete_files"].append(("CLAUDE.md", "no user content"))
|
|
272
|
+
else:
|
|
273
|
+
# Has user content - modify
|
|
274
|
+
targets["modify_files"].append(
|
|
275
|
+
("CLAUDE.md", "remove invar regions, keep user content")
|
|
276
|
+
)
|
|
159
277
|
|
|
160
278
|
# .mcp.json - modify or delete
|
|
161
279
|
mcp_json = path / ".mcp.json"
|
|
@@ -167,6 +285,20 @@ def collect_removal_targets(path: Path) -> dict:
|
|
|
167
285
|
else:
|
|
168
286
|
targets["delete_files"].append((".mcp.json", "only had invar config"))
|
|
169
287
|
|
|
288
|
+
# settings.local.json - remove hooks section or delete if empty
|
|
289
|
+
settings_local = path / ".claude" / "settings.local.json"
|
|
290
|
+
if settings_local.exists():
|
|
291
|
+
modified, new_content = remove_hooks_from_settings(path)
|
|
292
|
+
if modified:
|
|
293
|
+
if new_content:
|
|
294
|
+
targets["modify_files"].append(
|
|
295
|
+
(".claude/settings.local.json", "remove hooks section")
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
targets["delete_files"].append(
|
|
299
|
+
(".claude/settings.local.json", "only had hooks config")
|
|
300
|
+
)
|
|
301
|
+
|
|
170
302
|
# Config files with region markers (DX-69: cursor/aider removed)
|
|
171
303
|
for file_name in [".pre-commit-config.yaml"]:
|
|
172
304
|
file_path = path / file_name
|
|
@@ -245,6 +377,11 @@ def execute_removal(path: Path, targets: dict) -> None:
|
|
|
245
377
|
if modified and new_content:
|
|
246
378
|
file_path.write_text(new_content)
|
|
247
379
|
console.print(f"[yellow]Modified[/yellow] {file_name}")
|
|
380
|
+
elif file_name == ".claude/settings.local.json":
|
|
381
|
+
modified, new_content = remove_hooks_from_settings(path)
|
|
382
|
+
if modified and new_content:
|
|
383
|
+
file_path.write_text(new_content)
|
|
384
|
+
console.print(f"[yellow]Modified[/yellow] {file_name}")
|
|
248
385
|
else:
|
|
249
386
|
content = file_path.read_text()
|
|
250
387
|
cleaned = remove_invar_regions(content)
|
|
@@ -270,6 +407,7 @@ def execute_removal(path: Path, targets: dict) -> None:
|
|
|
270
407
|
console.print("[dim]Removed empty[/dim] .claude/")
|
|
271
408
|
|
|
272
409
|
|
|
410
|
+
# @shell_complexity: CLI entry point with confirmation prompts and multi-target removal
|
|
273
411
|
def uninstall(
|
|
274
412
|
path: Path = typer.Argument(
|
|
275
413
|
Path(),
|
invar/shell/contract_coverage.py
CHANGED
|
@@ -121,6 +121,7 @@ def count_contracts_in_file(
|
|
|
121
121
|
return Success(result)
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
# @shell_complexity: Git status parsing requires multiple branch conditions
|
|
124
125
|
def get_changed_python_files(path: Path) -> Result[list[Path], str]:
|
|
125
126
|
"""Get Python files changed in git."""
|
|
126
127
|
try:
|
|
@@ -153,6 +154,7 @@ def get_changed_python_files(path: Path) -> Result[list[Path], str]:
|
|
|
153
154
|
return Failure("Git not found")
|
|
154
155
|
|
|
155
156
|
|
|
157
|
+
# @shell_complexity: Coverage calculation with multiple file/directory handling paths
|
|
156
158
|
def calculate_contract_coverage(
|
|
157
159
|
path: Path, changed_only: bool = False
|
|
158
160
|
) -> Result[ContractCoverageReport, str]:
|
|
@@ -207,6 +209,7 @@ def calculate_contract_coverage(
|
|
|
207
209
|
return Success(report)
|
|
208
210
|
|
|
209
211
|
|
|
212
|
+
# @shell_complexity: Batch detection with git status parsing and threshold logic
|
|
210
213
|
def detect_batch_creation(
|
|
211
214
|
path: Path, threshold: int = 3
|
|
212
215
|
) -> Result[BatchWarning | None, str]:
|
|
@@ -263,7 +266,7 @@ def detect_batch_creation(
|
|
|
263
266
|
return Success(None)
|
|
264
267
|
|
|
265
268
|
|
|
266
|
-
# @
|
|
269
|
+
# @shell_complexity: Report formatting with multiple conditional sections
|
|
267
270
|
def format_contract_coverage_report(report: ContractCoverageReport) -> str:
|
|
268
271
|
"""Format coverage report for human-readable output."""
|
|
269
272
|
lines = [
|
invar/shell/templates.py
CHANGED
|
@@ -76,11 +76,18 @@ def copy_template(
|
|
|
76
76
|
|
|
77
77
|
# @shell_complexity: Config addition with existing file detection
|
|
78
78
|
def add_config(path: Path, console) -> Result[bool, str]:
|
|
79
|
-
"""Add configuration to project. Returns Success(True) if added, Success(False) if skipped.
|
|
79
|
+
"""Add configuration to project. Returns Success(True) if added, Success(False) if skipped.
|
|
80
|
+
|
|
81
|
+
DX-70: Creates .invar/config.toml instead of invar.toml for cleaner organization.
|
|
82
|
+
Backward compatible: still reads from invar.toml if it exists.
|
|
83
|
+
"""
|
|
80
84
|
pyproject = path / "pyproject.toml"
|
|
81
|
-
|
|
85
|
+
invar_dir = path / ".invar"
|
|
86
|
+
invar_config = invar_dir / "config.toml"
|
|
87
|
+
legacy_invar_toml = path / "invar.toml"
|
|
82
88
|
|
|
83
89
|
try:
|
|
90
|
+
# Priority 1: Add to pyproject.toml if it exists
|
|
84
91
|
if pyproject.exists():
|
|
85
92
|
content = pyproject.read_text()
|
|
86
93
|
if "[tool.invar]" not in content:
|
|
@@ -90,9 +97,15 @@ def add_config(path: Path, console) -> Result[bool, str]:
|
|
|
90
97
|
return Success(True)
|
|
91
98
|
return Success(False)
|
|
92
99
|
|
|
93
|
-
if
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
# Skip if legacy invar.toml exists (backward compatibility)
|
|
101
|
+
if legacy_invar_toml.exists():
|
|
102
|
+
return Success(False)
|
|
103
|
+
|
|
104
|
+
# Create .invar/config.toml (DX-70: new default location)
|
|
105
|
+
if not invar_config.exists():
|
|
106
|
+
invar_dir.mkdir(exist_ok=True)
|
|
107
|
+
invar_config.write_text(_DEFAULT_INVAR_TOML)
|
|
108
|
+
console.print("[green]Created[/green] .invar/config.toml")
|
|
96
109
|
return Success(True)
|
|
97
110
|
|
|
98
111
|
return Success(False)
|
|
@@ -197,6 +210,7 @@ AGENT_CONFIGS = {
|
|
|
197
210
|
}
|
|
198
211
|
|
|
199
212
|
|
|
213
|
+
# @shell_complexity: Multi-agent config detection with file existence checks
|
|
200
214
|
def detect_agent_configs(path: Path) -> Result[dict[str, str], str]:
|
|
201
215
|
"""
|
|
202
216
|
Detect existing agent configuration files.
|
|
@@ -392,36 +406,28 @@ The server communicates via stdio and should be managed by your AI agent.
|
|
|
392
406
|
|
|
393
407
|
# @shell_complexity: Git hooks installation with backup
|
|
394
408
|
def install_hooks(path: Path, console) -> Result[bool, str]:
|
|
395
|
-
"""
|
|
409
|
+
"""Run 'pre-commit install' if config exists (file created by sync_templates)."""
|
|
396
410
|
import subprocess
|
|
397
411
|
|
|
398
412
|
pre_commit_config = path / ".pre-commit-config.yaml"
|
|
399
413
|
|
|
400
|
-
if pre_commit_config.exists():
|
|
401
|
-
|
|
414
|
+
if not pre_commit_config.exists():
|
|
415
|
+
# File should be created by sync_templates; skip if missing
|
|
402
416
|
return Success(False)
|
|
403
417
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
subprocess.run(
|
|
414
|
-
["pre-commit", "install"],
|
|
415
|
-
cwd=path,
|
|
416
|
-
check=True,
|
|
417
|
-
capture_output=True,
|
|
418
|
-
)
|
|
419
|
-
console.print("[green]Installed[/green] pre-commit hooks")
|
|
420
|
-
except FileNotFoundError:
|
|
421
|
-
console.print("[dim]Run: pre-commit install (pre-commit not in PATH)[/dim]")
|
|
422
|
-
except subprocess.CalledProcessError:
|
|
423
|
-
console.print("[dim]Run: pre-commit install (not a git repo?)[/dim]")
|
|
424
|
-
|
|
418
|
+
# Auto-install hooks (Automatic > Opt-in)
|
|
419
|
+
try:
|
|
420
|
+
subprocess.run(
|
|
421
|
+
["pre-commit", "install"],
|
|
422
|
+
cwd=path,
|
|
423
|
+
check=True,
|
|
424
|
+
capture_output=True,
|
|
425
|
+
)
|
|
426
|
+
console.print("[green]Installed[/green] pre-commit hooks")
|
|
425
427
|
return Success(True)
|
|
428
|
+
except FileNotFoundError:
|
|
429
|
+
console.print("[dim]Run: pre-commit install (pre-commit not in PATH)[/dim]")
|
|
430
|
+
except subprocess.CalledProcessError:
|
|
431
|
+
console.print("[dim]Run: pre-commit install (not a git repo?)[/dim]")
|
|
426
432
|
|
|
427
433
|
return Success(False)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: invar-tools
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.1
|
|
4
4
|
Summary: AI-native software engineering tools with design-by-contract verification
|
|
5
5
|
Project-URL: Homepage, https://github.com/tefx/invar
|
|
6
6
|
Project-URL: Documentation, https://github.com/tefx/invar#readme
|
|
@@ -28,6 +28,7 @@ Requires-Dist: jinja2>=3.0
|
|
|
28
28
|
Requires-Dist: mcp>=1.0
|
|
29
29
|
Requires-Dist: pre-commit>=3.0
|
|
30
30
|
Requires-Dist: pydantic>=2.0
|
|
31
|
+
Requires-Dist: questionary>=2.0
|
|
31
32
|
Requires-Dist: returns>=0.20
|
|
32
33
|
Requires-Dist: rich>=13.0
|
|
33
34
|
Requires-Dist: typer>=0.9
|
|
@@ -129,35 +130,24 @@ Guard passed.
|
|
|
129
130
|
|
|
130
131
|
**Why uvx?** Always uses latest version, doesn't pollute project dependencies, auto-detects your project's venv.
|
|
131
132
|
|
|
132
|
-
###
|
|
133
|
+
### 🎯 Setup
|
|
133
134
|
|
|
134
135
|
```bash
|
|
135
|
-
# 1. Enter your project directory
|
|
136
136
|
cd your-project
|
|
137
137
|
|
|
138
|
-
#
|
|
138
|
+
# Interactive mode - choose what to install
|
|
139
|
+
uvx invar-tools init
|
|
140
|
+
|
|
141
|
+
# Or quick setup for Claude Code (skip prompts)
|
|
139
142
|
uvx invar-tools init --claude
|
|
140
143
|
|
|
141
|
-
#
|
|
144
|
+
# Add runtime contracts to your project
|
|
142
145
|
pip install invar-runtime
|
|
143
|
-
# Or add to pyproject.toml: dependencies = ["invar-runtime"]
|
|
144
|
-
|
|
145
|
-
# 4. Start coding with AI
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
cd your-project
|
|
152
|
-
|
|
153
|
-
# Update managed files, preserve your customizations
|
|
154
|
-
uvx invar-tools init --claude
|
|
155
|
-
|
|
156
|
-
# Or without Claude Code integration
|
|
157
|
-
uvx invar-tools init
|
|
158
|
-
```
|
|
148
|
+
**Safe and idempotent** — Run `invar init` anytime. It always **merges** with existing files, preserving your content.
|
|
159
149
|
|
|
160
|
-
|
|
150
|
+
> 💡 **After `claude /init`?** Just run `invar init` again to restore Invar configuration.
|
|
161
151
|
|
|
162
152
|
### 💬 Example Interaction
|
|
163
153
|
|
|
@@ -375,39 +365,57 @@ AlphaCodium · Parsel · Reflexion · Clover
|
|
|
375
365
|
|
|
376
366
|
---
|
|
377
367
|
|
|
378
|
-
## 🖥️
|
|
368
|
+
## 🖥️ Agent Support
|
|
369
|
+
|
|
370
|
+
| Agent | Status | Setup |
|
|
371
|
+
|-------|--------|-------|
|
|
372
|
+
| **Claude Code** | ✅ Full | `invar init` → select Claude Code |
|
|
373
|
+
| **Pi / Cursor** | 🚧 In progress | `invar init` → select Other, include `AGENT.md` in prompt |
|
|
374
|
+
| **Other** | 📝 Manual | `invar init` → select Other, include `AGENT.md` in prompt |
|
|
379
375
|
|
|
380
|
-
|
|
381
|
-
|---------|-------------|---------------|
|
|
382
|
-
| CLI verification (`invar guard`) | ✅ | ✅ |
|
|
383
|
-
| Protocol document (INVAR.md) | ✅ | ✅ |
|
|
384
|
-
| MCP tool integration | ✅ Auto-configured | Manual setup possible |
|
|
385
|
-
| Workflow skills | ✅ Auto-configured | Include in system prompt |
|
|
386
|
-
| Pre-commit hooks | ✅ | ✅ |
|
|
387
|
-
| Sub-agent review | ✅ | — |
|
|
376
|
+
### Claude Code (Full Experience)
|
|
388
377
|
|
|
389
|
-
|
|
378
|
+
All features auto-configured:
|
|
379
|
+
- MCP tools (`invar_guard`, `invar_sig`, `invar_map`)
|
|
380
|
+
- Workflow skills (`/develop`, `/review`, `/investigate`, `/propose`)
|
|
381
|
+
- Claude Code hooks (tool guidance, verification reminders)
|
|
382
|
+
- Pre-commit hooks
|
|
390
383
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
384
|
+
### Pi / Cursor (In Progress)
|
|
385
|
+
|
|
386
|
+
Currently available:
|
|
387
|
+
- Protocol document (INVAR.md)
|
|
388
|
+
- CLI verification (`invar guard`)
|
|
389
|
+
- Pre-commit hooks
|
|
390
|
+
- MCP server (manual configuration)
|
|
391
|
+
|
|
392
|
+
### Other Editors (Manual)
|
|
393
|
+
|
|
394
|
+
1. Run `invar init` → select "Other (AGENT.md)"
|
|
395
|
+
2. Include generated `AGENT.md` in your agent's prompt
|
|
396
|
+
3. Configure MCP server if supported
|
|
397
|
+
4. Use CLI commands (`invar guard`) for verification
|
|
395
398
|
|
|
396
399
|
---
|
|
397
400
|
|
|
398
401
|
## 📂 What Gets Installed
|
|
399
402
|
|
|
400
|
-
`invar init
|
|
403
|
+
`invar init` creates (select in interactive mode):
|
|
404
|
+
|
|
405
|
+
| File/Directory | Purpose | Category |
|
|
406
|
+
|----------------|---------|----------|
|
|
407
|
+
| `INVAR.md` | Protocol for AI agents | Required |
|
|
408
|
+
| `.invar/` | Config, context, examples | Required |
|
|
409
|
+
| `.pre-commit-config.yaml` | Verification before commit | Optional |
|
|
410
|
+
| `src/core/`, `src/shell/` | Recommended structure | Optional |
|
|
411
|
+
| `CLAUDE.md` | Agent instructions | Claude Code |
|
|
412
|
+
| `.claude/skills/` | Workflow automation | Claude Code |
|
|
413
|
+
| `.claude/commands/` | User commands (/audit, /guard) | Claude Code |
|
|
414
|
+
| `.claude/hooks/` | Tool guidance | Claude Code |
|
|
415
|
+
| `.mcp.json` | MCP server config | Claude Code |
|
|
416
|
+
| `AGENT.md` | Universal agent instructions | Other agents |
|
|
401
417
|
|
|
402
|
-
|
|
403
|
-
|----------------|---------|-----------|
|
|
404
|
-
| `INVAR.md` | Protocol for AI agents | No (managed) |
|
|
405
|
-
| `CLAUDE.md` | Project configuration | Yes |
|
|
406
|
-
| `.claude/skills/` | Workflow skills | Yes |
|
|
407
|
-
| `.claude/hooks/` | Tool call interception | Yes |
|
|
408
|
-
| `.invar/examples/` | Reference patterns | No (managed) |
|
|
409
|
-
| `.invar/context.md` | Project state, lessons | Yes |
|
|
410
|
-
| `pyproject.toml` | `[tool.invar]` section | Yes |
|
|
418
|
+
**Note:** If `pyproject.toml` exists, Guard configuration goes there as `[tool.invar.guard]` instead of `.invar/config.toml`.
|
|
411
419
|
|
|
412
420
|
**Recommended structure:**
|
|
413
421
|
|
|
@@ -473,7 +481,9 @@ rules = ["missing_contract", "shell_result"]
|
|
|
473
481
|
| `invar guard` | Full verification (static + doctest + property + symbolic) |
|
|
474
482
|
| `invar guard --changed` | Only git-modified files |
|
|
475
483
|
| `invar guard --static` | Static analysis only (~0.5s) |
|
|
476
|
-
| `invar init` | Initialize or update project |
|
|
484
|
+
| `invar init` | Initialize or update project (interactive) |
|
|
485
|
+
| `invar init --claude` | Quick setup for Claude Code |
|
|
486
|
+
| `invar uninstall` | Remove Invar from project (preserves user content) |
|
|
477
487
|
| `invar sig <file>` | Show signatures and contracts |
|
|
478
488
|
| `invar map` | Symbol map with reference counts |
|
|
479
489
|
| `invar rules` | List all rules |
|
|
@@ -30,7 +30,7 @@ invar/core/tautology.py,sha256=Pmn__a0Bt55W0lAQo1G5q8Ory9KuE23dRknKw45xxbs,9221
|
|
|
30
30
|
invar/core/template_parser.py,sha256=vH3H8OX55scZ1hWh3xoA8oJMhgleKufCOhkTvsSuu_4,14730
|
|
31
31
|
invar/core/timeout_inference.py,sha256=BS2fJGmwOrLpYZUku4qrizgNDSIXVLFBslW-6sRAvpc,3451
|
|
32
32
|
invar/core/trivial_detection.py,sha256=KYP8jJb7QDeusAxFdX5NAML_H0NL5wLgMeBWDQmNqfU,6086
|
|
33
|
-
invar/core/utils.py,sha256=
|
|
33
|
+
invar/core/utils.py,sha256=PyW8dcTLUEFD81xcvkz-LNnCwjIQefn08OUh23fM_Po,14266
|
|
34
34
|
invar/core/verification_routing.py,sha256=_jXi1txFCcUdnB3-Yavtuyk8N-XhEO_Vu_051Vuz27Y,5020
|
|
35
35
|
invar/core/patterns/__init__.py,sha256=79a3ucN0BI54RnIOe49lngKASpADygs1hll9ROCrP6s,1429
|
|
36
36
|
invar/core/patterns/detector.py,sha256=lZ2HPtDJDiGps8Y3e7jds3naZa1oGqDDscHRSX5Vv0s,8297
|
|
@@ -45,9 +45,9 @@ invar/mcp/__init__.py,sha256=n3S7QwMjSMqOMT8cI2jf9E0yZPjKmBOJyIYhq4WZ8TQ,226
|
|
|
45
45
|
invar/mcp/__main__.py,sha256=ZcIT2U6xUyGOWucl4jq422BDE3lRLjqyxb9pFylRBdk,219
|
|
46
46
|
invar/mcp/server.py,sha256=ay-w2YfSa1kTmBFx3x3jEgmNRC3NFEW0EYuZRt7M39w,12244
|
|
47
47
|
invar/shell/__init__.py,sha256=FFw1mNbh_97PeKPcHIqQpQ7mw-JoIvyLM1yOdxLw5uk,204
|
|
48
|
-
invar/shell/claude_hooks.py,sha256=
|
|
48
|
+
invar/shell/claude_hooks.py,sha256=KJAQ-a7-mvabJ2fgsC9wPMakvi7J43GCU6ZcpWwKYFg,16180
|
|
49
49
|
invar/shell/config.py,sha256=6-kbo6--SxfROXoyU-v7InSLR8f_U1Mar_xEOdCXFkY,17633
|
|
50
|
-
invar/shell/contract_coverage.py,sha256=
|
|
50
|
+
invar/shell/contract_coverage.py,sha256=UPn-lEqrAFu00fl7v9PnSvNwS7KX3_SV1K_GhynQ9cw,12023
|
|
51
51
|
invar/shell/coverage.py,sha256=m01o898IFIdBztEBQLwwL1Vt5PWrpUntO4lv4nWEkls,11344
|
|
52
52
|
invar/shell/fs.py,sha256=wVD7DPWsCIJXuTyY_pi-5_LS82mXRdn_grJCOLn9zpU,3699
|
|
53
53
|
invar/shell/git.py,sha256=s6RQxEDQuLrmK3mru88EoYP8__4hiFW8AozlcxmY47E,2784
|
|
@@ -59,19 +59,19 @@ invar/shell/pattern_integration.py,sha256=pRcjfq3NvMW_tvQCnaXZnD1k5AVEWK8CYOE2jN
|
|
|
59
59
|
invar/shell/property_tests.py,sha256=N9JreyH5PqR89oF5yLcX7ZAV-Koyg5BKo-J05-GUPsA,9109
|
|
60
60
|
invar/shell/subprocess_env.py,sha256=9oXl3eMEbzLsFEgMHqobEw6oW_wV0qMEP7pklwm58Pw,11453
|
|
61
61
|
invar/shell/template_engine.py,sha256=IzOiGsKVFo0lDUdtg27wMzIJJKToclv151RDZuDnHHo,11027
|
|
62
|
-
invar/shell/templates.py,sha256=
|
|
62
|
+
invar/shell/templates.py,sha256=30eT61HXPA6DmsgUyUqljZNGGtB9TSR-aAaI4fmKAco,13722
|
|
63
63
|
invar/shell/testing.py,sha256=rTNBH0Okh2qtG9ohSXOz487baQ2gXrWT3s_WECW3HJs,11143
|
|
64
64
|
invar/shell/commands/__init__.py,sha256=MEkKwVyjI9DmkvBpJcuumXo2Pg_FFkfEr-Rr3nrAt7A,284
|
|
65
65
|
invar/shell/commands/guard.py,sha256=vDBGOFb9mQ1D8eXrMvQB505GpjO1XLeCLrv2ig9-6dU,21718
|
|
66
66
|
invar/shell/commands/hooks.py,sha256=W-SOnT4VQyUvXwipozkJwgEYfiOJGz7wksrbcdWegUg,2356
|
|
67
|
-
invar/shell/commands/init.py,sha256=
|
|
67
|
+
invar/shell/commands/init.py,sha256=wAPPIDpoJyNOhrD09LldRg8w76mWv0r5qTcqlPWc920,13411
|
|
68
68
|
invar/shell/commands/merge.py,sha256=nuvKo8m32-OL-SCQlS4SLKmOZxQ3qj-1nGCx1Pgzifw,8183
|
|
69
69
|
invar/shell/commands/mutate.py,sha256=GwemiO6LlbGCBEQsBFnzZuKhF-wIMEl79GAMnKUWc8U,5765
|
|
70
70
|
invar/shell/commands/perception.py,sha256=TyH_HpqyKkmE3-zcU4YyBG8ghwJaSFeRC-OQMVBDTbQ,3837
|
|
71
71
|
invar/shell/commands/sync_self.py,sha256=nmqBry7V2_enKwy2zzHg8UoedZNicLe3yKDhjmBeZ68,3880
|
|
72
72
|
invar/shell/commands/template_sync.py,sha256=wVZ-UvJ1wpN2UBcWMfbei0n46XHYx-zRbMA2oX6FSi4,13723
|
|
73
73
|
invar/shell/commands/test.py,sha256=goMf-ovvzEyWQMheq4YlJ-mwK5-w3lDj0cq0IA_1-_c,4205
|
|
74
|
-
invar/shell/commands/uninstall.py,sha256=
|
|
74
|
+
invar/shell/commands/uninstall.py,sha256=u1fqE2jCod2KxFMEYbtC4UFK1zuxzoaQKQzZdAjQcDE,17368
|
|
75
75
|
invar/shell/commands/update.py,sha256=0V5F8vxQ6PHPHPVYDmxdRD7xXeQEFypiJMYpY5ryiek,1349
|
|
76
76
|
invar/shell/prove/__init__.py,sha256=ZqlbmyMFJf6yAle8634jFuPRv8wNvHps8loMlOJyf8A,240
|
|
77
77
|
invar/shell/prove/accept.py,sha256=cnY_6jzU1EBnpLF8-zWUWcXiSXtCwxPsXEYXsSVPG38,3717
|
|
@@ -82,13 +82,12 @@ invar/templates/CLAUDE.md.template,sha256=eaGU3SyRO_NEifw5b26k3srgQH4jyeujjCJ-Hb
|
|
|
82
82
|
invar/templates/__init__.py,sha256=cb3ht8KPK5oBn5oG6HsTznujmo9WriJ_P--fVxJwycc,45
|
|
83
83
|
invar/templates/context.md.template,sha256=FKyI1ghpqcf4wftyv9-auIFHor8Nm8lETN45Ja-L8Og,2386
|
|
84
84
|
invar/templates/manifest.toml,sha256=cEe7yEOOeaLmOF-PrwZXxiPGjHhsSJYkWBKRHDmSbac,4268
|
|
85
|
-
invar/templates/pre-commit-config.yaml.template,sha256=ZMqiStLCf9cC4uL2JoF59aYv_G4AV-roGdkj1tWHCBc,1763
|
|
86
85
|
invar/templates/proposal.md.template,sha256=UP7SpQ7gk8jVlHGLQCSQ5c-kCj1DBQEz8M-vEStK77I,1573
|
|
87
86
|
invar/templates/commands/audit.md,sha256=OrotO8420zTKnlNyAyL1Eos0VIaihzEU4AHdfDv68Oc,4162
|
|
88
87
|
invar/templates/commands/guard.md,sha256=N_C_AXd9kI85W1B0aTEycjiDp_jdaP8eeq8O0FQ_WQ8,1227
|
|
89
88
|
invar/templates/config/CLAUDE.md.jinja,sha256=VbtDWxn3H8qiE9-DV1hlG3DJ-GcBQU4ZiUHbFh6Bxxk,7814
|
|
90
89
|
invar/templates/config/context.md.jinja,sha256=_kJ8erEQNJMLDCKrv4BXWkO6OaGzE-zW9biCf7144aY,3103
|
|
91
|
-
invar/templates/config/pre-commit.yaml.jinja,sha256=
|
|
90
|
+
invar/templates/config/pre-commit.yaml.jinja,sha256=nUPxLxkTHAgZwhFAuOMDbZ8v0NQV9FlQPbr2MDEOsoA,1778
|
|
92
91
|
invar/templates/examples/README.md,sha256=xMcJZ1KEcfLJi5Ope_4FIbqDWKK3mRleAgllvgeNT6I,572
|
|
93
92
|
invar/templates/examples/conftest.py,sha256=uKA4NR7nyZWeSzY0URdZtw5zCcJpU32jNcaSKrI1Mxc,152
|
|
94
93
|
invar/templates/examples/contracts.py,sha256=uqJ6Y1GADo246MjFKoLY-_2E74cfBQsLO4vTqYcR81c,3241
|
|
@@ -104,10 +103,10 @@ invar/templates/skills/develop/SKILL.md.jinja,sha256=3coPSZGh1-YKN9Xc_xcEkfEP3S0
|
|
|
104
103
|
invar/templates/skills/investigate/SKILL.md.jinja,sha256=bOLdLMH5WUVBYOo4NpsfyvI6xx7I1lCNr_X-8bMe_kg,2744
|
|
105
104
|
invar/templates/skills/propose/SKILL.md.jinja,sha256=_iDLYN6-cfzA8n0_8sv-Dnpm1xq9IIpcDyM10mU2WUA,2420
|
|
106
105
|
invar/templates/skills/review/SKILL.md.jinja,sha256=e7HULz1jjLOlk2LYejQMk2F-cu7dDIwvh6lWNjx3j-Q,14123
|
|
107
|
-
invar_tools-1.7.
|
|
108
|
-
invar_tools-1.7.
|
|
109
|
-
invar_tools-1.7.
|
|
110
|
-
invar_tools-1.7.
|
|
111
|
-
invar_tools-1.7.
|
|
112
|
-
invar_tools-1.7.
|
|
113
|
-
invar_tools-1.7.
|
|
106
|
+
invar_tools-1.7.1.dist-info/METADATA,sha256=dBk4N2qRISfYrc6Rr66eY08_me9Rqfyq34RDBCaEj1s,17600
|
|
107
|
+
invar_tools-1.7.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
108
|
+
invar_tools-1.7.1.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
|
|
109
|
+
invar_tools-1.7.1.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
|
|
110
|
+
invar_tools-1.7.1.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
|
|
111
|
+
invar_tools-1.7.1.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
|
|
112
|
+
invar_tools-1.7.1.dist-info/RECORD,,
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# invar:begin
|
|
2
|
-
# Invar Pre-commit Hooks
|
|
3
|
-
# Install: pre-commit install
|
|
4
|
-
#
|
|
5
|
-
# Default runs full verification (static + doctests + CrossHair + Hypothesis).
|
|
6
|
-
# Incremental mode makes this fast: ~5s first commit, ~2s subsequent (cached).
|
|
7
|
-
#
|
|
8
|
-
# Smart Guard: Detects rule-affecting file changes and runs full guard when needed.
|
|
9
|
-
# Structure Protection: Warns if INVAR.md is modified directly.
|
|
10
|
-
|
|
11
|
-
repos:
|
|
12
|
-
- repo: local
|
|
13
|
-
hooks:
|
|
14
|
-
# Warn if INVAR.md is modified directly (use --no-verify if intentional)
|
|
15
|
-
- id: invar-md-protected
|
|
16
|
-
name: INVAR.md Protection
|
|
17
|
-
entry: bash -c 'if git diff --cached --name-only | grep -q "^INVAR.md$"; then echo "Warning - INVAR.md was modified directly. Use git commit --no-verify if intentional."; exit 1; fi'
|
|
18
|
-
language: system
|
|
19
|
-
pass_filenames: false
|
|
20
|
-
stages: [pre-commit]
|
|
21
|
-
|
|
22
|
-
# Invar Guard (full verification by default)
|
|
23
|
-
# Uses incremental mode: only verifies changed files, caches results
|
|
24
|
-
# Smart mode: runs full guard when rule files change, --changed otherwise
|
|
25
|
-
- id: invar-guard
|
|
26
|
-
name: Invar Guard
|
|
27
|
-
entry: bash -c '
|
|
28
|
-
RULE_FILES="rule_meta.py rules.py contracts.py purity.py pyproject.toml"
|
|
29
|
-
STAGED=$(git diff --cached --name-only)
|
|
30
|
-
FULL=false
|
|
31
|
-
for f in $RULE_FILES; do
|
|
32
|
-
if echo "$STAGED" | grep -q "$f"; then FULL=true; break; fi
|
|
33
|
-
done
|
|
34
|
-
if [ "$FULL" = true ]; then
|
|
35
|
-
echo "⚠️ Rule change detected - running FULL guard"
|
|
36
|
-
invar guard
|
|
37
|
-
else
|
|
38
|
-
invar guard --changed
|
|
39
|
-
fi
|
|
40
|
-
'
|
|
41
|
-
language: python
|
|
42
|
-
additional_dependencies: ['invar-tools']
|
|
43
|
-
pass_filenames: false
|
|
44
|
-
stages: [pre-commit]
|
|
45
|
-
types: [python]
|
|
46
|
-
# invar:end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|