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.
Files changed (98) hide show
  1. invar/__init__.py +1 -0
  2. invar/core/contracts.py +80 -10
  3. invar/core/entry_points.py +367 -0
  4. invar/core/extraction.py +5 -6
  5. invar/core/format_specs.py +195 -0
  6. invar/core/format_strategies.py +197 -0
  7. invar/core/formatter.py +32 -10
  8. invar/core/hypothesis_strategies.py +50 -10
  9. invar/core/inspect.py +1 -1
  10. invar/core/lambda_helpers.py +3 -2
  11. invar/core/models.py +30 -18
  12. invar/core/must_use.py +2 -1
  13. invar/core/parser.py +13 -6
  14. invar/core/postcondition_scope.py +128 -0
  15. invar/core/property_gen.py +86 -42
  16. invar/core/purity.py +13 -7
  17. invar/core/purity_heuristics.py +5 -9
  18. invar/core/references.py +8 -6
  19. invar/core/review_trigger.py +370 -0
  20. invar/core/rule_meta.py +69 -2
  21. invar/core/rules.py +91 -28
  22. invar/core/shell_analysis.py +247 -0
  23. invar/core/shell_architecture.py +171 -0
  24. invar/core/strategies.py +7 -14
  25. invar/core/suggestions.py +92 -0
  26. invar/core/sync_helpers.py +238 -0
  27. invar/core/tautology.py +103 -37
  28. invar/core/template_parser.py +467 -0
  29. invar/core/timeout_inference.py +4 -7
  30. invar/core/utils.py +63 -18
  31. invar/core/verification_routing.py +155 -0
  32. invar/mcp/server.py +113 -13
  33. invar/shell/commands/__init__.py +11 -0
  34. invar/shell/{cli.py → commands/guard.py} +152 -44
  35. invar/shell/{init_cmd.py → commands/init.py} +200 -28
  36. invar/shell/commands/merge.py +256 -0
  37. invar/shell/commands/mutate.py +184 -0
  38. invar/shell/{perception.py → commands/perception.py} +2 -0
  39. invar/shell/commands/sync_self.py +113 -0
  40. invar/shell/commands/template_sync.py +366 -0
  41. invar/shell/{test_cmd.py → commands/test.py} +3 -1
  42. invar/shell/commands/update.py +48 -0
  43. invar/shell/config.py +247 -10
  44. invar/shell/coverage.py +351 -0
  45. invar/shell/fs.py +5 -2
  46. invar/shell/git.py +2 -0
  47. invar/shell/guard_helpers.py +116 -20
  48. invar/shell/guard_output.py +106 -24
  49. invar/shell/mcp_config.py +3 -0
  50. invar/shell/mutation.py +314 -0
  51. invar/shell/property_tests.py +75 -24
  52. invar/shell/prove/__init__.py +9 -0
  53. invar/shell/prove/accept.py +113 -0
  54. invar/shell/{prove.py → prove/crosshair.py} +69 -30
  55. invar/shell/prove/hypothesis.py +293 -0
  56. invar/shell/subprocess_env.py +393 -0
  57. invar/shell/template_engine.py +345 -0
  58. invar/shell/templates.py +53 -0
  59. invar/shell/testing.py +77 -37
  60. invar/templates/CLAUDE.md.template +86 -9
  61. invar/templates/aider.conf.yml.template +16 -14
  62. invar/templates/commands/audit.md +138 -0
  63. invar/templates/commands/guard.md +77 -0
  64. invar/templates/config/CLAUDE.md.jinja +206 -0
  65. invar/templates/config/context.md.jinja +92 -0
  66. invar/templates/config/pre-commit.yaml.jinja +44 -0
  67. invar/templates/context.md.template +33 -0
  68. invar/templates/cursorrules.template +25 -13
  69. invar/templates/examples/README.md +2 -0
  70. invar/templates/examples/conftest.py +3 -0
  71. invar/templates/examples/contracts.py +4 -2
  72. invar/templates/examples/core_shell.py +10 -4
  73. invar/templates/examples/workflow.md +81 -0
  74. invar/templates/manifest.toml +137 -0
  75. invar/templates/protocol/INVAR.md +210 -0
  76. invar/templates/skills/develop/SKILL.md.jinja +318 -0
  77. invar/templates/skills/investigate/SKILL.md.jinja +106 -0
  78. invar/templates/skills/propose/SKILL.md.jinja +104 -0
  79. invar/templates/skills/review/SKILL.md.jinja +125 -0
  80. invar_tools-1.3.0.dist-info/METADATA +377 -0
  81. invar_tools-1.3.0.dist-info/RECORD +95 -0
  82. invar_tools-1.3.0.dist-info/entry_points.txt +2 -0
  83. invar_tools-1.3.0.dist-info/licenses/LICENSE +190 -0
  84. invar_tools-1.3.0.dist-info/licenses/LICENSE-GPL +674 -0
  85. invar_tools-1.3.0.dist-info/licenses/NOTICE +63 -0
  86. invar/contracts.py +0 -152
  87. invar/decorators.py +0 -94
  88. invar/invariant.py +0 -57
  89. invar/resource.py +0 -99
  90. invar/shell/prove_fallback.py +0 -183
  91. invar/shell/update_cmd.py +0 -191
  92. invar/templates/INVAR.md +0 -134
  93. invar_tools-1.0.0.dist-info/METADATA +0 -321
  94. invar_tools-1.0.0.dist-info/RECORD +0 -64
  95. invar_tools-1.0.0.dist-info/entry_points.txt +0 -2
  96. invar_tools-1.0.0.dist-info/licenses/LICENSE +0 -21
  97. /invar/shell/{prove_cache.py → prove/cache.py} +0 -0
  98. {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]")