cov-loupe 3.0.0 → 4.0.0.pre

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 (281) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +230 -0
  3. data/CLAUDE.md +5 -0
  4. data/CODE_OF_CONDUCT.md +62 -0
  5. data/CONTRIBUTING.md +102 -0
  6. data/GEMINI.md +5 -0
  7. data/README.md +154 -51
  8. data/RELEASE_NOTES.md +452 -0
  9. data/dev/images/cov-loupe-icon-lores.png +0 -0
  10. data/dev/images/cov-loupe-icon-square.png +0 -0
  11. data/dev/images/cov-loupe-icon.png +0 -0
  12. data/dev/images/cov-loupe-logo.png +0 -0
  13. data/dev/prompts/README.md +74 -0
  14. data/dev/prompts/archive/architectural-review-and-actions-prompt.md +53 -0
  15. data/dev/prompts/archive/investigate-and-report-issues-prompt.md +33 -0
  16. data/dev/prompts/archive/produce-action-items-prompt.md +25 -0
  17. data/dev/prompts/guidelines/ai-code-evaluator-guidelines.md +337 -0
  18. data/dev/prompts/improve/refactor-test-suite.md +18 -0
  19. data/dev/prompts/improve/simplify-code-logic.md +133 -0
  20. data/dev/prompts/improve/update-documentation.md +21 -0
  21. data/dev/prompts/review/comprehensive-codebase-review.md +176 -0
  22. data/dev/prompts/review/identify-action-items.md +143 -0
  23. data/dev/prompts/review/verify-code-changes.md +54 -0
  24. data/dev/prompts/validate/create-screencast-outline.md +234 -0
  25. data/dev/prompts/validate/test-documentation-examples.md +180 -0
  26. data/docs/QUICKSTART.md +63 -0
  27. data/docs/assets/images/cov-loupe-logo-lores.png +0 -0
  28. data/docs/assets/images/cov-loupe-logo.png +0 -0
  29. data/docs/assets/images/favicon.png +0 -0
  30. data/docs/assets/stylesheets/branding.css +16 -0
  31. data/docs/assets/stylesheets/extra.css +15 -0
  32. data/docs/code_of_conduct.md +1 -0
  33. data/docs/contributing.md +1 -0
  34. data/docs/dev/ARCHITECTURE.md +56 -11
  35. data/docs/dev/DEVELOPMENT.md +116 -12
  36. data/docs/dev/FUTURE_ENHANCEMENTS.md +14 -0
  37. data/docs/dev/README.md +3 -2
  38. data/docs/dev/RELEASING.md +2 -0
  39. data/docs/dev/arch-decisions/README.md +10 -7
  40. data/docs/dev/arch-decisions/application-architecture.md +259 -0
  41. data/docs/dev/arch-decisions/coverage-data-quality.md +193 -0
  42. data/docs/dev/arch-decisions/output-character-mode.md +217 -0
  43. data/docs/dev/arch-decisions/path-resolution.md +90 -0
  44. data/docs/dev/arch-decisions/{004-x-arch-decision.md → policy-validation.md} +32 -28
  45. data/docs/dev/arch-decisions/{005-x-arch-decision.md → simplecov-integration.md} +47 -44
  46. data/docs/dev/presentations/cov-loupe-presentation.md +15 -13
  47. data/docs/examples/mcp-inputs.md +3 -0
  48. data/docs/examples/prompts.md +3 -0
  49. data/docs/examples/success_predicates.md +3 -0
  50. data/docs/fixtures/demo_project/.resultset.json +170 -0
  51. data/docs/fixtures/demo_project/README.md +6 -0
  52. data/docs/fixtures/demo_project/app/controllers/admin/audit_logs_controller.rb +19 -0
  53. data/docs/fixtures/demo_project/app/controllers/orders_controller.rb +26 -0
  54. data/docs/fixtures/demo_project/app/models/order.rb +20 -0
  55. data/docs/fixtures/demo_project/app/models/user.rb +19 -0
  56. data/docs/fixtures/demo_project/lib/api/client.rb +22 -0
  57. data/docs/fixtures/demo_project/lib/ops/jobs/cleanup_job.rb +16 -0
  58. data/docs/fixtures/demo_project/lib/ops/jobs/report_job.rb +17 -0
  59. data/docs/fixtures/demo_project/lib/payments/processor.rb +15 -0
  60. data/docs/fixtures/demo_project/lib/payments/refund_service.rb +15 -0
  61. data/docs/fixtures/demo_project/lib/payments/reporting/exporter.rb +16 -0
  62. data/docs/index.md +1 -0
  63. data/docs/license.md +3 -0
  64. data/docs/release_notes.md +3 -0
  65. data/docs/user/ADVANCED_USAGE.md +208 -115
  66. data/docs/user/CLI_FALLBACK_FOR_LLMS.md +2 -0
  67. data/docs/user/CLI_USAGE.md +276 -101
  68. data/docs/user/ERROR_HANDLING.md +4 -4
  69. data/docs/user/EXAMPLES.md +121 -128
  70. data/docs/user/INSTALLATION.md +9 -28
  71. data/docs/user/LIBRARY_API.md +227 -122
  72. data/docs/user/MCP_INTEGRATION.md +114 -203
  73. data/docs/user/README.md +5 -1
  74. data/docs/user/TROUBLESHOOTING.md +49 -27
  75. data/docs/user/installing-a-prelease-version-of-covloupe.md +43 -0
  76. data/docs/user/{V2-BREAKING-CHANGES.md → migrations/MIGRATING_TO_V2.md} +62 -72
  77. data/docs/user/migrations/MIGRATING_TO_V3.md +72 -0
  78. data/docs/user/migrations/MIGRATING_TO_V4.md +591 -0
  79. data/docs/user/migrations/README.md +22 -0
  80. data/docs/user/prompts/README.md +9 -0
  81. data/docs/user/prompts/non-web-coverage-analysis-prompt.md +103 -0
  82. data/docs/user/prompts/rails-coverage-analysis-prompt.md +94 -0
  83. data/docs/user/prompts/use-cli-not-mcp-prompt.md +53 -0
  84. data/examples/cli_demo.sh +77 -0
  85. data/examples/filter_and_table_demo-output.md +114 -0
  86. data/examples/filter_and_table_demo.rb +174 -0
  87. data/examples/fixtures/demo_project/coverage/.resultset.json +10 -0
  88. data/examples/mcp-inputs/README.md +66 -0
  89. data/examples/mcp-inputs/coverage_detailed.json +1 -0
  90. data/examples/mcp-inputs/coverage_raw.json +1 -0
  91. data/examples/mcp-inputs/coverage_summary.json +1 -0
  92. data/examples/mcp-inputs/list.json +1 -0
  93. data/examples/mcp-inputs/uncovered_lines.json +1 -0
  94. data/examples/prompts/README.md +27 -0
  95. data/examples/prompts/custom_resultset.txt +2 -0
  96. data/examples/prompts/detailed_with_source.txt +2 -0
  97. data/examples/prompts/list_lowest.txt +2 -0
  98. data/examples/prompts/summary.txt +2 -0
  99. data/examples/prompts/uncovered.txt +2 -0
  100. data/examples/success_predicates/README.md +198 -0
  101. data/examples/success_predicates/all_files_above_threshold_predicate.rb +21 -0
  102. data/examples/success_predicates/directory_specific_thresholds_predicate.rb +30 -0
  103. data/examples/success_predicates/project_coverage_minimum_predicate.rb +6 -0
  104. data/lib/cov_loupe/base_tool.rb +229 -20
  105. data/lib/cov_loupe/cli.rb +132 -23
  106. data/lib/cov_loupe/commands/base_command.rb +25 -6
  107. data/lib/cov_loupe/commands/command_factory.rb +0 -1
  108. data/lib/cov_loupe/commands/detailed_command.rb +10 -5
  109. data/lib/cov_loupe/commands/list_command.rb +2 -1
  110. data/lib/cov_loupe/commands/raw_command.rb +7 -5
  111. data/lib/cov_loupe/commands/summary_command.rb +12 -7
  112. data/lib/cov_loupe/commands/totals_command.rb +74 -10
  113. data/lib/cov_loupe/commands/uncovered_command.rb +7 -5
  114. data/lib/cov_loupe/commands/validate_command.rb +11 -3
  115. data/lib/cov_loupe/commands/version_command.rb +6 -4
  116. data/lib/cov_loupe/{app_config.rb → config/app_config.rb} +13 -5
  117. data/lib/cov_loupe/config/app_context.rb +43 -0
  118. data/lib/cov_loupe/config/boolean_type.rb +91 -0
  119. data/lib/cov_loupe/config/logger.rb +92 -0
  120. data/lib/cov_loupe/{option_normalizers.rb → config/option_normalizers.rb} +55 -24
  121. data/lib/cov_loupe/{option_parser_builder.rb → config/option_parser_builder.rb} +46 -24
  122. data/lib/cov_loupe/coverage/coverage_calculator.rb +53 -0
  123. data/lib/cov_loupe/coverage/coverage_reporter.rb +63 -0
  124. data/lib/cov_loupe/coverage/coverage_table_formatter.rb +133 -0
  125. data/lib/cov_loupe/{error_handler.rb → errors/error_handler.rb} +21 -33
  126. data/lib/cov_loupe/{errors.rb → errors/errors.rb} +48 -71
  127. data/lib/cov_loupe/formatters/formatters.rb +75 -0
  128. data/lib/cov_loupe/formatters/source_formatter.rb +18 -7
  129. data/lib/cov_loupe/formatters/table_formatter.rb +80 -0
  130. data/lib/cov_loupe/loaders/all.rb +15 -0
  131. data/lib/cov_loupe/loaders/all_cli.rb +10 -0
  132. data/lib/cov_loupe/loaders/all_mcp.rb +23 -0
  133. data/lib/cov_loupe/loaders/resultset_loader.rb +147 -0
  134. data/lib/cov_loupe/mcp_server.rb +3 -2
  135. data/lib/cov_loupe/model/model.rb +520 -0
  136. data/lib/cov_loupe/model/model_data.rb +13 -0
  137. data/lib/cov_loupe/model/model_data_cache.rb +116 -0
  138. data/lib/cov_loupe/option_parsers/env_options_parser.rb +17 -6
  139. data/lib/cov_loupe/option_parsers/error_helper.rb +16 -10
  140. data/lib/cov_loupe/output_chars.rb +192 -0
  141. data/lib/cov_loupe/paths/glob_utils.rb +100 -0
  142. data/lib/cov_loupe/{path_relativizer.rb → paths/path_relativizer.rb} +5 -13
  143. data/lib/cov_loupe/paths/path_utils.rb +265 -0
  144. data/lib/cov_loupe/paths/volume_case_sensitivity.rb +173 -0
  145. data/lib/cov_loupe/presenters/base_coverage_presenter.rb +9 -13
  146. data/lib/cov_loupe/presenters/coverage_payload_presenter.rb +21 -0
  147. data/lib/cov_loupe/presenters/payload_caching.rb +23 -0
  148. data/lib/cov_loupe/presenters/project_coverage_presenter.rb +73 -21
  149. data/lib/cov_loupe/presenters/project_totals_presenter.rb +16 -10
  150. data/lib/cov_loupe/repositories/coverage_repository.rb +149 -0
  151. data/lib/cov_loupe/resolvers/coverage_line_resolver.rb +90 -76
  152. data/lib/cov_loupe/resolvers/{resolver_factory.rb → resolver_helpers.rb} +6 -5
  153. data/lib/cov_loupe/resolvers/resultset_path_resolver.rb +40 -12
  154. data/lib/cov_loupe/scripts/command_execution.rb +113 -0
  155. data/lib/cov_loupe/scripts/latest_ci_status.rb +97 -0
  156. data/lib/cov_loupe/scripts/pre_release_check.rb +164 -0
  157. data/lib/cov_loupe/scripts/setup_doc_server.rb +23 -0
  158. data/lib/cov_loupe/scripts/start_doc_server.rb +24 -0
  159. data/lib/cov_loupe/staleness/stale_status.rb +23 -0
  160. data/lib/cov_loupe/staleness/staleness_checker.rb +328 -0
  161. data/lib/cov_loupe/staleness/staleness_message_formatter.rb +91 -0
  162. data/lib/cov_loupe/tools/coverage_detailed_tool.rb +14 -15
  163. data/lib/cov_loupe/tools/coverage_raw_tool.rb +14 -14
  164. data/lib/cov_loupe/tools/coverage_summary_tool.rb +16 -16
  165. data/lib/cov_loupe/tools/coverage_table_tool.rb +139 -21
  166. data/lib/cov_loupe/tools/coverage_totals_tool.rb +31 -13
  167. data/lib/cov_loupe/tools/help_tool.rb +16 -20
  168. data/lib/cov_loupe/tools/list_tool.rb +65 -0
  169. data/lib/cov_loupe/tools/uncovered_lines_tool.rb +14 -14
  170. data/lib/cov_loupe/tools/validate_tool.rb +18 -24
  171. data/lib/cov_loupe/tools/version_tool.rb +8 -3
  172. data/lib/cov_loupe/version.rb +1 -1
  173. data/lib/cov_loupe.rb +83 -55
  174. metadata +184 -154
  175. data/docs/dev/BRANCH_ONLY_COVERAGE.md +0 -158
  176. data/docs/dev/arch-decisions/001-x-arch-decision.md +0 -95
  177. data/docs/dev/arch-decisions/002-x-arch-decision.md +0 -159
  178. data/docs/dev/arch-decisions/003-x-arch-decision.md +0 -165
  179. data/lib/cov_loupe/app_context.rb +0 -26
  180. data/lib/cov_loupe/constants.rb +0 -22
  181. data/lib/cov_loupe/coverage_reporter.rb +0 -31
  182. data/lib/cov_loupe/formatters.rb +0 -51
  183. data/lib/cov_loupe/mode_detector.rb +0 -56
  184. data/lib/cov_loupe/model.rb +0 -339
  185. data/lib/cov_loupe/presenters/coverage_detailed_presenter.rb +0 -14
  186. data/lib/cov_loupe/presenters/coverage_raw_presenter.rb +0 -14
  187. data/lib/cov_loupe/presenters/coverage_summary_presenter.rb +0 -14
  188. data/lib/cov_loupe/presenters/coverage_uncovered_presenter.rb +0 -14
  189. data/lib/cov_loupe/resultset_loader.rb +0 -131
  190. data/lib/cov_loupe/staleness_checker.rb +0 -247
  191. data/lib/cov_loupe/table_formatter.rb +0 -64
  192. data/lib/cov_loupe/tools/all_files_coverage_tool.rb +0 -51
  193. data/lib/cov_loupe/util.rb +0 -88
  194. data/spec/MCP_INTEGRATION_TESTS_README.md +0 -111
  195. data/spec/TIMESTAMPS.md +0 -48
  196. data/spec/all_files_coverage_tool_spec.rb +0 -53
  197. data/spec/app_config_spec.rb +0 -142
  198. data/spec/base_tool_spec.rb +0 -62
  199. data/spec/cli/show_default_report_spec.rb +0 -33
  200. data/spec/cli_enumerated_options_spec.rb +0 -90
  201. data/spec/cli_error_spec.rb +0 -184
  202. data/spec/cli_format_spec.rb +0 -123
  203. data/spec/cli_json_options_spec.rb +0 -50
  204. data/spec/cli_source_spec.rb +0 -44
  205. data/spec/cli_spec.rb +0 -192
  206. data/spec/cli_table_spec.rb +0 -28
  207. data/spec/cli_usage_spec.rb +0 -42
  208. data/spec/commands/base_command_spec.rb +0 -107
  209. data/spec/commands/command_factory_spec.rb +0 -76
  210. data/spec/commands/detailed_command_spec.rb +0 -34
  211. data/spec/commands/list_command_spec.rb +0 -28
  212. data/spec/commands/raw_command_spec.rb +0 -69
  213. data/spec/commands/summary_command_spec.rb +0 -34
  214. data/spec/commands/totals_command_spec.rb +0 -34
  215. data/spec/commands/uncovered_command_spec.rb +0 -55
  216. data/spec/commands/validate_command_spec.rb +0 -213
  217. data/spec/commands/version_command_spec.rb +0 -38
  218. data/spec/constants_spec.rb +0 -61
  219. data/spec/cov_loupe/formatters/source_formatter_spec.rb +0 -267
  220. data/spec/cov_loupe/formatters_spec.rb +0 -76
  221. data/spec/cov_loupe/presenters/base_coverage_presenter_spec.rb +0 -79
  222. data/spec/cov_loupe_model_spec.rb +0 -454
  223. data/spec/cov_loupe_module_spec.rb +0 -37
  224. data/spec/cov_loupe_opts_spec.rb +0 -185
  225. data/spec/coverage_reporter_spec.rb +0 -102
  226. data/spec/coverage_table_tool_spec.rb +0 -59
  227. data/spec/coverage_totals_tool_spec.rb +0 -37
  228. data/spec/error_handler_spec.rb +0 -197
  229. data/spec/error_mode_spec.rb +0 -139
  230. data/spec/errors_edge_cases_spec.rb +0 -312
  231. data/spec/errors_stale_spec.rb +0 -83
  232. data/spec/file_based_mcp_tools_spec.rb +0 -99
  233. data/spec/help_tool_spec.rb +0 -26
  234. data/spec/integration_spec.rb +0 -789
  235. data/spec/logging_fallback_spec.rb +0 -128
  236. data/spec/mcp_logging_spec.rb +0 -44
  237. data/spec/mcp_server_integration_spec.rb +0 -23
  238. data/spec/mcp_server_spec.rb +0 -106
  239. data/spec/mode_detector_spec.rb +0 -153
  240. data/spec/model_error_handling_spec.rb +0 -269
  241. data/spec/model_staleness_spec.rb +0 -79
  242. data/spec/option_normalizers_spec.rb +0 -203
  243. data/spec/option_parsers/env_options_parser_spec.rb +0 -221
  244. data/spec/option_parsers/error_helper_spec.rb +0 -222
  245. data/spec/path_relativizer_spec.rb +0 -98
  246. data/spec/presenters/coverage_detailed_presenter_spec.rb +0 -19
  247. data/spec/presenters/coverage_raw_presenter_spec.rb +0 -15
  248. data/spec/presenters/coverage_summary_presenter_spec.rb +0 -15
  249. data/spec/presenters/coverage_uncovered_presenter_spec.rb +0 -16
  250. data/spec/presenters/project_coverage_presenter_spec.rb +0 -87
  251. data/spec/presenters/project_totals_presenter_spec.rb +0 -144
  252. data/spec/resolvers/coverage_line_resolver_spec.rb +0 -282
  253. data/spec/resolvers/resolver_factory_spec.rb +0 -61
  254. data/spec/resolvers/resultset_path_resolver_spec.rb +0 -60
  255. data/spec/resultset_loader_spec.rb +0 -167
  256. data/spec/shared_examples/README.md +0 -115
  257. data/spec/shared_examples/coverage_presenter_examples.rb +0 -66
  258. data/spec/shared_examples/file_based_mcp_tools.rb +0 -179
  259. data/spec/shared_examples/formatted_command_examples.rb +0 -64
  260. data/spec/shared_examples/mcp_tool_text_json_response.rb +0 -16
  261. data/spec/spec_helper.rb +0 -127
  262. data/spec/staleness_checker_spec.rb +0 -374
  263. data/spec/staleness_more_spec.rb +0 -42
  264. data/spec/support/cli_helpers.rb +0 -22
  265. data/spec/support/control_flow_helpers.rb +0 -20
  266. data/spec/support/fake_mcp.rb +0 -40
  267. data/spec/support/io_helpers.rb +0 -29
  268. data/spec/support/mcp_helpers.rb +0 -35
  269. data/spec/support/mcp_runner.rb +0 -66
  270. data/spec/support/mocking_helpers.rb +0 -30
  271. data/spec/table_format_spec.rb +0 -70
  272. data/spec/tools/validate_tool_spec.rb +0 -132
  273. data/spec/tools_error_handling_spec.rb +0 -130
  274. data/spec/util_spec.rb +0 -154
  275. data/spec/version_spec.rb +0 -123
  276. data/spec/version_tool_spec.rb +0 -141
  277. /data/{spec/fixtures/project1 → examples/fixtures/demo_project}/lib/bar.rb +0 -0
  278. /data/{spec/fixtures/project1 → examples/fixtures/demo_project}/lib/foo.rb +0 -0
  279. /data/lib/cov_loupe/{config_parser.rb → config/config_parser.rb} +0 -0
  280. /data/lib/cov_loupe/{predicate_evaluator.rb → config/predicate_evaluator.rb} +0 -0
  281. /data/lib/cov_loupe/{error_handler_factory.rb → errors/error_handler_factory.rb} +0 -0
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../base_tool'
4
- require_relative '../model'
5
- require_relative '../predicate_evaluator'
4
+ require_relative '../model/model'
5
+ require_relative '../config/predicate_evaluator'
6
6
 
