cov-loupe 3.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 (171) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +329 -0
  4. data/docs/dev/ARCHITECTURE.md +80 -0
  5. data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
  6. data/docs/dev/DEVELOPMENT.md +83 -0
  7. data/docs/dev/README.md +10 -0
  8. data/docs/dev/RELEASING.md +146 -0
  9. data/docs/dev/arch-decisions/001-x-arch-decision.md +95 -0
  10. data/docs/dev/arch-decisions/002-x-arch-decision.md +159 -0
  11. data/docs/dev/arch-decisions/003-x-arch-decision.md +165 -0
  12. data/docs/dev/arch-decisions/004-x-arch-decision.md +203 -0
  13. data/docs/dev/arch-decisions/005-x-arch-decision.md +189 -0
  14. data/docs/dev/arch-decisions/README.md +60 -0
  15. data/docs/dev/presentations/cov-loupe-presentation.md +255 -0
  16. data/docs/fixtures/demo_project/README.md +9 -0
  17. data/docs/user/ADVANCED_USAGE.md +777 -0
  18. data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
  19. data/docs/user/CLI_USAGE.md +750 -0
  20. data/docs/user/ERROR_HANDLING.md +93 -0
  21. data/docs/user/EXAMPLES.md +588 -0
  22. data/docs/user/INSTALLATION.md +130 -0
  23. data/docs/user/LIBRARY_API.md +693 -0
  24. data/docs/user/MCP_INTEGRATION.md +490 -0
  25. data/docs/user/README.md +14 -0
  26. data/docs/user/TROUBLESHOOTING.md +197 -0
  27. data/docs/user/V2-BREAKING-CHANGES.md +472 -0
  28. data/exe/cov-loupe +23 -0
  29. data/lib/cov_loupe/app_config.rb +56 -0
  30. data/lib/cov_loupe/app_context.rb +26 -0
  31. data/lib/cov_loupe/base_tool.rb +102 -0
  32. data/lib/cov_loupe/cli.rb +178 -0
  33. data/lib/cov_loupe/commands/base_command.rb +67 -0
  34. data/lib/cov_loupe/commands/command_factory.rb +45 -0
  35. data/lib/cov_loupe/commands/detailed_command.rb +38 -0
  36. data/lib/cov_loupe/commands/list_command.rb +13 -0
  37. data/lib/cov_loupe/commands/raw_command.rb +38 -0
  38. data/lib/cov_loupe/commands/summary_command.rb +41 -0
  39. data/lib/cov_loupe/commands/totals_command.rb +53 -0
  40. data/lib/cov_loupe/commands/uncovered_command.rb +45 -0
  41. data/lib/cov_loupe/commands/validate_command.rb +60 -0
  42. data/lib/cov_loupe/commands/version_command.rb +33 -0
  43. data/lib/cov_loupe/config_parser.rb +32 -0
  44. data/lib/cov_loupe/constants.rb +22 -0
  45. data/lib/cov_loupe/coverage_reporter.rb +31 -0
  46. data/lib/cov_loupe/error_handler.rb +165 -0
  47. data/lib/cov_loupe/error_handler_factory.rb +31 -0
  48. data/lib/cov_loupe/errors.rb +191 -0
  49. data/lib/cov_loupe/formatters/source_formatter.rb +152 -0
  50. data/lib/cov_loupe/formatters.rb +51 -0
  51. data/lib/cov_loupe/mcp_server.rb +42 -0
  52. data/lib/cov_loupe/mode_detector.rb +56 -0
  53. data/lib/cov_loupe/model.rb +339 -0
  54. data/lib/cov_loupe/option_normalizers.rb +113 -0
  55. data/lib/cov_loupe/option_parser_builder.rb +147 -0
  56. data/lib/cov_loupe/option_parsers/env_options_parser.rb +48 -0
  57. data/lib/cov_loupe/option_parsers/error_helper.rb +110 -0
  58. data/lib/cov_loupe/path_relativizer.rb +64 -0
  59. data/lib/cov_loupe/predicate_evaluator.rb +72 -0
  60. data/lib/cov_loupe/presenters/base_coverage_presenter.rb +42 -0
  61. data/lib/cov_loupe/presenters/coverage_detailed_presenter.rb +14 -0
  62. data/lib/cov_loupe/presenters/coverage_raw_presenter.rb +14 -0
  63. data/lib/cov_loupe/presenters/coverage_summary_presenter.rb +14 -0
  64. data/lib/cov_loupe/presenters/coverage_uncovered_presenter.rb +14 -0
  65. data/lib/cov_loupe/presenters/project_coverage_presenter.rb +50 -0
  66. data/lib/cov_loupe/presenters/project_totals_presenter.rb +27 -0
  67. data/lib/cov_loupe/resolvers/coverage_line_resolver.rb +122 -0
  68. data/lib/cov_loupe/resolvers/resolver_factory.rb +28 -0
  69. data/lib/cov_loupe/resolvers/resultset_path_resolver.rb +76 -0
  70. data/lib/cov_loupe/resultset_loader.rb +131 -0
  71. data/lib/cov_loupe/staleness_checker.rb +247 -0
  72. data/lib/cov_loupe/table_formatter.rb +64 -0
  73. data/lib/cov_loupe/tools/all_files_coverage_tool.rb +51 -0
  74. data/lib/cov_loupe/tools/coverage_detailed_tool.rb +35 -0
  75. data/lib/cov_loupe/tools/coverage_raw_tool.rb +34 -0
  76. data/lib/cov_loupe/tools/coverage_summary_tool.rb +34 -0
  77. data/lib/cov_loupe/tools/coverage_table_tool.rb +50 -0
  78. data/lib/cov_loupe/tools/coverage_totals_tool.rb +44 -0
  79. data/lib/cov_loupe/tools/help_tool.rb +115 -0
  80. data/lib/cov_loupe/tools/uncovered_lines_tool.rb +34 -0
  81. data/lib/cov_loupe/tools/validate_tool.rb +72 -0
  82. data/lib/cov_loupe/tools/version_tool.rb +32 -0
  83. data/lib/cov_loupe/util.rb +88 -0
  84. data/lib/cov_loupe/version.rb +5 -0
  85. data/lib/cov_loupe.rb +140 -0
  86. data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
  87. data/spec/TIMESTAMPS.md +48 -0
  88. data/spec/all_files_coverage_tool_spec.rb +53 -0
  89. data/spec/app_config_spec.rb +142 -0
  90. data/spec/base_tool_spec.rb +62 -0
  91. data/spec/cli/show_default_report_spec.rb +33 -0
  92. data/spec/cli_enumerated_options_spec.rb +90 -0
  93. data/spec/cli_error_spec.rb +184 -0
  94. data/spec/cli_format_spec.rb +123 -0
  95. data/spec/cli_json_options_spec.rb +50 -0
  96. data/spec/cli_source_spec.rb +44 -0
  97. data/spec/cli_spec.rb +192 -0
  98. data/spec/cli_table_spec.rb +28 -0
  99. data/spec/cli_usage_spec.rb +42 -0
  100. data/spec/commands/base_command_spec.rb +107 -0
  101. data/spec/commands/command_factory_spec.rb +76 -0
  102. data/spec/commands/detailed_command_spec.rb +34 -0
  103. data/spec/commands/list_command_spec.rb +28 -0
  104. data/spec/commands/raw_command_spec.rb +69 -0
  105. data/spec/commands/summary_command_spec.rb +34 -0
  106. data/spec/commands/totals_command_spec.rb +34 -0
  107. data/spec/commands/uncovered_command_spec.rb +55 -0
  108. data/spec/commands/validate_command_spec.rb +213 -0
  109. data/spec/commands/version_command_spec.rb +38 -0
  110. data/spec/constants_spec.rb +61 -0
  111. data/spec/cov_loupe/formatters/source_formatter_spec.rb +267 -0
  112. data/spec/cov_loupe/formatters_spec.rb +76 -0
  113. data/spec/cov_loupe/presenters/base_coverage_presenter_spec.rb +79 -0
  114. data/spec/cov_loupe_model_spec.rb +454 -0
  115. data/spec/cov_loupe_module_spec.rb +37 -0
  116. data/spec/cov_loupe_opts_spec.rb +185 -0
  117. data/spec/coverage_reporter_spec.rb +102 -0
  118. data/spec/coverage_table_tool_spec.rb +59 -0
  119. data/spec/coverage_totals_tool_spec.rb +37 -0
  120. data/spec/error_handler_spec.rb +197 -0
  121. data/spec/error_mode_spec.rb +139 -0
  122. data/spec/errors_edge_cases_spec.rb +312 -0
  123. data/spec/errors_stale_spec.rb +83 -0
  124. data/spec/file_based_mcp_tools_spec.rb +99 -0
  125. data/spec/fixtures/project1/lib/bar.rb +5 -0
  126. data/spec/fixtures/project1/lib/foo.rb +6 -0
  127. data/spec/help_tool_spec.rb +26 -0
  128. data/spec/integration_spec.rb +789 -0
  129. data/spec/logging_fallback_spec.rb +128 -0
  130. data/spec/mcp_logging_spec.rb +44 -0
  131. data/spec/mcp_server_integration_spec.rb +23 -0
  132. data/spec/mcp_server_spec.rb +106 -0
  133. data/spec/mode_detector_spec.rb +153 -0
  134. data/spec/model_error_handling_spec.rb +269 -0
  135. data/spec/model_staleness_spec.rb +79 -0
  136. data/spec/option_normalizers_spec.rb +203 -0
  137. data/spec/option_parsers/env_options_parser_spec.rb +221 -0
  138. data/spec/option_parsers/error_helper_spec.rb +222 -0
  139. data/spec/path_relativizer_spec.rb +98 -0
  140. data/spec/presenters/coverage_detailed_presenter_spec.rb +19 -0
  141. data/spec/presenters/coverage_raw_presenter_spec.rb +15 -0
  142. data/spec/presenters/coverage_summary_presenter_spec.rb +15 -0
  143. data/spec/presenters/coverage_uncovered_presenter_spec.rb +16 -0
  144. data/spec/presenters/project_coverage_presenter_spec.rb +87 -0
  145. data/spec/presenters/project_totals_presenter_spec.rb +144 -0
  146. data/spec/resolvers/coverage_line_resolver_spec.rb +282 -0
  147. data/spec/resolvers/resolver_factory_spec.rb +61 -0
  148. data/spec/resolvers/resultset_path_resolver_spec.rb +60 -0
  149. data/spec/resultset_loader_spec.rb +167 -0
  150. data/spec/shared_examples/README.md +115 -0
  151. data/spec/shared_examples/coverage_presenter_examples.rb +66 -0
  152. data/spec/shared_examples/file_based_mcp_tools.rb +179 -0
  153. data/spec/shared_examples/formatted_command_examples.rb +64 -0
  154. data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
  155. data/spec/spec_helper.rb +127 -0
  156. data/spec/staleness_checker_spec.rb +374 -0
  157. data/spec/staleness_more_spec.rb +42 -0
  158. data/spec/support/cli_helpers.rb +22 -0
  159. data/spec/support/control_flow_helpers.rb +20 -0
  160. data/spec/support/fake_mcp.rb +40 -0
  161. data/spec/support/io_helpers.rb +29 -0
  162. data/spec/support/mcp_helpers.rb +35 -0
  163. data/spec/support/mcp_runner.rb +66 -0
  164. data/spec/support/mocking_helpers.rb +30 -0
  165. data/spec/table_format_spec.rb +70 -0
  166. data/spec/tools/validate_tool_spec.rb +132 -0
  167. data/spec/tools_error_handling_spec.rb +130 -0
  168. data/spec/util_spec.rb +154 -0
  169. data/spec/version_spec.rb +123 -0
  170. data/spec/version_tool_spec.rb +141 -0
  171. metadata +290 -0
