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.
@@ -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 except user region."""
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 - modify, not delete
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 there's user content
155
- has_user_region = "<!--invar:user-->" in content
156
- targets["modify_files"].append(
157
- ("CLAUDE.md", f"remove invar regions{', keep user region' if has_user_region else ''}")
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(),
@@ -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
- # @shell_orchestration: Report formatting tightly coupled with CLI output
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
- invar_toml = path / "invar.toml"
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 not invar_toml.exists():
94
- invar_toml.write_text(_DEFAULT_INVAR_TOML)
95
- console.print("[green]Created[/green] invar.toml")
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
- """Install pre-commit hooks configuration and activate them."""
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
- console.print("[yellow]Skipped[/yellow] .pre-commit-config.yaml (already exists)")
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
- result = copy_template("pre-commit-config.yaml.template", path, ".pre-commit-config.yaml")
405
- if isinstance(result, Failure):
406
- return result
407
-
408
- if result.unwrap():
409
- console.print("[green]Created[/green] .pre-commit-config.yaml")
410
-
411
- # Auto-install hooks (Automatic > Opt-in)
412
- try:
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,3 +1,4 @@
1
+ # invar:begin
1
2
  # Invar Pre-commit Hooks v{{ version }}
2
3
  # Install: pre-commit install
3
4
  #
@@ -42,3 +43,4 @@ repos:
42
43
  pass_filenames: false
43
44
  stages: [pre-commit]
44
45
  types: [python]
46
+ # invar:end
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: invar-tools
3
- Version: 1.7.0
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
- ### 🆕 New Project
133
+ ### 🎯 Setup
133
134
 
134
135
  ```bash
135
- # 1. Enter your project directory
136
136
  cd your-project
137
137
 
138
- # 2. Initialize with Claude Code (full experience)
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
- # 3. Add runtime contracts to your project
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
- ### 📁 Existing Project
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
- Invar's init is idempotent—safe to run multiple times. It detects existing configuration and updates only managed regions.
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
- ## 🖥️ Platform Experience
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
- | Feature | Claude Code | Other Editors |
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
- **Claude Code** provides the full experience—MCP tools, skill routing, and hooks are auto-configured by `invar init --claude`.
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
- **Other editors** can achieve similar results by:
392
- 1. Adding INVAR.md content to system prompts
393
- 2. Manually configuring MCP servers (if supported)
394
- 3. Using CLI commands for verification
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 --claude` creates:
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
- | File/Directory | Purpose | Editable? |
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=4ani-6XcWF__sD0c_tKcCA2FunaF2pYIfvR5BACWkDg,14168
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=kxkdF2gwTWcGpglccDi6-8IN1zRwelDG6Lg1VPYQgyA,12912
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=2RiXC9RBV__cKLHu0KKOWRxEgYVQNNAPAdwBjYenNHQ,11780
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=wVG78wBoVB-SOugfqaKj6xklwSDpEyjeEOGE6EKcS0s,13387
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=7PhXVjnhsY-1o2kBWRfoUPvSjopB0jRjazLe4P_FyZQ,16339
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=f8kGBwkdYycQ5eIdfveIefTw9ZSU6D4OyUuHt8ScUTY,12166
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=Qflmii8hngHciSgfa8mIlg3-E3D4b0xflm0-Q-cWcCc,1752
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.0.dist-info/METADATA,sha256=JKBCWf7HkEIrD3m3VC7fHLA7REki-dlk1l0HxsgPFW4,16964
108
- invar_tools-1.7.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
109
- invar_tools-1.7.0.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
110
- invar_tools-1.7.0.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
111
- invar_tools-1.7.0.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
112
- invar_tools-1.7.0.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
113
- invar_tools-1.7.0.dist-info/RECORD,,
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