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
invar/shell/git.py CHANGED
@@ -28,6 +28,7 @@ def _run_git(args: list[str], cwd: Path) -> Result[str, str]:
28
28
  return Failure(f"Git error: {e}")
29
29
 
30
30
 
31
+ # @shell_orchestration: Helper for git output parsing, tightly coupled to Shell
31
32
  def _parse_py_files(output: str, project_root: Path) -> set[Path]:
32
33
  """Parse git output and return Python file paths."""
33
34
  files: set[Path] = set()
@@ -37,6 +38,7 @@ def _parse_py_files(output: str, project_root: Path) -> set[Path]:
37
38
  return files
38
39
 
39
40
 
41
+ # @shell_complexity: Git operations require multiple subprocess calls with error handling
40
42
  def get_changed_files(project_root: Path) -> Result[set[Path], str]:
41
43
  """
42
44
  Get Python files modified according to git (staged, unstaged, untracked).
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
19
19
  console = Console()
20
20
 
21
21
 
22
+ # @shell_complexity: Git changed mode with file collection
22
23
  def handle_changed_mode(
23
24
  path: Path,
24
25
  ) -> Result[tuple[set[Path], list[Path]], str]:
@@ -42,6 +43,8 @@ def handle_changed_mode(
42
43
  return Success((only_files, list(only_files)))
43
44
 
44
45
 
46
+ # @shell_orchestration: Coordinates path classification and file collection
47
+ # @shell_complexity: File collection with path normalization
45
48
  def collect_files_to_check(
46
49
  path: Path, checked_files: list[Path]
47
50
  ) -> list[Path]:
@@ -77,34 +80,52 @@ def collect_files_to_check(
77
80
  return result_files
78
81
 
79
82
 
83
+ # @shell_orchestration: Coordinates doctest execution via testing module
80
84
  def run_doctests_phase(
81
- checked_files: list[Path], explain: bool
82
- ) -> tuple[bool, str]:
85
+ checked_files: list[Path],
86
+ explain: bool,
87
+ timeout: int = 60,
88
+ collect_coverage: bool = False,
89
+ ) -> tuple[bool, str, dict | None]:
83
90
  """Run doctests on collected files.
84
91
 
85
- Returns (passed, output).
92
+ Args:
93
+ checked_files: Files to run doctests on
94
+ explain: Show verbose output
95
+ timeout: Maximum time in seconds (default: 60, from RuleConfig.timeout_doctest)
96
+ collect_coverage: DX-37: If True, collect branch coverage data
97
+
98
+ Returns (passed, output, coverage_data).
86
99
  """
87
100
  from invar.shell.testing import run_doctests_on_files
88
101
 
89
102
  if not checked_files:
90
- return True, ""
103
+ return True, "", None
91
104
 
92
- doctest_result = run_doctests_on_files(checked_files, verbose=explain)
105
+ doctest_result = run_doctests_on_files(
106
+ checked_files, verbose=explain, timeout=timeout, collect_coverage=collect_coverage
107
+ )
93
108
  if isinstance(doctest_result, Success):
94
109
  result_data = doctest_result.unwrap()
95
110
  passed = result_data.get("status") in ("passed", "skipped")
96
111
  output = result_data.get("stdout", "")
97
- return passed, output
112
+ # DX-37: Return coverage data if collected
113
+ coverage_data = {"collected": result_data.get("coverage_collected", False)}
114
+ return passed, output, coverage_data if collect_coverage else None
98
115
 
99
- return False, doctest_result.failure()
116
+ return False, doctest_result.failure(), None
100
117
 
101
118
 
119
+ # @shell_orchestration: Coordinates CrossHair verification via prove module
120
+ # @shell_complexity: CrossHair phase with conditional execution
102
121
  def run_crosshair_phase(
103
122
  path: Path,
104
123
  checked_files: list[Path],
105
124
  doctest_passed: bool,
106
125
  static_exit_code: int,
107
126
  changed_mode: bool = False,
127
+ timeout: int = 300,
128
+ per_condition_timeout: int = 30,
108
129
  ) -> tuple[bool, dict]:
109
130
  """Run CrossHair verification phase.
110
131
 
