invar-tools 1.0.0__py3-none-any.whl → 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- invar/__init__.py +1 -0
- invar/core/contracts.py +80 -10
- invar/core/entry_points.py +367 -0
- invar/core/extraction.py +5 -6
- invar/core/format_specs.py +195 -0
- invar/core/format_strategies.py +197 -0
- invar/core/formatter.py +32 -10
- invar/core/hypothesis_strategies.py +50 -10
- invar/core/inspect.py +1 -1
- invar/core/lambda_helpers.py +3 -2
- invar/core/models.py +30 -18
- invar/core/must_use.py +2 -1
- invar/core/parser.py +13 -6
- invar/core/postcondition_scope.py +128 -0
- invar/core/property_gen.py +86 -42
- invar/core/purity.py +13 -7
- invar/core/purity_heuristics.py +5 -9
- invar/core/references.py +8 -6
- invar/core/review_trigger.py +370 -0
- invar/core/rule_meta.py +69 -2
- invar/core/rules.py +91 -28
- invar/core/shell_analysis.py +247 -0
- invar/core/shell_architecture.py +171 -0
- invar/core/strategies.py +7 -14
- invar/core/suggestions.py +92 -0
- invar/core/sync_helpers.py +238 -0
- invar/core/tautology.py +103 -37
- invar/core/template_parser.py +467 -0
- invar/core/timeout_inference.py +4 -7
- invar/core/utils.py +63 -18
- invar/core/verification_routing.py +155 -0
- invar/mcp/server.py +113 -13
- invar/shell/commands/__init__.py +11 -0
- invar/shell/{cli.py → commands/guard.py} +152 -44
- invar/shell/{init_cmd.py → commands/init.py} +200 -28
- invar/shell/commands/merge.py +256 -0
- invar/shell/commands/mutate.py +184 -0
- invar/shell/{perception.py → commands/perception.py} +2 -0
- invar/shell/commands/sync_self.py +113 -0
- invar/shell/commands/template_sync.py +366 -0
- invar/shell/{test_cmd.py → commands/test.py} +3 -1
- invar/shell/commands/update.py +48 -0
- invar/shell/config.py +247 -10
- invar/shell/coverage.py +351 -0
- invar/shell/fs.py +5 -2
- invar/shell/git.py +2 -0
- invar/shell/guard_helpers.py +116 -20
- invar/shell/guard_output.py +106 -24
- invar/shell/mcp_config.py +3 -0
- invar/shell/mutation.py +314 -0
- invar/shell/property_tests.py +75 -24
- invar/shell/prove/__init__.py +9 -0
- invar/shell/prove/accept.py +113 -0
- invar/shell/{prove.py → prove/crosshair.py} +69 -30
- invar/shell/prove/hypothesis.py +293 -0
- invar/shell/subprocess_env.py +393 -0
- invar/shell/template_engine.py +345 -0
- invar/shell/templates.py +53 -0
- invar/shell/testing.py +77 -37
- invar/templates/CLAUDE.md.template +86 -9
- invar/templates/aider.conf.yml.template +16 -14
- invar/templates/commands/audit.md +138 -0
- invar/templates/commands/guard.md +77 -0
- invar/templates/config/CLAUDE.md.jinja +206 -0
- invar/templates/config/context.md.jinja +92 -0
- invar/templates/config/pre-commit.yaml.jinja +44 -0
- invar/templates/context.md.template +33 -0
- invar/templates/cursorrules.template +25 -13
- invar/templates/examples/README.md +2 -0
- invar/templates/examples/conftest.py +3 -0
- invar/templates/examples/contracts.py +4 -2
- invar/templates/examples/core_shell.py +10 -4
- invar/templates/examples/workflow.md +81 -0
- invar/templates/manifest.toml +137 -0
- invar/templates/protocol/INVAR.md +210 -0
- invar/templates/skills/develop/SKILL.md.jinja +318 -0
- invar/templates/skills/investigate/SKILL.md.jinja +106 -0
- invar/templates/skills/propose/SKILL.md.jinja +104 -0
- invar/templates/skills/review/SKILL.md.jinja +125 -0
- invar_tools-1.3.0.dist-info/METADATA +377 -0
- invar_tools-1.3.0.dist-info/RECORD +95 -0
- invar_tools-1.3.0.dist-info/entry_points.txt +2 -0
- invar_tools-1.3.0.dist-info/licenses/LICENSE +190 -0
- invar_tools-1.3.0.dist-info/licenses/LICENSE-GPL +674 -0
- invar_tools-1.3.0.dist-info/licenses/NOTICE +63 -0
- invar/contracts.py +0 -152
- invar/decorators.py +0 -94
- invar/invariant.py +0 -57
- invar/resource.py +0 -99
- invar/shell/prove_fallback.py +0 -183
- invar/shell/update_cmd.py +0 -191
- invar/templates/INVAR.md +0 -134
- invar_tools-1.0.0.dist-info/METADATA +0 -321
- invar_tools-1.0.0.dist-info/RECORD +0 -64
- invar_tools-1.0.0.dist-info/entry_points.txt +0 -2
- invar_tools-1.0.0.dist-info/licenses/LICENSE +0 -21
- /invar/shell/{prove_cache.py → prove/cache.py} +0 -0
- {invar_tools-1.0.0.dist-info → invar_tools-1.3.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DX-56: Unified template sync engine.
|
|
3
|
+
|
|
4
|
+
Shell module: Core sync logic shared by init and dev sync commands.
|
|
5
|
+
Handles state detection, manifest-driven file lists, region-based updates,
|
|
6
|
+
syntax switching, and project additions injection.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import shutil
|
|
12
|
+
from datetime import date
|
|
13
|
+
from pathlib import Path # noqa: TC003 - used at runtime
|
|
14
|
+
|
|
15
|
+
from returns.result import Failure, Result, Success
|
|
16
|
+
|
|
17
|
+
from invar.core.sync_helpers import (
|
|
18
|
+
SyncConfig,
|
|
19
|
+
SyncReport,
|
|
20
|
+
detect_region_scheme,
|
|
21
|
+
get_sync_file_lists,
|
|
22
|
+
should_skip_file,
|
|
23
|
+
)
|
|
24
|
+
from invar.core.template_parser import (
|
|
25
|
+
detect_claude_md_state,
|
|
26
|
+
format_preserved_content,
|
|
27
|
+
parse_invar_regions,
|
|
28
|
+
reconstruct_file,
|
|
29
|
+
strip_invar_markers,
|
|
30
|
+
)
|
|
31
|
+
from invar.shell.template_engine import (
|
|
32
|
+
get_templates_dir,
|
|
33
|
+
load_manifest,
|
|
34
|
+
render_template_file,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Re-export for convenience
|
|
38
|
+
__all__ = ["SyncConfig", "SyncReport", "sync_templates"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# =============================================================================
|
|
42
|
+
# Core Sync Logic
|
|
43
|
+
# =============================================================================
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# @shell_complexity: Multi-path sync with state detection and region handling
|
|
47
|
+
def sync_templates(path: Path, config: SyncConfig) -> Result[SyncReport, str]:
|
|
48
|
+
"""Unified template sync engine.
|
|
49
|
+
|
|
50
|
+
DX-56: Core sync logic shared by init and dev sync commands.
|
|
51
|
+
|
|
52
|
+
Handles:
|
|
53
|
+
1. State detection (DX-55: intact/partial/missing/absent)
|
|
54
|
+
2. Manifest-driven file list
|
|
55
|
+
3. Region-based updates (managed/user/project/skill/extensions)
|
|
56
|
+
4. Syntax switching (CLI vs MCP)
|
|
57
|
+
5. Project additions injection
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
path: Project root directory
|
|
61
|
+
config: Sync configuration
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Success with SyncReport, or Failure with error message
|
|
65
|
+
"""
|
|
66
|
+
templates_dir = get_templates_dir()
|
|
67
|
+
manifest_result = load_manifest(templates_dir)
|
|
68
|
+
if isinstance(manifest_result, Failure):
|
|
69
|
+
return manifest_result
|
|
70
|
+
|
|
71
|
+
manifest = manifest_result.unwrap()
|
|
72
|
+
report = SyncReport()
|
|
73
|
+
|
|
74
|
+
# Build variables for template rendering
|
|
75
|
+
variables = {**manifest.get("variables", {}), "syntax": config.syntax}
|
|
76
|
+
|
|
77
|
+
# Load project additions if enabled
|
|
78
|
+
project_additions = _load_project_additions(path) if config.inject_project_additions else ""
|
|
79
|
+
|
|
80
|
+
# Get file lists from manifest
|
|
81
|
+
fully_managed, region_managed, create_only = get_sync_file_lists(manifest)
|
|
82
|
+
|
|
83
|
+
# Process fully managed files (direct overwrite)
|
|
84
|
+
for dest_rel, src_rel in fully_managed:
|
|
85
|
+
if should_skip_file(dest_rel, config.skip_patterns):
|
|
86
|
+
continue
|
|
87
|
+
result = _sync_fully_managed(path, templates_dir, dest_rel, src_rel, config, report)
|
|
88
|
+
if isinstance(result, Failure):
|
|
89
|
+
report.errors.append(result.failure())
|
|
90
|
+
|
|
91
|
+
# Process region-managed files
|
|
92
|
+
for dest_rel, src_rel in region_managed:
|
|
93
|
+
if should_skip_file(dest_rel, config.skip_patterns):
|
|
94
|
+
continue
|
|
95
|
+
result = _sync_region_managed(
|
|
96
|
+
path, templates_dir, dest_rel, src_rel,
|
|
97
|
+
config, variables, project_additions, report
|
|
98
|
+
)
|
|
99
|
+
if isinstance(result, Failure):
|
|
100
|
+
report.errors.append(result.failure())
|
|
101
|
+
|
|
102
|
+
# Process create-only files (only if not exists)
|
|
103
|
+
for dest_rel in create_only:
|
|
104
|
+
if should_skip_file(dest_rel, config.skip_patterns):
|
|
105
|
+
continue
|
|
106
|
+
if dest_rel in manifest.get("templates", {}):
|
|
107
|
+
template_config = manifest["templates"][dest_rel]
|
|
108
|
+
result = _sync_create_only(
|
|
109
|
+
path, templates_dir, dest_rel, template_config, variables, report
|
|
110
|
+
)
|
|
111
|
+
if isinstance(result, Failure):
|
|
112
|
+
report.errors.append(result.failure())
|
|
113
|
+
|
|
114
|
+
return Success(report)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _load_project_additions(path: Path) -> str:
|
|
118
|
+
"""Load project-additions.md content if it exists."""
|
|
119
|
+
pa_path = path / ".invar" / "project-additions.md"
|
|
120
|
+
if pa_path.exists():
|
|
121
|
+
try:
|
|
122
|
+
return pa_path.read_text()
|
|
123
|
+
except OSError:
|
|
124
|
+
pass
|
|
125
|
+
return ""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# @shell_complexity: File I/O with multiple existence/content checks
|
|
129
|
+
def _sync_fully_managed(
|
|
130
|
+
path: Path,
|
|
131
|
+
templates_dir: Path,
|
|
132
|
+
dest_rel: str,
|
|
133
|
+
src_rel: str,
|
|
134
|
+
config: SyncConfig,
|
|
135
|
+
report: SyncReport,
|
|
136
|
+
) -> Result[str, str]:
|
|
137
|
+
"""Sync a fully managed file (direct overwrite)."""
|
|
138
|
+
dest_file = path / dest_rel
|
|
139
|
+
src_file = templates_dir / src_rel
|
|
140
|
+
|
|
141
|
+
if not src_file.exists():
|
|
142
|
+
return Failure(f"Template not found: {src_rel}")
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
new_content = src_file.read_text()
|
|
146
|
+
except OSError as e:
|
|
147
|
+
return Failure(f"Failed to read template {src_rel}: {e}")
|
|
148
|
+
|
|
149
|
+
# Check if update needed
|
|
150
|
+
if dest_file.exists() and not config.force:
|
|
151
|
+
try:
|
|
152
|
+
if dest_file.read_text() == new_content:
|
|
153
|
+
report.skipped.append(dest_rel)
|
|
154
|
+
return Success("skipped")
|
|
155
|
+
except OSError:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
# Write file (unless check mode)
|
|
159
|
+
if not config.check:
|
|
160
|
+
try:
|
|
161
|
+
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
dest_file.write_text(new_content)
|
|
163
|
+
except OSError as e:
|
|
164
|
+
return Failure(f"Failed to write {dest_rel}: {e}")
|
|
165
|
+
|
|
166
|
+
report.updated.append(dest_rel) if dest_file.exists() else report.created.append(dest_rel)
|
|
167
|
+
return Success("synced")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# @shell_complexity: Region-based sync with DX-55 state detection and content merging
|
|
171
|
+
def _sync_region_managed(
|
|
172
|
+
path: Path,
|
|
173
|
+
templates_dir: Path,
|
|
174
|
+
dest_rel: str,
|
|
175
|
+
src_rel: str,
|
|
176
|
+
config: SyncConfig,
|
|
177
|
+
variables: dict,
|
|
178
|
+
project_additions: str,
|
|
179
|
+
report: SyncReport,
|
|
180
|
+
) -> Result[str, str]:
|
|
181
|
+
"""Sync a region-managed file (update managed regions, preserve user)."""
|
|
182
|
+
dest_file = path / dest_rel
|
|
183
|
+
src_file = templates_dir / src_rel
|
|
184
|
+
|
|
185
|
+
if not src_file.exists():
|
|
186
|
+
return Failure(f"Template not found: {src_rel}")
|
|
187
|
+
|
|
188
|
+
# Render template
|
|
189
|
+
render_result = render_template_file(src_file, variables)
|
|
190
|
+
if isinstance(render_result, Failure):
|
|
191
|
+
return render_result
|
|
192
|
+
|
|
193
|
+
new_content = render_result.unwrap()
|
|
194
|
+
new_parsed = parse_invar_regions(new_content)
|
|
195
|
+
|
|
196
|
+
# Detect region scheme
|
|
197
|
+
region_scheme = detect_region_scheme(new_parsed)
|
|
198
|
+
if region_scheme is None:
|
|
199
|
+
return Failure(f"No region markers in template: {src_rel}")
|
|
200
|
+
|
|
201
|
+
primary_region, user_region = region_scheme
|
|
202
|
+
|
|
203
|
+
# Handle new file
|
|
204
|
+
if not dest_file.exists():
|
|
205
|
+
return _create_new_region_file(
|
|
206
|
+
dest_file, dest_rel, new_content, new_parsed, project_additions, config, report
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Read existing file
|
|
210
|
+
try:
|
|
211
|
+
existing_content = dest_file.read_text()
|
|
212
|
+
except UnicodeDecodeError:
|
|
213
|
+
# Binary content - replace entirely
|
|
214
|
+
if not config.check:
|
|
215
|
+
dest_file.unlink()
|
|
216
|
+
dest_file.write_text(new_content)
|
|
217
|
+
report.updated.append(dest_rel)
|
|
218
|
+
return Success("replaced_binary")
|
|
219
|
+
except OSError as e:
|
|
220
|
+
return Failure(f"Failed to read {dest_rel}: {e}")
|
|
221
|
+
|
|
222
|
+
# Process based on DX-55 state
|
|
223
|
+
final_content = _merge_region_content(
|
|
224
|
+
existing_content, new_content, new_parsed,
|
|
225
|
+
primary_region, user_region, dest_rel, project_additions, config
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Check if changed
|
|
229
|
+
if final_content == existing_content and not config.force:
|
|
230
|
+
report.skipped.append(dest_rel)
|
|
231
|
+
return Success("skipped")
|
|
232
|
+
|
|
233
|
+
# Write updated file
|
|
234
|
+
if not config.check:
|
|
235
|
+
try:
|
|
236
|
+
dest_file.write_text(final_content)
|
|
237
|
+
except OSError as e:
|
|
238
|
+
return Failure(f"Failed to write {dest_rel}: {e}")
|
|
239
|
+
|
|
240
|
+
report.updated.append(dest_rel)
|
|
241
|
+
return Success("updated")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _create_new_region_file(
|
|
245
|
+
dest_file: Path,
|
|
246
|
+
dest_rel: str,
|
|
247
|
+
new_content: str,
|
|
248
|
+
new_parsed,
|
|
249
|
+
project_additions: str,
|
|
250
|
+
config: SyncConfig,
|
|
251
|
+
report: SyncReport,
|
|
252
|
+
) -> Result[str, str]:
|
|
253
|
+
"""Create a new region-managed file."""
|
|
254
|
+
final_content = new_content
|
|
255
|
+
|
|
256
|
+
# Inject project additions for CLAUDE.md
|
|
257
|
+
if dest_rel == "CLAUDE.md" and project_additions and "project" in new_parsed.regions:
|
|
258
|
+
updates = {"project": project_additions}
|
|
259
|
+
final_content = reconstruct_file(new_parsed, updates)
|
|
260
|
+
|
|
261
|
+
if not config.check:
|
|
262
|
+
try:
|
|
263
|
+
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
|
264
|
+
dest_file.write_text(final_content)
|
|
265
|
+
except OSError as e:
|
|
266
|
+
return Failure(f"Failed to write {dest_rel}: {e}")
|
|
267
|
+
|
|
268
|
+
report.created.append(dest_rel)
|
|
269
|
+
return Success("created")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# @shell_orchestration: DX-55 state-based merge logic with multiple recovery paths
|
|
273
|
+
# @shell_complexity: Multiple state branches (intact/partial/missing)
|
|
274
|
+
def _merge_region_content(
|
|
275
|
+
existing_content: str,
|
|
276
|
+
new_content: str,
|
|
277
|
+
new_parsed,
|
|
278
|
+
primary_region: str,
|
|
279
|
+
user_region: str,
|
|
280
|
+
dest_rel: str,
|
|
281
|
+
project_additions: str,
|
|
282
|
+
config: SyncConfig,
|
|
283
|
+
) -> str:
|
|
284
|
+
"""Merge existing content with new template based on DX-55 state."""
|
|
285
|
+
state = detect_claude_md_state(existing_content)
|
|
286
|
+
updates: dict[str, str] = {}
|
|
287
|
+
|
|
288
|
+
if state.state == "intact":
|
|
289
|
+
# Just update managed region, preserve user
|
|
290
|
+
existing_parsed = parse_invar_regions(existing_content)
|
|
291
|
+
updates[primary_region] = new_parsed.regions[primary_region].content
|
|
292
|
+
if dest_rel == "CLAUDE.md" and project_additions and "project" in existing_parsed.regions:
|
|
293
|
+
updates["project"] = project_additions
|
|
294
|
+
return reconstruct_file(existing_parsed, updates)
|
|
295
|
+
|
|
296
|
+
elif state.state == "partial":
|
|
297
|
+
# Corruption: salvage user content
|
|
298
|
+
user_content = state.user_content or strip_invar_markers(existing_content)
|
|
299
|
+
if user_content:
|
|
300
|
+
user_content = format_preserved_content(user_content, date.today().isoformat())
|
|
301
|
+
parsed = parse_invar_regions(new_content)
|
|
302
|
+
if user_region in parsed.regions and user_content:
|
|
303
|
+
updates = {user_region: "\n" + user_content + "\n"}
|
|
304
|
+
if dest_rel == "CLAUDE.md" and project_additions and "project" in parsed.regions:
|
|
305
|
+
updates["project"] = project_additions
|
|
306
|
+
return reconstruct_file(parsed, updates)
|
|
307
|
+
return new_content
|
|
308
|
+
|
|
309
|
+
elif state.state == "missing":
|
|
310
|
+
# No Invar markers - preserve entire content as user content
|
|
311
|
+
preserved = format_preserved_content(existing_content, date.today().isoformat())
|
|
312
|
+
parsed = parse_invar_regions(new_content)
|
|
313
|
+
if user_region in parsed.regions:
|
|
314
|
+
updates = {user_region: "\n" + preserved + "\n"}
|
|
315
|
+
if dest_rel == "CLAUDE.md" and project_additions and "project" in parsed.regions:
|
|
316
|
+
updates["project"] = project_additions
|
|
317
|
+
return reconstruct_file(parsed, updates)
|
|
318
|
+
return new_content
|
|
319
|
+
|
|
320
|
+
return new_content
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# @shell_complexity: File creation with multiple template types
|
|
324
|
+
def _sync_create_only(
|
|
325
|
+
path: Path,
|
|
326
|
+
templates_dir: Path,
|
|
327
|
+
dest_rel: str,
|
|
328
|
+
template_config: dict,
|
|
329
|
+
variables: dict,
|
|
330
|
+
report: SyncReport,
|
|
331
|
+
) -> Result[str, str]:
|
|
332
|
+
"""Sync a create-only file (only create if not exists)."""
|
|
333
|
+
dest_file = path / dest_rel
|
|
334
|
+
src_rel = template_config.get("src", "")
|
|
335
|
+
template_type = template_config.get("type", "copy")
|
|
336
|
+
src_file = templates_dir / src_rel
|
|
337
|
+
|
|
338
|
+
# Skip if already exists
|
|
339
|
+
if dest_file.exists():
|
|
340
|
+
report.skipped.append(dest_rel)
|
|
341
|
+
return Success("skipped")
|
|
342
|
+
|
|
343
|
+
if not src_file.exists():
|
|
344
|
+
return Failure(f"Template not found: {src_rel}")
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
|
348
|
+
|
|
349
|
+
if template_type == "copy":
|
|
350
|
+
dest_file.write_text(src_file.read_text())
|
|
351
|
+
elif template_type == "jinja":
|
|
352
|
+
result = render_template_file(src_file, variables)
|
|
353
|
+
if isinstance(result, Failure):
|
|
354
|
+
return result
|
|
355
|
+
dest_file.write_text(result.unwrap())
|
|
356
|
+
elif template_type == "copy_dir":
|
|
357
|
+
if src_file.is_dir():
|
|
358
|
+
shutil.copytree(src_file, dest_file)
|
|
359
|
+
else:
|
|
360
|
+
return Failure(f"Expected directory: {src_rel}")
|
|
361
|
+
|
|
362
|
+
report.created.append(dest_rel)
|
|
363
|
+
return Success("created")
|
|
364
|
+
|
|
365
|
+
except OSError as e:
|
|
366
|
+
return Failure(f"Failed to create {dest_rel}: {e}")
|
|
@@ -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)"),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Update command for Invar.
|
|
3
|
+
|
|
4
|
+
DX-55: Now an alias for 'invar init' (unified idempotent command).
|
|
5
|
+
Maintained for backwards compatibility.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from invar.shell.commands.init import init as init_command
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def update(
|
|
21
|
+
path: Path = typer.Argument(Path(), help="Project root directory"),
|
|
22
|
+
check: bool = typer.Option(False, "--check", help="Preview changes"),
|
|
23
|
+
force: bool = typer.Option(False, "--force", "-f", help="Update even if current"),
|
|
24
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Accept defaults without prompting"),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Alias for 'invar init' (DX-55).
|
|
28
|
+
|
|
29
|
+
Maintained for backwards compatibility.
|
|
30
|
+
Both commands are now idempotent and do the same thing.
|
|
31
|
+
|
|
32
|
+
Use 'invar init --check' to preview changes.
|
|
33
|
+
Use 'invar init --force' to refresh even if current.
|
|
34
|
+
"""
|
|
35
|
+
console.print("[dim]Note: 'update' is now an alias for 'init'[/dim]")
|
|
36
|
+
# Pass all init parameters with explicit defaults to avoid typer.Option object issues
|
|
37
|
+
return init_command(
|
|
38
|
+
path=path,
|
|
39
|
+
claude=False,
|
|
40
|
+
mcp_method=None,
|
|
41
|
+
dirs=None,
|
|
42
|
+
hooks=True,
|
|
43
|
+
skills=True,
|
|
44
|
+
yes=yes,
|
|
45
|
+
check=check,
|
|
46
|
+
force=force,
|
|
47
|
+
reset=False,
|
|
48
|
+
)
|