7
7
  module CovLoupe
8
8
  module Tools
@@ -10,12 +10,12 @@ module CovLoupe
10
10
  description <<~DESC
11
11
  Validates coverage data against a predicate (Ruby code that evaluates to true/false).
12
12
  Use this to enforce coverage policies programmatically.
13
- Inputs: Either 'code' (Ruby string) OR 'file' (path to Ruby file), plus optional root/resultset/staleness/error_mode.
13
+ Inputs: Either 'code' (Ruby string) OR 'file' (path to Ruby file), plus optional root/resultset/raise_on_stale/error_mode.
14
14
  Output: JSON object {"result": Boolean} where true means policy passed, false means failed.
15
15
  On error (syntax error, file not found, etc.), returns an MCP error response.
16
16
  Security Warning: Predicates execute as arbitrary Ruby code with full system privileges.
17
17
  Examples:
18
- - "Check if all files have at least 80% coverage" → {"code": "->(m) { m.all_files.all? { |f| f['percentage'] >= 80 } }"}
18
+ - "Check if all files have at least 80% coverage" → {"code": "->(m) { m.list.all? { |f| f['percentage'] >= 80 } }"}
19
19
  - "Run coverage policy from file" → {"file": "coverage_policy.rb"}
20
20
  DESC
21
21
 
