invar-tools 1.0.0__py3-none-any.whl → 1.2.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/core/contracts.py +75 -5
- invar/core/entry_points.py +294 -0
- invar/core/format_specs.py +196 -0
- invar/core/format_strategies.py +197 -0
- invar/core/formatter.py +27 -4
- invar/core/hypothesis_strategies.py +47 -5
- invar/core/lambda_helpers.py +1 -0
- invar/core/models.py +23 -17
- invar/core/parser.py +6 -2
- invar/core/property_gen.py +81 -40
- invar/core/purity.py +10 -4
- invar/core/review_trigger.py +298 -0
- invar/core/rule_meta.py +61 -2
- invar/core/rules.py +83 -19
- invar/core/shell_analysis.py +252 -0
- invar/core/shell_architecture.py +171 -0
- invar/core/suggestions.py +6 -0
- invar/core/tautology.py +1 -0
- invar/core/utils.py +51 -4
- invar/core/verification_routing.py +158 -0
- invar/invariant.py +1 -0
- invar/mcp/server.py +20 -3
- invar/shell/cli.py +59 -31
- invar/shell/config.py +259 -10
- invar/shell/fs.py +5 -2
- invar/shell/git.py +2 -0
- invar/shell/guard_helpers.py +78 -3
- invar/shell/guard_output.py +100 -24
- invar/shell/init_cmd.py +27 -7
- invar/shell/mcp_config.py +3 -0
- invar/shell/mutate_cmd.py +184 -0
- invar/shell/mutation.py +314 -0
- invar/shell/perception.py +2 -0
- invar/shell/property_tests.py +17 -2
- invar/shell/prove.py +35 -3
- invar/shell/prove_accept.py +113 -0
- invar/shell/prove_fallback.py +148 -46
- invar/shell/templates.py +34 -0
- invar/shell/test_cmd.py +3 -1
- invar/shell/testing.py +6 -17
- invar/shell/update_cmd.py +2 -0
- invar/templates/CLAUDE.md.template +65 -9
- invar/templates/INVAR.md +96 -23
- invar/templates/aider.conf.yml.template +16 -14
- invar/templates/commands/review.md +200 -0
- invar/templates/cursorrules.template +22 -13
- invar/templates/examples/contracts.py +3 -1
- invar/templates/examples/core_shell.py +3 -1
- {invar_tools-1.0.0.dist-info → invar_tools-1.2.0.dist-info}/METADATA +81 -15
- invar_tools-1.2.0.dist-info/RECORD +77 -0
- invar_tools-1.2.0.dist-info/licenses/LICENSE +190 -0
- invar_tools-1.2.0.dist-info/licenses/LICENSE-GPL +674 -0
- invar_tools-1.2.0.dist-info/licenses/NOTICE +63 -0
- invar_tools-1.0.0.dist-info/RECORD +0 -64
- invar_tools-1.0.0.dist-info/licenses/LICENSE +0 -21
- {invar_tools-1.0.0.dist-info → invar_tools-1.2.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.0.0.dist-info → invar_tools-1.2.0.dist-info}/entry_points.txt +0 -0
invar/shell/prove_fallback.py
CHANGED
|
@@ -3,17 +3,70 @@ Hypothesis fallback for proof verification.
|
|
|
3
3
|
|
|
4
4
|
DX-12: Provides Hypothesis as automatic fallback when CrossHair
|
|
5
5
|
is unavailable, times out, or skips files.
|
|
6
|
+
|
|
7
|
+
DX-22: Smart routing - detects C extension imports and routes
|
|
8
|
+
directly to Hypothesis without wasting time on CrossHair.
|
|
6
9
|
"""
|
|
7
10
|
|
|
8
11
|
from __future__ import annotations
|
|
9
12
|
|
|
10
13
|
import subprocess
|
|
11
14
|
import sys
|
|
15
|
+
from dataclasses import dataclass, field
|
|
12
16
|
from pathlib import Path
|
|
13
17
|
|
|
14
18
|
from returns.result import Failure, Result, Success
|
|
15
19
|
|
|
20
|
+
from invar.core.verification_routing import get_incompatible_imports
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class FileRouting:
|
|
25
|
+
"""DX-22: Classification of files for smart verification routing."""
|
|
26
|
+
|
|
27
|
+
crosshair_files: list[Path] = field(default_factory=list)
|
|
28
|
+
hypothesis_files: list[Path] = field(default_factory=list)
|
|
29
|
+
skip_files: list[Path] = field(default_factory=list)
|
|
30
|
+
incompatible_reasons: dict[str, set[str]] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# @shell_complexity: File I/O with error handling for import detection
|
|
34
|
+
def classify_files_for_verification(files: list[Path]) -> FileRouting:
|
|
35
|
+
"""
|
|
36
|
+
Classify files for smart verification routing.
|
|
37
|
+
|
|
38
|
+
DX-22: Detects C extension imports and routes files appropriately:
|
|
39
|
+
- Pure Python with contracts -> CrossHair (can prove)
|
|
40
|
+
- C extensions (numpy, pandas, etc.) -> Hypothesis (cannot prove)
|
|
41
|
+
- No contracts -> Skip
|
|
42
|
+
|
|
43
|
+
Returns FileRouting with classified files.
|
|
44
|
+
"""
|
|
45
|
+
routing = FileRouting()
|
|
46
|
+
|
|
47
|
+
for file_path in files:
|
|
48
|
+
if not file_path.exists() or file_path.suffix != ".py":
|
|
49
|
+
routing.skip_files.append(file_path)
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
source = file_path.read_text()
|
|
54
|
+
except Exception:
|
|
55
|
+
routing.skip_files.append(file_path)
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Check for incompatible imports
|
|
59
|
+
incompatible = get_incompatible_imports(source)
|
|
60
|
+
if incompatible:
|
|
61
|
+
routing.hypothesis_files.append(file_path)
|
|
62
|
+
routing.incompatible_reasons[str(file_path)] = incompatible
|
|
63
|
+
else:
|
|
64
|
+
routing.crosshair_files.append(file_path)
|
|
65
|
+
|
|
66
|
+
return routing
|
|
67
|
+
|
|
16
68
|
|
|
69
|
+
# @shell_complexity: Fallback verification with hypothesis availability check
|
|
17
70
|
def run_hypothesis_fallback(
|
|
18
71
|
files: list[Path],
|
|
19
72
|
max_examples: int = 100,
|
|
@@ -101,6 +154,8 @@ def run_hypothesis_fallback(
|
|
|
101
154
|
return Failure(f"Hypothesis error: {e}")
|
|
102
155
|
|
|
103
156
|
|
|
157
|
+
# @shell_orchestration: DX-22 smart routing + DX-12/13 fallback chain
|
|
158
|
+
# @shell_complexity: Multiple verification phases with error handling paths
|
|
104
159
|
def run_prove_with_fallback(
|
|
105
160
|
files: list[Path],
|
|
106
161
|
crosshair_timeout: int = 10,
|
|
@@ -109,9 +164,16 @@ def run_prove_with_fallback(
|
|
|
109
164
|
cache_dir: Path | None = None,
|
|
110
165
|
) -> Result[dict, str]:
|
|
111
166
|
"""
|
|
112
|
-
Run proof verification with automatic
|
|
167
|
+
Run proof verification with smart routing and automatic fallback.
|
|
113
168
|
|
|
114
|
-
DX-
|
|
169
|
+
DX-22: Smart routing - routes C extension code directly to Hypothesis.
|
|
170
|
+
DX-12 + DX-13: CrossHair with caching, falls back to Hypothesis on failure.
|
|
171
|
+
|
|
172
|
+
Flow:
|
|
173
|
+
1. Classify files (CrossHair-compatible vs C-extension)
|
|
174
|
+
2. Run CrossHair on compatible files only
|
|
175
|
+
3. Run Hypothesis on incompatible files (no wasted CrossHair attempt)
|
|
176
|
+
4. Merge results with de-duplicated statistics
|
|
115
177
|
|
|
116
178
|
Args:
|
|
117
179
|
files: List of Python file paths to verify
|
|
@@ -121,63 +183,103 @@ def run_prove_with_fallback(
|
|
|
121
183
|
cache_dir: Cache directory (default: .invar/cache/prove)
|
|
122
184
|
|
|
123
185
|
Returns:
|
|
124
|
-
Success with verification results
|
|
186
|
+
Success with verification results including routing statistics
|
|
125
187
|
"""
|
|
126
188
|
# Import here to avoid circular import
|
|
127
189
|
from invar.shell.prove import CrossHairStatus, run_crosshair_parallel
|
|
128
190
|
from invar.shell.prove_cache import ProveCache
|
|
129
191
|
|
|
130
|
-
# DX-
|
|
192
|
+
# DX-22: Smart routing - classify files before verification
|
|
193
|
+
routing = classify_files_for_verification(files)
|
|
194
|
+
|
|
195
|
+
# Initialize result structure with DX-22 routing stats
|
|
196
|
+
result = {
|
|
197
|
+
"status": "passed",
|
|
198
|
+
"routing": {
|
|
199
|
+
"crosshair_files": len(routing.crosshair_files),
|
|
200
|
+
"hypothesis_files": len(routing.hypothesis_files),
|
|
201
|
+
"skip_files": len(routing.skip_files),
|
|
202
|
+
"incompatible_reasons": {
|
|
203
|
+
k: list(v) for k, v in routing.incompatible_reasons.items()
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
"crosshair": None,
|
|
207
|
+
"hypothesis": None,
|
|
208
|
+
"files": [str(f) for f in files],
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# DX-13: Initialize cache for CrossHair
|
|
131
212
|
cache = None
|
|
132
213
|
if use_cache:
|
|
133
214
|
if cache_dir is None:
|
|
134
215
|
cache_dir = Path(".invar/cache/prove")
|
|
135
216
|
cache = ProveCache(cache_dir=cache_dir)
|
|
136
217
|
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
218
|
+
# Phase 1: Run CrossHair on compatible files
|
|
219
|
+
if routing.crosshair_files:
|
|
220
|
+
crosshair_result = run_crosshair_parallel(
|
|
221
|
+
routing.crosshair_files,
|
|
222
|
+
max_iterations=5, # Fast mode
|
|
223
|
+
max_workers=None, # Auto-detect
|
|
224
|
+
cache=cache,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if isinstance(crosshair_result, Success):
|
|
228
|
+
xh_data = crosshair_result.unwrap()
|
|
229
|
+
result["crosshair"] = xh_data
|
|
230
|
+
|
|
231
|
+
# Check if CrossHair needs fallback for any files
|
|
232
|
+
xh_status = xh_data.get("status", "")
|
|
233
|
+
needs_fallback = (
|
|
234
|
+
xh_status == CrossHairStatus.SKIPPED
|
|
235
|
+
or xh_status == CrossHairStatus.TIMEOUT
|
|
236
|
+
or "not installed" in xh_data.get("reason", "")
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if needs_fallback:
|
|
240
|
+
# CrossHair failed, add these files to Hypothesis batch
|
|
241
|
+
routing.hypothesis_files.extend(routing.crosshair_files)
|
|
242
|
+
result["crosshair"]["fallback_triggered"] = True
|
|
243
|
+
else:
|
|
244
|
+
# CrossHair error, fallback all to Hypothesis
|
|
245
|
+
routing.hypothesis_files.extend(routing.crosshair_files)
|
|
246
|
+
result["crosshair"] = {
|
|
247
|
+
"status": "error",
|
|
248
|
+
"error": str(crosshair_result.failure()),
|
|
249
|
+
"fallback_triggered": True,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# Phase 2: Run Hypothesis on incompatible files + fallback files
|
|
253
|
+
if routing.hypothesis_files:
|
|
161
254
|
hypothesis_result = run_hypothesis_fallback(
|
|
162
|
-
|
|
255
|
+
routing.hypothesis_files, max_examples=hypothesis_max_examples
|
|
163
256
|
)
|
|
164
257
|
|
|
165
258
|
if isinstance(hypothesis_result, Success):
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
259
|
+
result["hypothesis"] = hypothesis_result.unwrap()
|
|
260
|
+
else:
|
|
261
|
+
result["hypothesis"] = {
|
|
262
|
+
"status": "error",
|
|
263
|
+
"error": str(hypothesis_result.failure()),
|
|
264
|
+
}
|
|
265
|
+
result["status"] = "failed"
|
|
266
|
+
|
|
267
|
+
# Determine overall status
|
|
268
|
+
xh_status = result.get("crosshair", {}).get("status", "passed")
|
|
269
|
+
hyp_status = result.get("hypothesis", {}).get("status", "passed")
|
|
270
|
+
|
|
271
|
+
if xh_status == "counterexample_found" or hyp_status == "failed":
|
|
272
|
+
result["status"] = "failed"
|
|
273
|
+
elif xh_status in ("error",) or hyp_status in ("error",):
|
|
274
|
+
result["status"] = "error"
|
|
275
|
+
|
|
276
|
+
# DX-22: Add de-duplicated statistics
|
|
277
|
+
result["stats"] = {
|
|
278
|
+
"crosshair_proven": len(
|
|
279
|
+
result.get("crosshair", {}).get("verified", [])
|
|
280
|
+
),
|
|
281
|
+
"hypothesis_tested": len(routing.hypothesis_files),
|
|
282
|
+
"total_verified": len(files) - len(routing.skip_files),
|
|
283
|
+
}
|
|
180
284
|
|
|
181
|
-
|
|
182
|
-
result_data["primary_tool"] = "crosshair"
|
|
183
|
-
return Success(result_data)
|
|
285
|
+
return Success(result)
|
invar/shell/templates.py
CHANGED
|
@@ -53,6 +53,7 @@ def get_template_path(name: str) -> Result[Path, str]:
|
|
|
53
53
|
return Failure(f"Failed to get template path: {e}")
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
# @shell_complexity: Template copy with path resolution
|
|
56
57
|
def copy_template(
|
|
57
58
|
template_name: str, dest: Path, dest_name: str | None = None
|
|
58
59
|
) -> Result[bool, str]:
|
|
@@ -73,6 +74,7 @@ def copy_template(
|
|
|
73
74
|
return Failure(f"Failed to copy template: {e}")
|
|
74
75
|
|
|
75
76
|
|
|
77
|
+
# @shell_complexity: Config addition with existing file detection
|
|
76
78
|
def add_config(path: Path, console) -> Result[bool, str]:
|
|
77
79
|
"""Add configuration to project. Returns Success(True) if added, Success(False) if skipped."""
|
|
78
80
|
pyproject = path / "pyproject.toml"
|
|
@@ -114,6 +116,7 @@ def create_directories(path: Path, console) -> None:
|
|
|
114
116
|
console.print("[green]Created[/green] src/shell/")
|
|
115
117
|
|
|
116
118
|
|
|
119
|
+
# @shell_complexity: Directory copy with file filtering
|
|
117
120
|
def copy_examples_directory(dest: Path, console) -> Result[bool, str]:
|
|
118
121
|
"""Copy examples directory to .invar/examples/. Returns Success(True) if copied."""
|
|
119
122
|
import shutil
|
|
@@ -139,6 +142,32 @@ def copy_examples_directory(dest: Path, console) -> Result[bool, str]:
|
|
|
139
142
|
return Failure(f"Failed to copy examples: {e}")
|
|
140
143
|
|
|
141
144
|
|
|
145
|
+
# @shell_complexity: Directory copy for Claude commands (DX-32)
|
|
146
|
+
def copy_commands_directory(dest: Path, console) -> Result[bool, str]:
|
|
147
|
+
"""Copy commands directory to .claude/commands/. Returns Success(True) if copied."""
|
|
148
|
+
import shutil
|
|
149
|
+
|
|
150
|
+
commands_dest = dest / ".claude" / "commands"
|
|
151
|
+
if commands_dest.exists():
|
|
152
|
+
return Success(False)
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
commands_src = Path(str(resources.files("invar.templates").joinpath("commands")))
|
|
156
|
+
if not commands_src.exists():
|
|
157
|
+
return Failure("Commands template directory not found")
|
|
158
|
+
|
|
159
|
+
# Create .claude if needed
|
|
160
|
+
claude_dir = dest / ".claude"
|
|
161
|
+
if not claude_dir.exists():
|
|
162
|
+
claude_dir.mkdir()
|
|
163
|
+
|
|
164
|
+
shutil.copytree(commands_src, commands_dest)
|
|
165
|
+
console.print("[green]Created[/green] .claude/commands/ (Claude Code skills)")
|
|
166
|
+
return Success(True)
|
|
167
|
+
except OSError as e:
|
|
168
|
+
return Failure(f"Failed to copy commands: {e}")
|
|
169
|
+
|
|
170
|
+
|
|
142
171
|
# Agent configuration for multi-agent support (DX-11, DX-17)
|
|
143
172
|
AGENT_CONFIGS = {
|
|
144
173
|
"claude": {
|
|
@@ -162,6 +191,7 @@ AGENT_CONFIGS = {
|
|
|
162
191
|
}
|
|
163
192
|
|
|
164
193
|
|
|
194
|
+
# @shell_complexity: Agent config detection across multiple locations
|
|
165
195
|
def detect_agent_configs(path: Path) -> Result[dict[str, str], str]:
|
|
166
196
|
"""
|
|
167
197
|
Detect existing agent configuration files.
|
|
@@ -195,6 +225,7 @@ def detect_agent_configs(path: Path) -> Result[dict[str, str], str]:
|
|
|
195
225
|
return Failure(f"Failed to detect agent configs: {e}")
|
|
196
226
|
|
|
197
227
|
|
|
228
|
+
# @shell_complexity: Reference addition with existing check
|
|
198
229
|
def add_invar_reference(path: Path, agent: str, console) -> Result[bool, str]:
|
|
199
230
|
"""Add Invar reference to an existing agent config file."""
|
|
200
231
|
if agent not in AGENT_CONFIGS:
|
|
@@ -220,6 +251,7 @@ def add_invar_reference(path: Path, agent: str, console) -> Result[bool, str]:
|
|
|
220
251
|
return Failure(f"Failed to update {config['file']}: {e}")
|
|
221
252
|
|
|
222
253
|
|
|
254
|
+
# @shell_complexity: Config creation with template selection
|
|
223
255
|
def create_agent_config(path: Path, agent: str, console) -> Result[bool, str]:
|
|
224
256
|
"""
|
|
225
257
|
Create agent config from template (DX-17).
|
|
@@ -248,6 +280,7 @@ def create_agent_config(path: Path, agent: str, console) -> Result[bool, str]:
|
|
|
248
280
|
return Success(False)
|
|
249
281
|
|
|
250
282
|
|
|
283
|
+
# @shell_complexity: MCP server config with JSON manipulation
|
|
251
284
|
def configure_mcp_server(path: Path, console) -> Result[list[str], str]:
|
|
252
285
|
"""
|
|
253
286
|
Configure MCP server for AI agents (DX-16).
|
|
@@ -407,6 +440,7 @@ The server communicates via stdio and should be managed by your AI agent.
|
|
|
407
440
|
"""
|
|
408
441
|
|
|
409
442
|
|
|
443
|
+
# @shell_complexity: Git hooks installation with backup
|
|
410
444
|
def install_hooks(path: Path, console) -> Result[bool, str]:
|
|
411
445
|
"""Install pre-commit hooks configuration and activate them."""
|
|
412
446
|
import subprocess
|
invar/shell/test_cmd.py
CHANGED
|
@@ -26,6 +26,7 @@ def _detect_agent_mode() -> bool:
|
|
|
26
26
|
return os.getenv("INVAR_MODE") == "agent" or not sys.stdout.isatty()
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
# @shell_complexity: Test command with file collection and output
|
|
29
30
|
def test(
|
|
30
31
|
target: str = typer.Argument(None, help="File to test (optional with --changed)"),
|
|
31
32
|
verbose: bool = typer.Option(False, "-v", "--verbose", help="Verbose output"),
|
|
@@ -33,7 +34,7 @@ def test(
|
|
|
33
34
|
changed: bool = typer.Option(False, "--changed", help="Test git-modified files only"),
|
|
34
35
|
max_examples: int = typer.Option(100, "--max-examples", help="Maximum Hypothesis examples per function"),
|
|
35
36
|
) -> None:
|
|
36
|
-
"""Run property-based tests using Hypothesis on contracted functions
|
|
37
|
+
"""Run property-based tests using Hypothesis on contracted functions."""
|
|
37
38
|
from invar.shell.property_tests import (
|
|
38
39
|
format_property_test_report,
|
|
39
40
|
run_property_tests_on_files,
|
|
@@ -75,6 +76,7 @@ def test(
|
|
|
75
76
|
raise typer.Exit(1)
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
# @shell_complexity: Verify command with CrossHair integration
|
|
78
80
|
def verify(
|
|
79
81
|
target: str = typer.Argument(None, help="File to verify (optional with --changed)"),
|
|
80
82
|
timeout: int = typer.Option(30, "--timeout", help="Timeout per function (seconds)"),
|
invar/shell/testing.py
CHANGED
|
@@ -40,7 +40,6 @@ __all__ = [
|
|
|
40
40
|
"ProveCache",
|
|
41
41
|
"VerificationLevel",
|
|
42
42
|
"VerificationResult",
|
|
43
|
-
"detect_verification_context",
|
|
44
43
|
"get_available_verifiers",
|
|
45
44
|
"get_files_to_prove",
|
|
46
45
|
"run_crosshair_on_files",
|
|
@@ -80,6 +79,7 @@ class VerificationResult:
|
|
|
80
79
|
errors: list[str] = field(default_factory=list)
|
|
81
80
|
|
|
82
81
|
|
|
82
|
+
# @shell_orchestration: Verifier discovery helper
|
|
83
83
|
def get_available_verifiers() -> list[str]:
|
|
84
84
|
"""
|
|
85
85
|
Detect installed verification tools.
|
|
@@ -111,21 +111,7 @@ def get_available_verifiers() -> list[str]:
|
|
|
111
111
|
return available
|
|
112
112
|
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
"""
|
|
116
|
-
Auto-detect appropriate verification depth based on context.
|
|
117
|
-
|
|
118
|
-
DX-19: Simplified to 2 levels. Always returns STANDARD (full verification).
|
|
119
|
-
STATIC is only used when explicitly requested via --static flag.
|
|
120
|
-
|
|
121
|
-
>>> detect_verification_context() == VerificationLevel.STANDARD
|
|
122
|
-
True
|
|
123
|
-
"""
|
|
124
|
-
# DX-19: Always use STANDARD (full verification) by default
|
|
125
|
-
# STATIC is only for explicit --static flag
|
|
126
|
-
return VerificationLevel.STANDARD
|
|
127
|
-
|
|
128
|
-
|
|
114
|
+
# @shell_complexity: Doctest execution with subprocess and result parsing
|
|
129
115
|
def run_doctests_on_files(
|
|
130
116
|
files: list[Path], verbose: bool = False
|
|
131
117
|
) -> Result[dict, str]:
|
|
@@ -173,6 +159,7 @@ def run_doctests_on_files(
|
|
|
173
159
|
return Failure(f"Doctest error: {e}")
|
|
174
160
|
|
|
175
161
|
|
|
162
|
+
# @shell_complexity: Property test orchestration with subprocess
|
|
176
163
|
def run_test(
|
|
177
164
|
target: str, json_output: bool = False, verbose: bool = False
|
|
178
165
|
) -> Result[dict, str]:
|
|
@@ -230,6 +217,7 @@ def run_test(
|
|
|
230
217
|
return Failure(f"Test error: {e}")
|
|
231
218
|
|
|
232
219
|
|
|
220
|
+
# @shell_complexity: CrossHair verification with subprocess
|
|
233
221
|
def run_verify(
|
|
234
222
|
target: str, json_output: bool = False, timeout: int = 30
|
|
235
223
|
) -> Result[dict, str]:
|
|
@@ -266,9 +254,10 @@ def run_verify(
|
|
|
266
254
|
try:
|
|
267
255
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout * 10)
|
|
268
256
|
|
|
257
|
+
# CrossHair format: "file:line: error: Err when calling func(...)"
|
|
269
258
|
counterexamples = [
|
|
270
259
|
line.strip() for line in result.stdout.split("\n")
|
|
271
|
-
if "error" in line.lower() or "counterexample" in line.lower()
|
|
260
|
+
if ": error:" in line.lower() or "counterexample" in line.lower()
|
|
272
261
|
]
|
|
273
262
|
|
|
274
263
|
verify_result = {
|
invar/shell/update_cmd.py
CHANGED
|
@@ -21,6 +21,7 @@ console = Console()
|
|
|
21
21
|
VERSION_PATTERN = re.compile(r"v(\d+)\.(\d+)(?:\.(\d+))?")
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
# @shell_orchestration: Version parsing helper for update command
|
|
24
25
|
def parse_version(text: str) -> tuple[int, int, int] | None:
|
|
25
26
|
"""
|
|
26
27
|
Parse version string from text.
|
|
@@ -113,6 +114,7 @@ def update_examples(path: Path, console: Console) -> Result[bool, str]:
|
|
|
113
114
|
return copy_examples_directory(path, console)
|
|
114
115
|
|
|
115
116
|
|
|
117
|
+
# @shell_complexity: Update command with template comparison
|
|
116
118
|
def update(
|
|
117
119
|
path: Path = typer.Argument(Path(), help="Project root directory"),
|
|
118
120
|
force: bool = typer.Option(
|
|
@@ -1,21 +1,40 @@
|
|
|
1
1
|
# Project Development Guide
|
|
2
2
|
|
|
3
|
-
> **Protocol:** Follow [INVAR.md](./INVAR.md) — includes
|
|
3
|
+
> **Protocol:** Follow [INVAR.md](./INVAR.md) — includes Check-In, USBV workflow, and Task Completion requirements.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Check-In
|
|
6
6
|
|
|
7
|
-
Your
|
|
7
|
+
Your first message MUST display:
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
|
|
11
|
-
invar_map(top=10) # or: invar map --top 10
|
|
10
|
+
✓ Check-In: guard PASS | top: <entry1>, <entry2>
|
|
12
11
|
```
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
Execute `invar_guard(changed=true)` and `invar_map(top=10)`, then show this one-line summary.
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
Example:
|
|
16
|
+
```
|
|
17
|
+
✓ Check-In: guard PASS | top: parse_file, check_rules
|
|
18
|
+
```
|
|
17
19
|
|
|
18
|
-
This
|
|
20
|
+
This is your sign-in. The user sees it immediately.
|
|
21
|
+
No visible check-in = Session not started.
|
|
22
|
+
|
|
23
|
+
Then read `.invar/context.md` for project state and lessons learned.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Final
|
|
28
|
+
|
|
29
|
+
Your last message for an implementation task MUST display:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
✓ Final: guard PASS | 0 errors, 2 warnings
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Execute `invar_guard()` and show this one-line summary.
|
|
36
|
+
|
|
37
|
+
This is your sign-out. Completes the Check-In/Final pair.
|
|
19
38
|
|
|
20
39
|
---
|
|
21
40
|
|
|
@@ -43,7 +62,44 @@ src/{project}/
|
|
|
43
62
|
| INVAR.md | Invar | No | Protocol (`invar update` to sync) |
|
|
44
63
|
| CLAUDE.md | User | Yes | Project customization (this file) |
|
|
45
64
|
| .invar/context.md | User | Yes | Project state, lessons learned |
|
|
46
|
-
| .invar/examples/ | Invar | No | **Must read:** Core/Shell patterns |
|
|
65
|
+
| .invar/examples/ | Invar | No | **Must read:** Core/Shell patterns, workflow |
|
|
66
|
+
|
|
67
|
+
## Visible Workflow (DX-30)
|
|
68
|
+
|
|
69
|
+
For complex tasks (3+ functions), show 3 checkpoints in TodoList:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
□ [UNDERSTAND] Task description, codebase context, constraints
|
|
73
|
+
□ [SPECIFY] Contracts (@pre/@post) and design decomposition
|
|
74
|
+
□ [VALIDATE] Guard results, Review Gate status, integration status
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**BUILD is internal work** — not shown in TodoList.
|
|
78
|
+
|
|
79
|
+
**Show contracts before code.** See `.invar/examples/workflow.md` for full example.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Agent Roles
|
|
84
|
+
|
|
85
|
+
| Command | Role | Purpose |
|
|
86
|
+
|---------|------|---------|
|
|
87
|
+
| `/review` | Reviewer | Adversarial code review (DX-31) |
|
|
88
|
+
|
|
89
|
+
### Review Modes (Auto-Selected)
|
|
90
|
+
|
|
91
|
+
`/review` automatically selects mode based on Guard output:
|
|
92
|
+
|
|
93
|
+
| Condition | Mode | Behavior |
|
|
94
|
+
|-----------|------|----------|
|
|
95
|
+
| `review_suggested` triggered | **Isolated** | Task tool sub-agent (fresh context) |
|
|
96
|
+
| No trigger | **Quick** | Same-context adversarial review |
|
|
97
|
+
| `--isolated` flag | **Isolated** | Force isolation |
|
|
98
|
+
| `--quick` flag | **Quick** | Force same-context |
|
|
99
|
+
|
|
100
|
+
Guard triggers `review_suggested` for: security-sensitive files, escape hatches >= 3, contract coverage < 50%.
|
|
101
|
+
|
|
102
|
+
---
|
|
47
103
|
|
|
48
104
|
## Project-Specific Rules
|
|
49
105
|
|