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,203 @@
1
+ # ADR 004: Ruby `instance_eval` for Success Predicates
2
+
3
+ [Back to main README](../../README.md)
4
+
5
+ ## Status
6
+
7
+ Accepted
8
+
9
+ ## Context
10
+
11
+ SimpleCov MCP needed a mechanism for users to define custom coverage policies beyond simple percentage thresholds. Different projects have different requirements:
12
+
13
+ - Some want all files above 80%, others allow a few files below threshold
14
+ - Some need different thresholds for different directories (e.g., 90% for API code, 60% for legacy)
15
+ - Some want total project coverage minimums
16
+ - CI/CD pipelines need exit codes based on policy compliance
17
+
18
+ We considered several approaches:
19
+
20
+ 1. **Built-in policy DSL**: Define a limited language for expressing policies (e.g., YAML/JSON config)
21
+ 2. **Plugin architecture**: Define a protocol/interface, require users to create Ruby classes implementing it
22
+ 3. **Ruby file evaluation**: Load and execute arbitrary Ruby code that returns a callable predicate
23
+ 4. **Sandboxed DSL**: Use a restricted Ruby environment (e.g., `$SAFE` levels, isolated VMs)
24
+
25
+ ### Key Requirements
26
+
27
+ - Flexibility: Support arbitrarily complex coverage policies
28
+ - Simplicity: Easy for users to write and understand
29
+ - Debuggability: Users can use standard Ruby debugging tools
30
+ - CI/CD integration: Clear exit codes (0 = pass, 1 = fail, 2 = error)
31
+ - Access to coverage data: Predicates need access to the full `CoverageModel` API
32
+
33
+ ### Why Not a Custom DSL?
34
+
35
+ A custom DSL would be:
36
+ - Limited in expressiveness (hard to predict all future use cases)
37
+ - Harder to debug (users can't use standard Ruby tools)
38
+ - More maintenance burden (parsing, validation, documentation)
39
+ - Still vulnerable to injection if it allowed any dynamic computation
40
+
41
+ ### Why Not Sandboxing?
42
+
43
+ Ruby's sandboxing options are limited:
44
+ - `$SAFE` levels were deprecated and removed in Ruby 2.7+
45
+ - Full VM isolation (Docker, etc.) is too heavy for a CLI tool
46
+ - Any Turing-complete sandbox can be escaped given enough effort
47
+ - True security requires not executing untrusted code at all
48
+
49
+ ## Decision
50
+
51
+ We chose to **evaluate Ruby files using `instance_eval`** with prominent security warnings rather than attempting to create a false sense of security through incomplete sandboxing.
52
+
53
+ ### Implementation
54
+
55
+ The implementation is in `lib/cov_loupe/cli.rb:191-214`:
56
+
57
+ ```ruby
58
+ def load_success_predicate(path)
59
+ unless File.exist?(path)
60
+ raise "Success predicate file not found: #{path}"
61
+ end
62
+
63
+ content = File.read(path)
64
+
65
+ # WARNING: The predicate code executes with full Ruby privileges.
66
+ # It has unrestricted access to the file system, network, and system commands.
67
+ # Only use predicate files from trusted sources.
68
+ #
69
+ # We evaluate in a fresh Object context to prevent accidental access to
70
+ # CLI internals, but this provides NO security isolation.
71
+ evaluation_context = Object.new
72
+ predicate = evaluation_context.instance_eval(content, path, 1)
73
+
74
+ unless predicate.respond_to?(:call)
75
+ raise "Success predicate must be callable (lambda, proc, or object with #call method)"
76
+ end
77
+
78
+ predicate
79
+ rescue SyntaxError => e
80
+ raise "Syntax error in success predicate file: #{e.message}"
81
+ end
82
+ ```
83
+
84
+ The predicate is then called with a `CoverageModel` instance:
85
+
86
+ ```ruby
87
+ def run_success_predicate
88
+ predicate = load_success_predicate(config.success_predicate)
89
+ model = CoverageModel.new(**config.model_options)
90
+
91
+ result = predicate.call(model)
92
+ exit(result ? 0 : 1) # 0 = success, 1 = failure
93
+ rescue => e
94
+ warn "Success predicate error: #{e.message}"
95
+ warn e.backtrace.first(5).join("\n") if config.error_mode == :debug
96
+ exit 2 # Exit code 2 for predicate errors
97
+ end
98
+ ```
99
+
100
+ ### Security Model: Treat as Executable Code
101
+
102
+ Rather than pretending to sandbox untrusted code, we treat success predicates **exactly like any other Ruby code in the project**:
103
+
104
+ 1. **Prominent warnings** in documentation (examples/success_predicates/README.md:5-17):
105
+ ```
106
+ ⚠️ SECURITY WARNING
107
+
108
+ Success predicates execute as arbitrary Ruby code with full system privileges.
109
+ Only use predicate files from trusted sources.
110
+ - Never use predicates from untrusted or unknown sources
111
+ - Review predicates before use, especially in CI/CD environments
112
+ - Store predicates in version control with code review
113
+ ```
114
+
115
+ 2. **Code review workflow**: Predicates live in version control alongside tests
116
+ 3. **CI/CD best practices**: Same permissions model as running tests themselves
117
+ 4. **Example predicates**: Well-documented examples showing safe patterns
118
+
119
+ ### Predicate API
120
+
121
+ Success predicates must be callable (lambda, proc, or object with `#call` method):
122
+
123
+ **Lambda example:**
124
+ ```ruby
125
+ ->(model) do
126
+ model.all_files.all? { |f| f['percentage'] >= 80 }
127
+ end
128
+ ```
129
+
130
+ **Class example:**
131
+
132
+ ```ruby
133
+
134
+ class CoveragePolicy
135
+ def call(model)
136
+ api_files = model.all_files.select { |f| f['file'].start_with?('lib/api/') }
137
+ api_files.all? { |f| f['percentage'] >= 90 }
138
+ end
139
+ end
140
+
141
+ AllFilesAboveThreshold.new
142
+ ```
143
+
144
+ The predicate receives a full `CoverageModel` instance with access to:
145
+ - `all_files(tracked_globs:, sort_order:)` - All file coverage data
146
+ - `summary_for(path)` - Coverage summary for a specific file
147
+ - `uncovered_for(path)` - Uncovered lines for a specific file
148
+ - `detailed_for(path)` - Per-line coverage data
149
+
150
+ ## Consequences
151
+
152
+ ### Positive
153
+
154
+ 1. **Maximum flexibility**: Users can express arbitrarily complex coverage policies using full Ruby
155
+ 2. **Familiar tooling**: Users can debug predicates with standard Ruby tools (pry, byebug, etc.)
156
+ 3. **Simplicity**: No custom DSL to learn, document, or maintain
157
+ 4. **Honesty**: Security model is clear and doesn't provide false confidence
158
+ 5. **Composability**: Users can require other libraries, define helper methods, etc.
159
+ 6. **Excellent examples**: We provide 5+ well-documented example predicates
160
+
161
+ ### Negative
162
+
163
+ 1. **Security responsibility**: Users must understand the security implications
164
+ 2. **Potential misuse**: Users might mistakenly trust untrusted predicate files
165
+ 3. **No isolation**: Buggy predicates can access/modify anything in the system
166
+ 4. **Documentation burden**: Must clearly communicate security model
167
+
168
+ ### Trade-offs
169
+
170
+ - **Versus custom DSL**: More powerful and debuggable, but requires user awareness of security
171
+ - **Versus plugin architecture**: Simpler (no gem dependencies, no protocol to learn), but same security profile
172
+ - **Versus incomplete sandboxing**: Honest about capabilities rather than security theater
173
+
174
+ ### Threat Model
175
+
176
+ This approach is **appropriate** when:
177
+ - Predicate files are stored in version control with code review
178
+ - Users treat predicates like any other code in their project (tests, Rakefile, etc.)
179
+ - CI/CD environments already execute arbitrary code (tests, build scripts)
180
+
181
+ This approach is **inappropriate** when:
182
+ - Processing untrusted predicate files from unknown sources
183
+ - Allowing users to upload predicates via web interface
184
+ - Running in a multi-tenant environment without isolation
185
+
186
+ ### Future Considerations
187
+
188
+ If demand arises for truly untrusted predicate execution, alternatives include:
189
+
190
+ 1. **JSON-based policy format**: Limited expressiveness but safe
191
+ 2. **WebAssembly sandbox**: Execute policies in an isolated WASM runtime
192
+ 3. **External process**: Run predicates in separate process with restricted permissions
193
+
194
+ However, for the primary use case (CI/CD policy enforcement), the current approach is simpler and more flexible than these alternatives.
195
+
196
+ ## References
197
+
198
+ - Implementation: `lib/cov_loupe/cli.rb:191-214` (load predicate), `lib/cov_loupe/cli.rb:179-189` (execute)
199
+ - Security warnings: `examples/success_predicates/README.md:5-17`
200
+ - Example predicates: `examples/success_predicates/*.rb`
201
+ - CoverageModel API: `lib/cov_loupe/model.rb`
202
+ - CLI config: `lib/cov_loupe/cli_config.rb:18` (success_predicate field)
203
+ - Option parsing: `lib/cov_loupe/option_parser_builder.rb` (--success-predicate flag)
@@ -0,0 +1,189 @@
1
+ # ADR 005: No SimpleCov Runtime Dependency
2
+
3
+ [Back to main README](../../README.md)
4
+
5
+ ## Status
6
+
7
+ Replaced – cov-loupe now requires SimpleCov at runtime so that multi-suite resultsets can be merged using SimpleCov’s combine helpers.
8
+
9
+ ## Context
10
+
11
+ SimpleCov MCP provides tooling for inspecting SimpleCov coverage reports. When designing the gem, we had to decide whether to depend on SimpleCov as a runtime dependency.
12
+
13
+ ### Alternative Approaches
14
+
15
+ 1. **Runtime dependency on SimpleCov**: Use SimpleCov's API to read and process coverage data
16
+ 2. **Development-only dependency**: Read SimpleCov's `.resultset.json` files directly without requiring SimpleCov at runtime
17
+ 3. **Support multiple coverage formats**: Parse coverage data from multiple tools (SimpleCov, Coverage, etc.)
18
+
19
+ ### Key Considerations
20
+
21
+ **Dependency weight**: SimpleCov itself has dependencies:
22
+ - `docile` (~> 1.1)
23
+ - `simplecov-html` (~> 0.11)
24
+ - `simplecov_json_formatter` (~> 0.1)
25
+
26
+ **Use case separation**:
27
+ - SimpleCov is needed when **running tests** to collect coverage
28
+ - SimpleCov MCP is needed when **inspecting coverage** after tests complete
29
+ - These are temporally separated activities
30
+
31
+ **Deployment contexts**:
32
+ - CI/CD: Coverage collection happens in test job, inspection might happen in a separate analysis job
33
+ - Production: Some teams want to analyze coverage data without installing test dependencies
34
+ - Developer machines: May want to inspect coverage without full test suite dependencies
35
+
36
+ **Format stability**:
37
+ - SimpleCov's `.resultset.json` format is stable and well-documented
38
+ - The format is simple JSON with predictable structure
39
+ - Breaking changes would affect all SimpleCov users, so the format is unlikely to change
40
+
41
+ ## Decision
42
+
43
+ We chose to **make SimpleCov a development dependency only** and read `.resultset.json` files directly using Ruby's standard library JSON parser.
44
+
45
+ ### Implementation
46
+
47
+ The gem only depends on `mcp` at runtime (cov-loupe.gemspec:26):
48
+ ```ruby
49
+ # Runtime deps (stdlib: json, time, pathname)
50
+ spec.add_runtime_dependency 'mcp', '~> 0.3'
51
+ spec.add_development_dependency 'simplecov', '~> 0.21'
52
+ ```
53
+
54
+ Coverage data is read directly from JSON files (lib/cov_loupe/model.rb:34-42):
55
+ ```ruby
56
+ rs = CovUtil.find_resultset(@root, resultset: resultset)
57
+ raw = JSON.parse(File.read(rs))
58
+ # SimpleCov typically writes a single test suite entry to .resultset.json
59
+ # Find the first entry that has coverage data (skip comment entries)
60
+ _suite, data = raw.find { |k, v| v.is_a?(Hash) && v.key?('coverage') }
61
+ raise "No test suite with coverage data found in resultset file: #{rs}" unless data
62
+ cov = data['coverage'] or raise "No 'coverage' key found in resultset file: #{rs}"
63
+ @cov = cov.transform_keys { |k| File.absolute_path(k, @root) }
64
+ @cov_timestamp = (data['timestamp'] || data['created_at'] || 0).to_i
65
+ ```
66
+
67
+ Coverage calculations use simple algorithms (lib/cov_loupe/util.rb:42-71):
68
+ ```ruby
69
+ def summary(arr)
70
+ total = 0
71
+ covered = 0
72
+ arr.compact.each do |hits|
73
+ total += 1
74
+ covered += 1 if hits.to_i > 0
75
+ end
76
+ percentage = total.zero? ? 100.0 : ((covered.to_f * 100.0 / total) * 100).round / 100.0
77
+ { 'covered' => covered, 'total' => total, 'percentage' => percentage }
78
+ end
79
+
80
+ def uncovered(arr)
81
+ out = []
82
+ arr.each_with_index do |hits, i|
83
+ next if hits.nil?
84
+ out << (i + 1) if hits.to_i.zero?
85
+ end
86
+ out
87
+ end
88
+
89
+ def detailed(arr)
90
+ rows = []
91
+ arr.each_with_index do |hits, i|
92
+ h = hits&.to_i
93
+ rows << { 'line' => i + 1, 'hits' => h, 'covered' => h.positive? } if h
94
+ end
95
+ rows
96
+ end
97
+ ```
98
+
99
+ ### SimpleCov .resultset.json Format
100
+
101
+ The format we parse has this structure:
102
+ ```json
103
+ {
104
+ "RSpec": {
105
+ "coverage": {
106
+ "/absolute/path/to/file.rb": {
107
+ "lines": [null, 1, 3, 0, null, 5, ...]
108
+ }
109
+ },
110
+ "timestamp": 1633072800
111
+ }
112
+ }
113
+ ```
114
+
115
+ Where:
116
+ - Top level keys are test suite names (e.g., "RSpec", "Minitest")
117
+ - `coverage` contains file paths mapped to coverage data
118
+ - `lines` is an array where each index represents a line number (0-indexed)
119
+ - Array values: `null` = not executable, `0` = not covered, `>0` = hit count
120
+ - `timestamp` is Unix timestamp when coverage was collected
121
+
122
+ ### Resultset Discovery
123
+
124
+ We implement flexible discovery of `.resultset.json` files (lib/cov_loupe/util.rb:6-10):
125
+ ```ruby
126
+ RESULTSET_CANDIDATES = [
127
+ '.resultset.json',
128
+ 'coverage/.resultset.json',
129
+ 'tmp/.resultset.json'
130
+ ].freeze
131
+ ```
132
+
133
+ This supports common SimpleCov configurations without requiring SimpleCov to be loaded.
134
+
135
+ ## Consequences
136
+
137
+ ### Positive
138
+
139
+ 1. **Lightweight installation**: No transitive dependencies beyond `mcp` gem
140
+ 2. **Deployment flexibility**: Can analyze coverage in environments without test dependencies
141
+ 3. **Faster installation**: Fewer gems to download and install
142
+ 4. **Clear separation of concerns**: Coverage collection vs. coverage analysis are independent
143
+ 5. **CI/CD optimization**: Analysis jobs don't need full test suite dependencies
144
+ 6. **Production-safe**: Can be deployed to production environments if needed (e.g., for monitoring)
145
+
146
+ ### Negative
147
+
148
+ 1. **Format dependency**: Tightly coupled to SimpleCov's JSON format
149
+ 2. **Breaking changes risk**: If SimpleCov changes `.resultset.json` structure, we must adapt
150
+ 3. **Limited to SimpleCov**: Cannot read coverage data from other Ruby coverage tools
151
+ 4. **Duplicate logic**: Coverage percentage calculations reimplemented (though simple)
152
+ 5. **Maintenance**: Must track SimpleCov format changes manually
153
+
154
+ ### Trade-offs
155
+
156
+ - **Versus runtime dependency**: Lighter weight but less resilient to format changes
157
+ - **Versus multi-format support**: Simpler implementation but locked to SimpleCov ecosystem
158
+ - **Versus using SimpleCov API**: More flexible deployment but requires understanding the file format
159
+
160
+ ### Risk Mitigation
161
+
162
+ 1. **Format stability**: SimpleCov has maintained `.resultset.json` compatibility for years
163
+ 2. **Simple format**: JSON structure is straightforward and unlikely to change dramatically
164
+ 3. **Development dependency**: We still use SimpleCov in our own tests, so format changes would be detected immediately
165
+ 4. **Documentation**: CLAUDE.md documents the format dependency explicitly
166
+ 5. **Error handling**: Robust error messages when format doesn't match expectations
167
+
168
+ ### Format Evolution Strategy
169
+
170
+ If SimpleCov's format changes:
171
+ 1. **Minor additions** (new keys): Ignore unknown keys, only parse what we need
172
+ 2. **Breaking changes** (structure changes): Version detection logic to support multiple formats
173
+ 3. **Alternative formats**: Could add support for other coverage tools' JSON formats if needed
174
+
175
+ ### Current Limitations Accepted
176
+
177
+ - Only supports SimpleCov (not Coverage gem, other tools)
178
+ - Assumes standard `.resultset.json` locations
179
+ - No support for merged coverage from multiple test runs (SimpleCov handles this before writing JSON)
180
+ - No support for branch coverage (SimpleCov feature not widely used yet)
181
+
182
+ ## References
183
+
184
+ - Gemspec dependencies: `cov-loupe.gemspec:26-28`
185
+ - JSON parsing: `lib/cov_loupe/model.rb:34-57`
186
+ - Coverage calculations: `lib/cov_loupe/util.rb:42-71`
187
+ - Resultset discovery: `lib/cov_loupe/util.rb:6-10`, `lib/cov_loupe/util.rb:34-36`
188
+ - SimpleCov format documentation: https://github.com/simplecov-ruby/simplecov
189
+ - Development usage: Uses SimpleCov in `spec/spec_helper.rb` to test itself
@@ -0,0 +1,60 @@
1
+ # Architecture Decision Records
2
+
3
+ [Back to main README](../../README.md)
4
+
5
+ ## What is an ADR?
6
+
7
+ An Architecture Decision Record (ADR) captures a significant architectural decision made during the development of this project, along with its context and consequences.
8
+
9
+ ## ADR Format
10
+
11
+ Each ADR follows this structure:
12
+
13
+ ### Title
14
+ A short phrase describing the decision (e.g., "Dual-Mode Operation: CLI and MCP Server")
15
+
16
+ ### Status
17
+ - **Accepted**: The decision has been made and is currently in effect
18
+ - **Proposed**: Under consideration
19
+ - **Deprecated**: No longer applicable
20
+ - **Superseded**: Replaced by a newer decision
21
+
22
+ ### Context
23
+ The background, problem statement, and constraints that led to the decision.
24
+
25
+ ### Decision
26
+ The architectural choice that was made.
27
+
28
+ ### Consequences
29
+ The implications of this decision, both positive and negative. This includes:
30
+ - Benefits gained
31
+ - Trade-offs accepted
32
+ - Complexity introduced
33
+ - Future constraints
34
+
35
+ ### References
36
+ Links to related code, issues, documentation, or other ADRs.
37
+
38
+ ## Index of ADRs
39
+
40
+ - [001: Dual-Mode Operation](001-x-arch-decision.md) - CLI vs MCP server mode detection
41
+ - [002: Context-Aware Error Handling](002-x-arch-decision.md) - Mode-specific error handling strategy
42
+ - [003: Coverage Staleness Detection](003-x-arch-decision.md) - Three-type staleness system
43
+ - [004: Ruby Instance Eval for Success Predicates](004-x-arch-decision.md) - Dynamic Ruby evaluation approach
44
+ - [005: No SimpleCov Runtime Dependency](005-x-arch-decision.md) - Superseded by the multi-suite merge work (runtime SimpleCov dependency)
45
+
46
+ ## When to Create an ADR
47
+
48
+ Create an ADR when:
49
+ - Making a decision that affects the structure or behavior of the system
50
+ - Choosing between multiple viable approaches
51
+ - Accepting significant trade-offs
52
+ - Making decisions that future maintainers should understand
53
+
54
+ ## Contributing
55
+
56
+ When adding a new ADR:
57
+ 1. Use the next sequential number (e.g., `006-x-arch-decision.md`)
58
+ 2. Follow the format outlined above
59
+ 3. Update this README's index
60
+ 4. Link to relevant code and documentation
@@ -0,0 +1,255 @@
1
+ [Back to main README](../../README.md)
2
+
3
+ ---
4
+ marp: true
5
+ theme: default
6
+ class: lead
7
+ paginate: true
8
+ backgroundColor: #fff
9
+ color: #333
10
+ ---
11
+
12
+ # SimpleCovMCP
13
+ ### MCP Server, CLI, and Library for SimpleCov Ruby Test Coverage
14
+
15
+ - Keith Bennett
16
+ - First presented to PhRUG (Philippines Ruby User Group), 2025-10-01
17
+
18
+ ---
19
+
20
+ ## What is SimpleCov MCP?
21
+
22
+ A **three-in-one** gem that makes SimpleCov coverage data accessible to:
23
+
24
+ - 🤖 **AI agents** via Model Context Protocol (MCP)
25
+ - 💻 **Command line** via its command line interface
26
+ - 📚 **Ruby scripts and applications** as a library
27
+
28
+ **Lazy dependency** on SimpleCov - single-suite resultsets avoid loading it; multi-suite files trigger a merge via SimpleCov’s combine helpers.
29
+
30
+ What is it *not*? It is not a replacement for SimpleCov's generated web presentation of the coverage data.
31
+
32
+
33
+ This code base requires a Ruby version >= 3.2.0, because this is required by the mcp gem it uses.
34
+
35
+ ---
36
+
37
+ ## High Level Objectives
38
+
39
+ - Query coverage programmatically
40
+ - Integrate with AI tools
41
+ - Automate coverage analysis
42
+ - Focus on specific files/patterns
43
+
44
+ ---
45
+
46
+ ## Key Features
47
+
48
+ - **Lazy SimpleCov dependency** - only loaded when multi-suite resultsets need merging
49
+ - **Flexible resultset location** - via CLI flags, passed parameter, or env var
50
+ - **Staleness detection** - warns or optionally errors when files newer than coverage
51
+ - **JSON output** - perfect for jq, scripts, CI/CD
52
+ - **Source code integration** - show uncovered lines with or without context
53
+ - **Colored output** - readable terminal display
54
+
55
+ ---
56
+
57
+ ## Demo 1: MCP Server Mode
58
+ ### AI Coverage Assistant
59
+
60
+ ```bash
61
+ # Test the MCP server manually
62
+ echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"coverage_summary_tool","arguments":{"path":"lib/cov_loupe/model.rb"}}}' | cov-loupe
63
+ ```
64
+
65
+ **What AI agents can do:**
66
+ - Analyze coverage gaps
67
+ - Suggest testing priorities
68
+ - Generate ad-hoc coverage reports
69
+
70
+ ---
71
+
72
+ ## MCP Tools (Functions) Available
73
+
74
+ | Tool | Purpose | Example CLI Command |
75
+ |---------------------------|----------------------------|------------------------------------------------------|
76
+ | `all_files_coverage_tool` | Project-wide coverage data | `cov-loupe list` |
77
+ | `coverage_detailed_tool` | Per-line hit counts | `cov-loupe detailed lib/cov_loupe/model.rb` |
78
+ | `coverage_raw_tool` | Raw SimpleCov lines array | `cov-loupe raw lib/cov_loupe/model.rb` |
79
+ | `coverage_summary_tool` | Get coverage % for a file | `cov-loupe summary lib/cov_loupe/model.rb` |
80
+ | `coverage_table_tool` | Formatted coverage table | `cov-loupe list` |
81
+ | `coverage_totals_tool` | Aggregated line totals | `cov-loupe totals` |
82
+ | `help_tool` | Tool usage guidance | `cov-loupe --help` |
83
+ | `uncovered_lines_tool` | Find missing test coverage | `cov-loupe uncovered lib/cov_loupe/cli.rb` |
84
+ | `version_tool` | Display version info | `cov-loupe version` |
85
+
86
+ ---
87
+
88
+ ## Demo 2: CLI Tool
89
+ ###
90
+
91
+ ```bash
92
+ # Show all files, worst coverage first
93
+ cov-loupe
94
+
95
+ # Focus on a specific file
96
+ cov-loupe summary lib/cov_loupe/cli.rb
97
+
98
+ # Find untested lines with source context
99
+ cov-loupe uncovered lib/cov_loupe/cli.rb --source=uncovered --source-context 3
100
+
101
+ # JSON for scripts
102
+ cov-loupe -fJ | jq '.files[] | select(.percentage < 80)'
103
+
104
+ # Ruby alternative:
105
+ cov-loupe -fJ | ruby -r json -e '
106
+ JSON.parse($stdin.read)["files"].select { |f| f["percentage"] < 80 }.each do |f|
107
+ puts JSON.pretty_generate(f)
108
+ end
109
+ '
110
+
111
+ # Rexe alternative:
112
+ cov-loupe -fJ | rexe -ij -mb -oJ 'self["files"].select { |f| f["percentage"] < 80 }'
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Demo 2: CLI Tool (cont'd.)
118
+
119
+ ```bash
120
+ # Custom resultset location
121
+ cov-loupe --resultset coverage-all/
122
+
123
+ # Sort by highest coverage
124
+ cov-loupe --sort-order d
125
+
126
+ # Staleness checking (file newer than coverage?)
127
+ cov-loupe --staleness error
128
+
129
+ # Track new files missing from coverage
130
+ cov-loupe --tracked-globs "lib/**/tools/*.rb"
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Demo 3: Ruby Library
136
+ ### Programmatic Integration
137
+
138
+ ```ruby
139
+ require 'cov_loupe'
140
+
141
+ model = CovLoupe::CoverageModel.new
142
+
143
+ # Get project overview
144
+ files = model.all_files
145
+ puts "Lowest coverage: #{files.first['percentage']}%"
146
+
147
+ # Focus on specific concerns
148
+ uncovered = model.uncovered_for("lib/wifi-wand/models/ubuntu_model.rb")
149
+ puts "Uncovered hash's keys: #{uncovered.keys.inspect}"
150
+ puts "Missing lines: #{uncovered['uncovered'].inspect}"
151
+
152
+ # Output:
153
+ # Lowest coverage: 17.0%
154
+ # Uncovered hash's keys: ["file", "uncovered", "summary"]
155
+ # Missing lines: [13, 17, 21,...200, 203]
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Custom Threshold Git Pre-Commit Hook
161
+
162
+ ```ruby
163
+ require 'cov_loupe'
164
+
165
+ files = CovLoupe::CoverageModel.new.all_files
166
+ critical, other = files.partition { |f| f['file'].include?('/lib/critical/') }
167
+
168
+ fails = critical.select { |f| f['percentage'] < 100.0 } +
169
+ other.select { |f| f['percentage'] < 90.0 }
170
+
171
+ if fails.any?
172
+ puts "❌ Coverage failures:"
173
+ fails.each { |f| puts " #{f['file']}: #{f['percentage']}%" }
174
+ exit 1
175
+ else
176
+ puts "✅ All thresholds met!"
177
+ end
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Architecture Overview
183
+
184
+ ```
185
+ lib/cov_loupe
186
+ ├── base_tool.rb
187
+ ├── cli.rb
188
+ ├── error_handler_factory.rb
189
+ ├── error_handler.rb
190
+ ├── errors.rb
191
+ ├── mcp_server.rb
192
+ ├── model.rb
193
+ ├── path_relativizer.rb
194
+ ├── staleness_checker.rb
195
+ ├── tools
196
+ │ ├── all_files_coverage_tool.rb
197
+ │ ├── coverage_detailed_tool.rb
198
+ │ ├── coverage_raw_tool.rb
199
+ │ ├── coverage_summary_tool.rb
200
+ │ ├── coverage_table_tool.rb
201
+ │ ├── coverage_totals_tool.rb
202
+ │ ├── help_tool.rb
203
+ │ ├── uncovered_lines_tool.rb
204
+ │ └── version_tool.rb
205
+ ├── util.rb
206
+ └── version.rb
207
+ ```
208
+
209
+ **Clean separation:** CLI ↔ Model ↔ MCP Tools
210
+
211
+ ---
212
+
213
+ ## MCP Plumbing - the MCP Gem
214
+
215
+ #### BaseTool subclasses the `mcp` gem's Tool class and defines a schema (see base_tool.rb):
216
+ ```ruby
217
+ class BaseTool < ::MCP::Tool
218
+ # ...
219
+ end
220
+ ```
221
+
222
+ * [BaseTool source](https://github.com/keithrbennett/cov-loupe/blob/main/lib/cov_loupe/base_tool.rb)
223
+ * [BaseTool subclass source](https://github.com/keithrbennett/cov-loupe/blob/main/lib/cov_loupe/tools/coverage_detailed_tool.rb)
224
+
225
+ The MCP tools available to the model subclass BaseTool and implement their respective tasks.
226
+
227
+ #### mcp_server.rb creates an instance of the mcp gem's Server class and runs it:
228
+
229
+ ```ruby
230
+ server = ::MCP::Server.new(
231
+ name: 'cov-loupe',
232
+ version: CovLoupe::VERSION,
233
+ tools: tools
234
+ )
235
+ ::MCP::Server::Transports::StdioTransport.new(server).open
236
+ ```
237
+
238
+
239
+ ----
240
+
241
+ ## Questions?
242
+
243
+ **Demo requests:**
244
+ - Specific MCP tool usage?
245
+ - CLI workflow examples?
246
+ - Library integration patterns?
247
+ - AI assistant setup?
248
+
249
+ **Contact:**
250
+ - GitHub issues for bugs/features
251
+ - Ruby community discussions
252
+
253
+ **Thank you!** 🙏
254
+
255
+ *Making test coverage accessible to humans and AI alike*