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,6 +1,6 @@
1
1
  # Library API Guide
2
2
 
3
- [Back to main README](../README.md)
3
+ [Back to main README](../index.md)
4
4
 
5
5
  Use this gem programmatically to inspect coverage without running the CLI or MCP server. The primary entry point is `CovLoupe::CoverageModel`.
6
6
 
@@ -11,7 +11,6 @@ Use this gem programmatically to inspect coverage without running the CLI or MCP
11
11
  - [Return Types](#return-types)
12
12
  - [Error Handling](#error-handling)
13
13
  - [Advanced Recipes](#advanced-recipes)
14
- - [API Stability](#api-stability)
15
14
 
16
15
  ## Quick Start
17
16
 
@@ -21,45 +20,54 @@ require "cov_loupe"
21
20
  # Defaults (omit args; shown here with comments):
22
21
  # - root: "."
23
22
  # - resultset: resolved from common paths under root
24
- # - staleness: "off" (no stale checks)
25
- # - tracked_globs: nil (no project-level file-set checks)
23
+ # - raise_on_stale: false (don't raise on stale data)
24
+ # - tracked_globs: [] (no project-level file-set checks)
26
25
  model = CovLoupe::CoverageModel.new
27
26
 
28
27
  # Custom configuration (non-default values):
29
28
  model = CovLoupe::CoverageModel.new(
30
- root: "/path/to/project", # non-default project root
31
- resultset: "build/coverage", # file or directory containing .resultset.json
32
- staleness: "error", # enable stale checks (raise on stale)
33
- tracked_globs: ["lib/**/*.rb"] # for 'all_files' staleness: flag new/missing files
29
+ root: File.join(Dir.home, 'project'), # non-default project root
30
+ resultset: "build/coverage", # file or directory containing .resultset.json
31
+ raise_on_stale: true, # enable strict staleness checks (raise on stale)
32
+ tracked_globs: ["lib/cov_loupe/tools/**/*.rb"] # for 'list' staleness: flag new/missing files
34
33
  )
35
34
 
36
35
  # List all files with coverage summary
37
- files = model.all_files
36
+ list_result = model.list
37
+ files = list_result['files']
38
38
  # Per-file queries
39
- summary = model.summary_for("lib/foo.rb")
40
- uncovered = model.uncovered_for("lib/foo.rb")
41
- detailed = model.detailed_for("lib/foo.rb")
42
- raw = model.raw_for("lib/foo.rb")
39
+
40
+ target = 'lib/cov_loupe/base_tool.rb'
41
+ summary = model.summary_for(target)
42
+ uncovered = model.uncovered_for(target)
43
+ detailed = model.detailed_for(target)
44
+ raw = model.raw_for(target)
43
45
  ```
44
46
 
45
47
  ## Method Reference
46
48
 
47
- ### `all_files(sort_order: :descending)`
49
+ ### `list(sort_order: :descending, raise_on_stale: nil, tracked_globs: [])`
48
50
 
49
51
  Returns coverage summary for all files in the resultset.
50
52
 
51
53
  **Parameters:**
52
54
  - `sort_order` (Symbol, optional): `:descending` (default) or `:ascending` by coverage percentage
55
+ - `raise_on_stale` (Boolean, optional): Whether to raise error if project is stale. Defaults to model setting.
56
+ - `tracked_globs` (Array<String>, optional): Patterns to filter files (also used for staleness checks)
53
57
 
54
- **Returns:** `Array<Hash>` - See [all_files return type](#all_files)
58
+ **Returns:** `Hash` - See [list return type](#list)
55
59
 
56
60
  **Example:**
57
61
  ```ruby
58
- files = model.all_files
59
- # => [ { 'file' => '/abs/path/lib/foo.rb', 'covered' => 12, 'total' => 14, 'percentage' => 85.71, 'stale' => false }, ... ]
62
+ list_result = model.list
63
+ files = list_result['files']
64
+ # => [ { 'file' => '/abs/path/lib/foo.rb', 'covered' => 12, 'total' => 14, 'percentage' => 85.71, 'stale' => "ok" }, ... ]
60
65
 
61
66
  # Get worst coverage first
62
- worst_files = model.all_files(sort_order: :ascending).first(10)
67
+ worst_files = model.list(sort_order: :ascending)['files'].first(10)
68
+
69
+ # Force staleness check
70
+ model.list(raise_on_stale: true)
63
71
  ```
64
72
 
65
73
  ### `summary_for(path)`
@@ -67,7 +75,7 @@ worst_files = model.all_files(sort_order: :ascending).first(10)
67
75
  Returns coverage summary for a specific file.
68
76
 
69
77
  **Parameters:**
70
- - `path` (String): File path (absolute, relative to root, or basename)
78
+ - `path` (String): File path (absolute or relative to root)
71
79
 
72
80
  **Returns:** `Hash` - See [summary_for return type](#summary_for)
73
81
 
@@ -75,8 +83,8 @@ Returns coverage summary for a specific file.
75
83
 
76
84
  **Example:**
77
85
  ```ruby
78
- summary = model.summary_for("lib/foo.rb")
79
- # => { 'file' => '/abs/.../lib/foo.rb', 'summary' => {'covered'=>12, 'total'=>14, 'percentage'=>85.71}, 'stale' => false }
86
+ summary = model.summary_for(target)
87
+ # => { 'file' => '/abs/.../lib/foo.rb', 'summary' => {'covered'=>12, 'total'=>14, 'percentage'=>85.71} }
80
88
  ```
81
89
 
82
90
  ### `uncovered_for(path)`
@@ -84,7 +92,7 @@ summary = model.summary_for("lib/foo.rb")
84
92
  Returns list of uncovered line numbers for a specific file.
85
93
 
86
94
  **Parameters:**
87
- - `path` (String): File path (absolute, relative to root, or basename)
95
+ - `path` (String): File path (absolute or relative to root)
88
96
 
89
97
  **Returns:** `Hash` - See [uncovered_for return type](#uncovered_for)
90
98
 
@@ -93,7 +101,7 @@ Returns list of uncovered line numbers for a specific file.
93
101
  **Example:**
94
102
  ```ruby
95
103
  uncovered = model.uncovered_for("lib/foo.rb")
96
- # => { 'file' => '/abs/.../lib/foo.rb', 'uncovered' => [5, 9, 12], 'summary' => { ... }, 'stale' => false }
104
+ # => { 'file' => '/abs/.../lib/foo.rb', 'uncovered' => [5, 9, 12], 'summary' => { ... } }
97
105
  ```
98
106
 
99
107
  ### `detailed_for(path)`
@@ -101,7 +109,7 @@ uncovered = model.uncovered_for("lib/foo.rb")
101
109
  Returns per-line coverage details with hit counts.
102
110
 
103
111
  **Parameters:**
104
- - `path` (String): File path (absolute, relative to root, or basename)
112
+ - `path` (String): File path (absolute or relative to root)
105
113
 
106
114
  **Returns:** `Hash` - See [detailed_for return type](#detailed_for)
107
115
 
@@ -110,7 +118,7 @@ Returns per-line coverage details with hit counts.
110
118
  **Example:**
111
119
  ```ruby
112
120
  detailed = model.detailed_for("lib/foo.rb")
113
- # => { 'file' => '/abs/.../lib/foo.rb', 'lines' => [{'line' => 1, 'hits' => 1, 'covered' => true}, ...], 'summary' => { ... }, 'stale' => false }
121
+ # => { 'file' => '/abs/.../lib/foo.rb', 'lines' => [{'line' => 1, 'hits' => 1, 'covered' => true}, ...], 'summary' => { ... } }
114
122
  ```
115
123
 
116
124
  ### `raw_for(path)`
@@ -118,7 +126,7 @@ detailed = model.detailed_for("lib/foo.rb")
118
126
  Returns raw SimpleCov lines array for a specific file.
119
127
 
120
128
  **Parameters:**
121
- - `path` (String): File path (absolute, relative to root, or basename)
129
+ - `path` (String): File path (absolute or relative to root)
122
130
 
123
131
  **Returns:** `Hash` - See [raw_for return type](#raw_for)
124
132
 
@@ -127,16 +135,18 @@ Returns raw SimpleCov lines array for a specific file.
127
135
  **Example:**
128
136
  ```ruby
129
137
  raw = model.raw_for("lib/foo.rb")
130
- # => { 'file' => '/abs/.../lib/foo.rb', 'lines' => [nil, 1, 0, 3, ...], 'stale' => false }
138
+ # => { 'file' => '/abs/.../lib/foo.rb', 'lines' => [nil, 1, 0, 3, ...] }
131
139
  ```
132
140
 
133
- ### `format_table(rows = nil, sort_order: :descending)`
141
+ ### `format_table(rows = nil, sort_order: :descending, raise_on_stale: nil, tracked_globs: nil)`
134
142
 
135
143
  Generates formatted ASCII table string.
136
144
 
137
145
  **Parameters:**
138
- - `rows` (Array<Hash>, optional): Custom row data; defaults to `all_files`
146
+ - `rows` (Array<Hash>, optional): Custom row data; defaults to `list`
139
147
  - `sort_order` (Symbol, optional): `:descending` (default) or `:ascending`
148
+ - `raise_on_stale` (Boolean, optional): Whether to raise error if project is stale. Defaults to model setting.
149
+ - `tracked_globs` (Array<String>, optional): Patterns to filter files.
140
150
 
141
151
  **Returns:** `String` - Formatted table with Unicode borders
142
152
 
@@ -147,29 +157,38 @@ table = model.format_table
147
157
  puts table
148
158
 
149
159
  # Custom rows
150
- lib_files = model.all_files.select { |f| f['file'].include?('/lib/') }
160
+ lib_files = model.list['files'].select { |f| f['file'].include?('/lib/') }
151
161
  lib_table = model.format_table(lib_files, sort_order: :descending)
152
162
  puts lib_table
153
163
  ```
154
164
 
155
- ### `project_totals(tracked_globs: nil)`
165
+ ### `project_totals(tracked_globs: [], raise_on_stale: nil)`
156
166
 
157
167
  Returns aggregated coverage totals across all files.
158
168
 
159
169
  **Parameters:**
160
170
  - `tracked_globs` (Array<String> or String, optional): Glob patterns to filter files
171
+ - `raise_on_stale` (Boolean, optional): Whether to raise error if project is stale. Defaults to model setting.
161
172
 
162
173
  **Returns:** `Hash` - See [project_totals return type](#project_totals)
163
174
 
164
175
  **Example:**
165
176
  ```ruby
166
177
  totals = model.project_totals
167
- # => { 'lines' => { 'total' => 123, 'covered' => 100, 'uncovered' => 23 }, 'percentage' => 81.3, 'files' => { 'total' => 5, 'ok' => 4, 'stale' => 1 } }
178
+ # => {
179
+ # 'lines' => { 'total' => 123, 'covered' => 100, 'uncovered' => 23, 'percent_covered' => 81.3 },
180
+ # 'tracking' => { 'enabled' => true, 'globs' => ['lib/**/*.rb'] },
181
+ # 'files' => { 'total' => 4, 'with_coverage' => { 'total' => 4, 'ok' => 4, 'stale' => { ... } } }
182
+ # }
168
183
 
169
184
  # Filter to specific directory
170
185
  lib_totals = model.project_totals(tracked_globs: 'lib/**/*.rb')
171
186
  ```
172
187
 
188
+ When `raise_on_stale: true` is set, the method raises on stale coverage instead of returning totals. Otherwise, totals exclude stale files (`M`, `T`, `L`, `E`) from line counts and report stale breakdowns under `files['with_coverage']['stale']`.
189
+
190
+ Note: The `without_coverage` hash will only be present if `tracked_globs` were specified.
191
+
173
192
  ### `relativize(data)`
174
193
 
175
194
  Converts absolute file paths in coverage data to relative paths from project root.
@@ -182,29 +201,40 @@ Converts absolute file paths in coverage data to relative paths from project roo
182
201
  **Example:**
183
202
  ```ruby
184
203
  summary = model.summary_for('lib/cov_loupe/model.rb')
185
- # => { 'file' => '/home/user/project/lib/cov_loupe/model.rb', ... }
204
+ # => { 'file' => '/path/to/project/lib/cov_loupe/model.rb', ... }
186
205
 
187
206
  relative_summary = model.relativize(summary)
188
207
  # => { 'file' => 'lib/cov_loupe/model.rb', ... }
189
208
 
190
209
  # Works with arrays too
191
- files = model.all_files
210
+ list_result = model.list
211
+ files = list_result['files']
192
212
  relative_files = model.relativize(files)
193
213
  ```
194
214
 
195
215
  ## Return Types
196
216
 
197
- ### `all_files`
217
+ ### `list`
198
218
 
199
- Returns `Array<Hash>` where each hash contains:
219
+ Returns `Hash` with file data and staleness metadata:
200
220
 
201
221
  ```ruby
202
222
  {
203
- 'file' => String, # Absolute file path
204
- 'covered' => Integer, # Number of covered lines
205
- 'total' => Integer, # Total relevant lines
206
- 'percentage' => Float, # Coverage percentage (0.00-100.00)
207
- 'stale' => false | String # Staleness indicator: false, 'M', 'T', or 'L'
223
+ 'files' => [
224
+ {
225
+ 'file' => String, # Absolute file path
226
+ 'covered' => Integer, # Number of covered lines
227
+ 'total' => Integer, # Total relevant lines
228
+ 'percentage' => Float, # Coverage percentage (0.00-100.00)
229
+ 'stale' => String # Staleness indicator: "ok", "error", "missing", "newer", or "length_mismatch"
230
+ }
231
+ ],
232
+ 'skipped_files' => Array<String>, # Files skipped due to coverage errors
233
+ 'missing_tracked_files' => Array<String>,# Tracked files missing from coverage
234
+ 'newer_files' => Array<String>, # Files newer than coverage
235
+ 'deleted_files' => Array<String>, # Coverage entries for deleted files
236
+ 'length_mismatch_files' => Array<String>,# Files whose line counts differ from coverage
237
+ 'unreadable_files' => Array<String> # Files that could not be read
208
238
  }
209
239
  ```
210
240
 
@@ -282,19 +312,44 @@ Returns `Hash`:
282
312
  ```ruby
283
313
  {
284
314
  'lines' => {
285
- 'total' => Integer, # Total relevant lines across all files
286
- 'covered' => Integer, # Total covered lines
287
- 'uncovered' => Integer # Total uncovered lines
315
+ 'total' => Integer, # Total relevant lines across all files
316
+ 'covered' => Integer, # Total covered lines
317
+ 'uncovered' => Integer, # Total uncovered lines
318
+ 'percent_covered' => Float # Overall percent covered
319
+ },
320
+ 'tracking' => {
321
+ 'enabled' => Boolean, # Whether tracked_globs are active
322
+ 'globs' => Array<String> # Active tracked globs (empty when disabled)
288
323
  },
289
- 'percentage' => Float, # Overall coverage percentage
290
324
  'files' => {
291
- 'total' => Integer, # Total number of files
292
- 'ok' => Integer, # Files with fresh coverage
293
- 'stale' => Integer # Files with stale coverage
325
+ 'total' => Integer, # Total number of files (with + without coverage)
326
+ 'with_coverage' => {
327
+ 'total' => Integer, # Files with coverage entries
328
+ 'ok' => Integer, # Fresh coverage entries
329
+ 'stale' => {
330
+ 'total' => Integer, # Stale coverage entries
331
+ 'by_type' => {
332
+ 'missing_from_disk' => Integer,
333
+ 'newer' => Integer,
334
+ 'length_mismatch' => Integer,
335
+ 'unreadable' => Integer
336
+ }
337
+ }
338
+ },
339
+ 'without_coverage' => {
340
+ 'total' => Integer, # Tracked files missing coverage entries
341
+ 'by_type' => {
342
+ 'missing_from_coverage' => Integer,
343
+ 'unreadable' => Integer,
344
+ 'skipped' => Integer
345
+ }
346
+ }
294
347
  }
295
348
  }
296
349
  ```
297
350
 
351
+ Note: The `without_coverage` hash will only be present if `tracked_globs` were specified.
352
+
298
353
  ## Error Handling
299
354
 
300
355
  ### Exception Types
@@ -303,7 +358,7 @@ The library raises these custom exceptions:
303
358
 
304
359
  - **`CovLoupe::ResultsetNotFoundError`** - Coverage data file not found
305
360
  - **`CovLoupe::FileError`** - Requested file not in coverage data
306
- - **`CovLoupe::CoverageDataStaleError`** - Coverage data is stale (only when `staleness: 'error'`)
361
+ - **`CovLoupe::CoverageDataStaleError`** - Coverage data is stale (only when `raise_on_stale: true`)
307
362
  - **`CovLoupe::CoverageDataError`** - Invalid coverage data format or structure
308
363
 
309
364
  All exceptions inherit from `CovLoupe::Error`.
@@ -331,8 +386,8 @@ end
331
386
 
332
387
  ```ruby
333
388
  # Option 1: Check staleness without raising
334
- model = CovLoupe::CoverageModel.new(staleness: "off")
335
- files = model.all_files
389
+ model = CovLoupe::CoverageModel.new(raise_on_stale: false)
390
+ files = model.list['files']
336
391
 
337
392
  stale_files = files.select { |f| f['stale'] }
338
393
  if stale_files.any?
@@ -344,8 +399,8 @@ end
344
399
 
345
400
  # Option 2: Raise on staleness
346
401
  begin
347
- model = CovLoupe::CoverageModel.new(staleness: "error")
348
- files = model.all_files
402
+ model = CovLoupe::CoverageModel.new(raise_on_stale: true)
403
+ files = model.list['files']
349
404
  rescue CovLoupe::CoverageDataStaleError => e
350
405
  puts "Stale coverage detected: #{e.message}"
351
406
  puts "Re-run tests: bundle exec rspec"
@@ -428,9 +483,9 @@ require "cov_loupe"
428
483
 
429
484
  class CoverageValidator
430
485
  THRESHOLDS = {
431
- 'lib/' => 90.0, # Core library needs 90%+
432
- 'app/' => 80.0, # Application code needs 80%+
433
- 'spec/' => 70.0, # Test helpers need 70%+
486
+ 'lib/api/' => 90.0, # API layer needs 90%+
487
+ 'app/models/' => 85.0, # Models need 85%+
488
+ 'app/controllers/' => 75.0, # Controllers need 75%+
434
489
  }
435
490
 
436
491
  def initialize(model)
@@ -438,7 +493,7 @@ class CoverageValidator
438
493
  end
439
494
 
440
495
  def validate!
441
- files = @model.all_files
496
+ files = @model.list['files']
442
497
  failures = []
443
498
 
444
499
  files.each do |file|
@@ -497,7 +552,7 @@ directory_stats = patterns.map do |pattern|
497
552
  {
498
553
  directory: pattern,
499
554
  files: totals['files']['total'],
500
- coverage: totals['percentage'].round(2),
555
+ coverage: totals['lines']['percent_covered'].round(2),
501
556
  covered: totals['lines']['covered'],
502
557
  total: totals['lines']['total']
503
558
  }
@@ -522,7 +577,7 @@ class CoverageDeltaTracker
522
577
  end
523
578
 
524
579
  def save_baseline
525
- current = @model.all_files
580
+ current = @model.list['files']
526
581
  File.write(@baseline_path, JSON.pretty_generate(current))
527
582
  puts "Saved coverage baseline (#{current.length} files)"
528
583
  end
@@ -534,7 +589,7 @@ class CoverageDeltaTracker
534
589
  end
535
590
 
536
591
  baseline = JSON.parse(File.read(@baseline_path))
537
- current = @model.all_files
592
+ current = @model.list['files']
538
593
 
539
594
  improved = []
540
595
  regressed = []
@@ -600,57 +655,64 @@ class CoverageReporter
600
655
  end
601
656
 
602
657
  def generate_markdown_report(output_path)
603
- files = @model.all_files
658
+ files = @model.list['files']
604
659
  totals = @model.project_totals
605
660
 
606
- File.open(output_path, 'w') do |f|
607
- f.puts "# Coverage Report"
608
- f.puts
609
- f.puts "Generated: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
610
- f.puts
611
-
612
- # Overall stats
613
- overall_percentage = totals['percentage']
614
- total_lines = totals['lines']['total']
615
- covered_lines = totals['lines']['covered']
616
- total_files = totals['files']['total']
617
-
618
- f.puts "## Overall Coverage: #{overall_percentage}%"
619
- f.puts
620
- f.puts "- Total Files: #{total_files}"
621
- f.puts "- Total Lines: #{total_lines}"
622
- f.puts "- Covered Lines: #{covered_lines}"
623
- f.puts
624
-
625
- # Files below threshold
626
- threshold = 80.0
627
- low_coverage = files.select { |file| file['percentage'] < threshold }
628
-
629
- if low_coverage.any?
630
- f.puts "## Files Below #{threshold}% Coverage"
631
- f.puts
632
- f.puts "| File | Coverage | Missing Lines |"
633
- f.puts "|------|----------|---------------|"
634
-
635
- low_coverage.sort_by { |file| file['percentage'] }.each do |file|
636
- uncovered = @model.uncovered_for(file['file'])
637
- missing_count = uncovered['uncovered'].length
638
- f.puts "| #{file['file']} | #{file['percentage']}% | #{missing_count} |"
639
- end
640
- f.puts
641
- end
661
+ # Overall stats
662
+ overall_percentage = totals['lines']['percent_covered']
663
+ total_lines = totals['lines']['total']
664
+ covered_lines = totals['lines']['covered']
665
+ total_files = totals['files']['total']
642
666
 
643
- # Top performers
644
- f.puts "## Top 10 Best Covered Files"
645
- f.puts
646
- f.puts "| File | Coverage |"
647
- f.puts "|------|----------|"
667
+ # Files below threshold
668
+ threshold = 80.0
669
+ low_coverage = files.select { |file| file['percentage'] < threshold }
648
670
 
649
- files.sort_by { |file| -file['percentage'] }.take(10).each do |file|
650
- f.puts "| #{file['file']} | #{file['percentage']}% |"
651
- end
671
+ # Build low coverage table
672
+ low_coverage_section = if low_coverage.any?
673
+ rows = low_coverage.sort_by { |file| file['percentage'] }.map do |file|
674
+ uncovered = @model.uncovered_for(file['file'])
675
+ missing_count = uncovered['uncovered'].length
676
+ "| #{file['file']} | #{file['percentage']}% | #{missing_count} |"
677
+ end.join("\n")
678
+
679
+ <<~LOW_COVERAGE_TABLE
680
+
681
+ ## Files Below #{threshold}% Coverage
682
+
683
+ | File | Coverage | Missing Lines |
684
+ |------|----------|---------------|
685
+ #{rows}
686
+ LOW_COVERAGE_TABLE
687
+ else
688
+ ""
652
689
  end
653
690
 
691
+ # Build top performers table
692
+ top_rows = files.sort_by { |file| -file['percentage'] }.take(10).map do |file|
693
+ "| #{file['file']} | #{file['percentage']}% |"
694
+ end.join("\n")
695
+
696
+ # Generate report
697
+ report = <<~COVERAGE_REPORT
698
+ # Coverage Report
699
+
700
+ Generated: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}
701
+
702
+ ## Overall Coverage: #{overall_percentage}%
703
+
704
+ - Total Files: #{total_files}
705
+ - Total Lines: #{total_lines}
706
+ - Covered Lines: #{covered_lines}
707
+ #{low_coverage_section}
708
+ ## Top 10 Best Covered Files
709
+
710
+ | File | Coverage |
711
+ |------|----------|
712
+ #{top_rows}
713
+ COVERAGE_REPORT
714
+
715
+ File.write(output_path, report)
654
716
  puts "Report saved to #{output_path}"
655
717
  end
656
718
  end
@@ -660,30 +722,73 @@ reporter = CoverageReporter.new(model)
660
722
  reporter.generate_markdown_report("coverage_report.md")
661
723
  ```
662
724
 
663
- ## Staleness Detection
725
+ ### Per-Model Context (Advanced)
726
+
727
+ By default, all `CoverageModel` instances share the global context for error handling and logging. For advanced scenarios where you need different models with different logging or error handling configurations in the same process, you can pass a custom context to each model.
728
+
729
+ ```ruby
730
+ require "cov_loupe"
731
+
732
+ # Scenario: Analyzing coverage for multiple projects in one script
733
+
734
+ # Project A: Detailed logging for debugging
735
+ context_a = CovLoupe.create_context(
736
+ error_handler: CovLoupe::ErrorHandlerFactory.for_library,
737
+ log_target: 'project_a_coverage.log'
738
+ )
664
739
 
665
- The `all_files` method returns a `'stale'` field for each file with one of these values:
740
+ model_a = CovLoupe::CoverageModel.new(
741
+ root: '/path/to/project_a',
742
+ resultset: '/path/to/project_a/coverage/.resultset.json',
743
+ context: context_a
744
+ )
666
745
 
667
- - `false` - Coverage data is current
668
- - `'M'` - **Missing**: File no longer exists on disk
669
- - `'T'` - **Timestamp**: File modified more recently than coverage data
670
- - `'L'` - **Length**: Source file line count differs from coverage data
746
+ # Project B: Different log file
747
+ context_b = context_a.with(log_target: 'project_b_coverage.log')
671
748
 
672
- **Note:** Per-file methods (`summary_for`, `uncovered_for`, `detailed_for`, `raw_for`) do not include staleness information in their return values. To check staleness for individual files, use `all_files` and filter the results.
749
+ model_b = CovLoupe::CoverageModel.new(
750
+ root: '/path/to/project_b',
751
+ resultset: '/path/to/project_b/coverage/.resultset.json',
752
+ context: context_b
753
+ )
754
+
755
+ # Each model logs to its own file
756
+ summary_a = model_a.summary_for('lib/foo.rb') # Logs to project_a_coverage.log
757
+ summary_b = model_b.summary_for('lib/bar.rb') # Logs to project_b_coverage.log
758
+
759
+ # You can also change a model's context at runtime
760
+ model_a.context = CovLoupe.context # Switch to global context
761
+ ```
762
+
763
+ **When to use per-model contexts:**
764
+ - Managing coverage for multiple projects in one script
765
+ - Different error handling strategies per model
766
+ - Separate log files for different data sources
767
+ - Testing scenarios requiring isolated configurations
768
+
769
+ **Simple use case (most common):**
770
+ ```ruby
771
+ # For most use cases, just configure the global context once
772
+ CovLoupe.error_handler = CovLoupe::ErrorHandlerFactory.for_library
773
+ CovLoupe.default_log_file = 'coverage_analysis.log'
774
+
775
+ # All models automatically use the global context
776
+ model = CovLoupe::CoverageModel.new
777
+ ```
778
+
779
+ ## Staleness Detection
673
780
 
674
- When `staleness: 'error'` mode is enabled in `CoverageModel.new`, the model will raise `CovLoupe::CoverageDataStaleError` exceptions when stale files are detected during method calls.
781
+ The `list` method returns a `'stale'` field for each file with one of these values:
675
782
 
676
- ## API Stability
783
+ - `"ok"` - Coverage data is current
784
+ - `"missing"` - **Missing**: File no longer exists on disk
785
+ - `"newer"` - **Timestamp**: File modified more recently than coverage data
786
+ - `"length_mismatch"` - **Length**: Source file line count differs from coverage data
787
+ - `"error"` - **Error**: Staleness check failed
677
788
 
678
- Consider the following public and stable under SemVer:
679
- - `CovLoupe::CoverageModel.new(root:, resultset:, staleness: 'off', tracked_globs: nil)`
680
- - `#raw_for(path)`, `#summary_for(path)`, `#uncovered_for(path)`, `#detailed_for(path)`, `#all_files(sort_order:)`, `#format_table(rows: nil, sort_order:, check_stale:, tracked_globs:)`
681
- - Return shapes shown in the [Return Types](#return-types) section
682
- - Exception types documented in [Error Handling](#error-handling)
789
+ **Note:** Per-file methods (`summary_for`, `uncovered_for`, `detailed_for`, `raw_for`) do not include staleness information in their return values. To check staleness for individual files, use `list` and filter the results.
683
790
 
684
- **Note:**
685
- - CLI (`CovLoupe.run(argv)`) and MCP tools remain stable but are separate surfaces
686
- - Internal helpers under `CovLoupe::CovUtil` may change; prefer `CoverageModel` unless you need low-level access
791
+ When `raise_on_stale: true` is enabled in `CoverageModel.new`, the model will raise `CovLoupe::CoverageDataStaleError` exceptions when stale files are detected during method calls.
687
792
 
688
793
  ## Related Documentation
689
794