@@ -114,10 +135,12 @@ def run_crosshair_phase(
114
135
  doctest_passed: Whether doctests passed
115
136
  static_exit_code: Exit code from static analysis
116
137
  changed_mode: If True, only verify git-changed files (--changed flag)
138
+ timeout: Max time per file in seconds (default: 300)
139
+ per_condition_timeout: Max time per contract in seconds (default: 30)
117
140
 
118
141
  Returns (passed, output_dict).
119
142
  """
120
- from invar.shell.prove_cache import ProveCache
143
+ from invar.shell.prove.cache import ProveCache
121
144
  from invar.shell.testing import get_files_to_prove, run_crosshair_parallel
122
145
 
123
146
  # Skip if prior failures
@@ -151,6 +174,8 @@ def run_crosshair_phase(
151
174
  max_iterations=5,
152
175
  max_workers=None,
153
176
  cache=cache,
177
+ timeout=timeout,
178
+ per_condition_timeout=per_condition_timeout,
154
179
  )
155
180
 
156
181
  if isinstance(crosshair_result, Success):
@@ -161,6 +186,7 @@ def run_crosshair_phase(
161
186
  return False, {"status": "error", "error": crosshair_result.failure()}
162
187
 
163
188
 
189
+ # @shell_complexity: Status output with multiple phases
164
190
  def output_verification_status(
165
191
  verification_level: VerificationLevel,
166
192
  static_exit_code: int,
@@ -169,17 +195,30 @@ def output_verification_status(
169
195
  crosshair_output: dict,
170
196
  explain: bool,
171
197
  property_output: dict | None = None,
198
+ strict: bool = False,
172
199
  ) -> None:
173
200
  """Output verification status for human-readable mode.
174
201
 
175
202
  DX-19: Simplified - STANDARD runs all phases (doctests + CrossHair + Hypothesis).
203
+ DX-26: Shows combined conclusion after all phase results.
176
204
  """
177
205
  from invar.shell.testing import VerificationLevel
178
206
 
179
- # STATIC mode: no runtime tests to report
207
+ # STATIC mode: no runtime tests to report (conclusion shown by output_rich)
180
208
  if verification_level == VerificationLevel.STATIC:
181
209
  return
182
210
 
211
+ # DX-26: Extract passed status from phase outputs
212
+ crosshair_passed = True
213
+ if crosshair_output:
214
+ crosshair_status = crosshair_output.get("status", "verified")
215
+ crosshair_passed = crosshair_status in ("verified", "skipped")
216
+
217
+ property_passed = True
218
+ if property_output:
219
+ property_status = property_output.get("status", "passed")
220
+ property_passed = property_status in ("passed", "skipped")
221
+
183
222
  # STANDARD mode: report all test results
184
223
  if static_exit_code == 0:
185
224
  # Doctest results
@@ -203,13 +242,29 @@ def output_verification_status(
203
242
  else:
204
243
  console.print("[dim]⊘ Runtime tests skipped (static errors)[/dim]")
205
244
 
245
+ # DX-26: Combined conclusion after all phases
246
+ console.print("-" * 40)
247
+ all_passed = (
248
+ static_exit_code == 0
249
+ and doctest_passed
250
+ and crosshair_passed
251
+ and property_passed
252
+ )
253
+ # In strict mode, warnings also cause failure (but exit code already reflects this)
254
+ status = "passed" if all_passed else "failed"
255
+ color = "green" if all_passed else "red"
256
+ console.print(f"[{color}]Guard {status}.[/{color}]")
257
+
206
258
 
259
+ # @shell_orchestration: Coordinates shell module calls for property testing
260
+ # @shell_complexity: Property tests with result aggregation
207
261
  def run_property_tests_phase(
208
262
  checked_files: list[Path],
209
263
  doctest_passed: bool,
210
264
  static_exit_code: int,
211
265
  max_examples: int = 100,
212
- ) -> tuple[bool, dict]:
266
+ collect_coverage: bool = False,
267
+ ) -> tuple[bool, dict, dict | None]:
213
268
  """Run property tests phase (DX-08).
214
269
 
215
270
  Args:
@@ -217,45 +272,62 @@ def run_property_tests_phase(
217
272
  doctest_passed: Whether doctests passed
218
273
  static_exit_code: Exit code from static analysis
219
274
  max_examples: Maximum Hypothesis examples per function
275
+ collect_coverage: DX-37: If True, collect branch coverage data
220
276
 
221
- Returns (passed, output_dict).
277
+ Returns (passed, output_dict, coverage_data).
222
278
  """
223
279
  from invar.shell.property_tests import run_property_tests_on_files
224
280
 
225
281
  # Skip if prior failures
226
282
  if not doctest_passed or static_exit_code != 0:
227
- return True, {"status": "skipped", "reason": "prior failures"}
283
+ return True, {"status": "skipped", "reason": "prior failures"}, None
228
284
 
229
285
  if not checked_files:
230
- return True, {"status": "skipped", "reason": "no files"}
286
+ return True, {"status": "skipped", "reason": "no files"}, None
231
287
 
232
288
  # Only test Core files (with contracts)
233
289
  core_files = [f for f in checked_files if "core" in str(f)]
234
290
  if not core_files:
235
- return True, {"status": "skipped", "reason": "no core files"}
291
+ return True, {"status": "skipped", "reason": "no core files"}, None
236
292
 
237
- result = run_property_tests_on_files(core_files, max_examples)
293
+ result = run_property_tests_on_files(core_files, max_examples, collect_coverage=collect_coverage)
238
294
 
239
295
  if isinstance(result, Success):
240
- report = result.unwrap()
296
+ report, coverage_data = result.unwrap()
297
+ # DX-26: Build structured failures array for actionable output
298
+ failures = [
299
+ {
300
+ "function": r.function_name,
301
+ "file_path": r.file_path,
302
+ "error": r.error,
303
+ "seed": r.seed,
304
+ }
305
+ for r in report.results
306
+ if not r.passed
307
+ ]
241
308
  return report.all_passed(), {
242
309
  "status": "passed" if report.all_passed() else "failed",
243
310
  "functions_tested": report.functions_tested,
244
311
  "functions_passed": report.functions_passed,
245
312
  "functions_failed": report.functions_failed,
246
313
  "total_examples": report.total_examples,
314
+ "failures": failures, # DX-26: Structured failure info
247
315
  "errors": report.errors,
248
- }
316
+ }, coverage_data
249
317
 
250
- return False, {"status": "error", "error": result.failure()}
318
+ return False, {"status": "error", "error": result.failure()}, None
251
319
 
252
320
 
321
+ # @shell_complexity: Property test status formatting
253
322
  def _output_property_tests_status(
254
323
  static_exit_code: int,
255
324
  doctest_passed: bool,
256
325
  property_output: dict,
257
326
  ) -> None:
258
- """Output property tests status (DX-08)."""
327
+ """Output property tests status (DX-08, DX-26).
328
+
329
+ DX-26: Show file::function format and reproduction command for failures.
330
+ """
259
331
  if static_exit_code != 0 or not doctest_passed:
260
332
  console.print("[dim]⊘ Property tests skipped (prior failures)[/dim]")
261
333
  return
@@ -275,12 +347,36 @@ def _output_property_tests_status(
275
347
  elif status == "failed":
276
348
  failed = property_output.get("functions_failed", 0)
277
349
  console.print(f"[red]✗ Property tests failed ({failed} functions)[/red]")
350
+ # DX-26: Show actionable failure info
351
+ for failure in property_output.get("failures", [])[:5]:
352
+ file_path = failure.get("file_path", "")
353
+ func_name = failure.get("function", "unknown")
354
+ seed = failure.get("seed")
355
+ error = failure.get("error", "")
356
+
357
+ # Show file::function format
358
+ location = f"{file_path}::{func_name}" if file_path else func_name
359
+ console.print(f" [red]✗[/red] {location}")
360
+
361
+ # Show truncated error
362
+ if error:
363
+ short_error = error[:100] + "..." if len(error) > 100 else error
364
+ console.print(f" {short_error}")
365
+
366
+ # Show reproduction command with seed
367
+ if seed:
368
+ console.print(
369
+ f" [dim]Reproduce: python -c \"from hypothesis import reproduce_failure; "
370
+ f"import {func_name}\" --seed={seed}[/dim]"
371
+ )
372
+ # Fallback for errors without structured failures
278
373
  for error in property_output.get("errors", [])[:5]:
279
- console.print(f" {error}")
374
+ console.print(f" [yellow]![/yellow] {error}")
280
375
  else:
281
376
  console.print(f"[yellow]! Property tests: {status}[/yellow]")
282
377
 
283
378
 
379
+ # @shell_complexity: CrossHair status formatting
284
380
  def _output_crosshair_status(
285
381
  static_exit_code: int,
286
382
  doctest_passed: bool,
@@ -3,18 +3,77 @@ Guard output formatters.
3
3
 
4
4
  Shell module: handles output formatting for guard command.
5
5
  Extracted from cli.py to reduce file size.
6
+
7
+ DX-22: Added verification routing statistics for de-duplication.
6
8
  """
7
9
 
8
10
  from __future__ import annotations
9
11
 
12
+ from dataclasses import dataclass
13
+
10
14
  from rich.console import Console
11
15
 
12
16
  from invar.core.formatter import format_guard_agent
13
17
  from invar.core.models import GuardReport, Severity
18
+ from invar.core.utils import get_combined_status
14
19
 
15
20
  console = Console()
16
21
 
17
22
 
23
+ @dataclass
24
+ class VerificationStats:
25
+ """
26
+ DX-22: De-duplicated verification statistics.
27
+
28
+ Tracks separate counts for CrossHair (proof) vs Hypothesis (testing)
29
+ to avoid misleading double-counting.
30
+ """
31
+
32
+ crosshair_proven: int = 0
33
+ hypothesis_tested: int = 0
34
+ doctests_passed: int = 0
35
+ routed_to_hypothesis: int = 0 # Files routed due to C extensions
36
+
37
+ @property
38
+ def total_verified(self) -> int:
39
+ """Total unique functions verified (no double-counting)."""
40
+ return self.crosshair_proven + self.hypothesis_tested
41
+
42
+ @property
43
+ def proof_coverage_pct(self) -> float:
44
+ """Percentage of verifiable code proven by CrossHair."""
45
+ total = self.crosshair_proven + self.hypothesis_tested
46
+ if total == 0:
47
+ return 0.0
48
+ return (self.crosshair_proven / total) * 100
49
+
50
+
51
+ # @shell_orchestration: Rich markup formatting tightly coupled to shell output
52
+ # @shell_complexity: Conditional formatting for each stat category
53
+ def format_verification_stats(stats: VerificationStats) -> str:
54
+ """
55
+ Format verification statistics for display.
56
+
57
+ DX-22: Shows de-duplicated counts distinguishing proof from testing.
58
+ """
59
+ lines = []
60
+ lines.append("Verification breakdown:")
61
+ if stats.crosshair_proven > 0:
62
+ lines.append(f" ✓ Proven (CrossHair): {stats.crosshair_proven} functions")
63
+ if stats.hypothesis_tested > 0:
64
+ lines.append(f" ✓ Tested (Hypothesis): {stats.hypothesis_tested} functions")
65
+ if stats.routed_to_hypothesis > 0:
66
+ lines.append(
67
+ f" [dim](C-extension routing: {stats.routed_to_hypothesis} files)[/dim]"
68
+ )
69
+ if stats.doctests_passed > 0:
70
+ lines.append(f" ✓ Doctests: {stats.doctests_passed} passed")
71
+ if stats.total_verified > 0:
72
+ lines.append(f" Proof coverage: {stats.proof_coverage_pct:.0f}%")
73
+ return "\n".join(lines)
74
+
75
+
76
+ # @shell_complexity: Context display with line range extraction
18
77
  def show_file_context(file_path: str) -> None:
19
78
  """
20
79
  Show INSPECT section for a file (Phase 9.2 P14).
@@ -47,12 +106,14 @@ def show_file_context(file_path: str) -> None:
47
106
  pass # Silently ignore errors in context display
48
107
 
49
108
 
109
+ # @shell_complexity: Rich formatting with conditional sections
50
110
  def output_rich(
51
111
  report: GuardReport,
52
112
  strict_pure: bool = False,
53
113
  changed_mode: bool = False,
54
114
  pedantic_mode: bool = False,
55
115
  explain_mode: bool = False,
116
+ static_mode: bool = False,
56
117
  ) -> None:
57
118
  """Output report using Rich formatting."""
58
119
  console.print("\n[bold]Invar Guard Report[/bold]")
@@ -60,6 +121,7 @@ def output_rich(
60
121
  mode_info = [
61
122
  m
62
123
  for m, c in [
124
+ ("static", static_mode),
63
125
  ("strict-pure", strict_pure),
64
126
  ("changed-only", changed_mode),
65
127
  ("pedantic", pedantic_mode),
@@ -174,51 +236,65 @@ def output_rich(
174
236
  "[dim]💡 Fix warnings in files you modified to improve code health.[/dim]"
175
237
  )
176
238
 
177
- console.print(
178
- f"\n[{'green' if report.passed else 'red'}]Guard {'passed' if report.passed else 'failed'}.[/]"
179
- )
180
- console.print(
181
- "\n[dim]Note: Guard performs static analysis only. "
182
- "Dynamic imports and runtime behavior are not checked.[/dim]"
183
- )
184
-
185
-
186
- def output_json(report: GuardReport) -> None:
187
- """Output report as JSON."""
188
- import json
189
-
190
- output = {
191
- "files_checked": report.files_checked,
192
- "errors": report.errors,
193
- "warnings": report.warnings,
194
- "infos": report.infos,
195
- "passed": report.passed,
196
- "violations": [v.model_dump() for v in report.violations],
197
- }
198
- console.print(json.dumps(output, indent=2))
239
+ # DX-26: Show static-only conclusion for --static mode
240
+ # Full mode shows conclusion after all phases in output_verification_status()
241
+ if static_mode:
242
+ console.print(
243
+ f"\n[{'green' if report.passed else 'red'}]Guard {'passed' if report.passed else 'failed'}.[/]"
244
+ )
245
+ console.print(
246
+ "\n[dim]Note: --static mode skips runtime tests (doctests, CrossHair, Hypothesis).[/dim]"
247
+ )
199
248
 
200
249
 
250
+ # @shell_complexity: JSON output assembly with multiple sections
201
251
  def output_agent(
202
252
  report: GuardReport,
253
+ strict: bool = False,
203
254
  doctest_passed: bool = True,
204
255
  doctest_output: str = "",
205
256
  crosshair_output: dict | None = None,
206
257
  verification_level: str = "standard",
207
258
  property_output: dict | None = None, # DX-08
259
+ routing_stats: dict | None = None, # DX-22
260
+ coverage_data: dict | None = None, # DX-37
208
261
  ) -> None:
209
- """Output report in Agent-optimized JSON format (Phase 8.2 + DX-06 + DX-08 + DX-09).
262
+ """Output report in Agent-optimized JSON format (Phase 8.2 + DX-06 + DX-08 + DX-09 + DX-22 + DX-26 + DX-37).
210
263
 
211
264
  Args:
212
265
  report: Guard analysis report
266
+ strict: Whether warnings are treated as errors
213
267
  doctest_passed: Whether doctests passed
214
268
  doctest_output: Doctest stdout (only if failed)
215
269
  crosshair_output: CrossHair results dict
216
270
  verification_level: Current level (static/standard)
217
271
  property_output: Property test results dict (DX-08)
272
+ routing_stats: Smart routing statistics (DX-22)
273
+ coverage_data: DX-37: Branch coverage data from doctest + hypothesis
274
+
275
+ DX-22: Adds routing stats showing CrossHair vs Hypothesis distribution.
276
+ DX-26: status now reflects ALL test phases, not just static analysis.
277
+ DX-37: Adds optional coverage data from doctest + hypothesis phases.
218
278
  """
219
279
  import json
220
280
 
221
- output = format_guard_agent(report)
281
+ # DX-26: Extract passed status from phase outputs
282
+ crosshair_passed = True
283
+ if crosshair_output:
284
+ crosshair_status = crosshair_output.get("status", "verified")
285
+ crosshair_passed = crosshair_status in ("verified", "skipped")
286
+
287
+ property_passed = True
288
+ if property_output:
289
+ property_status = property_output.get("status", "passed")
290
+ property_passed = property_status in ("passed", "skipped")
291
+
292
+ # DX-26: Calculate combined status including all test phases
293
+ combined_status = get_combined_status(
294
+ report, strict, doctest_passed, crosshair_passed, property_passed
295
+ )
296
+
297
+ output = format_guard_agent(report, combined_status=combined_status)
222
298
  # DX-09: Add verification level for Agent transparency
223
299
  output["verification_level"] = verification_level
224
300
  # DX-06: Add doctest results to agent output
@@ -232,4 +308,10 @@ def output_agent(
232
308
  # DX-08: Add property test results if available
233
309
  if property_output:
234
310
  output["property_tests"] = property_output
311
+ # DX-22: Add smart routing statistics if available
312
+ if routing_stats:
313
+ output["routing"] = routing_stats
314
+ # DX-37: Add coverage data if collected
315
+ if coverage_data:
316
+ output["coverage"] = coverage_data
235
317
  console.print(json.dumps(output, indent=2))
invar/shell/mcp_config.py CHANGED
@@ -100,6 +100,7 @@ def detect_available_methods() -> list[McpExecConfig]:
100
100
  return methods
101
101
 
102
102
 
103
+ # @shell_orchestration: MCP method selection helper
103
104
  def get_recommended_method() -> McpExecConfig:
104
105
  """
105
106
  Get the recommended MCP execution method.
@@ -115,6 +116,7 @@ def get_recommended_method() -> McpExecConfig:
115
116
  return methods[0]
116
117
 
117
118
 
119
+ # @shell_orchestration: MCP method lookup helper
118
120
  def get_method_by_name(name: str) -> McpExecConfig | None:
119
121
  """
120
122
  Get a specific MCP method by name.
@@ -139,6 +141,7 @@ def get_method_by_name(name: str) -> McpExecConfig | None:
139
141
  return None
140
142
 
141
143
 
144
+ # @shell_orchestration: MCP configuration generator
142
145
  def generate_mcp_json(config: McpExecConfig | None = None) -> dict[str, Any]:
143
146
  """
144
147
  Generate .mcp.json content for the given configuration.