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
@@ -0,0 +1,259 @@
1
+ # Application Architecture
2
+
3
+ [Back to main README](../../index.md)
4
+
5
+ This document describes the core architectural decisions that shape how cov-loupe operates: its dual-mode design and context-aware error handling strategy.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Dual-Mode Operation (CLI and MCP Server)](#dual-mode-operation-cli-and-mcp-server)
10
+ - [Context-Aware Error Handling](#context-aware-error-handling)
11
+
12
+ ---
13
+
14
+ ## Dual-Mode Operation (CLI and MCP Server)
15
+
16
+ ### Status
17
+
18
+ Accepted
19
+
20
+ ### Context
21
+
22
+ cov-loupe needed to serve two distinct use cases:
23
+
24
+ 1. **Human users** wanting a command-line tool to inspect coverage reports in their terminal
25
+ 2. **AI agents and MCP clients** needing programmatic access to coverage data via the Model Context Protocol (MCP) over JSON-RPC
26
+
27
+ We considered three approaches:
28
+
29
+ 1. **Separate binaries/gems**: Create `simplecov-cli` and `cov-loupe` as separate projects
30
+ 2. **Single binary with explicit mode flags**: Require users to pass `--mode mcp` to run as MCP server
31
+ 3. **Automatic mode detection**: Single binary that automatically detects the operating mode based on input (TTY status, stdin)
32
+
33
+ #### Key Constraints
34
+
35
+ - MCP servers communicate via JSON-RPC over stdin/stdout, so any human-readable output would corrupt the protocol
36
+ - CLI users expect immediate, readable output without ceremony
37
+ - The gem should be simple to install and use for both audiences
38
+ - Mode selection must be reliable and unambiguous
39
+
40
+ ### Decision (v4.0.0+)
41
+
42
+ We implemented **explicit mode selection** via the `-m/--mode` flag. The default mode is `cli`, and MCP users must pass `-m mcp` or `--mode mcp` to run the server.
43
+
44
+ #### Mode Selection Logic
45
+
46
+ The mode is determined by parsing the `-m/--mode` flag from argv (including environment variables via `COV_LOUPE_OPTS`):
47
+
48
+ - **Default**: CLI mode (when `-m/--mode` is not specified)
49
+ - **MCP mode**: Must explicitly pass `-m mcp` or `--mode mcp`
50
+
51
+ The implementation parses the configuration from the command-line arguments and routes to either `CoverageCLI` or `MCPServer` based on the mode setting.
52
+
53
+ #### Why This Works
54
+
55
+ - **MCP clients** are configured once with `-m mcp` or `--mode mcp` in their server config → always routes to MCP server
56
+ - **CLI users** don't need to specify anything → defaults to CLI mode
57
+ - **No ambiguity**: Mode is explicit and deterministic based on the `-m/--mode` flag
58
+
59
+ #### Historical Note
60
+
61
+ Prior to v4.0.0, cov-loupe used automatic mode detection based on TTY status and presence of subcommands. This was removed because:
62
+ - Automatic detection caused issues with piped input (`cov-loupe --format json > output.json` would hang in MCP mode)
63
+ - CI environments and non-TTY contexts were unpredictable
64
+ - CLI-only flags without subcommands (`--format`, `--sort-order`) couldn't be reliably detected
65
+ - Explicit mode selection is more predictable and follows standard practice for language servers
66
+
67
+ ### Consequences
68
+
69
+ #### Positive
70
+
71
+ 1. **User convenience**: Single gem to install (`gem install cov-loupe`), single executable (`cov-loupe`)
72
+ 2. **Predictable behavior**: Mode is explicit and deterministic - no surprises based on environment
73
+ 3. **Simpler implementation**: No complex mode detection logic to maintain
74
+ 4. **Clear separation**: CLI and MCP server implementations remain completely separate after routing
75
+ 5. **Follows conventions**: Matches standard practice for language servers (e.g., `typescript-language-server --stdio`)
76
+
77
+ #### Negative
78
+
79
+ 1. **Breaking change**: Users upgrading from v3.x must update MCP server configuration to include `-m mcp` or `--mode mcp`
80
+ 2. **Slight verbosity**: MCP users must include `-m mcp` or `--mode mcp` in their server config (but this is one-time setup)
81
+ 3. **Shared dependencies**: Some components (error handling, coverage model) must work correctly in both modes
82
+
83
+ #### Trade-offs
84
+
85
+ - **Versus automatic detection**: More explicit, but eliminates ambiguity and edge cases
86
+ - **Versus separate gems**: Single installation is simpler, but requires mode flag for MCP
87
+
88
+ #### Future Constraints
89
+
90
+ - Shared components (like `CoverageModel`) must never output to stdout/stderr in ways that differ by mode
91
+ - Default mode must remain `cli` for backward compatibility with existing CLI users
92
+
93
+ ### References
94
+
95
+ - Implementation: `lib/cov_loupe.rb` (`CovLoupe.run`)
96
+ - Configuration: `lib/cov_loupe/app_config.rb`
97
+ - CLI implementation: `lib/cov_loupe/cli.rb`
98
+ - MCP server implementation: `lib/cov_loupe/mcp_server.rb`
99
+ - Related section: [Context-Aware Error Handling](#context-aware-error-handling)
100
+
101
+ ---
102
+
103
+ ## Context-Aware Error Handling
104
+
105
+ ### Status
106
+
107
+ Accepted
108
+
109
+ ### Context
110
+
111
+ cov-loupe operates in three distinct contexts, each with different error handling requirements:
112
+
113
+ 1. **CLI mode**: Human users expect friendly error messages, exit codes, and optional debug traces
114
+ 2. **MCP server mode**: AI agents/clients need structured error responses that don't crash the server
115
+ 3. **Library mode**: Embedding applications need exceptions they can catch and handle programmatically
116
+
117
+ Initially, we considered uniform error handling across all modes, but this created poor user experiences:
118
+
119
+ - CLI users saw raw exceptions with stack traces (scary and unhelpful)
120
+ - MCP servers crashed on errors instead of returning error responses
121
+ - Library users got friendly messages logged to stderr (unwanted side effects in their applications)
122
+
123
+ #### Key Requirements
124
+
125
+ - **CLI**: User-friendly messages, meaningful exit codes, optional stack traces for debugging
126
+ - **MCP Server**: Logged errors (to file, not stdout), structured JSON-RPC error responses, no server crashes
127
+ - **Library**: Raise custom exceptions with no logging, allowing consumers to handle errors as needed
128
+ - **Consistency**: Same underlying error types, but different presentation strategies
129
+
130
+ ### Decision
131
+
132
+ We implemented a **context-aware error handling strategy** using three components:
133
+
134
+ #### 1. Custom Exception Hierarchy
135
+
136
+ All errors inherit from `CovLoupe::Error` (lib/cov_loupe/errors.rb) with a `user_friendly_message` method:
137
+
138
+ ```ruby
139
+ class Error < StandardError
140
+ def user_friendly_message
141
+ message # Can be overridden in subclasses
142
+ end
143
+ end
144
+
145
+ class FileNotFoundError < FileError; end
146
+ class CoverageDataError < Error; end
147
+ class ResultsetNotFoundError < CoverageDataError; end
148
+ # ... etc
149
+ ```
150
+
151
+ This provides a unified interface for presenting errors to users while preserving exception types for programmatic handling.
152
+
153
+ #### 2. ErrorHandler Class
154
+
155
+ The `ErrorHandler` class (see `lib/cov_loupe/error_handler.rb`) provides configurable error handling behavior:
156
+
157
+ ```ruby
158
+ class ErrorHandler
159
+ attr_accessor :error_mode, :logger
160
+
161
+ VALID_ERROR_MODES = [:off, :log, :debug].freeze
162
+
163
+ def initialize(error_mode: :log, logger: nil)
164
+ @error_mode = error_mode
165
+ @logger = logger
166
+ end
167
+
168
+ def handle_error(error, context: nil, reraise: true)
169
+ log_error(error, context)
170
+ if reraise
171
+ raise error.is_a?(CovLoupe::Error) ? error : convert_standard_error(error)
172
+ end
173
+ end
174
+ end
175
+ ```
176
+
177
+ The `convert_standard_error` method transforms Ruby's standard errors into user-friendly custom exceptions:
178
+
179
+ - `Errno::ENOENT` → `FileNotFoundError`
180
+ - `JSON::ParserError` → `CoverageDataError`
181
+ - `Errno::EACCES` → `FilePermissionError`
182
+
183
+ #### 3. ErrorHandlerFactory
184
+
185
+ The `ErrorHandlerFactory` (defined in `lib/cov_loupe/error_handler_factory.rb`) creates mode-specific handlers:
186
+
187
+ ```ruby
188
+ module ErrorHandlerFactory
189
+ def self.for_cli(error_mode: :log)
190
+ ErrorHandler.new(error_mode: error_mode)
191
+ end
192
+
193
+ def self.for_library(error_mode: :off)
194
+ ErrorHandler.new(error_mode: :off) # No logging
195
+ end
196
+
197
+ def self.for_mcp_server(error_mode: :log)
198
+ ErrorHandler.new(error_mode: :log) # Logs to file
199
+ end
200
+ end
201
+ ```
202
+
203
+ #### Error Flow by Mode
204
+
205
+ **CLI Mode** (lib/cov_loupe/cli.rb):
206
+ 1. Catches all exceptions in the main run loop
207
+ 2. Uses `for_cli` handler to log errors if debug mode is enabled
208
+ 3. Displays `user_friendly_message` to the user
209
+ 4. Exits with appropriate code (1 for errors, 2 for usage errors)
210
+
211
+ **MCP Server Mode** (`lib/cov_loupe/base_tool.rb`):
212
+ 1. Each tool wraps execution in a rescue block
213
+ 2. Uses `for_mcp_server` handler to log errors to `~/cov_loupe.log`
214
+ 3. Returns structured JSON-RPC error response
215
+ 4. Server continues running (no crashes)
216
+
217
+ **Library Mode** (`lib/cov_loupe.rb`):
218
+ 1. Uses `for_library` handler with `error_mode: :off` (no logging)
219
+ 2. Raises custom exceptions directly
220
+ 3. Consumers catch and handle `CovLoupe::Error` subclasses
221
+
222
+ ### Consequences
223
+
224
+ #### Positive
225
+
226
+ 1. **Excellent UX**: Each context gets appropriate error handling behavior
227
+ 2. **Robustness**: MCP server never crashes on tool errors
228
+ 3. **Debuggability**: CLI users can enable stack traces with error modes, MCP errors are logged
229
+ 4. **Clean library API**: No unwanted side effects (logging, stderr output) when used as a library
230
+ 5. **Type safety**: Custom exceptions allow programmatic error handling by type
231
+
232
+ #### Negative
233
+
234
+ 1. **Complexity**: Three error handling paths to maintain and test
235
+ 2. **Coordination required**: All error types must implement `user_friendly_message` consistently
236
+ 3. **Error conversion overhead**: Standard errors must be converted to custom exceptions
237
+
238
+ #### Trade-offs
239
+
240
+ - **Versus uniform error handling**: More code complexity, but dramatically better UX in each context
241
+ - **Versus separate error classes per mode**: Single error hierarchy is simpler, factory pattern adds mode-specific behavior
242
+
243
+ #### Implementation Notes
244
+
245
+ The `ErrorHandler.convert_standard_error` method uses pattern matching on exception types and error messages to provide helpful, context-aware error messages. This includes:
246
+
247
+ - Extracting filenames from system error messages
248
+ - Detecting SimpleCov-specific error patterns
249
+ - Providing actionable suggestions ("please run your tests first")
250
+
251
+ ### References
252
+
253
+ - Custom exceptions: `lib/cov_loupe/errors.rb`
254
+ - ErrorHandler implementation: `lib/cov_loupe/error_handler.rb`
255
+ - ErrorHandlerFactory: `lib/cov_loupe/error_handler_factory.rb`
256
+ - CLI error handling: `lib/cov_loupe/cli.rb` (rescue block in `CoverageCLI#run`)
257
+ - MCP tool error handling: `lib/cov_loupe/base_tool.rb` (`BaseTool#call`)
258
+ - Library mode: `lib/cov_loupe.rb` (error handling within `CovLoupe.run`)
259
+ - Related section: [Dual-Mode Operation](#dual-mode-operation-cli-and-mcp-server)
@@ -0,0 +1,193 @@
1
+ # Coverage Data Quality
2
+
3
+ [Back to main README](../../index.md)
4
+
5
+ This document describes how cov-loupe ensures the accuracy and reliability of coverage data through staleness detection.
6
+
7
+ ## Coverage Staleness Detection
8
+
9
+ ### Status
10
+
11
+ Accepted
12
+
13
+ ### Context
14
+
15
+ Coverage data can become outdated when source files are modified after tests run. This creates misleading results:
16
+
17
+ - Coverage percentages appear lower/higher than reality
18
+ - Line numbers in coverage reports don't match the current source
19
+ - AI agents and users may make decisions based on stale data
20
+
21
+ We needed a staleness detection system that could:
22
+
23
+ 1. Detect when source files have been modified since coverage was collected
24
+ 2. Detect when source files have different line counts than coverage data
25
+ 3. Handle edge cases (deleted files)
26
+ 4. Support both file-level and project-level checks
27
+ 5. Allow users to control whether staleness is reported or causes errors
28
+
29
+ #### Alternative Approaches Considered
30
+
31
+ 1. **No staleness checking**: Simple, but leads to confusing/incorrect reports
32
+ 2. **Single timestamp check**: Fast, but misses line count mismatches (files edited and reverted)
33
+ 3. **Content hashing**: Accurate, but expensive for large projects
34
+ 4. **Multi-type detection with modes**: More complex, but provides accurate detection with user control
35
+
36
+ ### Decision
37
+
38
+ We implemented a **staleness detection system** with configurable error modes that can identify four distinct staleness conditions.
39
+
40
+ #### Four Staleness Types
41
+
42
+ The `StalenessChecker` class (defined in `lib/cov_loupe/staleness/staleness_checker.rb`) detects four distinct types of staleness:
43
+
44
+ 1. **Type "error" (Error)**: The staleness check itself failed
45
+ - Returned by `CoverageModel#staleness_for` when an exception is raised during staleness checking
46
+ - Example: File permission errors, resolver failures, or other unexpected issues
47
+ - The error is logged but execution continues with an "error" status instead of crashing
48
+
49
+ 2. **Type "missing" (Missing)**: The source file exists in coverage but is now deleted/missing
50
+ - Returned by `file_staleness_status` when `File.file?(file_abs)` returns false
51
+ - Example: File was deleted after tests ran
52
+
53
+ 3. **Type "newer" (Timestamp)**: The source file's mtime is newer than coverage timestamp
54
+ - Detected by comparing `File.mtime(file_abs)` with coverage timestamp
55
+ - Example: File was edited after tests ran
56
+
57
+ 4. **Type "length_mismatch" (Length)**: The source file line count doesn't match the coverage lines array length
58
+ - Detected by comparing `File.foreach(path).count` with `coverage_lines.length`
59
+ - Example: Lines were added/removed without changing mtime (rare but possible with version control)
60
+
61
+ 5. **Type "ok" (Not stale)**: The file is not stale
62
+ - Returned when none of the above staleness conditions apply
63
+ - Indicates the coverage data is current and accurate
64
+
65
+ #### Implementation Details
66
+
67
+ The core algorithm lives in `CovLoupe::StalenessChecker#compute_file_staleness_details`:
68
+
69
+ ```ruby
70
+ def compute_file_staleness_details(file_abs, coverage_lines)
71
+ coverage_ts = coverage_timestamp
72
+ exists = File.file?(file_abs)
73
+ file_mtime = exists ? File.mtime(file_abs) : nil
74
+
75
+ cov_len = coverage_lines.respond_to?(:length) ? coverage_lines.length : 0
76
+ src_len = exists ? safe_count_lines(file_abs) : 0
77
+
78
+ # If coverage timestamp is 0 (missing/invalid), we cannot determine if file is newer
79
+ newer = if coverage_ts.to_i > 0
80
+ !!(file_mtime && file_mtime.to_i > coverage_ts.to_i)
81
+ else
82
+ false
83
+ end
84
+
85
+ len_mismatch = (cov_len.positive? && src_len != cov_len)
86
+ newer &&= !len_mismatch # Prioritize length mismatch over timestamp
87
+
88
+ {
89
+ exists: exists,
90
+ file_mtime: file_mtime,
91
+ coverage_timestamp: coverage_ts,
92
+ cov_len: cov_len,
93
+ src_len: src_len,
94
+ newer: newer,
95
+ len_mismatch: len_mismatch
96
+ }
97
+ end
98
+ ```
99
+
100
+ #### Staleness Modes
101
+
102
+ The checker supports two modes, configured when instantiating `StalenessChecker`:
103
+
104
+ - **`:off`** (default): Staleness is detected but only reported in responses, never raises errors
105
+ - **"error"**: Staleness raises `CoverageDataStaleError` or `CoverageDataProjectStaleError`
106
+
107
+ This allows:
108
+ - Interactive tools to show warnings without crashing
109
+ - CI systems to fail builds on stale coverage
110
+ - AI agents to decide how to handle staleness based on their goals
111
+
112
+ #### File-Level vs Project-Level Checks
113
+
114
+ **File-level** (`check_file!` and `file_staleness_status`):
115
+ - Checks a single file's staleness
116
+ - Returns one of the staleness status strings ("ok", "missing", "newer", "length_mismatch", "error")
117
+ - Used by single-file tools (summary, detailed, uncovered)
118
+
119
+ **Project-level** (`check_project!`):
120
+ - Checks all covered files plus optionally tracked files
121
+ - Detects:
122
+ - Files newer than coverage timestamp
123
+ - Files deleted since coverage was collected
124
+ - Tracked files missing from coverage (newly added files)
125
+ - Raises `CoverageDataProjectStaleError` with lists of problematic files
126
+ - Used by `list_tool` and `coverage_table_tool`
127
+
128
+ **Totals behavior**:
129
+ - `project_totals` excludes any stale files ("missing", "newer", "length_mismatch", "error") from aggregate counts.
130
+ - Totals include explicit `with_coverage`/`without_coverage` breakdowns so callers can reconcile what was omitted.
131
+ - The `without_coverage` payload includes counts for three categories:
132
+ - `missing_from_coverage`: Tracked files that have no coverage data in the resultset
133
+ - `unreadable`: Files that exist but could not be read (e.g., due to permission errors, I/O issues, or staleness check failures)
134
+ - `skipped`: Files that were skipped during list processing due to coverage data errors (e.g., malformed entries)
135
+ - The `unreadable` count is populated from `list_result['unreadable_files']`, which is collected during staleness checking when files exist but cannot be accessed or validated.
136
+
137
+ #### Tracked Globs Feature
138
+
139
+ The project-level check supports `tracked_globs` parameter to detect newly added files:
140
+
141
+ ```ruby
142
+ # Detects if lib/**/*.rb files exist that have no coverage data
143
+ checker.check_project!(coverage_map) # with tracked_globs: ['lib/**/*.rb']
144
+ ```
145
+
146
+ This helps teams ensure new files are included in test runs.
147
+
148
+ #### Resultset Path Consistency (SimpleCov)
149
+
150
+ SimpleCov can emit mixed path forms for the same file when resultsets are merged across suites or
151
+ environments (for example, absolute vs relative paths, or different roots). This is a SimpleCov
152
+ data consistency risk, not a cov-loupe behavior. Downstream tools that normalize paths may treat
153
+ one entry as overriding another when multiple keys map to the same absolute path.
154
+
155
+ **Guidance:** Keep `SimpleCov.root` consistent across all suites and avoid manual path rewriting
156
+ before merging resultsets.
157
+
158
+ ### Consequences
159
+
160
+ #### Positive
161
+
162
+ 1. **Accurate detection**: Three types catch different staleness scenarios comprehensively
163
+ 2. **User control**: Modes allow errors or warnings based on use case
164
+ 3. **Detailed information**: Staleness errors include specific file lists and timestamps
165
+ 4. **Project awareness**: Can detect newly added files that lack coverage
166
+ 5. **Conservative totals**: Aggregate totals only include fresh coverage data
167
+
168
+ #### Negative
169
+
170
+ 1. **Complexity**: Three staleness types are harder to understand than a single timestamp check
171
+ 2. **Performance**: Line counting and mtime checks for every file add overhead
172
+ 3. **Ambiguity**: When multiple staleness types apply, prioritization logic (length > timestamp) may surprise users
173
+
174
+ #### Trade-offs
175
+
176
+ - **Versus timestamp-only**: More accurate but slower and more complex
177
+ - **Versus content hashing**: Fast enough for most projects, but can't detect "edit then revert" scenarios
178
+ - **Versus no checking**: Essential for reliable coverage reporting, worth the complexity
179
+
180
+ #### Edge Cases Handled
181
+
182
+ 1. **Deleted files**: Appear as "missing" type staleness
183
+ 2. **Empty files**: `cov_len.positive?` guard prevents false positives
184
+ 3. **No coverage timestamp**: Defaults to 0, effectively disabling timestamp checks
185
+
186
+ ### References
187
+
188
+ - Implementation: `lib/cov_loupe/staleness_checker.rb` (`StalenessChecker` class)
189
+ - File-level checking: `StalenessChecker#check_file!` and `#file_staleness_status`
190
+ - Project-level checking: `StalenessChecker#check_project!`
191
+ - Staleness detail computation: `StalenessChecker#compute_file_staleness_details`
192
+ - Error types: `lib/cov_loupe/errors.rb` (`CoverageDataStaleError`, `CoverageDataProjectStaleError`)
193
+ - Usage in tools: `lib/cov_loupe/tools/list_tool.rb`, `lib/cov_loupe/model.rb`
@@ -0,0 +1,217 @@
1
+ # Output Character Mode
2
+
3
+ [Back to main README](../../index.md)
4
+
5
+ This document describes the architectural decision for implementing a global output character mode that controls ASCII vs Unicode output across CLI and MCP interfaces.
6
+
7
+ ## Status
8
+
9
+ Accepted
10
+
11
+ ## Context
12
+
13
+ cov-loupe outputs data in multiple formats across two interfaces:
14
+
15
+ 1. **CLI mode**: Human users read terminal output including tables, error messages, and formatted coverage reports
16
+ 2. **MCP server mode**: AI agents receive JSON responses containing coverage data and metadata
17
+
18
+ ### The Problem
19
+
20
+ Modern projects often contain file paths with Unicode characters (e.g., accented characters, non-Latin scripts). The original implementation used Unicode characters throughout:
21
+
22
+ - Table borders using box-drawing characters (│ ─ ┌ ┐ └ ┘ ├ ┤ ┬ ┴ ┼)
23
+ - Source code markers (✓ for covered, · for uncovered)
24
+ - Error messages with file paths preserved as-is
25
+
26
+ This caused issues in environments that don't support Unicode:
27
+
28
+ - Windows terminals with legacy encoding
29
+ - CI/CD systems with ASCII-only terminals
30
+ - Piped output to files or tools expecting ASCII
31
+ - Legacy systems without UTF-8 support
32
+
33
+ Users experienced garbled output, corrupted tables, and unreadable error messages.
34
+
35
+ ### Requirements
36
+
37
+ - **ASCII mode**: Must produce ASCII-only output (0-127 characters) when requested
38
+ - **Fancy mode**: Should use Unicode characters for enhanced readability when supported
39
+ - **Auto-detection**: Default mode should intelligently choose based on environment
40
+ - **MCP integration**: MCP tools must support the same output modes as CLI
41
+ - **Comprehensive coverage**: All output channels must respect the mode setting
42
+ - **Backward compatibility**: Existing behavior (Unicode) should remain the default when supported
43
+
44
+ ### Considered Approaches
45
+
46
+ 1. **Separate ASCII formatters**: Create duplicate formatter implementations for ASCII output
47
+ - Too much code duplication
48
+ - Maintenance burden (two implementations of each formatter)
49
+
50
+ 2. **Post-process all output**: Apply ASCII conversion after formatting
51
+ - Inefficient (convert entire formatted output)
52
+ - Could corrupt already-encoded data (JSON structure)
53
+
54
+ 3. **Centralized conversion with charsets**: Define separate charsets and convert at formatting time
55
+ - Clean separation of concerns
56
+ - Efficient (convert only what's displayed)
57
+ - Consistent across all formatters
58
+
59
+ ## Decision
60
+
61
+ We implemented **global output character mode** with centralized conversion using charset definitions.
62
+
63
+ ### Mode Options
64
+
65
+ Three modes are available:
66
+ - `default`: Auto-detects terminal UTF-8 support at runtime → fancy if supported, otherwise ASCII
67
+ - `fancy`: Forces Unicode output with box-drawing characters and fancy markers
68
+ - `ascii`: Forces ASCII-only output with transliteration fallback to `?` for unknown characters
69
+
70
+ ### Configuration
71
+
72
+ - **CLI**: `-O/--output-chars MODE` flag (case-insensitive, short forms `d|f|a`)
73
+ - **MCP**: Optional `output_chars` parameter in tool requests (overrides server default)
74
+ - **No environment variable**: Intentionally omitted to keep configuration simple and explicit
75
+
76
+ ### Core Implementation
77
+
78
+ The `OutputChars` module (`lib/cov_loupe/output_chars.rb`) provides:
79
+
80
+ ```ruby
81
+ module OutputChars
82
+ # Mode resolution
83
+ def self.resolve_mode(mode)
84
+ return :fancy if mode == :fancy
85
+ return :ascii if mode == :ascii
86
+ # default: detect terminal UTF-8 support
87
+ stdout_utf8? ? :fancy : :ascii
88
+ end
89
+
90
+ # Character conversion using transliteration map
91
+ def self.convert(text, mode)
92
+ return text unless mode == :ascii
93
+ text.chars.map { |c| TRANSLITERATIONS[c] || c.ascii_only? ? c : '?' }.join
94
+ end
95
+
96
+ # Charset selection
97
+ def self.charset_for(mode)
98
+ mode == :fancy ? FANCY_CHARSET : ASCII_CHARSET
99
+ end
100
+ end
101
+ ```
102
+
103
+ ### Transliteration Strategy
104
+
105
+ Instead of a generic library (like `ActiveSupport::Multibyte`), we use an internal `TRANSLITERATIONS` hash mapping common characters to ASCII equivalents:
106
+
107
+ - Accented Latin characters (á → a, é → e, ñ → n, etc.)
108
+ - Symbols and punctuation (→ ->, — --, © (C), etc.)
109
+ - Box-drawing characters (│ → |, ─ → -, ┌ → +, etc.)
110
+
111
+ Characters without defined mappings fall back to `?` to maintain ASCII-only output.
112
+
113
+ ### Formatter Integration
114
+
115
+ All formatters respect the `output_chars` parameter:
116
+
117
+ 1. **JSON**: Uses `JSON.generate(..., ascii_only: true)` in ASCII mode
118
+ 2. **YAML**: Post-processes through `OutputChars.convert`
119
+ 3. **AmazingPrint**: Post-processes through `OutputChars.convert`
120
+ 4. **Tables**: Uses appropriate charset (`OutputChars.charset_for`) and converts cell contents
121
+ 5. **Source**: Uses ASCII-safe markers (`+`/`-` instead of `✓`/`·`) and converts source code
122
+
123
+ ### Error Message Integration
124
+
125
+ - CLI error handlers convert messages via `OutputChars.convert`
126
+ - Staleness error messages convert file paths via `convert_path` lambda
127
+ - Option parser errors converted before display
128
+ - Backtrace lines converted in debug mode
129
+
130
+ ### Scope of Conversion
131
+
132
+ **Converted in ASCII mode:**
133
+ - All CLI error messages and option parser errors
134
+ - Staleness error messages and file paths
135
+ - Command literal strings (via `convert_text` helper in BaseCommand)
136
+ - MCP tool JSON responses (via `respond_json` with `ascii_only: true`)
137
+ - All formatted output (tables, source, JSON, YAML)
138
+
139
+ **Not converted in ASCII mode:**
140
+ - **Log files**: Preserved in original encoding for debugging fidelity. Log files are system/debugging artifacts, not user-facing output. Converting would lose exact file paths and error details needed for troubleshooting, create inconsistency between logged paths and actual filesystem paths, and provides no user value since logs are developer artifacts.
141
+ - **Gem post-install message**: Intentionally left unchanged per requirements
142
+
143
+ ## Consequences
144
+
145
+ ### Positive
146
+
147
+ 1. **Broad compatibility**: Works in any terminal environment, including legacy systems
148
+ 2. **Better UX**: Fancy mode provides enhanced readability when Unicode is supported
149
+ 3. **Auto-detection**: Default mode adapts to environment without user configuration
150
+ 4. **Comprehensive coverage**: All output channels respect the mode setting
151
+ 5. **MCP parity**: CLI and MCP interfaces have identical behavior
152
+ 6. **No dependencies**: Internal transliteration map avoids external dependencies
153
+ 7. **Consistent behavior**: Single source of truth for character conversion
154
+
155
+ ### Negative
156
+
157
+ 1. **Complexity**: Additional configuration option and conversion logic to maintain
158
+ 2. **Transliteration coverage**: Not all Unicode characters have mappings (falls back to `?`)
159
+ 3. **Performance**: Conversion overhead for every output operation (minimal in practice)
160
+ 4. **Test burden**: Comprehensive tests needed across all formatters and modes
161
+
162
+ ### Trade-offs
163
+
164
+ - **Internal vs external transliteration**: Internal map is less comprehensive but avoids dependencies and keeps behavior predictable
165
+ - **Charset vs post-processing**: Charsets are cleaner but require formatter awareness; post-processing is simpler but can corrupt structured data
166
+ - **Auto-detection vs explicit default**: Auto-detection is more convenient but less predictable; explicit default is clearer but requires configuration
167
+
168
+ ### Future Constraints
169
+
170
+ - Any new formatters must respect `output_chars` parameter
171
+ - New output channels (e.g., HTML) need ASCII mode support
172
+ - Transliteration map must be maintained as new characters are encountered
173
+ - Log files must never be converted (documented design decision)
174
+
175
+ ## Implementation Notes
176
+
177
+ ### Mode Precedence
178
+
179
+ 1. Explicit mode parameter (CLI flag or MCP tool parameter)
180
+ 2. Server default (for MCP)
181
+ 3. Built-in default (auto-detect UTF-8 support)
182
+
183
+ ### Performance Considerations
184
+
185
+ - Conversion only applies in ASCII mode (fancy mode is a no-op)
186
+ - Transliteration map lookup is O(1) per character
187
+ - JSON `ascii_only: true` is optimized by the json gem
188
+ - Overall performance impact is negligible (< 1ms for typical outputs)
189
+
190
+ ### Testing Strategy
191
+
192
+ Comprehensive test coverage ensures correctness:
193
+
194
+ - Mode resolution and normalization tests
195
+ - Formatter tests for both ASCII and fancy modes
196
+ - CLI option parsing tests for `--output-chars` flag
197
+ - MCP tool output mode tests
198
+ - Staleness error message tests with Unicode file paths
199
+ - Integration tests across all subcommands with Unicode file names
200
+
201
+ ## References
202
+
203
+ - Core implementation: `lib/cov_loupe/output_chars.rb`
204
+ - Configuration: `lib/cov_loupe/config/app_config.rb`, `lib/cov_loupe/config/option_normalizers.rb`
205
+ - Formatters:
206
+ - `lib/cov_loupe/formatters/formatters.rb` (JSON, YAML, AmazingPrint)
207
+ - `lib/cov_loupe/formatters/table_formatter.rb` (tables)
208
+ - `lib/cov_loupe/formatters/source_formatter.rb` (source code)
209
+ - Error handling: `lib/cov_loupe/cli.rb`, `lib/cov_loupe/errors/error_handler.rb`
210
+ - MCP integration: `lib/cov_loupe/base_tool.rb`, `lib/cov_loupe/tools/*.rb`
211
+ - CLI option parsing: `lib/cov_loupe/config/option_parser_builder.rb`
212
+ - Tests:
213
+ - `spec/cov_loupe/output_chars_spec.rb`
214
+ - `spec/cov_loupe/formatters/*_spec.rb`
215
+ - `spec/cov_loupe/cli/cli_output_chars_spec.rb`
216
+ - `spec/cov_loupe/tools/*_spec.rb`
217
+ - Review document: `docs/dev/output-chars-review.md`