simplecov-mcp 0.3.0 → 1.0.0

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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/README.md +173 -356
  4. data/docs/ADVANCED_USAGE.md +967 -0
  5. data/docs/ARCHITECTURE.md +79 -0
  6. data/docs/BRANCH_ONLY_COVERAGE.md +81 -0
  7. data/docs/CLI_USAGE.md +637 -0
  8. data/docs/DEVELOPMENT.md +82 -0
  9. data/docs/ERROR_HANDLING.md +93 -0
  10. data/docs/EXAMPLES.md +430 -0
  11. data/docs/INSTALLATION.md +352 -0
  12. data/docs/LIBRARY_API.md +635 -0
  13. data/docs/MCP_INTEGRATION.md +488 -0
  14. data/docs/TROUBLESHOOTING.md +276 -0
  15. data/docs/arch-decisions/001-x-arch-decision.md +93 -0
  16. data/docs/arch-decisions/002-x-arch-decision.md +157 -0
  17. data/docs/arch-decisions/003-x-arch-decision.md +163 -0
  18. data/docs/arch-decisions/004-x-arch-decision.md +199 -0
  19. data/docs/arch-decisions/005-x-arch-decision.md +187 -0
  20. data/docs/arch-decisions/README.md +60 -0
  21. data/docs/presentations/simplecov-mcp-presentation.md +249 -0
  22. data/exe/simplecov-mcp +4 -4
  23. data/lib/simplecov_mcp/app_context.rb +26 -0
  24. data/lib/simplecov_mcp/base_tool.rb +74 -0
  25. data/lib/simplecov_mcp/cli.rb +234 -0
  26. data/lib/simplecov_mcp/cli_config.rb +56 -0
  27. data/lib/simplecov_mcp/commands/base_command.rb +78 -0
  28. data/lib/simplecov_mcp/commands/command_factory.rb +39 -0
  29. data/lib/simplecov_mcp/commands/detailed_command.rb +24 -0
  30. data/lib/simplecov_mcp/commands/list_command.rb +13 -0
  31. data/lib/simplecov_mcp/commands/raw_command.rb +22 -0
  32. data/lib/simplecov_mcp/commands/summary_command.rb +24 -0
  33. data/lib/simplecov_mcp/commands/uncovered_command.rb +26 -0
  34. data/lib/simplecov_mcp/commands/version_command.rb +18 -0
  35. data/lib/simplecov_mcp/constants.rb +22 -0
  36. data/lib/simplecov_mcp/error_handler.rb +124 -0
  37. data/lib/simplecov_mcp/error_handler_factory.rb +31 -0
  38. data/lib/simplecov_mcp/errors.rb +179 -0
  39. data/lib/simplecov_mcp/formatters/source_formatter.rb +148 -0
  40. data/lib/simplecov_mcp/mcp_server.rb +40 -0
  41. data/lib/simplecov_mcp/mode_detector.rb +55 -0
  42. data/lib/simplecov_mcp/model.rb +300 -0
  43. data/lib/simplecov_mcp/option_normalizers.rb +92 -0
  44. data/lib/simplecov_mcp/option_parser_builder.rb +134 -0
  45. data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +50 -0
  46. data/lib/simplecov_mcp/option_parsers/error_helper.rb +109 -0
  47. data/lib/simplecov_mcp/path_relativizer.rb +61 -0
  48. data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +44 -0
  49. data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +16 -0
  50. data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +16 -0
  51. data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +16 -0
  52. data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +16 -0
  53. data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +52 -0
  54. data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +126 -0
  55. data/lib/simplecov_mcp/resolvers/resolver_factory.rb +28 -0
  56. data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +78 -0
  57. data/lib/simplecov_mcp/resultset_loader.rb +136 -0
  58. data/lib/simplecov_mcp/staleness_checker.rb +243 -0
  59. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/all_files_coverage_tool.rb +31 -13
  60. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_detailed_tool.rb +7 -7
  61. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_raw_tool.rb +7 -7
  62. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_summary_tool.rb +7 -7
  63. data/lib/simplecov_mcp/tools/coverage_table_tool.rb +90 -0
  64. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/help_tool.rb +13 -4
  65. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/uncovered_lines_tool.rb +7 -7
  66. data/lib/{simple_cov_mcp → simplecov_mcp}/tools/version_tool.rb +11 -3
  67. data/lib/simplecov_mcp/util.rb +82 -0
  68. data/lib/{simple_cov_mcp → simplecov_mcp}/version.rb +1 -1
  69. data/lib/simplecov_mcp.rb +144 -2
  70. data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
  71. data/spec/TIMESTAMPS.md +48 -0
  72. data/spec/all_files_coverage_tool_spec.rb +29 -25
  73. data/spec/base_tool_spec.rb +11 -10
  74. data/spec/cli/show_default_report_spec.rb +33 -0
  75. data/spec/cli_config_spec.rb +137 -0
  76. data/spec/cli_enumerated_options_spec.rb +68 -0
  77. data/spec/cli_error_spec.rb +105 -47
  78. data/spec/cli_source_spec.rb +82 -23
  79. data/spec/cli_spec.rb +140 -5
  80. data/spec/cli_success_predicate_spec.rb +141 -0
  81. data/spec/cli_table_spec.rb +1 -1
  82. data/spec/cli_usage_spec.rb +10 -26
  83. data/spec/commands/base_command_spec.rb +187 -0
  84. data/spec/commands/command_factory_spec.rb +72 -0
  85. data/spec/commands/detailed_command_spec.rb +48 -0
  86. data/spec/commands/raw_command_spec.rb +46 -0
  87. data/spec/commands/summary_command_spec.rb +47 -0
  88. data/spec/commands/uncovered_command_spec.rb +49 -0
  89. data/spec/constants_spec.rb +61 -0
  90. data/spec/coverage_table_tool_spec.rb +17 -33
  91. data/spec/error_handler_spec.rb +22 -13
  92. data/spec/error_mode_spec.rb +143 -0
  93. data/spec/errors_edge_cases_spec.rb +239 -0
  94. data/spec/errors_stale_spec.rb +2 -2
  95. data/spec/file_based_mcp_tools_spec.rb +99 -0
  96. data/spec/fixtures/project1/lib/bar.rb +0 -1
  97. data/spec/fixtures/project1/lib/foo.rb +0 -1
  98. data/spec/help_tool_spec.rb +11 -17
  99. data/spec/integration_spec.rb +845 -0
  100. data/spec/logging_fallback_spec.rb +128 -0
  101. data/spec/mcp_logging_spec.rb +44 -0
  102. data/spec/mcp_server_integration_spec.rb +23 -0
  103. data/spec/mcp_server_spec.rb +15 -4
  104. data/spec/mode_detector_spec.rb +148 -0
  105. data/spec/model_error_handling_spec.rb +210 -0
  106. data/spec/model_staleness_spec.rb +40 -10
  107. data/spec/option_normalizers_spec.rb +204 -0
  108. data/spec/option_parsers/env_options_parser_spec.rb +233 -0
  109. data/spec/option_parsers/error_helper_spec.rb +222 -0
  110. data/spec/path_relativizer_spec.rb +83 -0
  111. data/spec/presenters/coverage_detailed_presenter_spec.rb +19 -0
  112. data/spec/presenters/coverage_raw_presenter_spec.rb +15 -0
  113. data/spec/presenters/coverage_summary_presenter_spec.rb +15 -0
  114. data/spec/presenters/coverage_uncovered_presenter_spec.rb +16 -0
  115. data/spec/presenters/project_coverage_presenter_spec.rb +86 -0
  116. data/spec/resolvers/coverage_line_resolver_spec.rb +57 -0
  117. data/spec/resolvers/resolver_factory_spec.rb +61 -0
  118. data/spec/resolvers/resultset_path_resolver_spec.rb +55 -0
  119. data/spec/resultset_loader_spec.rb +167 -0
  120. data/spec/shared_examples/README.md +115 -0
  121. data/spec/shared_examples/coverage_presenter_examples.rb +66 -0
  122. data/spec/shared_examples/file_based_mcp_tools.rb +174 -0
  123. data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
  124. data/spec/simple_cov_mcp_module_spec.rb +16 -0
  125. data/spec/simplecov_mcp_model_spec.rb +340 -9
  126. data/spec/simplecov_mcp_opts_spec.rb +182 -0
  127. data/spec/spec_helper.rb +147 -4
  128. data/spec/staleness_checker_spec.rb +373 -0
  129. data/spec/staleness_more_spec.rb +16 -13
  130. data/spec/support/mcp_runner.rb +64 -0
  131. data/spec/tools_error_handling_spec.rb +144 -0
  132. data/spec/util_spec.rb +109 -34
  133. data/spec/version_spec.rb +117 -9
  134. data/spec/version_tool_spec.rb +131 -10
  135. metadata +120 -63
  136. data/lib/simple_cov/mcp.rb +0 -9
  137. data/lib/simple_cov_mcp/base_tool.rb +0 -70
  138. data/lib/simple_cov_mcp/cli.rb +0 -390
  139. data/lib/simple_cov_mcp/error_handler.rb +0 -131
  140. data/lib/simple_cov_mcp/error_handler_factory.rb +0 -38
  141. data/lib/simple_cov_mcp/errors.rb +0 -176
  142. data/lib/simple_cov_mcp/mcp_server.rb +0 -30
  143. data/lib/simple_cov_mcp/model.rb +0 -104
  144. data/lib/simple_cov_mcp/staleness_checker.rb +0 -125
  145. data/lib/simple_cov_mcp/tools/coverage_table_tool.rb +0 -61
  146. data/lib/simple_cov_mcp/util.rb +0 -122
  147. data/lib/simple_cov_mcp.rb +0 -102
  148. data/spec/coverage_detailed_tool_spec.rb +0 -36
  149. data/spec/coverage_raw_tool_spec.rb +0 -32
  150. data/spec/coverage_summary_tool_spec.rb +0 -39
  151. data/spec/legacy_shim_spec.rb +0 -13
  152. data/spec/uncovered_lines_tool_spec.rb +0 -33