@@ -34,36 +34,30 @@ module CovLoupe
34
34
  }
35
35
  ))
36
36
  class << self
37
- def call(code: nil, file: nil, root: '.', resultset: nil, staleness: :off,
38
- error_mode: 'log', server_context:)
39
- with_error_handling('ValidateTool', error_mode: error_mode) do
40
- # Re-use logic from ValidateCommand, but adapt for MCP return format
41
- require_relative '../cli'
42
-
43
- # Create a minimal CLI shim to reuse command logic
44
- cli = CoverageCLI.new
45
- cli.config.root = root
46
- cli.config.resultset = resultset
47
- cli.config.staleness = staleness.to_sym
48
- cli.config.error_mode = error_mode.to_sym
49
-
50
- # We need to capture the boolean result instead of letting it exit
51
- # Commands::ValidateCommand is designed to exit, so we'll use the model and evaluator directly
52
- # This duplicates some logic from ValidateCommand#execute but avoids the exit(status) call
53
-
54
- model = CoverageModel.new(**cli.config.model_options)
37
+ def call(code: nil, file: nil, root: nil, resultset: nil, raise_on_stale: nil,
38
+ error_mode: 'log', output_chars: nil, server_context:)
39
+ # Normalize output_chars before error handling so errors also get converted
40
+ output_chars_sym = resolve_output_chars(output_chars, server_context)
41
+ with_error_handling('ValidateTool', error_mode: error_mode, output_chars: output_chars_sym) do
42
+ model, config = create_configured_model(
43
+ server_context: server_context,
44
+ root: root,
45
+ resultset: resultset,
46
+ raise_on_stale: raise_on_stale
47
+ )
55
48
 
