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,247 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'time'
4
- require 'pathname'
5
- require_relative 'errors'
6
- require_relative 'util'
7
-
8
- module CovLoupe
9
- # Lightweight service object to check staleness of coverage vs. sources
10
- class StalenessChecker
11
- MODES = [:off, :error].freeze
12
-
13
- def initialize(root:, resultset:, mode: :off, tracked_globs: nil, timestamp: nil)
14
- @root = File.absolute_path(root || '.')
15
- @resultset = resultset
16
- @mode = (mode || :off).to_sym
17
- @tracked_globs = tracked_globs
18
- @cov_timestamp = timestamp
19
- @resultset_path = nil
20
- end
21
-
22
- def off?
23
- @mode == :off
24
- end
25
-
26
- # Raise CoverageDataStaleError if stale (only in error mode)
27
- def check_file!(file_abs, coverage_lines)
28
- return if off?
29
-
30
- d = compute_file_staleness_details(file_abs, coverage_lines)
31
- # For single-file checks, missing files with recorded coverage count as stale
32
- # via length mismatch; project-level checks also handle deleted files explicitly.
33
- if d[:newer] || d[:len_mismatch]
34
- raise CoverageDataStaleError.new(
35
- nil,
36
- nil,
37
- file_path: rel(file_abs),
38
- file_mtime: d[:file_mtime],
39
- cov_timestamp: d[:coverage_timestamp],
40
- src_len: d[:src_len],
41
- cov_len: d[:cov_len],
42
- resultset_path: resultset_path
43
- )
44
- end
45
- end
46
-
47
- # Compute whether a specific file appears stale relative to coverage.
48
- # Ignores mode and never raises; returns true when:
49
- # - the file is missing/deleted, or
50
- # - the file mtime is newer than the coverage timestamp, or
51
- # - the source line count differs from the coverage lines array length (when present).
52
- def stale_for_file?(file_abs, coverage_lines)
53
- d = compute_file_staleness_details(file_abs, coverage_lines)
54
- return 'M' unless d[:exists]
55
- return 'T' if d[:newer]
56
- return 'L' if d[:len_mismatch]
57
-
58
- false
59
- end
60
-
61
- # Raise CoverageDataProjectStaleError if any covered file is newer or if
62
- # tracked files are missing from coverage, or coverage includes deleted files.
63
- def check_project!(coverage_map)
64
- return if off?
65
-
66
- ts = coverage_timestamp
67
- coverage_files = coverage_map.keys
68
-
69
- newer, deleted = compute_newer_and_deleted_files(coverage_files, ts)
70
- missing = compute_missing_files(coverage_files)
71
-
72
- return if newer.empty? && missing.empty? && deleted.empty?
73
-
74
- raise CoverageDataProjectStaleError.new(
75
- nil,
76
- nil,
77
- cov_timestamp: ts,
78
- newer_files: newer,
79
- missing_files: missing,
80
- deleted_files: deleted,
81
- resultset_path: resultset_path
82
- )
83
- end
84
-
85
- private def compute_newer_and_deleted_files(coverage_files, timestamp)
86
- existing, deleted_abs = coverage_files.partition { |abs| File.file?(abs) }
87
-
88
- newer = existing
89
- .select { |abs| File.mtime(abs).to_i > timestamp.to_i }
90
- .map { |abs| rel(abs) }
91
- deleted = deleted_abs.map { |abs| rel(abs) }
92
-
93
- [newer, deleted]
94
- end
95
-
96
- # Identifies tracked files that are missing from coverage.
97
- # Returns array of relative paths for files matched by tracked_globs but not in coverage.
98
- private def compute_missing_files(coverage_files)
99
- return [] unless @tracked_globs && Array(@tracked_globs).any?
100
-
101
- patterns = Array(@tracked_globs).map { |g| File.absolute_path(g, @root) }
102
- tracked = patterns
103
- .flat_map { |p| Dir.glob(p, File::FNM_EXTGLOB | File::FNM_PATHNAME) }
104
- .select { |p| File.file?(p) }
105
-
106
- covered_set = coverage_files.to_set
107
- tracked.reject { |abs| covered_set.include?(abs) }.map { |abs| rel(abs) }
108
- end
109
-
110
- private def coverage_timestamp
111
- @cov_timestamp || 0
112
- end
113
-
114
- private def resultset_path
115
- @resultset_path ||= CovUtil.find_resultset(@root, resultset: @resultset)
116
- rescue
117
- nil
118
- end
119
-
120
- private def safe_count_lines(path)
121
- return 0 unless File.file?(path)
122
-
123
- File.foreach(path).count
124
- rescue
125
- 0
126
- end
127
-
128
- private def missing_trailing_newline?(path)
129
- return false unless File.file?(path)
130
-
131
- File.open(path, 'rb') do |f|
132
- size = f.size
133
- return false if size.zero?
134
-
135
- f.seek(-1, IO::SEEK_END)
136
- f.getbyte != 0x0A
137
- end
138
- rescue
139
- false
140
- end
141
-
142
- private def rel(path)
143
- # Handle relative vs absolute path mismatches that cause ArgumentError
144
- Pathname.new(path).relative_path_from(Pathname.new(@root)).to_s
145
- rescue ArgumentError
146
- # Path is outside the project root or has a different prefix type, fall back to absolute path
147
- path.to_s
148
- end
149
-
150
- # Centralized computation of staleness-related details for a single file.
151
- # Returns a Hash with keys:
152
- # :exists, :file_mtime, :coverage_timestamp, :cov_len, :src_len, :newer, :len_mismatch
153
- private def compute_file_staleness_details(file_abs, coverage_lines)
154
- coverage_ts = coverage_timestamp
155
-
156
- exists = File.file?(file_abs)
157
- file_mtime = exists ? File.mtime(file_abs) : nil
158
-
159
- cov_len = coverage_lines.respond_to?(:length) ? coverage_lines.length : 0
160
- src_len = exists ? safe_count_lines(file_abs) : 0
161
-
162
- # Adjust source line count to handle edge cases with missing trailing newlines
163
- adjusted_src_len = adjust_line_count_for_missing_newline(
164
- file_abs: file_abs,
165
- exists: exists,
166
- cov_len: cov_len,
167
- src_len: src_len
168
- )
169
-
170
- # Check if the source file has been modified since coverage was generated
171
- len_mismatch = length_mismatch?(cov_len, adjusted_src_len)
172
- newer = check_file_newer_than_coverage(file_mtime, coverage_ts, len_mismatch)
173
-
174
- {
175
- exists: exists,
176
- file_mtime: file_mtime,
177
- coverage_timestamp: coverage_ts,
178
- cov_len: cov_len,
179
- src_len: src_len,
180
- newer: newer,
181
- len_mismatch: len_mismatch
182
- }
183
- end
184
-
185
- # Adjusts the source line count to account for files missing trailing newlines.
186
- #
187
- # Why this edge case exists:
188
- # - File.foreach counts lines by separator (typically \n)
189
- # - For a file with no trailing newline, File.foreach still counts all lines correctly
190
- # - However, some editors or file operations may report one extra line when checking
191
- # if the file doesn't end with a newline
192
- # - SimpleCov's coverage array length matches the logical line count (excluding trailing newline)
193
- # - If src_len is exactly one more than cov_len AND the file is missing a trailing newline,
194
- # we adjust src_len down by 1 to match SimpleCov's convention
195
- #
196
- # Example: A file with "line1\nline2\nline3" (no final \n)
197
- # - File.foreach counts: 3 lines
198
- # - SimpleCov coverage array length: 3
199
- # - No adjustment needed
200
- #
201
- # However, in certain edge cases where the file system or parsing reports an extra line:
202
- # - Reported line count: 4
203
- # - SimpleCov coverage array length: 3
204
- # - Missing trailing newline: true
205
- # - Adjustment: 4 - 1 = 3 (now matches)
206
- private def adjust_line_count_for_missing_newline(file_abs:, exists:, cov_len:, src_len:)
207
- # Only adjust if:
208
- # 1. File exists (can't check newlines for missing files)
209
- # 2. Coverage data is present (cov_len > 0)
210
- # 3. Source has exactly one more line than coverage
211
- # 4. File is missing a trailing newline
212
- needs_adjusting =
213
- exists && cov_len.positive? && src_len == cov_len + 1 && missing_trailing_newline?(file_abs)
214
- needs_adjusting ? src_len - 1 : src_len
215
- end
216
-
217
- # Checks if the source line count differs from the coverage line count.
218
- #
219
- # Why this check exists:
220
- # - When a file is modified after coverage is generated, the line count often changes
221
- # - A mismatch indicates the coverage data is stale and no longer represents the current file
222
- # - We only flag as mismatch when coverage data exists (cov_len > 0)
223
- #
224
- # Note: Empty coverage (cov_len == 0) is not considered a mismatch, as it may represent
225
- # files that were never executed or files that are legitimately empty.
226
- private def length_mismatch?(cov_len, adjusted_src_len)
227
- cov_len.positive? && adjusted_src_len != cov_len
228
- end
229
-
230
- # Determines if a file has been modified more recently than the coverage timestamp.
231
- #
232
- # Why this check exists:
233
- # - Files modified after coverage generation may have behavioral changes not captured
234
- # - However, if there's already a length mismatch, we prioritize that as the staleness indicator
235
- # - This prevents double-flagging: if lines changed, the file is already stale (length mismatch)
236
- #
237
- # The logic: newer &&= !len_mismatch means:
238
- # - If len_mismatch is true, set newer to false (length mismatch takes precedence)
239
- # - This way, staleness is categorized as either 'T' (time-based) OR 'L' (length-based), not both
240
- private def check_file_newer_than_coverage(file_mtime, coverage_ts, len_mismatch)
241
- newer = !!(file_mtime && file_mtime.to_i > coverage_ts.to_i)
242
- # If there's a length mismatch, don't also flag as "newer" - the mismatch is more specific
243
- newer &&= !len_mismatch
244
- newer
245
- end
246
- end
247
- end
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CovLoupe
4
- # General-purpose table formatter with box-drawing characters
5
- # Used by commands to create consistent formatted output
6
- class TableFormatter
7
- # Format data as a table with box-drawing characters
8
- # @param headers [Array<String>] Column headers
9
- # @param rows [Array<Array>] Data rows (each row is an array of cell values)
10
- # @param alignments [Array<Symbol>] Column alignments (:left, :right, :center)
11
- # @return [String] Formatted table
12
- def self.format(headers:, rows:, alignments: nil)
13
- return 'No data to display' if rows.empty?
14
-
15
- alignments ||= [:left] * headers.size
16
- all_rows = [headers] + rows.map { |row| row.map(&:to_s) }
17
-
18
- # Calculate column widths
19
- widths = headers.size.times.map do |col|
20
- all_rows.map { |row| row[col].to_s.length }.max
21
- end
22
-
23
- lines = []
24
- lines << border_line(widths, '┌', '┬', '┐')
25
- lines << data_row(headers, widths, alignments)
26
- lines << border_line(widths, '├', '┼', '┤')
27
- rows.each { |row| lines << data_row(row, widths, alignments) }
28
- lines << border_line(widths, '└', '┴', '┘')
29
-
30
- lines.join("\n")
31
- end
32
-
33
- # Format a single key-value table (vertical layout)
34
- # @param data [Hash] Key-value pairs
35
- # @return [String] Formatted table
36
- def self.format_vertical(data)
37
- rows = data.map { |k, v| [k.to_s, v.to_s] }
38
- format(headers: ['Key', 'Value'], rows: rows, alignments: [:left, :left])
39
- end
40
-
41
- private_class_method def self.border_line(widths, left, mid, right)
42
- segments = widths.map { |w| '─' * (w + 2) }
43
- left + segments.join(mid) + right
44
- end
45
-
46
- private_class_method def self.data_row(cells, widths, alignments)
47
- formatted = cells.each_with_index.map do |cell, i|
48
- align_cell(cell.to_s, widths[i], alignments[i])
49
- end
50
- "│ #{formatted.join(' │ ')} │"
51
- end
52
-
53
- private_class_method def self.align_cell(content, width, alignment)
54
- case alignment
55
- when :right
56
- content.rjust(width)
57
- when :center
58
- content.center(width)
59
- else # :left
60
- content.ljust(width)
61
- end
62
- end
63
- end
64
- end
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../model'
4
- require_relative '../base_tool'
5
- require_relative '../presenters/project_coverage_presenter'
6
-
7
- module CovLoupe
8
- module Tools
9
- class AllFilesCoverageTool < BaseTool
10
- description <<~DESC
11
- Use this when the user wants coverage percentages for every tracked file in the project.
12
- Do not use this for single-file stats; prefer coverage.summary or coverage.uncovered_lines for that.
13
- Inputs: optional project root, alternate .resultset path, sort order, staleness mode, and tracked_globs to alert on new files.
14
- Output: JSON {"files": [{"file","covered","total","percentage","stale"}, ...], "counts": {"total", "ok", "stale"}} sorted as requested. "stale" is a string ('M', 'T', 'L') or false.
15
- Examples: "List files with the lowest coverage"; "Show repo coverage sorted descending".
16
- DESC
17
- input_schema(**coverage_schema(
18
- additional_properties: {
19
- sort_order: {
20
- type: 'string',
21
- description: 'Sort order for coverage percentages.' \
22
- "'ascending' highlights the riskiest files first.",
23
- default: 'ascending',
24
- enum: ['ascending', 'descending']
25
- },
26
- tracked_globs: TRACKED_GLOBS_PROPERTY
27
- }
28
- ))
29
- class << self
30
- def call(root: '.', resultset: nil, sort_order: 'ascending', staleness: :off,
31
- tracked_globs: nil, error_mode: 'log', server_context:)
32
- with_error_handling('AllFilesCoverageTool', error_mode: error_mode) do
33
- # Convert string inputs from MCP to symbols for internal use
34
- sort_order_sym = sort_order.to_sym
35
- staleness_sym = staleness.to_sym
36
-
37
- model = CoverageModel.new(root: root, resultset: resultset, staleness: staleness_sym,
38
- tracked_globs: tracked_globs)
39
- presenter = Presenters::ProjectCoveragePresenter.new(
40
- model: model,
41
- sort_order: sort_order_sym,
42
- check_stale: (staleness_sym == :error),
43
- tracked_globs: tracked_globs
44
- )
45
- respond_json(presenter.relativized_payload, name: 'all_files_coverage.json')
46
- end
47
- end
48
- end
49
- end
50
- end
51
- end
@@ -1,88 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'resolvers/resolver_factory'
4
-
5
- module CovLoupe
6
- RESULTSET_CANDIDATES = [
7
- '.resultset.json',
8
- 'coverage/.resultset.json',
9
- 'tmp/.resultset.json'
10
- ].freeze
11
-
12
- DEFAULT_LOG_FILESPEC = './cov_loupe.log'
13
-
14
- module CovUtil
15
- module_function def log(msg)
16
- log_file = CovLoupe.active_log_file
17
-
18
- case log_file
19
- when 'stdout'
20
- $stdout.puts "[#{Time.now.iso8601}] #{msg}"
21
- when 'stderr'
22
- $stderr.puts "[#{Time.now.iso8601}] #{msg}"
23
- else
24
- # Handles both nil (default) and custom file paths
25
- path_to_log = log_file || DEFAULT_LOG_FILESPEC
26
- File.open(File.expand_path(path_to_log), 'a') { |f| f.puts "[#{Time.now.iso8601}] #{msg}" }
27
- end
28
- rescue => e
29
- # Fallback to stderr if file logging fails, but suppress in MCP mode
30
- # to avoid interfering with JSON-RPC protocol
31
- unless CovLoupe.context.mcp_mode?
32
- begin
33
- $stderr.puts "[#{Time.now.iso8601}] LOGGING ERROR: #{e.message}"
34
- $stderr.puts "[#{Time.now.iso8601}] #{msg}"
35
- rescue
36
- # Silently ignore only stderr fallback failures
37
- end
38
- end
39
- end
40
-
41
- # Safe logging that never raises - use when logging should not interrupt execution.
42
- # Unlike `log`, this method guarantees it will never propagate exceptions.
43
- module_function def safe_log(msg)
44
- log(msg)
45
- rescue
46
- # Silently ignore all logging failures
47
- end
48
-
49
- module_function def find_resultset(root, resultset: nil)
50
- Resolvers::ResolverFactory.find_resultset(root, resultset: resultset)
51
- end
52
-
53
- module_function def lookup_lines(cov, file_abs)
54
- Resolvers::ResolverFactory.lookup_lines(cov, file_abs)
55
- end
56
-
57
- module_function def summary(arr)
58
- total = 0
59
- covered = 0
60
- arr.compact.each do |hits|
61
- total += 1
62
- covered += 1 if hits.to_i > 0
63
- end
64
- percentage = total.zero? ? 100.0 : ((covered.to_f * 100.0 / total) * 100).round / 100.0
65
- { 'covered' => covered, 'total' => total, 'percentage' => percentage }
66
- end
67
-
68
- module_function def uncovered(arr)
69
- out = []
70
-
71
- arr.each_with_index do |hits, i|
72
- next if hits.nil?
73
-
74
- out << (i + 1) if hits.to_i.zero?
75
- end
76
- out
77
- end
78
-
79
- module_function def detailed(arr)
80
- rows = []
81
- arr.each_with_index do |hits, i|
82
- h = hits&.to_i
83
- rows << { 'line' => i + 1, 'hits' => h, 'covered' => h.positive? } if h
84
- end
85
- rows
86
- end
87
- end
88
- end
@@ -1,111 +0,0 @@
1
- # MCP Server Protocol Integration Tests
2
-
3
- ## Overview
4
-
5
- This document describes the comprehensive integration tests added for the SimpleCov MCP server protocol in `spec/integration_spec.rb`.
6
-
7
- ## Test Coverage
8
-
9
- The integration tests spawn the actual MCP server as a subprocess and communicate with it via JSON-RPC over stdio, testing the complete end-to-end protocol implementation.
10
-
11
- ### Tests Added (12 total)
12
-
13
- 1. **starts MCP server without errors** - Verifies the server starts and responds to basic requests without NameError or other initialization issues
14
- 2. **handles tools/list request** - Confirms all 8 expected tools are properly registered
15
- 3. **executes coverage_summary_tool via JSON-RPC** - Tests single-file coverage summary queries
16
- 4. **executes all_files_coverage_tool via JSON-RPC** - Tests project-wide coverage listing
17
- 5. **executes uncovered_lines_tool via JSON-RPC** - Tests uncovered line detection
18
- 6. **executes help_tool via JSON-RPC** - Tests help/documentation retrieval
19
- 7. **executes version_tool via JSON-RPC** - Tests version information queries
20
- 8. **handles error responses for invalid tool calls** - Verifies graceful error handling
21
- 9. **handles malformed JSON-RPC requests** - Tests robustness against invalid input
22
- 10. **respects --log-file configuration in MCP mode** - Tests logging configuration
23
- 11. **prohibits stdout logging in MCP mode** - Ensures stdout isn't corrupted
24
- 12. **handles multiple sequential requests** - Tests statelessness and multi-request handling
25
-
26
- ## Why These Tests Are Critical
27
-
28
- ### Issue #1 from Analysis: Missing `require 'optparse'`
29
-
30
- The critical bug (missing `require 'optparse'` in `lib/cov_loupe.rb:110`) was not caught by existing tests because:
31
-
32
- - Unit tests loaded the full gem which transitively required optparse through the CLI
33
- - MCP tools were tested in-process without spawning the server
34
- - No integration tests verified the MCP server startup sequence
35
-
36
- ### What These Tests Catch
37
-
38
- * ✅ **Server Initialization Errors**: NameError, LoadError, missing requires
39
- * ✅ **Protocol Compliance**: Valid JSON-RPC request/response format
40
- * ✅ **Tool Registration**: All tools properly configured and accessible
41
- * ✅ **Data Accuracy**: Coverage data correctly passed from fixtures
42
- * ✅ **Error Handling**: Graceful responses for invalid requests
43
- * ✅ **Configuration**: Environment variables and options properly handled
44
- * ✅ **Statelessness**: Multiple requests handled independently
45
- * ✅ **Stream Integrity**: Stdout not corrupted by logging
46
-
47
- ## Test Architecture
48
-
49
- ### Helper Methods
50
-
51
- - **`run_mcp_request(request_hash, timeout: 5)`**: Spawns MCP server, sends JSON-RPC request, returns stdout/stderr/status
52
- - **`parse_jsonrpc_response(output)`**: Extracts JSON-RPC response from output (handles mixed stderr/stdout)
53
-
54
- ### Test Fixtures
55
-
56
- Uses `spec/fixtures/project1/` with known coverage data:
57
- - `lib/foo.rb`: 66.67% coverage (2/3 lines, line 2 uncovered)
58
- - `lib/bar.rb`: 33.33% coverage (1/3 lines)
59
-
60
- ### Test Execution
61
-
62
- ```bash
63
- # Run all MCP integration tests
64
- bundle exec rspec spec/integration_spec.rb --tag slow
65
-
66
- # Run specific integration test
67
- bundle exec rspec spec/integration_spec.rb:363
68
- ```
69
-
70
- ## Performance
71
-
72
- - Total execution time: ~2.1 seconds for all 12 tests
73
- - Tagged with `:slow` to allow exclusion from quick test runs
74
- - Uses `Open3.popen3` for subprocess management
75
- - 5-second timeout per request (configurable)
76
-
77
- ## Coverage Impact
78
-
79
- These tests increased the overall test count from 272 to 284 examples and improved confidence in the MCP server mode, which is the primary use case for AI assistant integration.
80
-
81
- ### Before Integration Tests
82
- - 272 examples
83
- - Missing `require 'optparse'` bug went undetected
84
- - MCP server mode untested end-to-end
85
-
86
- ### After Integration Tests
87
- - 284 examples
88
- - MCP server startup verified
89
- - Full JSON-RPC protocol tested
90
- - Would catch Issue #1 immediately
91
-
92
- ## Future Enhancements
93
-
94
- Potential additions:
95
- - Test connection lifecycle (startup, multiple sessions, shutdown)
96
- - Test concurrent requests (if supported)
97
- - Test large coverage datasets (performance)
98
- - Test network transport (if added)
99
- - Test authentication/authorization (if added)
100
-
101
- ## Related Files
102
-
103
- - `spec/integration_spec.rb` - Main integration test file (lines 308-683)
104
- - `lib/cov_loupe.rb` - Entry point with mode detection
105
- - `lib/cov_loupe/mcp_server.rb` - MCP server implementation
106
- - `exe/cov-loupe` - Executable entry point
107
-
108
- ## References
109
-
110
- - [MCP Protocol Specification](https://modelcontextprotocol.io/)
111
- - [JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification)
data/spec/TIMESTAMPS.md DELETED
@@ -1,48 +0,0 @@
1
- # Test Timestamp Documentation
2
-
3
- This document explains the timestamp constants used throughout the test suite for consistent and documented test data.
4
-
5
- ## Constants (defined in `spec_helper.rb`)
6
-
7
- ### `FIXTURE_COVERAGE_TIMESTAMP = 1_720_000_000`
8
- - **Human readable**: 2024-07-03 16:26:40 UTC (July 3rd, 2024)
9
- - **Purpose**: The "generated" timestamp for coverage data in `spec/fixtures/project1/coverage/.resultset.json`
10
- - **Usage**: Used in tests that verify timestamp parsing and calculations with realistic coverage data
11
-
12
- ### `VERY_OLD_TIMESTAMP = 0`
13
- - **Human readable**: 1970-01-01 00:00:00 UTC (Unix epoch)
14
- - **Purpose**: Simulates extremely stale coverage data (much older than any real file)
15
- - **Usage**: Used in staleness tests to force stale coverage scenarios
16
-
17
- ### `TEST_FILE_TIMESTAMP = 1_000`
18
- - **Human readable**: 1970-01-01 00:16:40 UTC (16 minutes and 40 seconds after epoch)
19
- - **Purpose**: Used for stale error formatting tests to create predictable time deltas
20
- - **Usage**: Creates a 1000-second (16m 40s) difference from `VERY_OLD_TIMESTAMP` for delta calculations
21
-
22
- ## Conversion Reference
23
-
24
- To convert timestamps for debugging:
25
-
26
- ```bash
27
- # Unix timestamp to human readable
28
- date -d @1720000000
29
- # Wed Jul 3 16:26:40 UTC 2024
30
-
31
- # Human readable to Unix timestamp
32
- date -d "2024-07-03 16:26:40 UTC" +%s
33
- # 1720000000
34
- ```
35
-
36
- ## Why These Values?
37
-
38
- - **Realistic but static**: `FIXTURE_COVERAGE_TIMESTAMP` is a realistic recent date that won't change
39
- - **Predictable deltas**: The differences between timestamps create predictable test scenarios
40
- - **Clear intent**: Named constants make it obvious what each timestamp represents in tests
41
-
42
- ## Files Using These Constants
43
-
44
- - `spec/util_spec.rb` - Tests timestamp parsing from fixture
45
- - `spec/model_staleness_spec.rb` - Tests staleness detection logic
46
- - `spec/errors_stale_spec.rb` - Tests stale error message formatting
47
- - `spec/cli_error_spec.rb` - Tests CLI error handling for stale coverage
48
- - `spec/fixtures/project1/coverage/.resultset.json` - Contains the actual timestamp data
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'spec_helper'
4
- require 'cov_loupe/tools/all_files_coverage_tool'
5
-
6
- RSpec.describe CovLoupe::Tools::AllFilesCoverageTool do
7
- subject(:call_tool) { described_class.call(root: root, server_context: server_context) }
8
-
9
- let(:root) { (FIXTURES_DIR / 'project1').to_s }
10
- let(:server_context) { instance_double('ServerContext').as_null_object }
11
-
12
- before do
13
- setup_mcp_response_stub
14
- model = instance_double(CovLoupe::CoverageModel)
15
- allow(CovLoupe::CoverageModel).to receive(:new).and_return(model)
16
-
17
- payload = {
18
- 'files' => [
19
- { 'file' => 'lib/foo.rb', 'percentage' => 100.0, 'covered' => 10, 'total' => 10,
20
- 'stale' => false },
21
- { 'file' => 'lib/bar.rb', 'percentage' => 50.0, 'covered' => 5, 'total' => 10,
22
- 'stale' => true }
23
- ],
24
- 'counts' => { 'total' => 2, 'ok' => 1, 'stale' => 1 }
25
- }
26
-
27
- presenter = instance_double(CovLoupe::Presenters::ProjectCoveragePresenter)
28
- allow(CovLoupe::Presenters::ProjectCoveragePresenter).to receive(:new).and_return(presenter)
29
- allow(presenter).to receive(:relativized_payload).and_return(payload)
30
- end
31
-
32
-
33
- it_behaves_like 'an MCP tool that returns text JSON'
34
-
35
- it 'returns all files coverage data with counts' do
36
- response = call_tool
37
- data, _item = expect_mcp_text_json(response, expected_keys: ['files', 'counts'])
38
-
39
- files = data['files']
40
- counts = data['counts']
41
-
42
- expect(files.length).to eq(2)
43
- expect(counts).to include('total' => 2).or include(total: 2)
44
- expect(files.map { |f| f['file'] }).to eq(['lib/foo.rb', 'lib/bar.rb'])
45
-
46
- # ok + stale equals total
47
- ok = counts[:ok] || counts['ok']
48
- stale = counts[:stale] || counts['stale']
49
- total = counts[:total] || counts['total']
50
- expect(ok + stale).to eq(total)
51
- expect(stale).to eq(1)
52
- end
53
- end