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,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DX-55: Smart merge logic for CLAUDE.md recovery.
|
|
3
|
+
|
|
4
|
+
Shell module: handles merging and recovering CLAUDE.md content.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import date
|
|
11
|
+
from typing import TYPE_CHECKING, Literal
|
|
12
|
+
|
|
13
|
+
from returns.result import Failure, Result, Success
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from invar.core.template_parser import (
|
|
19
|
+
ClaudeMdState,
|
|
20
|
+
detect_claude_md_state,
|
|
21
|
+
format_preserved_content,
|
|
22
|
+
parse_invar_regions,
|
|
23
|
+
reconstruct_file,
|
|
24
|
+
strip_invar_markers,
|
|
25
|
+
)
|
|
26
|
+
from invar.shell.template_engine import generate_from_manifest
|
|
27
|
+
|
|
28
|
+
# =============================================================================
|
|
29
|
+
# DX-55: Project State Detection
|
|
30
|
+
# =============================================================================
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ProjectState:
|
|
35
|
+
"""Overall project initialization state.
|
|
36
|
+
|
|
37
|
+
DX-55: Captures full state for idempotent init decision.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
initialized: bool
|
|
41
|
+
claude_md_state: ClaudeMdState
|
|
42
|
+
version: str
|
|
43
|
+
needs_update: bool
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def action(self) -> Literal["full_init", "update", "recover", "create", "none"]:
|
|
47
|
+
"""Determine what action to take."""
|
|
48
|
+
if not self.initialized:
|
|
49
|
+
return "full_init"
|
|
50
|
+
|
|
51
|
+
match self.claude_md_state.state:
|
|
52
|
+
case "intact":
|
|
53
|
+
return "update" if self.needs_update else "none"
|
|
54
|
+
case "partial" | "missing":
|
|
55
|
+
return "recover"
|
|
56
|
+
case "absent":
|
|
57
|
+
return "create"
|
|
58
|
+
case _:
|
|
59
|
+
return "none"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# @shell_complexity: State detection requires multiple file existence checks
|
|
63
|
+
def detect_project_state(path: Path) -> ProjectState:
|
|
64
|
+
"""Detect Invar initialization state.
|
|
65
|
+
|
|
66
|
+
DX-55: Core state detection for idempotent init.
|
|
67
|
+
"""
|
|
68
|
+
from invar import __protocol_version__
|
|
69
|
+
|
|
70
|
+
invar_md = path / "INVAR.md"
|
|
71
|
+
invar_dir = path / ".invar"
|
|
72
|
+
claude_md = path / "CLAUDE.md"
|
|
73
|
+
|
|
74
|
+
initialized = invar_md.exists() and invar_dir.exists()
|
|
75
|
+
|
|
76
|
+
# Detect CLAUDE.md state
|
|
77
|
+
if claude_md.exists():
|
|
78
|
+
try:
|
|
79
|
+
content = claude_md.read_text()
|
|
80
|
+
claude_state = detect_claude_md_state(content)
|
|
81
|
+
except UnicodeDecodeError:
|
|
82
|
+
# Binary or non-UTF-8 content - treat as corrupt, will be replaced
|
|
83
|
+
claude_state = ClaudeMdState(state="partial")
|
|
84
|
+
else:
|
|
85
|
+
claude_state = ClaudeMdState(state="absent")
|
|
86
|
+
|
|
87
|
+
# Extract protocol version from existing INVAR.md
|
|
88
|
+
version = ""
|
|
89
|
+
if invar_md.exists():
|
|
90
|
+
content = invar_md.read_text()
|
|
91
|
+
import re
|
|
92
|
+
|
|
93
|
+
match = re.search(r"Invar (?:Protocol )?v([\d.]+)", content)
|
|
94
|
+
if match:
|
|
95
|
+
version = match.group(1)
|
|
96
|
+
|
|
97
|
+
# Check if update needed (compare protocol versions, not package versions)
|
|
98
|
+
needs_update = initialized and version != __protocol_version__
|
|
99
|
+
|
|
100
|
+
return ProjectState(
|
|
101
|
+
initialized=initialized,
|
|
102
|
+
claude_md_state=claude_state,
|
|
103
|
+
version=version,
|
|
104
|
+
needs_update=needs_update,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# =============================================================================
|
|
109
|
+
# DX-55: Smart Merge Functions
|
|
110
|
+
# =============================================================================
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# @shell_complexity: Smart merge with multiple state handling paths
|
|
114
|
+
def merge_claude_md(path: Path, state: ClaudeMdState) -> Result[str, str]:
|
|
115
|
+
"""Smart merge CLAUDE.md based on detected state.
|
|
116
|
+
|
|
117
|
+
DX-55: Preserves user content while restoring Invar regions.
|
|
118
|
+
"""
|
|
119
|
+
from pathlib import Path as PathLib # Runtime import for Path operations
|
|
120
|
+
|
|
121
|
+
claude_md = PathLib(path) / "CLAUDE.md"
|
|
122
|
+
|
|
123
|
+
# Read existing content (handle binary/corrupt files)
|
|
124
|
+
existing_content = ""
|
|
125
|
+
if claude_md.exists():
|
|
126
|
+
try:
|
|
127
|
+
existing_content = claude_md.read_text()
|
|
128
|
+
except UnicodeDecodeError:
|
|
129
|
+
# Binary content - delete and recreate
|
|
130
|
+
claude_md.unlink()
|
|
131
|
+
state = ClaudeMdState(state="absent")
|
|
132
|
+
|
|
133
|
+
match state.state:
|
|
134
|
+
case "intact":
|
|
135
|
+
# Just update managed region, preserve user exactly
|
|
136
|
+
return _update_managed_only(path, existing_content)
|
|
137
|
+
|
|
138
|
+
case "partial":
|
|
139
|
+
# Corruption: try to salvage user content
|
|
140
|
+
return _recover_from_partial(path, existing_content, state)
|
|
141
|
+
|
|
142
|
+
case "missing":
|
|
143
|
+
# No Invar markers - treat entire file as user content
|
|
144
|
+
return _merge_with_preserved(path, existing_content)
|
|
145
|
+
|
|
146
|
+
case "absent":
|
|
147
|
+
# Just create new file
|
|
148
|
+
return Success("create_new")
|
|
149
|
+
|
|
150
|
+
return Success("no_action")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# @shell_complexity: Template regeneration with region extraction
|
|
154
|
+
def _update_managed_only(path: Path, existing_content: str) -> Result[str, str]:
|
|
155
|
+
"""Update only the managed region, preserve user content."""
|
|
156
|
+
# Parse existing
|
|
157
|
+
parsed = parse_invar_regions(existing_content)
|
|
158
|
+
|
|
159
|
+
if "user" not in parsed.regions:
|
|
160
|
+
return Failure("No user region found")
|
|
161
|
+
|
|
162
|
+
# Generate fresh template
|
|
163
|
+
template_result = generate_from_manifest(
|
|
164
|
+
path, syntax="cli", files_to_generate=["CLAUDE.md"]
|
|
165
|
+
)
|
|
166
|
+
if isinstance(template_result, Failure):
|
|
167
|
+
return template_result
|
|
168
|
+
|
|
169
|
+
# Re-read and extract managed
|
|
170
|
+
new_content = (path / "CLAUDE.md").read_text()
|
|
171
|
+
new_parsed = parse_invar_regions(new_content)
|
|
172
|
+
|
|
173
|
+
if "managed" not in new_parsed.regions:
|
|
174
|
+
return Failure("Template missing managed region")
|
|
175
|
+
|
|
176
|
+
# Reconstruct with new managed but old user
|
|
177
|
+
updates = {"managed": new_parsed.regions["managed"].content}
|
|
178
|
+
final_content = reconstruct_file(parsed, updates)
|
|
179
|
+
|
|
180
|
+
(path / "CLAUDE.md").write_text(final_content)
|
|
181
|
+
return Success("updated_managed")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# @shell_complexity: Recovery with content salvage logic
|
|
185
|
+
def _recover_from_partial(
|
|
186
|
+
path: Path, existing_content: str, state: ClaudeMdState
|
|
187
|
+
) -> Result[str, str]:
|
|
188
|
+
"""Recover from partial corruption."""
|
|
189
|
+
from pathlib import Path as PathLib # Runtime import for Path operations
|
|
190
|
+
|
|
191
|
+
# Try to salvage user content
|
|
192
|
+
if state.user_content:
|
|
193
|
+
user_content = state.user_content
|
|
194
|
+
else:
|
|
195
|
+
# Strip markers and treat rest as user content
|
|
196
|
+
user_content = strip_invar_markers(existing_content)
|
|
197
|
+
if user_content:
|
|
198
|
+
user_content = format_preserved_content(
|
|
199
|
+
user_content, date.today().isoformat()
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Remove existing CLAUDE.md so generate_from_manifest creates fresh template
|
|
203
|
+
claude_md = PathLib(path) / "CLAUDE.md"
|
|
204
|
+
if claude_md.exists():
|
|
205
|
+
claude_md.unlink()
|
|
206
|
+
|
|
207
|
+
# Generate fresh template
|
|
208
|
+
result = generate_from_manifest(
|
|
209
|
+
path, syntax="cli", files_to_generate=["CLAUDE.md"]
|
|
210
|
+
)
|
|
211
|
+
if isinstance(result, Failure):
|
|
212
|
+
return result
|
|
213
|
+
|
|
214
|
+
# Inject recovered user content
|
|
215
|
+
if user_content:
|
|
216
|
+
new_content = claude_md.read_text()
|
|
217
|
+
parsed = parse_invar_regions(new_content)
|
|
218
|
+
if "user" in parsed.regions:
|
|
219
|
+
updates = {"user": "\n" + user_content + "\n"}
|
|
220
|
+
final_content = reconstruct_file(parsed, updates)
|
|
221
|
+
claude_md.write_text(final_content)
|
|
222
|
+
|
|
223
|
+
return Success("recovered")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _merge_with_preserved(path: Path, existing_content: str) -> Result[str, str]:
|
|
227
|
+
"""Merge overwritten content as preserved user content."""
|
|
228
|
+
from pathlib import Path as PathLib # Runtime import for Path operations
|
|
229
|
+
|
|
230
|
+
# Format existing content as preserved
|
|
231
|
+
preserved = format_preserved_content(
|
|
232
|
+
existing_content, date.today().isoformat()
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Remove existing CLAUDE.md so generate_from_manifest creates fresh template
|
|
236
|
+
claude_md = PathLib(path) / "CLAUDE.md"
|
|
237
|
+
if claude_md.exists():
|
|
238
|
+
claude_md.unlink()
|
|
239
|
+
|
|
240
|
+
# Generate fresh template
|
|
241
|
+
result = generate_from_manifest(
|
|
242
|
+
path, syntax="cli", files_to_generate=["CLAUDE.md"]
|
|
243
|
+
)
|
|
244
|
+
if isinstance(result, Failure):
|
|
245
|
+
return result
|
|
246
|
+
|
|
247
|
+
# Inject preserved content into user region
|
|
248
|
+
new_content = claude_md.read_text()
|
|
249
|
+
parsed = parse_invar_regions(new_content)
|
|
250
|
+
|
|
251
|
+
if "user" in parsed.regions:
|
|
252
|
+
updates = {"user": "\n" + preserved + "\n"}
|
|
253
|
+
final_content = reconstruct_file(parsed, updates)
|
|
254
|
+
claude_md.write_text(final_content)
|
|
255
|
+
|
|
256
|
+
return Success("merged")
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mutation testing command for Invar CLI.
|
|
3
|
+
|
|
4
|
+
DX-28: `invar mutate` wraps mutmut to detect undertested code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json as json_lib
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from returns.result import Failure
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from invar.shell.mutation import (
|
|
17
|
+
MutationResult,
|
|
18
|
+
check_mutmut_installed,
|
|
19
|
+
get_surviving_mutants,
|
|
20
|
+
run_mutation_test,
|
|
21
|
+
show_mutant,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# @shell:entry - CLI command entry point
|
|
28
|
+
# @invar:allow entry_point_too_thick: CLI orchestration with multiple output modes
|
|
29
|
+
def mutate(
|
|
30
|
+
target: Path = typer.Argument(
|
|
31
|
+
Path(),
|
|
32
|
+
help="File or directory to mutate",
|
|
33
|
+
exists=True,
|
|
34
|
+
),
|
|
35
|
+
tests: Path = typer.Option(
|
|
36
|
+
None,
|
|
37
|
+
"--tests",
|
|
38
|
+
"-t",
|
|
39
|
+
help="Test directory (auto-detected if not specified)",
|
|
40
|
+
),
|
|
41
|
+
timeout: int = typer.Option(
|
|
42
|
+
300,
|
|
43
|
+
"--timeout",
|
|
44
|
+
help="Maximum time in seconds",
|
|
45
|
+
),
|
|
46
|
+
show_survivors: bool = typer.Option(
|
|
47
|
+
False,
|
|
48
|
+
"--survivors",
|
|
49
|
+
"-s",
|
|
50
|
+
help="Show surviving mutants",
|
|
51
|
+
),
|
|
52
|
+
json_output: bool = typer.Option(
|
|
53
|
+
False,
|
|
54
|
+
"--json",
|
|
55
|
+
help="Output as JSON",
|
|
56
|
+
),
|
|
57
|
+
) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Run mutation testing to find undertested code.
|
|
60
|
+
|
|
61
|
+
DX-28: Uses mutmut to automatically mutate code (e.g., `in` → `not in`)
|
|
62
|
+
and check if tests catch the mutations. Surviving mutants indicate
|
|
63
|
+
weak test coverage.
|
|
64
|
+
|
|
65
|
+
Examples:
|
|
66
|
+
|
|
67
|
+
invar mutate src/myapp/core/parser.py
|
|
68
|
+
|
|
69
|
+
invar mutate src/myapp --tests tests/ --timeout 600
|
|
70
|
+
|
|
71
|
+
invar mutate --survivors # Show surviving mutants from last run
|
|
72
|
+
"""
|
|
73
|
+
# Check if mutmut is installed
|
|
74
|
+
install_check = check_mutmut_installed()
|
|
75
|
+
if isinstance(install_check, Failure):
|
|
76
|
+
if json_output:
|
|
77
|
+
console.print(json_lib.dumps({"error": install_check.failure()}))
|
|
78
|
+
else:
|
|
79
|
+
console.print(f"[red]Error:[/red] {install_check.failure()}")
|
|
80
|
+
console.print("\n[dim]Install with: pip install mutmut[/dim]")
|
|
81
|
+
raise typer.Exit(1)
|
|
82
|
+
|
|
83
|
+
# If just showing survivors from last run
|
|
84
|
+
if show_survivors:
|
|
85
|
+
result = get_surviving_mutants(target)
|
|
86
|
+
if isinstance(result, Failure):
|
|
87
|
+
if json_output:
|
|
88
|
+
console.print(json_lib.dumps({"error": result.failure()}))
|
|
89
|
+
else:
|
|
90
|
+
console.print(f"[red]Error:[/red] {result.failure()}")
|
|
91
|
+
raise typer.Exit(1)
|
|
92
|
+
|
|
93
|
+
survivors = result.unwrap()
|
|
94
|
+
if json_output:
|
|
95
|
+
console.print(json_lib.dumps({"survivors": survivors}))
|
|
96
|
+
else:
|
|
97
|
+
if survivors:
|
|
98
|
+
console.print(f"[yellow]Surviving mutants ({len(survivors)}):[/yellow]")
|
|
99
|
+
for s in survivors:
|
|
100
|
+
console.print(f" {s}")
|
|
101
|
+
else:
|
|
102
|
+
console.print("[green]No surviving mutants![/green]")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# Run mutation testing
|
|
106
|
+
if not json_output:
|
|
107
|
+
console.print(f"[bold]Running mutation testing on {target}...[/bold]")
|
|
108
|
+
console.print("[dim]This may take a while.[/dim]\n")
|
|
109
|
+
|
|
110
|
+
result = run_mutation_test(target, tests, timeout)
|
|
111
|
+
|
|
112
|
+
if isinstance(result, Failure):
|
|
113
|
+
if json_output:
|
|
114
|
+
console.print(json_lib.dumps({"error": result.failure()}))
|
|
115
|
+
else:
|
|
116
|
+
console.print(f"[red]Error:[/red] {result.failure()}")
|
|
117
|
+
raise typer.Exit(1)
|
|
118
|
+
|
|
119
|
+
mutation_result = result.unwrap()
|
|
120
|
+
_display_mutation_result(mutation_result, json_output)
|
|
121
|
+
|
|
122
|
+
# Exit with error if mutation score is too low
|
|
123
|
+
if not mutation_result.passed:
|
|
124
|
+
raise typer.Exit(1)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# @shell_complexity: Result display with dual output modes (JSON/human)
|
|
128
|
+
def _display_mutation_result(result: MutationResult, json_output: bool) -> None:
|
|
129
|
+
"""Display mutation testing results."""
|
|
130
|
+
if json_output:
|
|
131
|
+
data = {
|
|
132
|
+
"total": result.total,
|
|
133
|
+
"killed": result.killed,
|
|
134
|
+
"survived": result.survived,
|
|
135
|
+
"timeout": result.timeout,
|
|
136
|
+
"score": round(result.score, 1),
|
|
137
|
+
"passed": result.passed,
|
|
138
|
+
"errors": result.errors,
|
|
139
|
+
"survivors": result.survivors,
|
|
140
|
+
}
|
|
141
|
+
console.print(json_lib.dumps(data, indent=2))
|
|
142
|
+
else:
|
|
143
|
+
# Human-readable output
|
|
144
|
+
score_color = "green" if result.passed else "red"
|
|
145
|
+
|
|
146
|
+
console.print("\n[bold]Mutation Testing Results[/bold]")
|
|
147
|
+
console.print(f" Total mutants: {result.total}")
|
|
148
|
+
console.print(f" [green]Killed:[/green] {result.killed}")
|
|
149
|
+
console.print(f" [red]Survived:[/red] {result.survived}")
|
|
150
|
+
if result.timeout > 0:
|
|
151
|
+
console.print(f" [yellow]Timeout:[/yellow] {result.timeout}")
|
|
152
|
+
|
|
153
|
+
console.print(
|
|
154
|
+
f"\n [{score_color}]Mutation Score: {result.score:.1f}%[/{score_color}]"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if result.passed:
|
|
158
|
+
console.print("\n[green]✓ Mutation testing passed (≥80% killed)[/green]")
|
|
159
|
+
else:
|
|
160
|
+
console.print("\n[red]✗ Mutation testing failed (<80% killed)[/red]")
|
|
161
|
+
console.print("[dim]Run with --survivors to see surviving mutants[/dim]")
|
|
162
|
+
|
|
163
|
+
if result.errors:
|
|
164
|
+
console.print("\n[yellow]Errors:[/yellow]")
|
|
165
|
+
for err in result.errors:
|
|
166
|
+
console.print(f" {err}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# @shell:entry - CLI command for showing mutant details
|
|
170
|
+
def mutant_show(
|
|
171
|
+
mutant_id: int = typer.Argument(..., help="Mutant ID to show"),
|
|
172
|
+
) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Show the diff for a specific mutant.
|
|
175
|
+
|
|
176
|
+
Use after `invar mutate --survivors` to investigate surviving mutants.
|
|
177
|
+
"""
|
|
178
|
+
result = show_mutant(mutant_id)
|
|
179
|
+
|
|
180
|
+
if isinstance(result, Failure):
|
|
181
|
+
console.print(f"[red]Error:[/red] {result.failure()}")
|
|
182
|
+
raise typer.Exit(1)
|
|
183
|
+
|
|
184
|
+
console.print(result.unwrap())
|
|
@@ -30,6 +30,7 @@ if TYPE_CHECKING:
|
|
|
30
30
|
console = Console()
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
# @shell_complexity: Symbol map generation with sorting and output modes
|
|
33
34
|
def run_map(path: Path, top_n: int, json_output: bool) -> Result[None, str]:
|
|
34
35
|
"""
|
|
35
36
|
Run the map command.
|
|
@@ -75,6 +76,7 @@ def run_map(path: Path, top_n: int, json_output: bool) -> Result[None, str]:
|
|
|
75
76
|
return Success(None)
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
# @shell_complexity: Signature extraction with symbol filtering
|
|
78
80
|
def run_sig(target: str, json_output: bool) -> Result[None, str]:
|
|
79
81
|
"""
|
|
80
82
|
Run the sig command.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DX-56: Developer sync command for Invar (unified sync engine).
|
|
3
|
+
|
|
4
|
+
Shell module: Special command for updating Invar's own project files.
|
|
5
|
+
Uses MCP syntax and injects project-additions.md content.
|
|
6
|
+
|
|
7
|
+
CLI: `invar dev sync` (formerly `invar sync-self`)
|
|
8
|
+
|
|
9
|
+
This is a thin wrapper around the unified template_sync engine.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
from returns.result import Failure
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
|
|
20
|
+
from invar.core.sync_helpers import SyncConfig
|
|
21
|
+
from invar.shell.commands.template_sync import sync_templates
|
|
22
|
+
from invar.shell.template_engine import is_invar_project
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# @shell_complexity: CLI command with result display and multiple output branches
|
|
28
|
+
def sync_self(
|
|
29
|
+
path: Path = typer.Argument(Path(), help="Invar project root"),
|
|
30
|
+
check: bool = typer.Option(
|
|
31
|
+
False, "--check", help="Preview changes without applying"
|
|
32
|
+
),
|
|
33
|
+
force: bool = typer.Option(
|
|
34
|
+
False, "--force", "-f", help="Update even if already current"
|
|
35
|
+
),
|
|
36
|
+
) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Synchronize Invar's own project files from templates.
|
|
39
|
+
|
|
40
|
+
DX-56: Now uses unified sync engine with manifest-driven file lists.
|
|
41
|
+
|
|
42
|
+
This command is for the Invar project only. It:
|
|
43
|
+
- Uses MCP syntax (invar_guard, invar_map, etc.)
|
|
44
|
+
- Injects .invar/project-additions.md into project region
|
|
45
|
+
- Updates managed regions while preserving user content
|
|
46
|
+
- Handles DX-55 state recovery (intact/partial/missing/absent)
|
|
47
|
+
|
|
48
|
+
Use --check to preview changes without applying them.
|
|
49
|
+
Use --force to update even if already current.
|
|
50
|
+
"""
|
|
51
|
+
# Verify this is the Invar project
|
|
52
|
+
if not is_invar_project(path):
|
|
53
|
+
console.print("[red]Error:[/red] This command is only for the Invar project itself.")
|
|
54
|
+
console.print("[dim]Use 'invar init' for other projects.[/dim]")
|
|
55
|
+
raise typer.Exit(1)
|
|
56
|
+
|
|
57
|
+
console.print("[bold]Syncing Invar project files...[/bold]")
|
|
58
|
+
console.print("[dim]Using MCP syntax for templates[/dim]")
|
|
59
|
+
console.print()
|
|
60
|
+
|
|
61
|
+
# Configure for Invar project
|
|
62
|
+
config = SyncConfig(
|
|
63
|
+
syntax="mcp",
|
|
64
|
+
inject_project_additions=True,
|
|
65
|
+
force=force,
|
|
66
|
+
check=check,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Run unified sync engine
|
|
70
|
+
result = sync_templates(path, config)
|
|
71
|
+
|
|
72
|
+
if isinstance(result, Failure):
|
|
73
|
+
console.print(f"[red]Error:[/red] {result.failure()}")
|
|
74
|
+
raise typer.Exit(1)
|
|
75
|
+
|
|
76
|
+
report = result.unwrap()
|
|
77
|
+
|
|
78
|
+
# Display results
|
|
79
|
+
if check:
|
|
80
|
+
console.print("[bold]Preview mode - no changes applied[/bold]")
|
|
81
|
+
console.print()
|
|
82
|
+
|
|
83
|
+
for file in report.created:
|
|
84
|
+
action = "Would create" if check else "Created"
|
|
85
|
+
console.print(f"[green]{action}[/green] {file}")
|
|
86
|
+
|
|
87
|
+
for file in report.updated:
|
|
88
|
+
action = "Would update" if check else "Updated"
|
|
89
|
+
console.print(f"[cyan]{action}[/cyan] {file}")
|
|
90
|
+
|
|
91
|
+
for file in report.skipped:
|
|
92
|
+
console.print(f"[dim]Skipped[/dim] {file} (unchanged)")
|
|
93
|
+
|
|
94
|
+
for error in report.errors:
|
|
95
|
+
console.print(f"[yellow]Warning:[/yellow] {error}")
|
|
96
|
+
|
|
97
|
+
# Summary
|
|
98
|
+
console.print()
|
|
99
|
+
if check:
|
|
100
|
+
console.print("[bold]Dry run complete.[/bold]")
|
|
101
|
+
console.print(f" Would create: {len(report.created)} files")
|
|
102
|
+
console.print(f" Would update: {len(report.updated)} files")
|
|
103
|
+
console.print(f" Would skip: {len(report.skipped)} files (unchanged)")
|
|
104
|
+
else:
|
|
105
|
+
console.print("[bold green]Sync complete![/bold green]")
|
|
106
|
+
console.print(f" Created: {len(report.created)} files")
|
|
107
|
+
console.print(f" Updated: {len(report.updated)} files")
|
|
108
|
+
console.print(f" Skipped: {len(report.skipped)} files (unchanged)")
|
|
109
|
+
|
|
110
|
+
console.print()
|
|
111
|
+
console.print("[dim]MCP syntax applied:[/dim]")
|
|
112
|
+
console.print("[dim] invar_guard(changed=true) instead of invar guard --changed[/dim]")
|
|
113
|
+
console.print("[dim] invar_map(top=10) instead of invar map --top 10[/dim]")
|