56
49
  result = if code
57
50
  PredicateEvaluator.evaluate_code(code, model)
58
51
  elsif file
59
52
  # Resolve file path relative to root if needed
60
- predicate_path = File.expand_path(file, root)
53
+ predicate_path = File.expand_path(file, config[:root])
61
54
  PredicateEvaluator.evaluate_file(predicate_path, model)
62
55
  else
63
56
  raise UsageError, "Either 'code' or 'file' must be provided"
64
57
  end
65
58
 
66
- respond_json({ result: result }, name: 'validate_result.json', pretty: true)
59
+ respond_json({ result: result }, name: 'validate_result.json', pretty: true,
60
+ output_chars: output_chars_sym)
67
61
  end
68
62
  end
69
63
  end
@@ -15,12 +15,17 @@ module CovLoupe
15
15
  type: 'object',
16
16
  additionalProperties: false,
17
17
  properties: {
18
- error_mode: ERROR_MODE_PROPERTY
18
+ error_mode: ERROR_MODE_PROPERTY,
19
+ output_chars: COMMON_PROPERTIES[:output_chars]
19
20
  }
20
21
  )
21
22
  class << self
22
- def call(error_mode: 'log', server_context: nil, **_args)
23
- with_error_handling('VersionTool', error_mode: error_mode) do
23
+ # NOTE: output_chars is accepted for consistency and used in error handling,
24
+ # though the version string itself is already ASCII-only.
25
+ def call(error_mode: 'log', output_chars: nil, server_context: nil, **_args)
26
+ # Normalize output_chars before error handling so errors also get converted
27
+ output_chars_sym = resolve_output_chars(output_chars, server_context)
28
+ with_error_handling('VersionTool', error_mode: error_mode, output_chars: output_chars_sym) do
24
29
  ::MCP::Tool::Response.new([
25
30
  { 'type' => 'text', 'text' => "CovLoupe version: #{CovLoupe::VERSION}" }
26
31
  ])
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CovLoupe
4
- VERSION = '3.0.0'
4
+ VERSION = '4.0.0.pre' unless defined?(CovLoupe::VERSION)
5
5
  end