@@ -0,0 +1,777 @@
1
+ # Advanced Usage Guide
2
+
3
+ [Back to main README](../README.md)
4
+
5
+ > Examples use `clp`, an alias pointed at the demo fixture with partial coverage:
6
+ > `alias clp='cov-loupe --root docs/fixtures/demo_project'`
7
+ > Swap `clp` with `cov-loupe` if you want to target your own project/resultset.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Advanced MCP Integration](#advanced-mcp-integration)
12
+ - [Staleness Detection & Validation](#staleness-detection--validation)
13
+ - [Advanced Path Resolution](#advanced-path-resolution)
14
+ - [Error Handling Strategies](#error-handling-strategies)
15
+ - [Custom Ruby Integration](#custom-ruby-integration)
16
+ - [CI/CD Integration Patterns](#cicd-integration-patterns)
17
+ - [Advanced Filtering & Glob Patterns](#advanced-filtering--glob-patterns)
18
+ - [Performance Optimization](#performance-optimization)
19
+ - [Custom Output Processing](#custom-output-processing)
20
+
21
+ ---
22
+
23
+ ## Advanced MCP Integration
24
+
25
+ ### MCP Error Handling
26
+
27
+ The MCP server uses structured error responses:
28
+
29
+ ```json
30
+ {
31
+ "jsonrpc": "2.0",
32
+ "error": {
33
+ "code": -32603,
34
+ "message": "Coverage data not found at coverage/.resultset.json",
35
+ "data": {
36
+ "type": "FileError",
37
+ "context": "MCP tool execution"
38
+ }
39
+ },
40
+ "id": 1
41
+ }
42
+ ```
43
+
44
+ ### MCP Server Logging
45
+
46
+ The MCP server logs to `cov_loupe.log` in the current directory by default.
47
+
48
+ To override the default log file location, specify the `--log-file` argument wherever and however you configure your MCP server. For example, to log to a different file path, include `--log-file /path/to/logfile.log` in your server configuration. To log to standard error, use `--log-file stderr`.
49
+
50
+ **Note:** Logging to `stdout` is not permitted in MCP mode.
51
+
52
+ ### Testing MCP Server Manually
53
+
54
+ Use JSON-RPC over stdin to test the MCP server:
55
+
56
+ ```sh
57
+ # Get version
58
+ echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"version_tool","arguments":{}}}' | clp
59
+
60
+ # Get file summary
61
+ echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"coverage_summary_tool","arguments":{"path":"app/models/order.rb"}}}' | clp
62
+
63
+ # List all files with sorting
64
+ echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"all_files_coverage_tool","arguments":{"sort_order":"ascending"}}}' | clp
65
+
66
+ # Get uncovered lines
67
+ echo '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"uncovered_lines_tool","arguments":{"path":"app/controllers/orders_controller.rb"}}}' | clp
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Staleness Detection & Validation
73
+
74
+ ### Understanding Staleness Modes
75
+
76
+ Staleness checking prevents using outdated coverage data. Two modes are available:
77
+
78
+ **Mode: `off` (default)**
79
+ - No validation, fastest operation
80
+ - Coverage data used as-is
81
+ - Stale indicators still computed but don't block operations
82
+
83
+ **Mode: `error`**
84
+ - Strict validation enabled
85
+ - Raises errors if coverage is outdated
86
+ - Perfect for CI/CD pipelines
87
+
88
+ ### File-Level Staleness
89
+
90
+ A file is considered stale when any of the following are true:
91
+ 1. Source file modified after coverage generation
92
+ 2. Line count differs from coverage array length
93
+ 3. File exists in coverage but deleted from filesystem
94
+
95
+ **CLI Usage:**
96
+ ```sh
97
+ # Fail if any file is stale (option before subcommand)
98
+ clp --staleness error summary app/models/order.rb
99
+ ```
100
+
101
+ **Ruby API:**
102
+ ```ruby
103
+ model = CovLoupe::CoverageModel.new(
104
+ staleness: 'error'
105
+ )
106
+
107
+ begin
108
+ summary = model.summary_for('app/models/order.rb')
109
+ rescue CovLoupe::CoverageDataStaleError => e
110
+ puts "File modified after coverage: #{e.file_path}"
111
+ puts "Coverage timestamp: #{e.cov_timestamp}"
112
+ puts "File mtime: #{e.file_mtime}"
113
+ puts "Source lines: #{e.src_len}, Coverage lines: #{e.cov_len}"
114
+ end
115
+ ```
116
+
117
+ ### Project-Level Staleness
118
+
119
+ Detects system-wide staleness issues:
120
+
121
+ **Conditions Checked:**
122
+ 1. **Newer files** - Any tracked file modified after coverage
123
+ 2. **Missing files** - Tracked files with no coverage data
124
+ 3. **Deleted files** - Coverage exists for non-existent files
125
+
126
+ **CLI Usage:**
127
+ ```sh
128
+ # Track specific patterns
129
+ clp --staleness error \
130
+ -g "lib/payments/**/*.rb" \
131
+ -g "lib/ops/jobs/**/*.rb" # -g = --tracked-globs
132
+
133
+ # Combine with JSON output for parsing
134
+ clp --staleness error -fJ list > stale-check.json
135
+ ```
136
+
137
+ **Ruby API:**
138
+ ```ruby
139
+ model = CovLoupe::CoverageModel.new(
140
+ staleness: 'error',
141
+ tracked_globs: ['lib/payments/**/*.rb', 'lib/ops/jobs/**/*.rb']
142
+ )
143
+
144
+ begin
145
+ files = model.all_files(check_stale: true)
146
+ rescue CovLoupe::CoverageDataProjectStaleError => e
147
+ puts "Newer files: #{e.newer_files.join(', ')}"
148
+ puts "Missing from coverage: #{e.missing_files.join(', ')}"
149
+ puts "Deleted but in coverage: #{e.deleted_files.join(', ')}"
150
+ end
151
+ ```
152
+
153
+ ### Staleness in CI/CD
154
+
155
+ Staleness checking is particularly useful in CI/CD pipelines to ensure coverage data is fresh:
156
+
157
+ ```sh
158
+ # Run tests to generate coverage
159
+ bundle exec rspec
160
+
161
+ # Validate coverage freshness (fails with exit code 1 if stale)
162
+ clp --staleness error -g "lib/**/*.rb"
163
+
164
+ # Export validated data for CI artifacts
165
+ clp -fJ list > coverage.json
166
+ ```
167
+
168
+ The `--staleness error` flag causes the command to exit with a non-zero status when coverage is outdated, making it suitable for pipeline failure conditions.
169
+
170
+ ---
171
+
172
+ ## Advanced Path Resolution
173
+
174
+ ### Multi-Strategy Path Matching
175
+
176
+ Path resolution order:
177
+
178
+ 1. **Exact absolute path match**
179
+ 2. **Relative path resolution from root**
180
+
181
+ ```ruby
182
+ model = CovLoupe::CoverageModel.new(root: '/project')
183
+
184
+ model.summary_for('/project/app/models/order.rb') # Absolute
185
+ model.summary_for('app/models/order.rb') # Relative
186
+ ```
187
+
188
+ ### Working with Multiple Projects
189
+
190
+ ```ruby
191
+ # Project A
192
+ model_a = CovLoupe::CoverageModel.new(
193
+ root: '/projects/service-a',
194
+ resultset: '/projects/service-a/coverage/.resultset.json'
195
+ )
196
+
197
+ # Project B
198
+ model_b = CovLoupe::CoverageModel.new(
199
+ root: '/projects/service-b',
200
+ resultset: '/projects/service-b/tmp/coverage/.resultset.json'
201
+ )
202
+
203
+ # Compare coverage
204
+ coverage_a = model_a.all_files
205
+ coverage_b = model_b.all_files
206
+ ```
207
+
208
+
209
+
210
+
211
+ ---
212
+
213
+ ## Error Handling Strategies
214
+
215
+ ### Context-Aware Error Handling
216
+
217
+ **CLI Mode:** user-facing messages, exit codes, optional debug mode
218
+
219
+ **Library Mode:** typed exceptions with full details
220
+
221
+ **MCP Server Mode:** JSON-RPC errors logged to file with structured data
222
+
223
+ ### Error Modes
224
+
225
+ **CLI Error Modes:**
226
+ ```sh
227
+ # Silent mode - minimal output
228
+ clp --error-mode off summary app/models/order.rb
229
+
230
+ # Standard mode - user-friendly errors (default)
231
+ clp --error-mode log summary app/models/order.rb
232
+
233
+ # Verbose mode - full stack traces
234
+ clp --error-mode debug summary app/models/order.rb
235
+ ```
236
+
237
+ **Ruby API Error Handling:**
238
+ ```ruby
239
+ require 'cov_loupe'
240
+
241
+ begin
242
+ model = CovLoupe::CoverageModel.new(
243
+ root: '/project',
244
+ resultset: '/nonexistent/.resultset.json'
245
+ )
246
+ rescue CovLoupe::FileError => e
247
+ # Handle missing resultset
248
+ puts "Coverage file not found: #{e.message}"
249
+ rescue CovLoupe::CoverageDataError => e
250
+ # Handle corrupt/invalid coverage data
251
+ puts "Invalid coverage data: #{e.message}"
252
+ end
253
+ ```
254
+
255
+ ### Custom Error Handlers
256
+
257
+ Provide custom error handlers when embedding the CLI:
258
+
259
+ ```ruby
260
+ class CustomErrorHandler
261
+ def handle_error(error, context: nil)
262
+ # Log to custom service
263
+ ErrorTracker.notify(error, context: context)
264
+
265
+ # Re-raise or handle gracefully
266
+ raise error
267
+ end
268
+ end
269
+
270
+ cli = CovLoupe::CoverageCLI.new(error_handler: CustomErrorHandler.new)
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Custom Ruby Integration
276
+
277
+ ### Building Custom Coverage Policies
278
+
279
+ Use the `validate` subcommand to enforce custom coverage policies in CI/CD. Example predicates are in [`examples/success_predicates/`](../../examples/success_predicates/).
280
+
281
+ > **⚠️ SECURITY WARNING**
282
+ >
283
+ > Success predicates execute as **arbitrary Ruby code with full system privileges**. They have unrestricted access to:
284
+ > - File system operations (read, write, delete)
285
+ > - Network operations (HTTP requests, sockets)
286
+ > - System commands (via backticks, `system()`, `exec()`, etc.)
287
+ > - Environment variables and sensitive data
288
+ >
289
+ > **Only use predicate files from trusted sources.** Treat them like any other executable code in your project.
290
+ > - Never use predicates from untrusted or unknown sources
291
+ > - Review predicates before use, especially in CI/CD environments
292
+ > - Store predicates in version control with code review
293
+ > - Be cautious when copying examples from the internet
294
+
295
+ **Quick Usage:**
296
+ ```sh
297
+ # All files must be >= 80%
298
+ clp validate examples/success_predicates/all_files_above_threshold_predicate.rb
299
+
300
+ # Total project coverage >= 85%
301
+ clp validate examples/success_predicates/project_coverage_minimum_predicate.rb
302
+
303
+ # Custom predicate from file
304
+ clp validate coverage_policy.rb
305
+
306
+ # Inline string mode
307
+ clp validate -i '->(m) { m.all_files.all? { |f| f["percentage"] >= 80 } }'
308
+ ```
309
+
310
+ **Creating a predicate:**
311
+ ```ruby
312
+ # coverage_policy.rb
313
+ ->(model) do
314
+ # All files must have >= 80% coverage
315
+ model.all_files.all? { |f| f['percentage'] >= 80 }
316
+ end
317
+ ```
318
+
319
+ **Advanced predicate with reporting:**
320
+
321
+ ```ruby
322
+ # coverage_policy.rb
323
+ class CoveragePolicy
324
+ def call(model)
325
+ threshold = 80
326
+ low_files = model.all_files.select { |f| f['percentage'] < threshold }
327
+
328
+ if low_files.empty?
329
+ puts "✓ All files have >= #{threshold}% coverage"
330
+ true
331
+ else
332
+ warn "✗ Files below #{threshold}%:"
333
+ low_files.each { |f| warn " #{f['file']}: #{f['percentage']}%" }
334
+ false
335
+ end
336
+ end
337
+ end
338
+
339
+ CoveragePolicy.new
340
+ ```
341
+
342
+ **Exit codes:**
343
+ - `0` - Predicate returned truthy (pass)
344
+ - `1` - Predicate returned falsy (fail)
345
+ - `2` - Predicate raised an error
346
+
347
+ See [examples/success_predicates/README.md](../../examples/success_predicates/README.md) for more examples.
348
+
349
+ ### Path Relativization
350
+
351
+ Convert absolute paths to relative for cleaner output:
352
+
353
+ ```ruby
354
+ model = CovLoupe::CoverageModel.new(root: '/project')
355
+
356
+ # Get data with absolute paths
357
+ data = model.summary_for('app/models/order.rb')
358
+ # => { 'file' => '/project/app/models/order.rb', ... }
359
+
360
+ # Relativize paths
361
+ relative_data = model.relativize(data)
362
+ # => { 'file' => 'app/models/order.rb', ... }
363
+
364
+ # Works with arrays too
365
+ files = model.all_files
366
+ relative_files = model.relativize(files)
367
+ ```
368
+
369
+ ---
370
+
371
+ ## CI/CD Integration
372
+
373
+ The CLI is designed for CI/CD use with features that integrate naturally into pipeline workflows:
374
+
375
+ ### Key Integration Features
376
+
377
+ - **Exit codes**: Non-zero on failure, making it suitable for pipeline failure conditions
378
+ - **JSON output**: `-fJ` format for parsing by CI tools and custom processing
379
+ - **Staleness checking**: `--stale error` to fail on outdated coverage data
380
+ - **Success predicates**: Custom Ruby policies for coverage enforcement
381
+
382
+ ### Basic CI Pattern
383
+
384
+ ```bash
385
+ # 1. Run tests to generate coverage
386
+ bundle exec rspec
387
+
388
+ # 2. Validate coverage freshness (fails with exit code 1 if stale)
389
+ clp --staleness error -g "lib/**/*.rb"
390
+
391
+ # 3. Export data for CI artifacts or further processing
392
+ clp -fJ list > coverage.json
393
+ ```
394
+
395
+ ### Using Coverage Validation
396
+
397
+ Enforce custom coverage policies with the `validate` subcommand:
398
+
399
+ ```bash
400
+ # Run tests
401
+ bundle exec rspec
402
+
403
+ # Apply coverage policy (fails with exit code 1 if predicate returns false)
404
+ clp validate coverage_policy.rb
405
+ ```
406
+
407
+ Exit codes:
408
+ - `0` - Success (coverage meets requirements)
409
+ - `1` - Failure (coverage policy not met or stale data detected)
410
+ - `2` - Error (invalid predicate or system error)
411
+
412
+ ### Platform-Specific Examples
413
+
414
+ For platform-specific integration examples (GitHub Actions, GitLab CI, Jenkins, CircleCI, etc.), see community contributions in the [GitHub Discussions](https://github.com/keithrbennett/cov-loupe/discussions).
415
+
416
+ ---
417
+
418
+ ## Advanced Filtering & Glob Patterns
419
+
420
+ ### Tracked Globs Overview
421
+
422
+ Tracked globs serve two purposes:
423
+ 1. **Filter output** - Only show matching files
424
+ 2. **Validate coverage** - Ensure new files have coverage
425
+
426
+ ### Pattern Syntax
427
+
428
+ Uses Ruby's `File.fnmatch` with extended glob support:
429
+
430
+ ```sh
431
+ # Single directory
432
+ --tracked-globs "lib/**/*.rb"
433
+
434
+ # Multiple patterns
435
+ --tracked-globs "lib/payments/**/*.rb" --tracked-globs "lib/ops/jobs/**/*.rb"
436
+
437
+ # Exclude patterns (use CLI filtering)
438
+ clp -fJ list | jq '.files[] | select(.file | test("spec") | not)'
439
+
440
+ # Ruby alternative:
441
+ clp -fJ list | ruby -r json -e '
442
+ JSON.parse($stdin.read)["files"].reject { |f| f["file"].include?("spec") }.each do |f|
443
+ puts JSON.pretty_generate(f)
444
+ end
445
+ '
446
+
447
+ # Rexe alternative:
448
+ clp -fJ list | rexe -ij -mb -oJ 'self["files"].reject { |f| f["file"].include?("spec") }'
449
+
450
+ # Complex patterns
451
+ --tracked-globs "lib/{models,controllers}/**/*.rb"
452
+ --tracked-globs "app/**/concerns/*.rb"
453
+ ```
454
+
455
+ ### Use Cases
456
+
457
+ **1. Monitor Subsystem Coverage:**
458
+ ```sh
459
+ # API layer only
460
+ clp -g "lib/api/**/*.rb" list
461
+
462
+ # Core business logic
463
+ clp -g "lib/domain/**/*.rb" list
464
+ ```
465
+
466
+ **2. Ensure New Files Have Coverage:**
467
+ ```sh
468
+ # Fail if any tracked file lacks coverage
469
+ clp --staleness error -g "lib/features/**/*.rb"
470
+ ```
471
+
472
+ **3. Multi-tier Reporting:**
473
+ ```sh
474
+ # Generate separate reports per layer
475
+ for layer in models views controllers; do
476
+ clp -g "app/${layer}/**/*.rb" -fJ list > "coverage-${layer}.json"
477
+ done
478
+ ```
479
+
480
+ ### Ruby API with Globs
481
+
482
+ ```ruby
483
+ model = CovLoupe::CoverageModel.new
484
+
485
+ # Filter files in output
486
+ api_files = model.all_files(
487
+ tracked_globs: ['lib/api/**/*.rb']
488
+ )
489
+
490
+ # Multi-pattern filtering
491
+ core_files = model.all_files(
492
+ tracked_globs: [
493
+ 'lib/core/**/*.rb',
494
+ 'lib/domain/**/*.rb'
495
+ ]
496
+ )
497
+
498
+ # Validate specific subsystems
499
+ begin
500
+ model.all_files(
501
+ check_stale: true,
502
+ tracked_globs: ['lib/critical/**/*.rb']
503
+ )
504
+ rescue CovLoupe::CoverageDataProjectStaleError => e
505
+ # Handle missing coverage for critical files
506
+ puts "Critical files missing coverage:"
507
+ e.missing_files.each { |f| puts " - #{f}" }
508
+ end
509
+ ```
510
+
511
+ ---
512
+
513
+ ## Performance Optimization
514
+
515
+ ### Minimizing Coverage Reads
516
+
517
+ The `CoverageModel` reads `.resultset.json` once at initialization:
518
+
519
+ ```ruby
520
+ # Good: Single model for multiple queries
521
+ model = CovLoupe::CoverageModel.new
522
+ files = model.all_files
523
+ file1 = model.summary_for('lib/a.rb')
524
+ file2 = model.summary_for('lib/b.rb')
525
+
526
+ # Bad: Re-reads coverage for each operation
527
+ model1 = CovLoupe::CoverageModel.new
528
+ files = model1.all_files
529
+
530
+ model2 = CovLoupe::CoverageModel.new
531
+ file1 = model2.summary_for('lib/a.rb')
532
+ ```
533
+
534
+ ### Batch Processing
535
+
536
+ ```ruby
537
+ # Process multiple files in one pass
538
+ files_to_analyze = ['lib/a.rb', 'lib/b.rb', 'lib/c.rb']
539
+ model = CovLoupe::CoverageModel.new
540
+
541
+ results = files_to_analyze.each_with_object({}) do |file, hash|
542
+ hash[file] = {
543
+ summary: model.summary_for(file),
544
+ uncovered: model.uncovered_for(file)
545
+ }
546
+ rescue CovLoupe::FileError
547
+ hash[file] = { error: 'No coverage' }
548
+ end
549
+ ```
550
+
551
+ ### Filtering Early
552
+
553
+ Use `tracked_globs` to reduce data processing:
554
+
555
+ ```ruby
556
+ # Bad: Filter after loading all data
557
+ all_files = model.all_files
558
+ api_files = all_files.select { |f| f['file'].include?('api') }
559
+
560
+ # Good: Filter during query
561
+ api_files = model.all_files(
562
+ tracked_globs: ['lib/api/**/*.rb']
563
+ )
564
+ ```
565
+
566
+ ### Caching Coverage Models
567
+
568
+ For long-running processes:
569
+
570
+ ```ruby
571
+ class CoverageCache
572
+ def initialize(ttl: 300) # 5 minute cache
573
+ @cache = {}
574
+ @ttl = ttl
575
+ end
576
+
577
+ def model_for(root)
578
+ key = root.to_s
579
+ now = Time.now
580
+
581
+ if @cache[key] && (now - @cache[key][:time] < @ttl)
582
+ @cache[key][:model]
583
+ else
584
+ @cache[key] = {
585
+ model: CovLoupe::CoverageModel.new(root: root),
586
+ time: now
587
+ }
588
+ @cache[key][:model]
589
+ end
590
+ end
591
+ end
592
+
593
+ cache = CoverageCache.new
594
+ model = cache.model_for('/project')
595
+ ```
596
+
597
+ ---
598
+
599
+ ## Custom Output Processing
600
+
601
+ ### Format Conversion
602
+
603
+ **CSV Export:**
604
+ ```ruby
605
+ require 'csv'
606
+
607
+ model = CovLoupe::CoverageModel.new
608
+ files = model.all_files
609
+
610
+ CSV.open('coverage.csv', 'w') do |csv|
611
+ csv << ['File', 'Coverage %', 'Lines Covered', 'Total Lines', 'Stale']
612
+ files.each do |f|
613
+ csv << [
614
+ model.relativize(f)['file'],
615
+ f['percentage'],
616
+ f['covered'],
617
+ f['total'],
618
+ f['stale']
619
+ ]
620
+ end
621
+ end
622
+ ```
623
+
624
+ **HTML Report:**
625
+ ```ruby
626
+ require 'erb'
627
+
628
+ template = ERB.new(<<~HTML)
629
+ <html>
630
+ <head><title>Coverage Report</title></head>
631
+ <body>
632
+ <h1>Coverage Report</h1>
633
+ <table>
634
+ <tr>
635
+ <th>File</th><th>Coverage</th><th>Covered</th><th>Total</th>
636
+ </tr>
637
+ <% files.each do |f| %>
638
+ <tr class="<%= f['percentage'] < 80 ? 'low' : 'ok' %>">
639
+ <td><%= f['file'] %></td>
640
+ <td><%= f['percentage'].round(2) %>%</td>
641
+ <td><%= f['covered'] %></td>
642
+ <td><%= f['total'] %></td>
643
+ </tr>
644
+ <% end %>
645
+ </table>
646
+ </body>
647
+ </html>
648
+ HTML
649
+
650
+ model = CovLoupe::CoverageModel.new
651
+ files = model.relativize(model.all_files)
652
+ File.write('coverage.html', template.result(binding))
653
+ ```
654
+
655
+ ### Annotated Source Output
656
+
657
+ The CLI supports annotated source viewing:
658
+
659
+ ```sh
660
+ # Show uncovered lines with context
661
+ clp uncovered app/models/order.rb \
662
+ --source uncovered \
663
+ --source-context 3
664
+
665
+ # Show full file with coverage annotations
666
+ clp uncovered app/models/order.rb \
667
+ --source full \
668
+ --source-context 0
669
+ ```
670
+
671
+ **Programmatic Source Annotation:**
672
+ ```ruby
673
+ def annotate_source(file_path)
674
+ model = CovLoupe::CoverageModel.new
675
+ details = model.detailed_for(file_path)
676
+ source_lines = File.readlines(file_path)
677
+
678
+ output = []
679
+ details['lines'].each do |line_data|
680
+ line_num = line_data['line']
681
+ hits = line_data['hits']
682
+ source = source_lines[line_num - 1]
683
+
684
+ marker = case hits
685
+ when nil then ' '
686
+ when 0 then ' ✗ '
687
+ else " #{hits} "
688
+ end
689
+
690
+ output << "#{marker}#{line_num.to_s.rjust(4)}: #{source}"
691
+ end
692
+
693
+ output.join
694
+ end
695
+
696
+ puts annotate_source('app/models/order.rb')
697
+ ```
698
+
699
+ ### Integration with Coverage Trackers
700
+
701
+ **Send to Codecov:**
702
+ ```sh
703
+ #!/bin/bash
704
+ bundle exec rspec
705
+ clp -fJ list > coverage.json
706
+
707
+ # Transform to Codecov format (example)
708
+ jq '{
709
+ coverage: [
710
+ .files[] | {
711
+ name: .file,
712
+ coverage: .percentage
713
+ }
714
+ ]
715
+ }' coverage.json | curl -X POST \
716
+ -H "Authorization: token $CODECOV_TOKEN" \
717
+ -d @- https://codecov.io/upload
718
+
719
+ # Ruby alternative:
720
+ ruby -r json -e '
721
+ data = JSON.parse(File.read("coverage.json"))
722
+ transformed = {
723
+ coverage: data["files"].map { |f|
724
+ {name: f["file"], coverage: f["percentage"]}
725
+ }
726
+ }
727
+ puts JSON.pretty_generate(transformed)
728
+ ' | curl -X POST \
729
+ -H "Authorization: token $CODECOV_TOKEN" \
730
+ -d @- https://codecov.io/upload
731
+
732
+ # Rexe alternative:
733
+ rexe -f coverage.json -oJ '
734
+ {
735
+ coverage: self["files"].map { |f|
736
+ {name: f["file"], coverage: f["percentage"]}
737
+ }
738
+ }
739
+ ' | curl -X POST \
740
+ -H "Authorization: token $CODECOV_TOKEN" \
741
+ -d @- https://codecov.io/upload
742
+ ```
743
+
744
+ **Send to Coveralls:**
745
+ ```ruby
746
+ require 'cov_loupe'
747
+ require 'net/http'
748
+ require 'json'
749
+
750
+ model = CovLoupe::CoverageModel.new
751
+ files = model.all_files
752
+
753
+ coveralls_data = {
754
+ repo_token: ENV['COVERALLS_REPO_TOKEN'],
755
+ source_files: files.map { |f|
756
+ {
757
+ name: f['file'],
758
+ coverage: model.raw_for(f['file'])['lines']
759
+ }
760
+ }
761
+ }
762
+
763
+ uri = URI('https://coveralls.io/api/v1/jobs')
764
+ Net::HTTP.post(uri, coveralls_data.to_json, {
765
+ 'Content-Type' => 'application/json'
766
+ })
767
+ ```
768
+
769
+ ---
770
+
771
+ ## Additional Resources
772
+
773
+ - [CLI Usage Guide](CLI_USAGE.md)
774
+ - [Library API Reference](LIBRARY_API.md)
775
+ - [MCP Integration Guide](MCP_INTEGRATION.md)
776
+ - [Error Handling Details](ERROR_HANDLING.md)
777
+ - [Troubleshooting](TROUBLESHOOTING.md)