kotoshu 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +18 -0
  4. data/CHANGELOG.md +182 -0
  5. data/CLAUDE.md +172 -0
  6. data/CODE_OF_CONDUCT.md +132 -0
  7. data/LICENSE +31 -0
  8. data/README.adoc +955 -0
  9. data/Rakefile +12 -0
  10. data/SECURITY.md +93 -0
  11. data/examples/01_basic_word_checking.rb +38 -0
  12. data/examples/02_text_document_checking.rb +77 -0
  13. data/examples/03_dictionary_backends.rb +137 -0
  14. data/examples/04_trie_data_structure.rb +146 -0
  15. data/examples/05_suggestion_algorithms.rb +239 -0
  16. data/examples/06_configuration_advanced.rb +287 -0
  17. data/examples/07_multi_language_dictionaries.rb +278 -0
  18. data/exe/kotoshu +6 -0
  19. data/lib/kotoshu/algorithms/capitalization.rb +276 -0
  20. data/lib/kotoshu/algorithms/lookup.rb +876 -0
  21. data/lib/kotoshu/algorithms/ngram_suggest.rb +270 -0
  22. data/lib/kotoshu/algorithms/permutations.rb +283 -0
  23. data/lib/kotoshu/algorithms/phonet_suggest.rb +167 -0
  24. data/lib/kotoshu/algorithms/suggest.rb +575 -0
  25. data/lib/kotoshu/algorithms.rb +14 -0
  26. data/lib/kotoshu/analyzers/semantic_analyzer.rb +295 -0
  27. data/lib/kotoshu/cache/base_cache.rb +596 -0
  28. data/lib/kotoshu/cache/cache.rb +91 -0
  29. data/lib/kotoshu/cache/frequency_cache.rb +224 -0
  30. data/lib/kotoshu/cache/language_cache.rb +454 -0
  31. data/lib/kotoshu/cache/lookup_cache.rb +166 -0
  32. data/lib/kotoshu/cache/model_cache.rb +513 -0
  33. data/lib/kotoshu/cache/suggestion_cache.rb +113 -0
  34. data/lib/kotoshu/cache.rb +40 -0
  35. data/lib/kotoshu/cli/auto_setup.rb +71 -0
  36. data/lib/kotoshu/cli/batch_reporter.rb +315 -0
  37. data/lib/kotoshu/cli/cache_command.rb +356 -0
  38. data/lib/kotoshu/cli/display_formatter.rb +431 -0
  39. data/lib/kotoshu/cli/errors.rb +36 -0
  40. data/lib/kotoshu/cli/interactive_reviewer.rb +319 -0
  41. data/lib/kotoshu/cli/language_resolver.rb +91 -0
  42. data/lib/kotoshu/cli/navigation_manager.rb +272 -0
  43. data/lib/kotoshu/cli/progress_reporter.rb +114 -0
  44. data/lib/kotoshu/cli/status_report.rb +130 -0
  45. data/lib/kotoshu/cli.rb +627 -0
  46. data/lib/kotoshu/commands/cache_command.rb +424 -0
  47. data/lib/kotoshu/commands/check_command.rb +312 -0
  48. data/lib/kotoshu/commands/model_command.rb +295 -0
  49. data/lib/kotoshu/components/passthrough_spell_checker.rb +72 -0
  50. data/lib/kotoshu/components/pos_tagger.rb +98 -0
  51. data/lib/kotoshu/components/spell_checker.rb +73 -0
  52. data/lib/kotoshu/components/synthesizer.rb +60 -0
  53. data/lib/kotoshu/components/tokenizer.rb +58 -0
  54. data/lib/kotoshu/components/whitespace_tokenizer.rb +96 -0
  55. data/lib/kotoshu/configuration/builder.rb +209 -0
  56. data/lib/kotoshu/configuration/resolver.rb +124 -0
  57. data/lib/kotoshu/configuration.rb +702 -0
  58. data/lib/kotoshu/core/exceptions.rb +165 -0
  59. data/lib/kotoshu/core/indexed_dictionary.rb +291 -0
  60. data/lib/kotoshu/core/models/affix_rule.rb +260 -0
  61. data/lib/kotoshu/core/models/result/document_result.rb +263 -0
  62. data/lib/kotoshu/core/models/result/word_result.rb +203 -0
  63. data/lib/kotoshu/core/models/word.rb +142 -0
  64. data/lib/kotoshu/core/trie/builder.rb +119 -0
  65. data/lib/kotoshu/core/trie/node.rb +94 -0
  66. data/lib/kotoshu/core/trie/trie.rb +249 -0
  67. data/lib/kotoshu/core.rb +28 -0
  68. data/lib/kotoshu/data/common_words/de.yml +1800 -0
  69. data/lib/kotoshu/data/common_words/en.yml +1215 -0
  70. data/lib/kotoshu/data/common_words/es.yml +750 -0
  71. data/lib/kotoshu/data/common_words/fr.yml +1015 -0
  72. data/lib/kotoshu/data/common_words/pt.yml +870 -0
  73. data/lib/kotoshu/data/common_words/ru.yml +484 -0
  74. data/lib/kotoshu/data/common_words_loader.rb +152 -0
  75. data/lib/kotoshu/data_structures/bloom_filter.rb +176 -0
  76. data/lib/kotoshu/debug_logger.rb +146 -0
  77. data/lib/kotoshu/debug_mode.rb +134 -0
  78. data/lib/kotoshu/defaults.rb +86 -0
  79. data/lib/kotoshu/dictionaries/catalog.rb +817 -0
  80. data/lib/kotoshu/dictionary/base.rb +237 -0
  81. data/lib/kotoshu/dictionary/cspell.rb +254 -0
  82. data/lib/kotoshu/dictionary/custom.rb +224 -0
  83. data/lib/kotoshu/dictionary/hunspell.rb +526 -0
  84. data/lib/kotoshu/dictionary/plain_text.rb +282 -0
  85. data/lib/kotoshu/dictionary/repository.rb +248 -0
  86. data/lib/kotoshu/dictionary/unified.rb +260 -0
  87. data/lib/kotoshu/dictionary/unix_words.rb +218 -0
  88. data/lib/kotoshu/documents/asciidoc_document.rb +441 -0
  89. data/lib/kotoshu/documents/document.rb +229 -0
  90. data/lib/kotoshu/documents/location.rb +139 -0
  91. data/lib/kotoshu/documents/markdown_document.rb +389 -0
  92. data/lib/kotoshu/documents/plain_text_document.rb +147 -0
  93. data/lib/kotoshu/embeddings/embedding_pipeline.rb +244 -0
  94. data/lib/kotoshu/embeddings/lru_cache.rb +233 -0
  95. data/lib/kotoshu/embeddings/onnx_runtime_model.rb +388 -0
  96. data/lib/kotoshu/embeddings/protocol.rb +83 -0
  97. data/lib/kotoshu/embeddings/protocols.rb +17 -0
  98. data/lib/kotoshu/embeddings/registry.rb +182 -0
  99. data/lib/kotoshu/embeddings/search.rb +192 -0
  100. data/lib/kotoshu/embeddings/similarity_engine.rb +248 -0
  101. data/lib/kotoshu/embeddings/similarity_search.rb +331 -0
  102. data/lib/kotoshu/embeddings/vocabulary.rb +257 -0
  103. data/lib/kotoshu/embeddings.rb +97 -0
  104. data/lib/kotoshu/fluent_checker.rb +91 -0
  105. data/lib/kotoshu/grammar/pattern_matchers/base_matcher.rb +48 -0
  106. data/lib/kotoshu/grammar/pattern_matchers/double_negative_matcher.rb +105 -0
  107. data/lib/kotoshu/grammar/pattern_matchers/possessive_context_matcher.rb +77 -0
  108. data/lib/kotoshu/grammar/pattern_matchers/vowel_sound_matcher.rb +83 -0
  109. data/lib/kotoshu/grammar/rule.rb +95 -0
  110. data/lib/kotoshu/grammar/rule_engine.rb +111 -0
  111. data/lib/kotoshu/grammar/rule_loader.rb +31 -0
  112. data/lib/kotoshu/grammar.rb +18 -0
  113. data/lib/kotoshu/integrity/audit_log.rb +88 -0
  114. data/lib/kotoshu/integrity/manifest.rb +117 -0
  115. data/lib/kotoshu/integrity/net_http.rb +46 -0
  116. data/lib/kotoshu/integrity.rb +25 -0
  117. data/lib/kotoshu/keyboard/layout.rb +115 -0
  118. data/lib/kotoshu/keyboard/layouts/azerty.rb +57 -0
  119. data/lib/kotoshu/keyboard/layouts/dvorak.rb +56 -0
  120. data/lib/kotoshu/keyboard/layouts/jcuken.rb +59 -0
  121. data/lib/kotoshu/keyboard/layouts/qwerty.rb +54 -0
  122. data/lib/kotoshu/keyboard/layouts/qwertz.rb +57 -0
  123. data/lib/kotoshu/keyboard/registry.rb +146 -0
  124. data/lib/kotoshu/keyboard.rb +60 -0
  125. data/lib/kotoshu/language/detector.rb +242 -0
  126. data/lib/kotoshu/language/identifier.rb +378 -0
  127. data/lib/kotoshu/language/languages/base.rb +256 -0
  128. data/lib/kotoshu/language/normalizer/base.rb +137 -0
  129. data/lib/kotoshu/language/registry.rb +147 -0
  130. data/lib/kotoshu/language/resources/ar/common_words.txt +6753 -0
  131. data/lib/kotoshu/language/resources/ar/confusion_sets.txt +11 -0
  132. data/lib/kotoshu/language/resources/de/common_words.txt +10003 -0
  133. data/lib/kotoshu/language/resources/de/confusion_sets.txt +246 -0
  134. data/lib/kotoshu/language/resources/en/common_words.txt +9979 -0
  135. data/lib/kotoshu/language/resources/en/confusion_sets.txt +871 -0
  136. data/lib/kotoshu/language/resources/es/common_words.txt +9992 -0
  137. data/lib/kotoshu/language/resources/es/confusion_sets.txt +17 -0
  138. data/lib/kotoshu/language/resources/fr/common_words.txt +9993 -0
  139. data/lib/kotoshu/language/resources/fr/confusion_sets.txt +76 -0
  140. data/lib/kotoshu/language/resources/pt/common_words.txt +9977 -0
  141. data/lib/kotoshu/language/resources/pt/confusion_sets.txt +18 -0
  142. data/lib/kotoshu/language/resources/ru/common_words.txt +9951 -0
  143. data/lib/kotoshu/language/resources/ru/confusion_sets.txt +5 -0
  144. data/lib/kotoshu/language/tokenizer/base.rb +170 -0
  145. data/lib/kotoshu/language/tokenizer/french_tokenizer.rb +170 -0
  146. data/lib/kotoshu/language/tokenizer/german_tokenizer.rb +41 -0
  147. data/lib/kotoshu/language/tokenizer/japanese_tokenizer.rb +60 -0
  148. data/lib/kotoshu/language/tokenizer/latin_tokenizer.rb +141 -0
  149. data/lib/kotoshu/language/tokenizer/portuguese_tokenizer.rb +160 -0
  150. data/lib/kotoshu/language/tokenizer/russian_tokenizer.rb +95 -0
  151. data/lib/kotoshu/language/tokenizer/spanish_tokenizer.rb +122 -0
  152. data/lib/kotoshu/language.rb +99 -0
  153. data/lib/kotoshu/languages/de/language.rb +546 -0
  154. data/lib/kotoshu/languages/en/language.rb +448 -0
  155. data/lib/kotoshu/languages/es/language.rb +459 -0
  156. data/lib/kotoshu/languages/fr/language.rb +493 -0
  157. data/lib/kotoshu/languages/ja/language.rb +477 -0
  158. data/lib/kotoshu/languages/pt/language.rb +423 -0
  159. data/lib/kotoshu/languages/ru/language.rb +404 -0
  160. data/lib/kotoshu/languages.rb +43 -0
  161. data/lib/kotoshu/metrics_collector.rb +222 -0
  162. data/lib/kotoshu/metrics_module.rb +110 -0
  163. data/lib/kotoshu/models/context.rb +119 -0
  164. data/lib/kotoshu/models/embedding_model.rb +182 -0
  165. data/lib/kotoshu/models/fasttext_model.rb +220 -0
  166. data/lib/kotoshu/models/nearest_neighbor.rb +87 -0
  167. data/lib/kotoshu/models/onnx_model.rb +333 -0
  168. data/lib/kotoshu/models/semantic_error.rb +165 -0
  169. data/lib/kotoshu/models/suggestion.rb +106 -0
  170. data/lib/kotoshu/models/word_embedding.rb +107 -0
  171. data/lib/kotoshu/paths.rb +53 -0
  172. data/lib/kotoshu/personal_dictionary.rb +94 -0
  173. data/lib/kotoshu/plugins/plugin.rb +61 -0
  174. data/lib/kotoshu/plugins/registry.rb +120 -0
  175. data/lib/kotoshu/project_config.rb +76 -0
  176. data/lib/kotoshu/readers/aff_data.rb +356 -0
  177. data/lib/kotoshu/readers/aff_reader.rb +375 -0
  178. data/lib/kotoshu/readers/condition_checker.rb +142 -0
  179. data/lib/kotoshu/readers/dic_reader.rb +118 -0
  180. data/lib/kotoshu/readers/file_reader.rb +347 -0
  181. data/lib/kotoshu/readers/lookup_builder.rb +299 -0
  182. data/lib/kotoshu/readers/readers.rb +6 -0
  183. data/lib/kotoshu/readers.rb +9 -0
  184. data/lib/kotoshu/resource_bundle.rb +30 -0
  185. data/lib/kotoshu/resource_manager.rb +295 -0
  186. data/lib/kotoshu/results/result.rb +165 -0
  187. data/lib/kotoshu/scripts/fasttext_to_onnx.py +275 -0
  188. data/lib/kotoshu/source_registry.rb +74 -0
  189. data/lib/kotoshu/spellchecker/parallel_checker.rb +90 -0
  190. data/lib/kotoshu/spellchecker.rb +298 -0
  191. data/lib/kotoshu/string_metrics.rb +153 -0
  192. data/lib/kotoshu/suggestions/context.rb +55 -0
  193. data/lib/kotoshu/suggestions/generator.rb +175 -0
  194. data/lib/kotoshu/suggestions/pipeline.rb +135 -0
  195. data/lib/kotoshu/suggestions/strategies/base_strategy.rb +296 -0
  196. data/lib/kotoshu/suggestions/strategies/composite_strategy.rb +140 -0
  197. data/lib/kotoshu/suggestions/strategies/edit_distance_strategy.rb +671 -0
  198. data/lib/kotoshu/suggestions/strategies/keyboard_proximity_strategy.rb +228 -0
  199. data/lib/kotoshu/suggestions/strategies/ngram_strategy.rb +130 -0
  200. data/lib/kotoshu/suggestions/strategies/phonetic_strategy.rb +329 -0
  201. data/lib/kotoshu/suggestions/strategies/semantic_strategy.rb +316 -0
  202. data/lib/kotoshu/suggestions/strategies/symspell_strategy.rb +275 -0
  203. data/lib/kotoshu/suggestions/suggestion.rb +174 -0
  204. data/lib/kotoshu/suggestions/suggestion_set.rb +238 -0
  205. data/lib/kotoshu/version.rb +5 -0
  206. data/lib/kotoshu.rb +493 -0
  207. data/script/validate_all_dictionaries.rb +444 -0
  208. data/sig/kotoshu.rbs +4 -0
  209. data/test_oop.rb +79 -0
  210. metadata +298 -0