data/lib/cov_loupe.rb CHANGED
@@ -5,49 +5,63 @@ require 'time'
5
5
  require 'pathname'
6
6
  require 'set' # rubocop:disable Lint/RedundantRequireStatement -- Ruby >= 3.4 requires explicit require for set; RuboCop targets 3.2
7
7
  require 'optparse'
8
- require 'mcp'
9
- require 'mcp/server/transports/stdio_transport'
10
8
 
11
9
  require_relative 'cov_loupe/version'
12
- require_relative 'cov_loupe/app_context'
13
- require_relative 'cov_loupe/util'
14
- require_relative 'cov_loupe/errors'
15
- require_relative 'cov_loupe/error_handler'
16
- require_relative 'cov_loupe/error_handler_factory'
17
- require_relative 'cov_loupe/path_relativizer'
18
- require_relative 'cov_loupe/resultset_loader'
19
- require_relative 'cov_loupe/mode_detector'
20
- require_relative 'cov_loupe/model'
21
- require_relative 'cov_loupe/coverage_reporter'
22
- require_relative 'cov_loupe/base_tool'
23
- require_relative 'cov_loupe/tools/coverage_raw_tool'
24
- require_relative 'cov_loupe/tools/coverage_summary_tool'
25
- require_relative 'cov_loupe/tools/uncovered_lines_tool'
26
- require_relative 'cov_loupe/tools/coverage_detailed_tool'
27
- require_relative 'cov_loupe/tools/all_files_coverage_tool'
28
- require_relative 'cov_loupe/tools/coverage_totals_tool'
29
- require_relative 'cov_loupe/tools/coverage_table_tool'
30
- require_relative 'cov_loupe/tools/validate_tool'
31
- require_relative 'cov_loupe/tools/version_tool'
32
- require_relative 'cov_loupe/tools/help_tool'
33
- require_relative 'cov_loupe/mcp_server'
34
- require_relative 'cov_loupe/cli'
10
+ require_relative 'cov_loupe/config/app_context'
11
+ require_relative 'cov_loupe/errors/errors'
12
+ require_relative 'cov_loupe/errors/error_handler'
13
+ require_relative 'cov_loupe/errors/error_handler_factory'
14
+ require_relative 'cov_loupe/paths/path_relativizer'
15
+ require_relative 'cov_loupe/loaders/resultset_loader'
16
+ require_relative 'cov_loupe/model/model'
17
+ require_relative 'cov_loupe/coverage/coverage_reporter'
35
18
 