@@ -0,0 +1,635 @@
1
+ # Library API Guide
2
+
3
+ Use this gem programmatically to inspect coverage without running the CLI or MCP server. The primary entry point is `SimpleCovMcp::CoverageModel`.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Quick Start](#quick-start)
8
+ - [Method Reference](#method-reference)
9
+ - [Return Types](#return-types)
10
+ - [Error Handling](#error-handling)
11
+ - [Advanced Recipes](#advanced-recipes)
12
+ - [API Stability](#api-stability)
13
+
14
+ ## Quick Start
15
+
16
+ ```ruby
17
+ require "simplecov_mcp"
18
+
19
+ # Defaults (omit args; shown here with comments):
20
+ # - root: "."
21
+ # - resultset: resolved from common paths under root
22
+ # - staleness: "off" (no stale checks)
23
+ # - tracked_globs: nil (no project-level file-set checks)
24
+ model = SimpleCovMcp::CoverageModel.new
25
+
26
+ # Custom configuration (non-default values):
27
+ model = SimpleCovMcp::CoverageModel.new(
28
+ root: "/path/to/project", # non-default project root
29
+ resultset: "build/coverage", # file or directory containing .resultset.json
30
+ staleness: "error", # enable stale checks (raise on stale)
31
+ tracked_globs: ["lib/**/*.rb"] # for 'all_files' staleness: flag new/missing files
32
+ )
33
+
34
+ # List all files with coverage summary
35
+ files = model.all_files
36
+ # Per-file queries
37
+ summary = model.summary_for("lib/foo.rb")
38
+ uncovered = model.uncovered_for("lib/foo.rb")
39
+ detailed = model.detailed_for("lib/foo.rb")
40
+ raw = model.raw_for("lib/foo.rb")
41
+ ```
42
+
43
+ ## Method Reference
44
+
45
+ ### `all_files(sort_order: :ascending)`
46
+
47
+ Returns coverage summary for all files in the resultset.
48
+
49
+ **Parameters:**
50
+ - `sort_order` (Symbol, optional): `:ascending` (default) or `:descending` by coverage percentage
51
+
52
+ **Returns:** `Array<Hash>` - See [all_files return type](#all_files)
53
+
54
+ **Example:**
55
+ ```ruby
56
+ files = model.all_files
57
+ # => [ { 'file' => '/abs/path/lib/foo.rb', 'covered' => 12, 'total' => 14, 'percentage' => 85.71, 'stale' => false }, ... ]
58
+
59
+ # Get worst coverage first
60
+ worst_files = model.all_files(sort_order: :ascending).first(10)
61
+ ```
62
+
63
+ ### `summary_for(path)`
64
+
65
+ Returns coverage summary for a specific file.
66
+
67
+ **Parameters:**
68
+ - `path` (String): File path (absolute, relative to root, or basename)
69
+
70
+ **Returns:** `Hash` - See [summary_for return type](#summary_for)
71
+
72
+ **Raises:** `SimpleCovMcp::FileError` if file not in coverage data
73
+
74
+ **Example:**
75
+ ```ruby
76
+ summary = model.summary_for("lib/foo.rb")
77
+ # => { 'file' => '/abs/.../lib/foo.rb', 'summary' => {'covered'=>12, 'total'=>14, 'pct'=>85.71}, 'stale' => false }
78
+ ```
79
+
80
+ ### `uncovered_for(path)`
81
+
82
+ Returns list of uncovered line numbers for a specific file.
83
+
84
+ **Parameters:**
85
+ - `path` (String): File path (absolute, relative to root, or basename)
86
+
87
+ **Returns:** `Hash` - See [uncovered_for return type](#uncovered_for)
88
+
89
+ **Raises:** `SimpleCovMcp::FileError` if file not in coverage data
90
+
91
+ **Example:**
92
+ ```ruby
93
+ uncovered = model.uncovered_for("lib/foo.rb")
94
+ # => { 'file' => '/abs/.../lib/foo.rb', 'uncovered' => [5, 9, 12], 'summary' => { ... }, 'stale' => false }
95
+ ```
96
+
97
+ ### `detailed_for(path)`
98
+
99
+ Returns per-line coverage details with hit counts.
100
+
101
+ **Parameters:**
102
+ - `path` (String): File path (absolute, relative to root, or basename)
103
+
104
+ **Returns:** `Hash` - See [detailed_for return type](#detailed_for)
105
+
106
+ **Raises:** `SimpleCovMcp::FileError` if file not in coverage data
107
+
108
+ **Example:**
109
+ ```ruby
110
+ detailed = model.detailed_for("lib/foo.rb")
111
+ # => { 'file' => '/abs/.../lib/foo.rb', 'lines' => [{'line' => 1, 'hits' => 1, 'covered' => true}, ...], 'summary' => { ... }, 'stale' => false }
112
+ ```
113
+
114
+ ### `raw_for(path)`
115
+
116
+ Returns raw SimpleCov lines array for a specific file.
117
+
118
+ **Parameters:**
119
+ - `path` (String): File path (absolute, relative to root, or basename)
120
+
121
+ **Returns:** `Hash` - See [raw_for return type](#raw_for)
122
+
123
+ **Raises:** `SimpleCovMcp::FileError` if file not in coverage data
124
+
125
+ **Example:**
126
+ ```ruby
127
+ raw = model.raw_for("lib/foo.rb")
128
+ # => { 'file' => '/abs/.../lib/foo.rb', 'lines' => [nil, 1, 0, 3, ...], 'stale' => false }
129
+ ```
130
+
131
+ ### `format_table(rows = nil, sort_order: :ascending)`
132
+
133
+ Generates formatted ASCII table string.
134
+
135
+ **Parameters:**
136
+ - `rows` (Array<Hash>, optional): Custom row data; defaults to `all_files`
137
+ - `sort_order` (Symbol, optional): `:ascending` (default) or `:descending`
138
+
139
+ **Returns:** `String` - Formatted table with Unicode borders
140
+
141
+ **Example:**
142
+ ```ruby
143
+ # Default: all files
144
+ table = model.format_table
145
+ puts table
146
+
147
+ # Custom rows
148
+ lib_files = model.all_files.select { |f| f['file'].include?('/lib/') }
149
+ lib_table = model.format_table(lib_files, sort_order: :descending)
150
+ puts lib_table
151
+ ```
152
+
153
+ ## Return Types
154
+
155
+ ### `all_files`
156
+
157
+ Returns `Array<Hash>` where each hash contains:
158
+
159
+ ```ruby
160
+ {
161
+ 'file' => String, # Absolute file path
162
+ 'covered' => Integer, # Number of covered lines
163
+ 'total' => Integer, # Total relevant lines
164
+ 'percentage' => Float, # Coverage percentage (0.00-100.00)
165
+ 'stale' => false | String # Staleness indicator: false, 'M', 'T', or 'L'
166
+ }
167
+ ```
168
+
169
+ ### `summary_for`
170
+
171
+ Returns `Hash`:
172
+
173
+ ```ruby
174
+ {
175
+ 'file' => String, # Absolute file path
176
+ 'summary' => {
177
+ 'covered' => Integer, # Number of covered lines
178
+ 'total' => Integer, # Total relevant lines
179
+ 'pct' => Float # Coverage percentage (0.00-100.00)
180
+ }
181
+ }
182
+ ```
183
+
184
+ ### `uncovered_for`
185
+
186
+ Returns `Hash`:
187
+
188
+ ```ruby
189
+ {
190
+ 'file' => String, # Absolute file path
191
+ 'uncovered' => Array<Integer>, # Line numbers that are not covered
192
+ 'summary' => {
193
+ 'covered' => Integer,
194
+ 'total' => Integer,
195
+ 'pct' => Float
196
+ }
197
+ }
198
+ ```
199
+
200
+ ### `detailed_for`
201
+
202
+ Returns `Hash`:
203
+
204
+ ```ruby
205
+ {
206
+ 'file' => String, # Absolute file path
207
+ 'lines' => Array<Hash>, # Per-line coverage details
208
+ 'summary' => {
209
+ 'covered' => Integer,
210
+ 'total' => Integer,
211
+ 'pct' => Float
212
+ }
213
+ }
214
+ ```
215
+
216
+ Each element in `lines` array:
217
+ ```ruby
218
+ {
219
+ 'line' => Integer, # Line number (1-indexed)
220
+ 'hits' => Integer, # Execution count (0 means not covered)
221
+ 'covered' => Boolean # true if hits > 0
222
+ }
223
+ ```
224
+
225
+ ### `raw_for`
226
+
227
+ Returns `Hash`:
228
+
229
+ ```ruby
230
+ {
231
+ 'file' => String, # Absolute file path
232
+ 'lines' => Array<Integer | nil> # SimpleCov lines array (nil = irrelevant, 0 = uncovered, >0 = hit count)
233
+ }
234
+ ```
235
+
236
+ ## Error Handling
237
+
238
+ ### Exception Types
239
+
240
+ The library raises these custom exceptions:
241
+
242
+ - **`SimpleCovMcp::ResultsetNotFoundError`** - Coverage data file not found
243
+ - **`SimpleCovMcp::FileError`** - Requested file not in coverage data
244
+ - **`SimpleCovMcp::CoverageDataStaleError`** - Coverage data is stale (only when `staleness: 'error'`)
245
+ - **`SimpleCovMcp::CoverageDataError`** - Invalid coverage data format or structure
246
+
247
+ All exceptions inherit from `SimpleCovMcp::Error`.
248
+
249
+ ### Basic Error Handling
250
+
251
+ ```ruby
252
+ require "simplecov_mcp"
253
+
254
+ begin
255
+ model = SimpleCovMcp::CoverageModel.new
256
+ summary = model.summary_for("lib/foo.rb")
257
+ puts "Coverage: #{summary['summary']['pct']}%"
258
+ rescue SimpleCovMcp::FileError => e
259
+ puts "File not in coverage data: #{e.message}"
260
+ rescue SimpleCovMcp::ResultsetNotFoundError => e
261
+ puts "Coverage data not found: #{e.message}"
262
+ puts "Run your tests first: bundle exec rspec"
263
+ rescue SimpleCovMcp::Error => e
264
+ puts "Coverage error: #{e.message}"
265
+ end
266
+ ```
267
+
268
+ ### Handling Stale Coverage
269
+
270
+ ```ruby
271
+ # Option 1: Check staleness without raising
272
+ model = SimpleCovMcp::CoverageModel.new(staleness: "off")
273
+ files = model.all_files
274
+
275
+ stale_files = files.select { |f| f['stale'] }
276
+ if stale_files.any?
277
+ puts "Warning: #{stale_files.length} files have stale coverage"
278
+ stale_files.each do |f|
279
+ puts " #{f['file']}: #{f['stale']}"
280
+ end
281
+ end
282
+
283
+ # Option 2: Raise on staleness
284
+ begin
285
+ model = SimpleCovMcp::CoverageModel.new(staleness: "error")
286
+ files = model.all_files
287
+ rescue SimpleCovMcp::CoverageDataStaleError => e
288
+ puts "Stale coverage detected: #{e.message}"
289
+ puts "Re-run tests: bundle exec rspec"
290
+ exit 1
291
+ end
292
+ ```
293
+
294
+ ### Graceful Degradation
295
+
296
+ ```ruby
297
+ # Try multiple file paths
298
+ def find_coverage(model, possible_paths)
299
+ possible_paths.each do |path|
300
+ begin
301
+ return model.summary_for(path)
302
+ rescue SimpleCovMcp::FileError
303
+ next
304
+ end
305
+ end
306
+ nil
307
+ end
308
+
309
+ summary = find_coverage(model, [
310
+ "lib/services/auth_service.rb",
311
+ "app/services/auth_service.rb",
312
+ "services/auth_service.rb"
313
+ ])
314
+
315
+ if summary
316
+ puts "Coverage: #{summary['summary']['pct']}%"
317
+ else
318
+ puts "File not found in coverage data"
319
+ end
320
+ ```
321
+
322
+ ## Advanced Recipes
323
+
324
+ ### Batch File Analysis
325
+
326
+ ```ruby
327
+ require "simplecov_mcp"
328
+
329
+ model = SimpleCovMcp::CoverageModel.new
330
+
331
+ # Analyze multiple files efficiently
332
+ files_to_check = [
333
+ "lib/auth_service.rb",
334
+ "lib/payment_processor.rb",
335
+ "lib/user_manager.rb"
336
+ ]
337
+
338
+ results = files_to_check.map do |path|
339
+ begin
340
+ summary = model.summary_for(path)
341
+ {
342
+ file: path,
343
+ coverage: summary['summary']['pct'],
344
+ status: summary['summary']['pct'] >= 80 ? :ok : :low
345
+ }
346
+ rescue SimpleCovMcp::FileError
347
+ {
348
+ file: path,
349
+ coverage: nil,
350
+ status: :missing
351
+ }
352
+ end
353
+ end
354
+
355
+ # Report
356
+ results.each do |r|
357
+ status_icon = { ok: '✓', low: '⚠', missing: '✗' }[r[:status]]
358
+ puts "#{status_icon} #{r[:file]}: #{r[:coverage] || 'N/A'}%"
359
+ end
360
+ ```
361
+
362
+ ### Coverage Threshold Validation
363
+
364
+ ```ruby
365
+ require "simplecov_mcp"
366
+
367
+ class CoverageValidator
368
+ THRESHOLDS = {
369
+ 'lib/' => 90.0, # Core library needs 90%+
370
+ 'app/' => 80.0, # Application code needs 80%+
371
+ 'spec/' => 70.0, # Test helpers need 70%+
372
+ }
373
+
374
+ def initialize(model)
375
+ @model = model
376
+ end
377
+
378
+ def validate!
379
+ files = @model.all_files
380
+ failures = []
381
+
382
+ files.each do |file|
383
+ threshold = threshold_for(file['file'])
384
+ next unless threshold
385
+
386
+ if file['percentage'] < threshold
387
+ failures << {
388
+ file: file['file'],
389
+ actual: file['percentage'],
390
+ required: threshold,
391
+ gap: threshold - file['percentage']
392
+ }
393
+ end
394
+ end
395
+
396
+ if failures.any?
397
+ puts "❌ #{failures.length} files below coverage threshold:"
398
+ failures.sort_by { |f| -f[:gap] }.each do |f|
399
+ puts " #{f[:file]}: #{f[:actual]}% (need #{f[:required]}%)"
400
+ end
401
+ exit 1
402
+ else
403
+ puts "✓ All files meet coverage thresholds"
404
+ end
405
+ end
406
+
407
+ private
408
+
409
+ def threshold_for(path)
410
+ THRESHOLDS.each do |prefix, threshold|
411
+ return threshold if path.include?(prefix)
412
+ end
413
+ nil
414
+ end
415
+ end
416
+
417
+ model = SimpleCovMcp::CoverageModel.new
418
+ validator = CoverageValidator.new(model)
419
+ validator.validate!
420
+ ```
421
+
422
+ ### Directory-Level Aggregation
423
+
424
+ ```ruby
425
+ require "simplecov_mcp"
426
+
427
+ model = SimpleCovMcp::CoverageModel.new
428
+ files = model.all_files
429
+
430
+ # Calculate coverage by directory
431
+ by_directory = files.group_by do |file|
432
+ # Get first two path components (e.g., "lib/services")
433
+ file['file'].split('/')[0..1].join('/')
434
+ end
435
+
436
+ directory_stats = by_directory.map do |dir, dir_files|
437
+ total_lines = dir_files.sum { |f| f['total'] }
438
+ covered_lines = dir_files.sum { |f| f['covered'] }
439
+ percentage = (covered_lines.to_f / total_lines * 100).round(2)
440
+
441
+ {
442
+ directory: dir,
443
+ files: dir_files.length,
444
+ coverage: percentage,
445
+ covered: covered_lines,
446
+ total: total_lines
447
+ }
448
+ end
449
+
450
+ # Display sorted by coverage
451
+ directory_stats.sort_by { |s| s[:coverage] }.each do |stat|
452
+ puts "#{stat[:directory]}: #{stat[:coverage]}% (#{stat[:files]} files)"
453
+ end
454
+ ```
455
+
456
+ ### Coverage Delta Tracking
457
+
458
+ ```ruby
459
+ require "simplecov_mcp"
460
+ require "json"
461
+
462
+ class CoverageDeltaTracker
463
+ def initialize(baseline_path: "coverage_baseline.json")
464
+ @baseline_path = baseline_path
465
+ @model = SimpleCovMcp::CoverageModel.new
466
+ end
467
+
468
+ def save_baseline
469
+ current = @model.all_files
470
+ File.write(@baseline_path, JSON.pretty_generate(current))
471
+ puts "Saved coverage baseline (#{current.length} files)"
472
+ end
473
+
474
+ def compare
475
+ unless File.exist?(@baseline_path)
476
+ puts "No baseline found. Run save_baseline first."
477
+ return
478
+ end
479
+
480
+ baseline = JSON.parse(File.read(@baseline_path))
481
+ current = @model.all_files
482
+
483
+ improved = []
484
+ regressed = []
485
+
486
+ current.each do |file|
487
+ baseline_file = baseline.find { |f| f['file'] == file['file'] }
488
+ next unless baseline_file
489
+
490
+ delta = file['percentage'] - baseline_file['percentage']
491
+
492
+ if delta > 0.1
493
+ improved << {
494
+ file: file['file'],
495
+ before: baseline_file['percentage'],
496
+ after: file['percentage'],
497
+ delta: delta
498
+ }
499
+ elsif delta < -0.1
500
+ regressed << {
501
+ file: file['file'],
502
+ before: baseline_file['percentage'],
503
+ after: file['percentage'],
504
+ delta: delta
505
+ }
506
+ end
507
+ end
508
+
509
+ if improved.any?
510
+ puts "\n✓ Coverage Improvements:"
511
+ improved.sort_by { |f| -f[:delta] }.each do |f|
512
+ puts " #{f[:file]}: #{f[:before]}% → #{f[:after]}% (+#{f[:delta].round(2)}%)"
513
+ end
514
+ end
515
+
516
+ if regressed.any?
517
+ puts "\n⚠ Coverage Regressions:"
518
+ regressed.sort_by { |f| f[:delta] }.each do |f|
519
+ puts " #{f[:file]}: #{f[:before]}% → #{f[:after]}% (#{f[:delta].round(2)}%)"
520
+ end
521
+ end
522
+
523
+ if improved.empty? && regressed.empty?
524
+ puts "No significant coverage changes"
525
+ end
526
+ end
527
+ end
528
+
529
+ # Usage
530
+ tracker = CoverageDeltaTracker.new
531
+ tracker.save_baseline # Run before making changes
532
+ # ... make code changes and re-run tests ...
533
+ tracker.compare # See what changed
534
+ ```
535
+
536
+ ### Custom Reporting
537
+
538
+ ```ruby
539
+ require "simplecov_mcp"
540
+
541
+ class CoverageReporter
542
+ def initialize(model)
543
+ @model = model
544
+ end
545
+
546
+ def generate_markdown_report(output_path)
547
+ files = @model.all_files
548
+
549
+ File.open(output_path, 'w') do |f|
550
+ f.puts "# Coverage Report"
551
+ f.puts
552
+ f.puts "Generated: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
553
+ f.puts
554
+
555
+ # Overall stats
556
+ total_lines = files.sum { |file| file['total'] }
557
+ covered_lines = files.sum { |file| file['covered'] }
558
+ overall_pct = (covered_lines.to_f / total_lines * 100).round(2)
559
+
560
+ f.puts "## Overall Coverage: #{overall_pct}%"
561
+ f.puts
562
+ f.puts "- Total Files: #{files.length}"
563
+ f.puts "- Total Lines: #{total_lines}"
564
+ f.puts "- Covered Lines: #{covered_lines}"
565
+ f.puts
566
+
567
+ # Files below threshold
568
+ threshold = 80.0
569
+ low_coverage = files.select { |file| file['percentage'] < threshold }
570
+
571
+ if low_coverage.any?
572
+ f.puts "## Files Below #{threshold}% Coverage"
573
+ f.puts
574
+ f.puts "| File | Coverage | Missing Lines |"
575
+ f.puts "|------|----------|---------------|"
576
+
577
+ low_coverage.sort_by { |file| file['percentage'] }.each do |file|
578
+ uncovered = @model.uncovered_for(file['file'])
579
+ missing_count = uncovered['uncovered'].length
580
+ f.puts "| #{file['file']} | #{file['percentage']}% | #{missing_count} |"
581
+ end
582
+ f.puts
583
+ end
584
+
585
+ # Top performers
586
+ f.puts "## Top 10 Best Covered Files"
587
+ f.puts
588
+ f.puts "| File | Coverage |"
589
+ f.puts "|------|----------|"
590
+
591
+ files.sort_by { |file| -file['percentage'] }.take(10).each do |file|
592
+ f.puts "| #{file['file']} | #{file['percentage']}% |"
593
+ end
594
+ end
595
+
596
+ puts "Report saved to #{output_path}"
597
+ end
598
+ end
599
+
600
+ model = SimpleCovMcp::CoverageModel.new
601
+ reporter = CoverageReporter.new(model)
602
+ reporter.generate_markdown_report("coverage_report.md")
603
+ ```
604
+
605
+ ## Staleness Detection
606
+
607
+ The `all_files` method returns a `'stale'` field for each file with one of these values:
608
+
609
+ - `false` - Coverage data is current
610
+ - `'M'` - **Missing**: File no longer exists on disk
611
+ - `'T'` - **Timestamp**: File modified more recently than coverage data
612
+ - `'L'` - **Length**: Source file line count differs from coverage data
613
+
614
+ **Note:** Per-file methods (`summary_for`, `uncovered_for`, `detailed_for`, `raw_for`) do not include staleness information in their return values. To check staleness for individual files, use `all_files` and filter the results.
615
+
616
+ When `staleness: 'error'` mode is enabled in `CoverageModel.new`, the model will raise `SimpleCovMcp::CoverageDataStaleError` exceptions when stale files are detected during method calls.
617
+
618
+ ## API Stability
619
+
620
+ Consider the following public and stable under SemVer:
621
+ - `SimpleCovMcp::CoverageModel.new(root:, resultset:, staleness: 'off', tracked_globs: nil)`
622
+ - `#raw_for(path)`, `#summary_for(path)`, `#uncovered_for(path)`, `#detailed_for(path)`, `#all_files(sort_order:)`, `#format_table(rows: nil, sort_order:, check_stale:, tracked_globs:)`
623
+ - Return shapes shown in the [Return Types](#return-types) section
624
+ - Exception types documented in [Error Handling](#error-handling)
625
+
626
+ **Note:**
627
+ - CLI (`SimpleCovMcp.run(argv)`) and MCP tools remain stable but are separate surfaces
628
+ - Internal helpers under `SimpleCovMcp::CovUtil` may change; prefer `CoverageModel` unless you need low-level access
629
+
630
+ ## Related Documentation
631
+
632
+ - [Examples](EXAMPLES.md) - Practical cookbook-style examples
633
+ - [CLI Usage](CLI_USAGE.md) - Command-line interface reference
634
+ - [Error Handling](ERROR_HANDLING.md) - Detailed error handling documentation
635
+ - [MCP Integration](MCP_INTEGRATION.md) - AI assistant integration