@@ -0,0 +1,431 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/context'
4
+ require_relative '../models/semantic_error'
5
+
6
+ module Kotoshu
7
+ module Cli
8
+ # Formats output for interactive review mode.
9
+ #
10
+ # Provides methods to display errors, context, suggestions,
11
+ # and navigation prompts in a user-friendly CLI format.
12
+ #
13
+ # @example Displaying an error
14
+ # formatter = DisplayFormatter.new(verbose: true)
15
+ # puts formatter.issue_screen(error, index: 1, total: 10)
16
+ class DisplayFormatter
17
+ # ANSI color codes for terminal output
18
+ COLORS = {
19
+ error: "\e[31m", # Red
20
+ warning: "\e[33m", # Yellow
21
+ success: "\e[32m", # Green
22
+ info: "\e[36m", # Cyan
23
+ dim: "\e[2m", # Dim
24
+ bold: "\e[1m", # Bold
25
+ reset: "\e[0m" # Reset
26
+ }.freeze
27
+
28
+ # Confidence level indicators
29
+ CONFIDENCE_LABELS = {
30
+ high: '✓ High',
31
+ medium: '~ Medium',
32
+ low: '? Low'
33
+ }.freeze
34
+
35
+ attr_reader :verbose, :color_enabled
36
+
37
+ # Create a new display formatter.
38
+ #
39
+ # @param verbose [Boolean] Enable verbose output
40
+ # @param color_enabled [Boolean] Enable ANSI colors (default: true for TTY)
41
+ def initialize(verbose: false, color_enabled: nil)
42
+ @verbose = verbose
43
+ @color_enabled = color_enabled.nil? ? $stdout.tty? : color_enabled
44
+ end
45
+
46
+ # Display summary screen for document review.
47
+ #
48
+ # Shows error breakdown by type and confidence, plus navigation hints.
49
+ #
50
+ # @param document [Document] The document being reviewed
51
+ # @param navigation [NavigationManager] Navigation state
52
+ # @return [String] Formatted summary screen
53
+ def summary_screen(document, navigation)
54
+ stats = navigation.statistics
55
+
56
+ lines = []
57
+ lines << ""
58
+ lines << colorize("═══ Document Review Summary", :bold)
59
+ lines << ""
60
+ lines << "Document: #{document.name}"
61
+ lines << "Format: #{Document::FORMATS[document.format]}"
62
+ lines << "Language: #{document.language_code}"
63
+ lines << ""
64
+ lines << colorize("Error Summary", :bold)
65
+ lines << "─" * 40
66
+
67
+ # Total counts
68
+ lines << "Total errors found: #{stats[:total]}"
69
+ lines << " • Pending: #{stats[:pending]}"
70
+ lines << " • Modified: #{stats[:modified]}"
71
+ lines << " • Skipped: #{stats[:skipped]}"
72
+ lines << ""
73
+
74
+ # Breakdown by type
75
+ if stats[:by_type]&.any?
76
+ lines << colorize("By Type", :bold)
77
+ stats[:by_type].each do |type, count|
78
+ label = SemanticError::ERROR_TYPES[type] || type.to_s.capitalize
79
+ lines << " • #{label}: #{count}"
80
+ end
81
+ lines << ""
82
+ end
83
+
84
+ # Breakdown by confidence
85
+ if stats[:by_confidence]&.any?
86
+ lines << colorize("By Confidence", :bold)
87
+ lines << " • High (>0.8): #{stats[:by_confidence][:high]}"
88
+ lines << " • Medium (0.5-0.8): #{stats[:by_confidence][:medium]}"
89
+ lines << " • Low (≤0.5): #{stats[:by_confidence][:low]}"
90
+ lines << ""
91
+ end
92
+
93
+ # Navigation hints
94
+ lines << colorize("Navigation", :bold)
95
+ lines << " [Enter] Next error [s] Skip [a] Accept suggestion"
96
+ lines << " [b] Back [q] Quit [l] List all errors"
97
+ lines << " [j] Jump to error [h] Help [?] Show this summary"
98
+ lines << ""
99
+
100
+ lines.join("\n")
101
+ end
102
+
103
+ # Display individual error screen.
104
+ #
105
+ # Shows the error with context, suggestions, and action prompt.
106
+ #
107
+ # @param error [SemanticError] The error to display
108
+ # @param index [Integer] Current error index (1-based)
109
+ # @param total [Integer] Total number of errors
110
+ # @param show_context [Boolean] Show context window (default: true)
111
+ # @return [String] Formatted error screen
112
+ def issue_screen(error, index:, total:, show_context: true)
113
+ lines = []
114
+ lines << ""
115
+ lines << colorize("═" * 70, :bold)
116
+ lines << colorize("Error #{index} of #{total}", :bold) + " — #{error_type_label(error.error_type)}"
117
+ lines << colorize("═" * 70, :bold)
118
+ lines << ""
119
+
120
+ # Error location
121
+ lines << colorize("Location:", :bold) + " #{error.location}"
122
+ lines << ""
123
+
124
+ # Error with highlighting
125
+ lines << colorize("Found:", :error) + " #{highlight_in_context(error)}"
126
+ lines << ""
127
+
128
+ # Context window
129
+ if show_context && error.context
130
+ lines << colorize("Context:", :bold)
131
+ lines << format_context(error.context)
132
+ lines << ""
133
+ end
134
+
135
+ # Confidence indicator
136
+ conf_label = confidence_label(error.confidence)
137
+ lines << colorize("Confidence:", :bold) + " #{conf_label} (#{(error.confidence * 100).round(1)}%)"
138
+ lines << ""
139
+
140
+ # Suggestions
141
+ if error.suggestions&.any?
142
+ lines << colorize("Suggestions:", :bold)
143
+ lines << format_suggestions(error.suggestions)
144
+ lines << ""
145
+ end
146
+
147
+ # Action prompt
148
+ lines << colorize("Actions:", :bold) + " [Enter] Next [1-#{error.suggestions&.size || 0}] Accept [s] Skip [b] Back [q] Quit"
149
+ lines << ""
150
+
151
+ lines.join("\n")
152
+ end
153
+
154
+ # Highlight the error word within its context.
155
+ #
156
+ # @param error [SemanticError] The error to highlight
157
+ # @return [String] Highlighted error with context
158
+ def highlight_error(error)
159
+ return error.original unless error.context
160
+
161
+ ctx = error.context
162
+ highlighted = ctx.current.gsub(/\b#{Regexp.escape(error.original)}\b/i) do |match|
163
+ colorize(match, :error)
164
+ end
165
+
166
+ # Truncate if too long
167
+ if highlighted.length > 100
168
+ "..." + highlighted[-100..-1]
169
+ else
170
+ highlighted
171
+ end
172
+ end
173
+
174
+ # Format suggestions list with confidence scores.
175
+ #
176
+ # @param suggestions [Array<Suggestion>] List of suggestions
177
+ # @param max_display [Integer] Maximum suggestions to show (default: 5)
178
+ # @return [String] Formatted suggestions
179
+ def format_suggestions(suggestions, max_display: 5)
180
+ return colorize("No suggestions", :dim) unless suggestions&.any?
181
+
182
+ lines = []
183
+ suggestions.first(max_display).each_with_index do |suggestion, idx|
184
+ number = colorize((idx + 1).to_s + '.', :info)
185
+ word = colorize(suggestion.word, suggestion.high_confidence? ? :success : :warning)
186
+ confidence = "(#{(suggestion.confidence * 100).round(0)}%)"
187
+
188
+ line = " #{number} #{word} #{confidence}"
189
+ line += " [#{suggestion.source}]" if suggestion.source && @verbose
190
+ lines << line
191
+ end
192
+
193
+ if suggestions.size > max_display
194
+ remaining = suggestions.size - max_display
195
+ lines << colorize(" ... and #{remaining} more", :dim)
196
+ end
197
+
198
+ lines.join("\n")
199
+ end
200
+
201
+ # Display all errors with status indicators.
202
+ #
203
+ # @param navigation [NavigationManager] Navigation state
204
+ # @return [String] Formatted error list
205
+ def list_all_errors(navigation)
206
+ lines = []
207
+ lines << ""
208
+ lines << colorize("All Errors (#{navigation.errors.size})", :bold)
209
+ lines << "─" * 70
210
+
211
+ navigation.list_all.each do |line|
212
+ # Add color coding based on status
213
+ colored_line = if line.include?('[DONE]')
214
+ colorize(line, :success)
215
+ elsif line.include?('[SKIP]')
216
+ colorize(line, :dim)
217
+ else
218
+ line
219
+ end
220
+ lines << colored_line
221
+ end
222
+
223
+ lines << ""
224
+ lines.join("\n")
225
+ end
226
+
227
+ # Display statistics summary.
228
+ #
229
+ # @param navigation [NavigationManager] Navigation state
230
+ # @return [String] Formatted statistics
231
+ def statistics(navigation)
232
+ stats = navigation.statistics
233
+
234
+ lines = []
235
+ lines << ""
236
+ lines << colorize("Review Statistics", :bold)
237
+ lines << "─" * 40
238
+ lines << "Total: #{stats[:total]}"
239
+ lines << "Pending: #{stats[:pending]}"
240
+ lines << colorize("Modified: #{stats[:modified]}", :success)
241
+ lines << colorize("Skipped: #{stats[:skipped]}", :dim)
242
+ lines << ""
243
+
244
+ if stats[:by_type]&.any?
245
+ lines << colorize("By Type:", :bold)
246
+ stats[:by_type].each do |type, count|
247
+ label = SemanticError::ERROR_TYPES[type] || type.to_s.capitalize
248
+ lines << " #{label}: #{count}"
249
+ end
250
+ lines << ""
251
+ end
252
+
253
+ lines.join("\n")
254
+ end
255
+
256
+ # Display help screen.
257
+ #
258
+ # @return [String] Formatted help text
259
+ def help_screen
260
+ lines = []
261
+ lines << ""
262
+ lines << colorize("═══ Interactive Review Help ═══", :bold)
263
+ lines << ""
264
+ lines << colorize("Navigation Commands:", :bold)
265
+ lines << " [Enter] or [n] Move to next error"
266
+ lines << " [b] Move to previous error"
267
+ lines << " [j] <number> Jump to error by number"
268
+ lines << " [f] Jump to first error"
269
+ lines << " [l] Jump to last error"
270
+ lines << ""
271
+ lines << colorize("Error Actions:", :bold)
272
+ lines << " [1-9] Accept suggestion by number"
273
+ lines << " [s] Skip current error"
274
+ lines << " [e] Edit custom replacement"
275
+ lines << ""
276
+ lines << colorize("Display Commands:", :bold)
277
+ lines << " [l] List all errors with status"
278
+ lines << " [t] Toggle show/hide context"
279
+ lines << " [v] Toggle verbose mode"
280
+ lines << " [?] Show this summary"
281
+ lines << ""
282
+ lines << colorize("Program Commands:", :bold)
283
+ lines << " [q] Quit and save changes"
284
+ lines << " [Q] Quit without saving"
285
+ lines << " [!] Export corrections to file"
286
+ lines << ""
287
+ lines << colorize("Filter Commands:", :bold)
288
+ lines << " [c] <level> Filter by confidence (high, medium, low)"
289
+ lines << " [y] <type> Filter by error type"
290
+ lines << " [a] Show all errors (clear filters)"
291
+ lines << ""
292
+
293
+ lines.join("\n")
294
+ end
295
+
296
+ # Display export summary.
297
+ #
298
+ # @param navigation [NavigationManager] Navigation state
299
+ # @param export_path [String] Path to export file
300
+ # @return [String] Formatted export summary
301
+ def export_summary(navigation, export_path)
302
+ corrections = navigation.export_corrections
303
+
304
+ lines = []
305
+ lines << ""
306
+ lines << colorize("═══ Export Summary ═══", :bold)
307
+ lines << ""
308
+ lines << "Exported #{corrections.size} corrections to:"
309
+ lines << colorize(export_path, :info)
310
+ lines << ""
311
+
312
+ if corrections.any? && @verbose
313
+ lines << colorize("Corrections:", :bold)
314
+ corrections.first(10).each do |corr|
315
+ lines << " Line #{corr[:line]}: #{corr[:original]} → #{corr[:replacement]}"
316
+ end
317
+
318
+ if corrections.size > 10
319
+ lines << colorize(" ... and #{corrections.size - 10} more", :dim)
320
+ end
321
+
322
+ lines << ""
323
+ end
324
+
325
+ lines.join("\n")
326
+ end
327
+
328
+ # Format input prompt.
329
+ #
330
+ # @param text [String] Prompt text
331
+ # @return [String] Formatted prompt
332
+ def prompt(text)
333
+ colorize("#{text} ", :info)
334
+ end
335
+
336
+ # Display warning message.
337
+ #
338
+ # @param message [String] Warning message
339
+ # @return [String] Formatted warning
340
+ def warning(message)
341
+ colorize("Warning: #{message}", :warning)
342
+ end
343
+
344
+ # Display error message.
345
+ #
346
+ # @param message [String] Error message
347
+ # @return [String] Formatted error
348
+ def error(message)
349
+ colorize("Error: #{message}", :error)
350
+ end
351
+
352
+ # Display success message.
353
+ #
354
+ # @param message [String] Success message
355
+ # @return [String] Formatted success
356
+ def success(message)
357
+ colorize(message, :success)
358
+ end
359
+
360
+ private
361
+
362
+ # Highlight error in context with visual markers.
363
+ #
364
+ # @param error [SemanticError] The error to highlight
365
+ # @return [String] Error with context markers
366
+ def highlight_in_context(error)
367
+ # For now, just show the error word with color
368
+ # In full implementation, would extract from context and add markers
369
+ colorize(error.original, :error)
370
+ end
371
+
372
+ # Format context for display.
373
+ #
374
+ # @param context [Context] The context object
375
+ # @return [String] Formatted context with line numbers
376
+ def format_context(context)
377
+ lines = []
378
+
379
+ # Before context
380
+ if context.before && !context.before.empty?
381
+ lines << colorize(context.before.split("\n").last(2).join("\n"), :dim)
382
+ end
383
+
384
+ # Current line with marker
385
+ current = "→ #{context.current}"
386
+ lines << current
387
+
388
+ # After context
389
+ if context.after && !context.after.empty?
390
+ lines << colorize(context.after.split("\n").first(2).join("\n"), :dim)
391
+ end
392
+
393
+ lines.join("\n")
394
+ end
395
+
396
+ # Get confidence label with indicator.
397
+ #
398
+ # @param confidence [Float] Confidence score (0.0 to 1.0)
399
+ # @return [String] Confidence label
400
+ def confidence_label(confidence)
401
+ case confidence
402
+ when 0.8..1.0 then colorize(CONFIDENCE_LABELS[:high], :success)
403
+ when 0.5..0.8 then colorize(CONFIDENCE_LABELS[:medium], :warning)
404
+ else colorize(CONFIDENCE_LABELS[:low], :dim)
405
+ end
406
+ end
407
+
408
+ # Get human-readable error type label.
409
+ #
410
+ # @param error_type [Symbol] Error type symbol
411
+ # @return [String] Human-readable label
412
+ def error_type_label(error_type)
413
+ SemanticError::ERROR_TYPES[error_type] || error_type.to_s.capitalize
414
+ end
415
+
416
+ # Apply color to text if colors are enabled.
417
+ #
418
+ # @param text [String] Text to colorize
419
+ # @param color [Symbol] Color symbol
420
+ # @return [String] Colorized text or original if colors disabled
421
+ def colorize(text, color)
422
+ return text unless @color_enabled
423
+
424
+ code = COLORS[color]
425
+ return text unless code
426
+
427
+ "#{code}#{text}#{COLORS[:reset]}"
428
+ end
429
+ end
430
+ end
431
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Kotoshu
6
+ module Cli
7
+ # Semantic error classes for the CLI. Subclass Thor::Error so the CLI's
8
+ # top-level dispatcher can catch them and exit with the appropriate code.
9
+ # Each class carries an `exit_status` that the dispatcher reads.
10
+ module Errors
11
+ # Base class for all CLI errors. Carries an exit_status.
12
+ class CliError < Thor::Error
13
+ attr_reader :exit_status
14
+
15
+ def initialize(message, exit_status:)
16
+ super(message)
17
+ @exit_status = exit_status
18
+ end
19
+ end
20
+
21
+ # Usage error: bad flags, missing argument, file not found (exit 2).
22
+ class UsageError < CliError
23
+ def initialize(message)
24
+ super(message, exit_status: 2)
25
+ end
26
+ end
27
+
28
+ # Resource unavailable: network down, offline+uncached, integrity failure (exit 3).
29
+ class ResourceUnavailable < CliError
30
+ def initialize(message)
31
+ super(message, exit_status: 3)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end