36
19
  module CovLoupe
37
20
  class << self
21
+ # === Context Management and Thread Safety ===
22
+ #
23
+ # CovLoupe manages configuration (logging, error handling) via `AppContext` objects.
24
+ # The resolution strategy is:
25
+ # 1. Thread-local: Use `Thread.current[:cov_loupe_context]` if set.
26
+ # 2. Global default: Fall back to `@internal_default_context`.
27
+ #
28
+ # This design supports both simple CLI usage (one global context) and multi-threaded
29
+ # library usage (per-thread contexts).
30
+ #
31
+ # Thread Safety:
32
+ # - `mutex` protects all reads/writes to `@internal_default_context`.
33
+ # - `default_log_file=` atomically updates the global default. Threads using the
34
+ # default (nil thread-local context) will immediately see the new value.
35
+ # - `active_log_file=` creates or updates a *thread-local* context, isolating
36
+ # changes to the current thread.
37
+ #
38
+ # This separation ensures that changing the global default is safe and predictable,
39
+ # while allowing threads to diverge when necessary without race conditions.
40
+
38
41
  THREAD_CONTEXT_KEY = :cov_loupe_context
42
+ private_constant :THREAD_CONTEXT_KEY
39
43
 
40
44
  def run(argv)
41
- # Prepend environment options once at entry point
42
- full_argv = extract_env_opts + argv
45
+ # Parse config to determine mode
46
+ require_relative 'cov_loupe/config/config_parser'
43
47
 
44
- if ModeDetector.cli_mode?(full_argv)
45
- # CLI mode: pass merged argv to CoverageCLI
48
+ begin
49
+ # Prepend environment options once at entry point
50
+ full_argv = extract_env_opts + argv
51
+ config = ConfigParser.parse(full_argv.dup)
52
+ rescue OptionParser::ParseError, ConfigurationError => e
53
+ warn "Error: #{e.message}"
54
+ warn "Run 'cov-loupe --help' for usage information."
55
+ exit 2
56
+ end
57
+
58
+ if config.mode == :cli
59
+ # CLI mode: load CLI components only
60
+ require_relative 'cov_loupe/loaders/all_cli'
46
61
  CoverageCLI.new.run(full_argv)
47
62
  else
48
- # MCP server mode: parse config once from full_argv
49
- require_relative 'cov_loupe/config_parser'
50
- config = ConfigParser.parse(full_argv)
63
+ # MCP server mode: load MCP server components only
64
+ require_relative 'cov_loupe/loaders/all_mcp'
51
65
 
52
66
  if config.log_file == 'stdout'
53
67
  raise ConfigurationError,
@@ -57,7 +71,7 @@ module CovLoupe
57
71
 
58
72
  handler = ErrorHandlerFactory.for_mcp_server(error_mode: config.error_mode)
59
73
  context = create_context(error_handler: handler, log_target: config.log_file,
60
- mode: :mcp)
74
+ mode: :mcp, app_config: config)
61
75
  with_context(context) { MCPServer.new(context: context).run }
62
76
  end
63
77
  end
@@ -74,11 +88,12 @@ module CovLoupe
74
88
  Thread.current[THREAD_CONTEXT_KEY] || default_context
75
89
  end
76
90
 
77
- def create_context(error_handler:, log_target: nil, mode: :library)
91
+ def create_context(error_handler:, log_target: nil, mode: :library, app_config: nil)
78
92
  AppContext.new(
79
93
  error_handler: error_handler,
80
94
  log_target: log_target.nil? ? default_context.log_target : log_target,
81
- mode: mode
95
+ mode: mode,
96
+ app_config: app_config
82
97
  )
83
98
  end
84
99
 
@@ -87,13 +102,11 @@ module CovLoupe
87
102
  end
88
103
 
89
104
  def default_log_file=(value)
90
- previous_default = default_context
91
- @default_context = previous_default.with_log_target(value)
92
- active = Thread.current[THREAD_CONTEXT_KEY]
93
- if active.nil? || active.log_target == previous_default.log_target
94
- Thread.current[THREAD_CONTEXT_KEY] = @default_context
105
+ mutex.synchronize do
106
+ previous_default = internal_default_context
107
+ @internal_default_context = previous_default.with(log_target: value)
95
108
  end
96
- value # rubocop:disable Lint/Void -- return assigned log target for symmetry
109
+ value # rubocop:disable Lint/Void -- Setter should return assigned value for direct calls.
97
110
  end
98
111
 
99
112
  def active_log_file
@@ -103,11 +116,11 @@ module CovLoupe
103
116
  def active_log_file=(value)
104
117
  current = Thread.current[THREAD_CONTEXT_KEY]
105
118
  Thread.current[THREAD_CONTEXT_KEY] = if current
106
- current.with_log_target(value)
119
+ current.with(log_target: value)
107
120
  else
108
- default_context.with_log_target(value)
121
+ base = mutex.synchronize { internal_default_context }
122
+ base.with(log_target: value)
109
123
  end
110
- value # rubocop:disable Lint/Void -- return assigned log target for symmetry
111
124
  end
112
125
 
113
126
  def error_handler
@@ -115,26 +128,41 @@ module CovLoupe
115
128
  end
116
129
 
117
130
  def error_handler=(handler)
118
- @default_context = default_context.with_error_handler(handler)
131
+ mutex.synchronize do
132
+ previous_default = internal_default_context
133
+ @internal_default_context = previous_default.with(error_handler: handler)
134
+ end
135
+ end
136
+
137
+ def logger
138
+ context.logger
139
+ end
140
+
141
+ # Returns true if running on Windows (mingw, mswin, cygwin).
142
+ def windows?
143
+ return @windows if defined?(@windows)
144
+
145
+ @windows = RUBY_PLATFORM.match?(/mingw|mswin|cygwin/)
146
+ end
147
+
148
+ private def mutex
149
+ @mutex ||= Mutex.new
119
150
  end
120
151
 
121
152
  private def default_context
122
- @default_context ||= AppContext.new(
153
+ mutex.synchronize { internal_default_context }
154
+ end
155
+
156
+ private def internal_default_context
157
+ @internal_default_context ||= AppContext.new(
123
158
  error_handler: ErrorHandlerFactory.for_cli,
124
159
  log_target: nil
125
160
  )
126
161
  end
127
162
 
128
163
  private def extract_env_opts
129
- require 'shellwords'
130
- opts_string = ENV['COV_LOUPE_OPTS']
131
- return [] unless opts_string && !opts_string.empty?
132
-
133
- begin
134
- Shellwords.split(opts_string)
135
- rescue ArgumentError
136
- [] # Ignore parsing errors
137
- end
164
+ require_relative 'cov_loupe/option_parsers/env_options_parser'
165
+ OptionParsers::EnvOptionsParser.new.parse_env_opts
138
166
  end
139
167
  end
140
168
  end