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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +18 -0
- data/CHANGELOG.md +182 -0
- data/CLAUDE.md +172 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE +31 -0
- data/README.adoc +955 -0
- data/Rakefile +12 -0
- data/SECURITY.md +93 -0
- data/examples/01_basic_word_checking.rb +38 -0
- data/examples/02_text_document_checking.rb +77 -0
- data/examples/03_dictionary_backends.rb +137 -0
- data/examples/04_trie_data_structure.rb +146 -0
- data/examples/05_suggestion_algorithms.rb +239 -0
- data/examples/06_configuration_advanced.rb +287 -0
- data/examples/07_multi_language_dictionaries.rb +278 -0
- data/exe/kotoshu +6 -0
- data/lib/kotoshu/algorithms/capitalization.rb +276 -0
- data/lib/kotoshu/algorithms/lookup.rb +876 -0
- data/lib/kotoshu/algorithms/ngram_suggest.rb +270 -0
- data/lib/kotoshu/algorithms/permutations.rb +283 -0
- data/lib/kotoshu/algorithms/phonet_suggest.rb +167 -0
- data/lib/kotoshu/algorithms/suggest.rb +575 -0
- data/lib/kotoshu/algorithms.rb +14 -0
- data/lib/kotoshu/analyzers/semantic_analyzer.rb +295 -0
- data/lib/kotoshu/cache/base_cache.rb +596 -0
- data/lib/kotoshu/cache/cache.rb +91 -0
- data/lib/kotoshu/cache/frequency_cache.rb +224 -0
- data/lib/kotoshu/cache/language_cache.rb +454 -0
- data/lib/kotoshu/cache/lookup_cache.rb +166 -0
- data/lib/kotoshu/cache/model_cache.rb +513 -0
- data/lib/kotoshu/cache/suggestion_cache.rb +113 -0
- data/lib/kotoshu/cache.rb +40 -0
- data/lib/kotoshu/cli/auto_setup.rb +71 -0
- data/lib/kotoshu/cli/batch_reporter.rb +315 -0
- data/lib/kotoshu/cli/cache_command.rb +356 -0
- data/lib/kotoshu/cli/display_formatter.rb +431 -0
- data/lib/kotoshu/cli/errors.rb +36 -0
- data/lib/kotoshu/cli/interactive_reviewer.rb +319 -0
- data/lib/kotoshu/cli/language_resolver.rb +91 -0
- data/lib/kotoshu/cli/navigation_manager.rb +272 -0
- data/lib/kotoshu/cli/progress_reporter.rb +114 -0
- data/lib/kotoshu/cli/status_report.rb +130 -0
- data/lib/kotoshu/cli.rb +627 -0
- data/lib/kotoshu/commands/cache_command.rb +424 -0
- data/lib/kotoshu/commands/check_command.rb +312 -0
- data/lib/kotoshu/commands/model_command.rb +295 -0
- data/lib/kotoshu/components/passthrough_spell_checker.rb +72 -0
- data/lib/kotoshu/components/pos_tagger.rb +98 -0
- data/lib/kotoshu/components/spell_checker.rb +73 -0
- data/lib/kotoshu/components/synthesizer.rb +60 -0
- data/lib/kotoshu/components/tokenizer.rb +58 -0
- data/lib/kotoshu/components/whitespace_tokenizer.rb +96 -0
- data/lib/kotoshu/configuration/builder.rb +209 -0
- data/lib/kotoshu/configuration/resolver.rb +124 -0
- data/lib/kotoshu/configuration.rb +702 -0
- data/lib/kotoshu/core/exceptions.rb +165 -0
- data/lib/kotoshu/core/indexed_dictionary.rb +291 -0
- data/lib/kotoshu/core/models/affix_rule.rb +260 -0
- data/lib/kotoshu/core/models/result/document_result.rb +263 -0
- data/lib/kotoshu/core/models/result/word_result.rb +203 -0
- data/lib/kotoshu/core/models/word.rb +142 -0
- data/lib/kotoshu/core/trie/builder.rb +119 -0
- data/lib/kotoshu/core/trie/node.rb +94 -0
- data/lib/kotoshu/core/trie/trie.rb +249 -0
- data/lib/kotoshu/core.rb +28 -0
- data/lib/kotoshu/data/common_words/de.yml +1800 -0
- data/lib/kotoshu/data/common_words/en.yml +1215 -0
- data/lib/kotoshu/data/common_words/es.yml +750 -0
- data/lib/kotoshu/data/common_words/fr.yml +1015 -0
- data/lib/kotoshu/data/common_words/pt.yml +870 -0
- data/lib/kotoshu/data/common_words/ru.yml +484 -0
- data/lib/kotoshu/data/common_words_loader.rb +152 -0
- data/lib/kotoshu/data_structures/bloom_filter.rb +176 -0
- data/lib/kotoshu/debug_logger.rb +146 -0
- data/lib/kotoshu/debug_mode.rb +134 -0
- data/lib/kotoshu/defaults.rb +86 -0
- data/lib/kotoshu/dictionaries/catalog.rb +817 -0
- data/lib/kotoshu/dictionary/base.rb +237 -0
- data/lib/kotoshu/dictionary/cspell.rb +254 -0
- data/lib/kotoshu/dictionary/custom.rb +224 -0
- data/lib/kotoshu/dictionary/hunspell.rb +526 -0
- data/lib/kotoshu/dictionary/plain_text.rb +282 -0
- data/lib/kotoshu/dictionary/repository.rb +248 -0
- data/lib/kotoshu/dictionary/unified.rb +260 -0
- data/lib/kotoshu/dictionary/unix_words.rb +218 -0
- data/lib/kotoshu/documents/asciidoc_document.rb +441 -0
- data/lib/kotoshu/documents/document.rb +229 -0
- data/lib/kotoshu/documents/location.rb +139 -0
- data/lib/kotoshu/documents/markdown_document.rb +389 -0
- data/lib/kotoshu/documents/plain_text_document.rb +147 -0
- data/lib/kotoshu/embeddings/embedding_pipeline.rb +244 -0
- data/lib/kotoshu/embeddings/lru_cache.rb +233 -0
- data/lib/kotoshu/embeddings/onnx_runtime_model.rb +388 -0
- data/lib/kotoshu/embeddings/protocol.rb +83 -0
- data/lib/kotoshu/embeddings/protocols.rb +17 -0
- data/lib/kotoshu/embeddings/registry.rb +182 -0
- data/lib/kotoshu/embeddings/search.rb +192 -0
- data/lib/kotoshu/embeddings/similarity_engine.rb +248 -0
- data/lib/kotoshu/embeddings/similarity_search.rb +331 -0
- data/lib/kotoshu/embeddings/vocabulary.rb +257 -0
- data/lib/kotoshu/embeddings.rb +97 -0
- data/lib/kotoshu/fluent_checker.rb +91 -0
- data/lib/kotoshu/grammar/pattern_matchers/base_matcher.rb +48 -0
- data/lib/kotoshu/grammar/pattern_matchers/double_negative_matcher.rb +105 -0
- data/lib/kotoshu/grammar/pattern_matchers/possessive_context_matcher.rb +77 -0
- data/lib/kotoshu/grammar/pattern_matchers/vowel_sound_matcher.rb +83 -0
- data/lib/kotoshu/grammar/rule.rb +95 -0
- data/lib/kotoshu/grammar/rule_engine.rb +111 -0
- data/lib/kotoshu/grammar/rule_loader.rb +31 -0
- data/lib/kotoshu/grammar.rb +18 -0
- data/lib/kotoshu/integrity/audit_log.rb +88 -0
- data/lib/kotoshu/integrity/manifest.rb +117 -0
- data/lib/kotoshu/integrity/net_http.rb +46 -0
- data/lib/kotoshu/integrity.rb +25 -0
- data/lib/kotoshu/keyboard/layout.rb +115 -0
- data/lib/kotoshu/keyboard/layouts/azerty.rb +57 -0
- data/lib/kotoshu/keyboard/layouts/dvorak.rb +56 -0
- data/lib/kotoshu/keyboard/layouts/jcuken.rb +59 -0
- data/lib/kotoshu/keyboard/layouts/qwerty.rb +54 -0
- data/lib/kotoshu/keyboard/layouts/qwertz.rb +57 -0
- data/lib/kotoshu/keyboard/registry.rb +146 -0
- data/lib/kotoshu/keyboard.rb +60 -0
- data/lib/kotoshu/language/detector.rb +242 -0
- data/lib/kotoshu/language/identifier.rb +378 -0
- data/lib/kotoshu/language/languages/base.rb +256 -0
- data/lib/kotoshu/language/normalizer/base.rb +137 -0
- data/lib/kotoshu/language/registry.rb +147 -0
- data/lib/kotoshu/language/resources/ar/common_words.txt +6753 -0
- data/lib/kotoshu/language/resources/ar/confusion_sets.txt +11 -0
- data/lib/kotoshu/language/resources/de/common_words.txt +10003 -0
- data/lib/kotoshu/language/resources/de/confusion_sets.txt +246 -0
- data/lib/kotoshu/language/resources/en/common_words.txt +9979 -0
- data/lib/kotoshu/language/resources/en/confusion_sets.txt +871 -0
- data/lib/kotoshu/language/resources/es/common_words.txt +9992 -0
- data/lib/kotoshu/language/resources/es/confusion_sets.txt +17 -0
- data/lib/kotoshu/language/resources/fr/common_words.txt +9993 -0
- data/lib/kotoshu/language/resources/fr/confusion_sets.txt +76 -0
- data/lib/kotoshu/language/resources/pt/common_words.txt +9977 -0
- data/lib/kotoshu/language/resources/pt/confusion_sets.txt +18 -0
- data/lib/kotoshu/language/resources/ru/common_words.txt +9951 -0
- data/lib/kotoshu/language/resources/ru/confusion_sets.txt +5 -0
- data/lib/kotoshu/language/tokenizer/base.rb +170 -0
- data/lib/kotoshu/language/tokenizer/french_tokenizer.rb +170 -0
- data/lib/kotoshu/language/tokenizer/german_tokenizer.rb +41 -0
- data/lib/kotoshu/language/tokenizer/japanese_tokenizer.rb +60 -0
- data/lib/kotoshu/language/tokenizer/latin_tokenizer.rb +141 -0
- data/lib/kotoshu/language/tokenizer/portuguese_tokenizer.rb +160 -0
- data/lib/kotoshu/language/tokenizer/russian_tokenizer.rb +95 -0
- data/lib/kotoshu/language/tokenizer/spanish_tokenizer.rb +122 -0
- data/lib/kotoshu/language.rb +99 -0
- data/lib/kotoshu/languages/de/language.rb +546 -0
- data/lib/kotoshu/languages/en/language.rb +448 -0
- data/lib/kotoshu/languages/es/language.rb +459 -0
- data/lib/kotoshu/languages/fr/language.rb +493 -0
- data/lib/kotoshu/languages/ja/language.rb +477 -0
- data/lib/kotoshu/languages/pt/language.rb +423 -0
- data/lib/kotoshu/languages/ru/language.rb +404 -0
- data/lib/kotoshu/languages.rb +43 -0
- data/lib/kotoshu/metrics_collector.rb +222 -0
- data/lib/kotoshu/metrics_module.rb +110 -0
- data/lib/kotoshu/models/context.rb +119 -0
- data/lib/kotoshu/models/embedding_model.rb +182 -0
- data/lib/kotoshu/models/fasttext_model.rb +220 -0
- data/lib/kotoshu/models/nearest_neighbor.rb +87 -0
- data/lib/kotoshu/models/onnx_model.rb +333 -0
- data/lib/kotoshu/models/semantic_error.rb +165 -0
- data/lib/kotoshu/models/suggestion.rb +106 -0
- data/lib/kotoshu/models/word_embedding.rb +107 -0
- data/lib/kotoshu/paths.rb +53 -0
- data/lib/kotoshu/personal_dictionary.rb +94 -0
- data/lib/kotoshu/plugins/plugin.rb +61 -0
- data/lib/kotoshu/plugins/registry.rb +120 -0
- data/lib/kotoshu/project_config.rb +76 -0
- data/lib/kotoshu/readers/aff_data.rb +356 -0
- data/lib/kotoshu/readers/aff_reader.rb +375 -0
- data/lib/kotoshu/readers/condition_checker.rb +142 -0
- data/lib/kotoshu/readers/dic_reader.rb +118 -0
- data/lib/kotoshu/readers/file_reader.rb +347 -0
- data/lib/kotoshu/readers/lookup_builder.rb +299 -0
- data/lib/kotoshu/readers/readers.rb +6 -0
- data/lib/kotoshu/readers.rb +9 -0
- data/lib/kotoshu/resource_bundle.rb +30 -0
- data/lib/kotoshu/resource_manager.rb +295 -0
- data/lib/kotoshu/results/result.rb +165 -0
- data/lib/kotoshu/scripts/fasttext_to_onnx.py +275 -0
- data/lib/kotoshu/source_registry.rb +74 -0
- data/lib/kotoshu/spellchecker/parallel_checker.rb +90 -0
- data/lib/kotoshu/spellchecker.rb +298 -0
- data/lib/kotoshu/string_metrics.rb +153 -0
- data/lib/kotoshu/suggestions/context.rb +55 -0
- data/lib/kotoshu/suggestions/generator.rb +175 -0
- data/lib/kotoshu/suggestions/pipeline.rb +135 -0
- data/lib/kotoshu/suggestions/strategies/base_strategy.rb +296 -0
- data/lib/kotoshu/suggestions/strategies/composite_strategy.rb +140 -0
- data/lib/kotoshu/suggestions/strategies/edit_distance_strategy.rb +671 -0
- data/lib/kotoshu/suggestions/strategies/keyboard_proximity_strategy.rb +228 -0
- data/lib/kotoshu/suggestions/strategies/ngram_strategy.rb +130 -0
- data/lib/kotoshu/suggestions/strategies/phonetic_strategy.rb +329 -0
- data/lib/kotoshu/suggestions/strategies/semantic_strategy.rb +316 -0
- data/lib/kotoshu/suggestions/strategies/symspell_strategy.rb +275 -0
- data/lib/kotoshu/suggestions/suggestion.rb +174 -0
- data/lib/kotoshu/suggestions/suggestion_set.rb +238 -0
- data/lib/kotoshu/version.rb +5 -0
- data/lib/kotoshu.rb +493 -0
- data/script/validate_all_dictionaries.rb +444 -0
- data/sig/kotoshu.rbs +4 -0
- data/test_oop.rb +79 -0
- metadata +298 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'navigation_manager'
|
|
4
|
+
require_relative 'display_formatter'
|
|
5
|
+
require_relative '../analyzers/semantic_analyzer'
|
|
6
|
+
|
|
7
|
+
module Kotoshu
|
|
8
|
+
module Cli
|
|
9
|
+
# Interactive review session for spell/grammar checking.
|
|
10
|
+
#
|
|
11
|
+
# Provides a user-friendly terminal interface for reviewing errors
|
|
12
|
+
# with full navigation support (forward, backward, jump, skip, accept).
|
|
13
|
+
#
|
|
14
|
+
# @example Starting an interactive session
|
|
15
|
+
# reviewer = InteractiveReviewer.new(document, analyzer)
|
|
16
|
+
# reviewer.run # Enters interactive loop
|
|
17
|
+
#
|
|
18
|
+
# @example Batch mode (non-interactive)
|
|
19
|
+
# reviewer = InteractiveReviewer.new(document, analyzer)
|
|
20
|
+
# reporter = reviewer.run_batch # Returns BatchReporter
|
|
21
|
+
class InteractiveReviewer
|
|
22
|
+
attr_reader :document, :analyzer, :navigation, :formatter
|
|
23
|
+
|
|
24
|
+
# Create a new interactive reviewer.
|
|
25
|
+
#
|
|
26
|
+
# @param document [Documents::Document] Document to review
|
|
27
|
+
# @param analyzer [Analyzers::SemanticAnalyzer] Error analyzer
|
|
28
|
+
# @param formatter [DisplayFormatter, nil] Display formatter (default: new instance)
|
|
29
|
+
def initialize(document, analyzer, formatter: nil)
|
|
30
|
+
raise ArgumentError, "Document required" unless document
|
|
31
|
+
raise ArgumentError, "Analyzer required" unless analyzer
|
|
32
|
+
|
|
33
|
+
@document = document
|
|
34
|
+
@analyzer = analyzer
|
|
35
|
+
|
|
36
|
+
# Analyze document for errors
|
|
37
|
+
errors = @analyzer.analyze(@document)
|
|
38
|
+
|
|
39
|
+
# Create navigation manager
|
|
40
|
+
@navigation = NavigationManager.new(errors)
|
|
41
|
+
|
|
42
|
+
# Create display formatter
|
|
43
|
+
@formatter = formatter || DisplayFormatter.new
|
|
44
|
+
|
|
45
|
+
@running = false
|
|
46
|
+
@show_context = true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Run the interactive review loop.
|
|
50
|
+
#
|
|
51
|
+
# @return [Hash] Session summary with statistics
|
|
52
|
+
def run
|
|
53
|
+
@running = true
|
|
54
|
+
|
|
55
|
+
# Show welcome message
|
|
56
|
+
show_welcome
|
|
57
|
+
|
|
58
|
+
# Show summary screen
|
|
59
|
+
puts @formatter.summary_screen(@document, @navigation)
|
|
60
|
+
|
|
61
|
+
# Main interactive loop
|
|
62
|
+
while @running && @navigation.current
|
|
63
|
+
show_current_error
|
|
64
|
+
process_input(get_user_input)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Show exit message
|
|
68
|
+
show_exit_summary
|
|
69
|
+
|
|
70
|
+
# Return session summary
|
|
71
|
+
session_summary
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Run in batch mode (non-interactive).
|
|
75
|
+
#
|
|
76
|
+
# @return [BatchReporter] Reporter with results
|
|
77
|
+
def run_batch
|
|
78
|
+
# Analyze all errors without user interaction
|
|
79
|
+
errors = @navigation.errors
|
|
80
|
+
|
|
81
|
+
# Apply all high-confidence corrections automatically
|
|
82
|
+
errors.each do |error|
|
|
83
|
+
if error.high_confidence? && error.suggestions&.any?
|
|
84
|
+
@navigation.accept_suggestion(error.recommended_suggestion)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Return batch reporter
|
|
89
|
+
require_relative 'batch_reporter'
|
|
90
|
+
BatchReporter.new(@document, @navigation, @formatter)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check if session has errors.
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean] True if there are errors to review
|
|
96
|
+
def has_errors?
|
|
97
|
+
@navigation.errors.any?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Get session statistics.
|
|
101
|
+
#
|
|
102
|
+
# @return [Hash] Statistics hash
|
|
103
|
+
def statistics
|
|
104
|
+
@navigation.statistics
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
# Show welcome message.
|
|
110
|
+
def show_welcome
|
|
111
|
+
puts ""
|
|
112
|
+
puts @formatter.colorize("╔═══════════════════════════════════════════════════════════════╗", :bold)
|
|
113
|
+
puts @formatter.colorize("║ Kotoshu Interactive Spell/Grammar Review ║", :bold)
|
|
114
|
+
puts @formatter.colorize("╚═══════════════════════════════════════════════════════════════╝", :bold)
|
|
115
|
+
puts ""
|
|
116
|
+
puts "Document: #{@document.name}"
|
|
117
|
+
puts "Language: #{@document.language_code}"
|
|
118
|
+
puts "Errors found: #{@navigation.errors.size}"
|
|
119
|
+
puts ""
|
|
120
|
+
puts "Type 'h' for help, 'q' to quit"
|
|
121
|
+
puts ""
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Show current error screen.
|
|
125
|
+
def show_current_error
|
|
126
|
+
error = @navigation.current
|
|
127
|
+
return unless error
|
|
128
|
+
|
|
129
|
+
index = @navigation.current_index + 1
|
|
130
|
+
total = @navigation.errors.size
|
|
131
|
+
|
|
132
|
+
puts @formatter.issue_screen(error, index: index, total: total, show_context: @show_context)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Get user input from terminal.
|
|
136
|
+
#
|
|
137
|
+
# @return [String] User input
|
|
138
|
+
def get_user_input
|
|
139
|
+
print @formatter.prompt("Action>")
|
|
140
|
+
$stdin.gets&.chomp || 'q'
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Process user input command.
|
|
144
|
+
#
|
|
145
|
+
# @param input [String] User input
|
|
146
|
+
def process_input(input)
|
|
147
|
+
return if input.nil? || input.empty?
|
|
148
|
+
|
|
149
|
+
case input
|
|
150
|
+
when '', 'n', 'enter'
|
|
151
|
+
# Move to next error
|
|
152
|
+
@navigation.forward
|
|
153
|
+
|
|
154
|
+
when 'b', 'back'
|
|
155
|
+
# Move to previous error
|
|
156
|
+
@navigation.backward
|
|
157
|
+
|
|
158
|
+
when 's', 'skip'
|
|
159
|
+
# Skip current error
|
|
160
|
+
@navigation.skip_current
|
|
161
|
+
|
|
162
|
+
when /^(\d+)$/
|
|
163
|
+
# Accept suggestion by number
|
|
164
|
+
suggestion_number = $1.to_i - 1 # Convert to 0-based index
|
|
165
|
+
accept_suggestion_by_number(suggestion_number)
|
|
166
|
+
|
|
167
|
+
when 'j', 'jump'
|
|
168
|
+
# Jump to error
|
|
169
|
+
jump_to_error
|
|
170
|
+
|
|
171
|
+
when 'f', 'first'
|
|
172
|
+
@navigation.first
|
|
173
|
+
|
|
174
|
+
when 'l', 'last'
|
|
175
|
+
@navigation.last
|
|
176
|
+
|
|
177
|
+
when 'l', 'list'
|
|
178
|
+
# List all errors
|
|
179
|
+
puts @formatter.list_all_errors(@navigation)
|
|
180
|
+
puts @formatter.prompt("Press Enter to continue...")
|
|
181
|
+
$stdin.gets
|
|
182
|
+
|
|
183
|
+
when 't', 'toggle'
|
|
184
|
+
# Toggle context display
|
|
185
|
+
@show_context = !@show_context
|
|
186
|
+
|
|
187
|
+
when 'v', 'verbose'
|
|
188
|
+
# Toggle verbose mode
|
|
189
|
+
@formatter.verbose = !@formatter.verbose
|
|
190
|
+
puts @formatter.success("Verbose mode: #{@formatter.verbose ? 'ON' : 'OFF'}")
|
|
191
|
+
|
|
192
|
+
when '?'
|
|
193
|
+
# Show summary
|
|
194
|
+
puts @formatter.summary_screen(@document, @navigation)
|
|
195
|
+
|
|
196
|
+
when 'h', 'help'
|
|
197
|
+
# Show help
|
|
198
|
+
puts @formatter.help_screen
|
|
199
|
+
puts @formatter.prompt("Press Enter to continue...")
|
|
200
|
+
$stdin.gets
|
|
201
|
+
|
|
202
|
+
when 'q', 'quit'
|
|
203
|
+
# Quit and save
|
|
204
|
+
quit_with_save
|
|
205
|
+
|
|
206
|
+
when 'Q', 'QUIT'
|
|
207
|
+
# Quit without saving
|
|
208
|
+
quit_without_save
|
|
209
|
+
|
|
210
|
+
when '!', 'export'
|
|
211
|
+
# Export corrections
|
|
212
|
+
export_corrections
|
|
213
|
+
|
|
214
|
+
else
|
|
215
|
+
# Unknown command
|
|
216
|
+
puts @formatter.warning("Unknown command: #{input}. Type 'h' for help.")
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Accept suggestion by number.
|
|
221
|
+
#
|
|
222
|
+
# @param number [Integer] Suggestion number (0-based)
|
|
223
|
+
def accept_suggestion_by_number(number)
|
|
224
|
+
error = @navigation.current
|
|
225
|
+
return unless error
|
|
226
|
+
|
|
227
|
+
suggestions = error.suggestions || []
|
|
228
|
+
if number < 0 || number >= suggestions.size
|
|
229
|
+
puts @formatter.warning("Invalid suggestion number: #{number + 1}")
|
|
230
|
+
return
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
suggestion = suggestions[number]
|
|
234
|
+
@navigation.accept_suggestion(suggestion)
|
|
235
|
+
puts @formatter.success("Accepted: #{error.original} → #{suggestion.word}")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Jump to specific error.
|
|
239
|
+
def jump_to_error
|
|
240
|
+
print @formatter.prompt("Jump to error number>")
|
|
241
|
+
input = $stdin.gets&.chomp
|
|
242
|
+
|
|
243
|
+
return unless input
|
|
244
|
+
|
|
245
|
+
number = input.to_i - 1 # Convert to 0-based
|
|
246
|
+
if number >= 0 && number < @navigation.errors.size
|
|
247
|
+
@navigation.jump_to(number)
|
|
248
|
+
else
|
|
249
|
+
puts @formatter.warning("Invalid error number: #{input}")
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Quit and save changes.
|
|
254
|
+
def quit_with_save
|
|
255
|
+
@running = false
|
|
256
|
+
puts @formatter.success("Changes saved!")
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Quit without saving.
|
|
260
|
+
def quit_without_save
|
|
261
|
+
@running = false
|
|
262
|
+
puts @formatter.warning("Quit without saving. No changes were made.")
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Export corrections to file.
|
|
266
|
+
def export_corrections
|
|
267
|
+
print @formatter.prompt("Export to file (default: corrections.json)>")
|
|
268
|
+
filepath = $stdin.gets&.chomp || 'corrections.json'
|
|
269
|
+
|
|
270
|
+
# Export corrections
|
|
271
|
+
corrections = @navigation.export_corrections
|
|
272
|
+
|
|
273
|
+
# Write to file
|
|
274
|
+
require 'json'
|
|
275
|
+
File.write(filepath, JSON.pretty_generate(corrections))
|
|
276
|
+
|
|
277
|
+
puts @formatter.export_summary(@navigation, filepath)
|
|
278
|
+
puts @formatter.prompt("Press Enter to continue...")
|
|
279
|
+
$stdin.gets
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Show exit summary.
|
|
283
|
+
def show_exit_summary
|
|
284
|
+
puts ""
|
|
285
|
+
puts @formatter.statistics(@navigation)
|
|
286
|
+
puts ""
|
|
287
|
+
|
|
288
|
+
if @navigation.modified.any?
|
|
289
|
+
puts @formatter.success("#{@navigation.modified.size} corrections applied")
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
if @navigation.skipped.any?
|
|
293
|
+
puts "#{@navigation.skipped.size} errors skipped"
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
pending = @navigation.errors.size - @navigation.modified.size - @navigation.skipped.size
|
|
297
|
+
if pending > 0
|
|
298
|
+
puts "#{pending} errors remaining"
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Get session summary.
|
|
303
|
+
#
|
|
304
|
+
# @return [Hash] Session summary
|
|
305
|
+
def session_summary
|
|
306
|
+
{
|
|
307
|
+
document: {
|
|
308
|
+
name: @document.name,
|
|
309
|
+
format: @document.format,
|
|
310
|
+
language: @document.language_code
|
|
311
|
+
},
|
|
312
|
+
statistics: @navigation.statistics,
|
|
313
|
+
corrections: @navigation.export_corrections,
|
|
314
|
+
history: @navigation.history_summary
|
|
315
|
+
}
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kotoshu
|
|
4
|
+
module Cli
|
|
5
|
+
# Resolves which language a check should run in.
|
|
6
|
+
#
|
|
7
|
+
# The CLI's --language flag accepts:
|
|
8
|
+
# - omitted / "auto" → detect from content, fall back if needed
|
|
9
|
+
# - "default" → Configuration.default_language (no detection)
|
|
10
|
+
# - any other code → used as-is
|
|
11
|
+
#
|
|
12
|
+
# Pure logic — no IO. Detection delegates to Kotoshu::Language.detect
|
|
13
|
+
# which uses character-set heuristics and needs no model download.
|
|
14
|
+
# A detection only "sticks" if the detected language is set up
|
|
15
|
+
# (Kotoshu.setup? returns true); otherwise the configured default
|
|
16
|
+
# is used and a fallback note is included in the result.
|
|
17
|
+
class LanguageResolver
|
|
18
|
+
Result = Struct.new(:language, :detected, :fallback, :note, keyword_init: true)
|
|
19
|
+
|
|
20
|
+
# @param flag_value [String, nil] Raw --language flag value.
|
|
21
|
+
# @param default_language [String, nil] Configuration.default_language.
|
|
22
|
+
# @param detector [#detect] Object responding to .detect(text) -> String|nil.
|
|
23
|
+
# Defaults to Kotoshu::Language.
|
|
24
|
+
# @param setup_predicate [#call] Callable returning true if a language is
|
|
25
|
+
# set up. Defaults to Kotoshu.method(:setup?).
|
|
26
|
+
def initialize(flag_value:, default_language:,
|
|
27
|
+
detector: Kotoshu::Language,
|
|
28
|
+
setup_predicate: Kotoshu.method(:setup?))
|
|
29
|
+
@flag_value = flag_value
|
|
30
|
+
@default_language = default_language
|
|
31
|
+
@detector = detector
|
|
32
|
+
@setup_predicate = setup_predicate
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param text [String] The document text, used only when flag is "auto".
|
|
36
|
+
# @return [Result]
|
|
37
|
+
def resolve(text:)
|
|
38
|
+
case @flag_value
|
|
39
|
+
when nil, "auto"
|
|
40
|
+
resolve_auto(text)
|
|
41
|
+
when "default"
|
|
42
|
+
Result.new(language: @default_language, detected: nil, fallback: nil,
|
|
43
|
+
note: nil)
|
|
44
|
+
else
|
|
45
|
+
Result.new(language: @flag_value, detected: nil, fallback: nil,
|
|
46
|
+
note: nil)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def resolve_auto(text)
|
|
53
|
+
detected = safe_detect(text)
|
|
54
|
+
if detected.nil?
|
|
55
|
+
return Result.new(language: @default_language, detected: nil,
|
|
56
|
+
fallback: @default_language,
|
|
57
|
+
note: "No language detected; using default '#{@default_language}'.")
|
|
58
|
+
end
|
|
59
|
+
if setup?(detected)
|
|
60
|
+
return Result.new(language: detected, detected: detected, fallback: nil,
|
|
61
|
+
note: "Detected: #{detected}.")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
Result.new(language: @default_language, detected: detected,
|
|
65
|
+
fallback: @default_language,
|
|
66
|
+
note: "Detected: #{detected} (fallback: #{@default_language}).")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def safe_detect(text)
|
|
70
|
+
return nil if text.nil? || text.strip.empty?
|
|
71
|
+
|
|
72
|
+
detected = @detector.detect(text)
|
|
73
|
+
return nil if detected.nil? || detected.strip.empty?
|
|
74
|
+
|
|
75
|
+
normalize(detected)
|
|
76
|
+
rescue StandardError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def normalize(code)
|
|
81
|
+
code.to_s.downcase.split(/[-_]/).first
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def setup?(lang)
|
|
85
|
+
return false if lang.nil? || lang.strip.empty?
|
|
86
|
+
|
|
87
|
+
@setup_predicate.call(lang)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kotoshu
|
|
4
|
+
module Cli
|
|
5
|
+
# Manages navigation through errors in interactive review mode.
|
|
6
|
+
#
|
|
7
|
+
# Tracks current position, user decisions, and provides filtering
|
|
8
|
+
# for efficient error review.
|
|
9
|
+
#
|
|
10
|
+
# @example Creating navigation for errors
|
|
11
|
+
# nav = NavigationManager.new(errors)
|
|
12
|
+
# nav.forward # Move to next error
|
|
13
|
+
# nav.accept_suggestion(error.suggestions.first)
|
|
14
|
+
class NavigationManager
|
|
15
|
+
attr_reader :errors, :current_index, :history, :skipped, :modified
|
|
16
|
+
|
|
17
|
+
# Create a new navigation manager.
|
|
18
|
+
#
|
|
19
|
+
# @param errors [Array<Models::SemanticError>] Sorted list of errors
|
|
20
|
+
def initialize(errors)
|
|
21
|
+
raise ArgumentError, "Errors cannot be nil" if errors.nil?
|
|
22
|
+
raise ArgumentError, "Errors must be an Array" unless errors.is_a?(Array)
|
|
23
|
+
|
|
24
|
+
@errors = errors.sort # Errors are Comparable
|
|
25
|
+
@current_index = 0
|
|
26
|
+
@history = []
|
|
27
|
+
@skipped = Set.new
|
|
28
|
+
@modified = Set.new
|
|
29
|
+
@filters = {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get the current error.
|
|
33
|
+
#
|
|
34
|
+
# @return [Models::SemanticError, nil] Current error or nil
|
|
35
|
+
def current
|
|
36
|
+
return nil if @current_index >= @errors.size
|
|
37
|
+
|
|
38
|
+
@errors[@current_index]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if there's a next error.
|
|
42
|
+
#
|
|
43
|
+
# @return [Boolean] True if there's a next error
|
|
44
|
+
def next?
|
|
45
|
+
@current_index < @errors.size - 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Check if there's a previous error.
|
|
49
|
+
#
|
|
50
|
+
# @return [Boolean] True if there's a previous error
|
|
51
|
+
def previous?
|
|
52
|
+
@current_index > 0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Navigation methods
|
|
56
|
+
|
|
57
|
+
# Move to next error.
|
|
58
|
+
#
|
|
59
|
+
# @return [Models::SemanticError, nil] Next error or nil
|
|
60
|
+
def forward
|
|
61
|
+
return nil unless next?
|
|
62
|
+
|
|
63
|
+
@current_index += 1
|
|
64
|
+
current
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Move to previous error.
|
|
68
|
+
#
|
|
69
|
+
# @return [Models::SemanticError, nil] Previous error or nil
|
|
70
|
+
def backward
|
|
71
|
+
return nil unless previous?
|
|
72
|
+
|
|
73
|
+
@current_index -= 1
|
|
74
|
+
current
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Jump to specific error by index.
|
|
78
|
+
#
|
|
79
|
+
# @param index [Integer] Error index (0-based)
|
|
80
|
+
# @return [Models::SemanticError, nil] Error at index or nil
|
|
81
|
+
def jump_to(index)
|
|
82
|
+
return nil if index < 0 || index >= @errors.size
|
|
83
|
+
|
|
84
|
+
@current_index = index
|
|
85
|
+
current
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Jump to first error.
|
|
89
|
+
#
|
|
90
|
+
# @return [Models::SemanticError, nil] First error or nil
|
|
91
|
+
def first
|
|
92
|
+
jump_to(0)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Jump to last error.
|
|
96
|
+
#
|
|
97
|
+
# @return [Models::SemanticError, nil] Last error or nil
|
|
98
|
+
def last
|
|
99
|
+
jump_to(@errors.size - 1)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Skip the current error.
|
|
103
|
+
#
|
|
104
|
+
# @return [Models::SemanticError, nil] Next error or nil
|
|
105
|
+
def skip_current
|
|
106
|
+
@skipped.add(@current_index)
|
|
107
|
+
forward
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Accept a suggestion for the current error.
|
|
111
|
+
#
|
|
112
|
+
# Records the decision and marks the error as modified.
|
|
113
|
+
#
|
|
114
|
+
# @param suggestion [Models::Suggestion] The suggestion to accept
|
|
115
|
+
# @return [Models::SemanticError, nil] Next error or nil
|
|
116
|
+
def accept_suggestion(suggestion)
|
|
117
|
+
error = current
|
|
118
|
+
return nil unless error
|
|
119
|
+
|
|
120
|
+
record_decision(
|
|
121
|
+
error_id: error.id,
|
|
122
|
+
action: :accept,
|
|
123
|
+
original: error.original,
|
|
124
|
+
replacement: suggestion.word,
|
|
125
|
+
confidence: error.confidence
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@modified.add(@current_index)
|
|
129
|
+
forward
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# List all errors with their status.
|
|
133
|
+
#
|
|
134
|
+
# @return [Array<String>] Formatted error list
|
|
135
|
+
def list_all
|
|
136
|
+
@errors.each_with_index.map do |error, idx|
|
|
137
|
+
status = status_for(idx)
|
|
138
|
+
"#{idx + 1}. #{status} #{error.abbreviated}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Filter errors by minimum confidence.
|
|
143
|
+
#
|
|
144
|
+
# @param min_confidence [Float] Minimum confidence threshold (0.0 to 1.0)
|
|
145
|
+
# @return [Array<Models::SemanticError>] Filtered errors
|
|
146
|
+
def filter_by_confidence(min_confidence: 0.0)
|
|
147
|
+
@errors.select { |e| e.confidence >= min_confidence }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Filter errors by type(s).
|
|
151
|
+
#
|
|
152
|
+
# @param types [Array<Symbol>] Error types to include
|
|
153
|
+
# @return [Array<Models::SemanticError>] Filtered errors
|
|
154
|
+
def filter_by_type(*types)
|
|
155
|
+
@errors.select { |e| types.include?(e.error_type) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Get only pending errors (not skipped or modified).
|
|
159
|
+
#
|
|
160
|
+
# @return [Array<Models::SemanticError>] Pending errors
|
|
161
|
+
def pending
|
|
162
|
+
@errors.each_with_index.reject do |_, idx|
|
|
163
|
+
@skipped.include?(idx) || @modified.include?(idx)
|
|
164
|
+
end.map(&:last)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Get statistics about the errors.
|
|
168
|
+
#
|
|
169
|
+
# @return [Hash] Statistics hash
|
|
170
|
+
def statistics
|
|
171
|
+
{
|
|
172
|
+
total: @errors.size,
|
|
173
|
+
skipped: @skipped.size,
|
|
174
|
+
modified: @modified.size,
|
|
175
|
+
pending: pending.size,
|
|
176
|
+
by_type: @errors.group_by(&:error_type).transform_values(&:size),
|
|
177
|
+
by_confidence: {
|
|
178
|
+
high: @errors.count(&:high_confidence?),
|
|
179
|
+
medium: @errors.count { |e| e.confidence > 0.5 && e.confidence <= 0.8 },
|
|
180
|
+
low: @errors.count { |e| e.confidence <= 0.5 }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Get all errors sorted by status (pending first).
|
|
186
|
+
#
|
|
187
|
+
# @return [Array<Models::SemanticError>] Errors sorted by status
|
|
188
|
+
def by_status
|
|
189
|
+
pending + @errors.each_with_index.select { |_, idx|
|
|
190
|
+
@modified.include?(idx)
|
|
191
|
+
}.map(&:last).reverse + @errors.each_with_index.select { |_, idx|
|
|
192
|
+
@skipped.include?(idx)
|
|
193
|
+
}.map(&:last).reverse
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Reset navigation state (clear all decisions).
|
|
197
|
+
#
|
|
198
|
+
# @return [void]
|
|
199
|
+
def reset
|
|
200
|
+
@current_index = 0
|
|
201
|
+
@history.clear
|
|
202
|
+
@skipped.clear
|
|
203
|
+
@modified.clear
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Export corrections as a list of changes.
|
|
207
|
+
#
|
|
208
|
+
# @return [Array<Hash>] List of correction changes
|
|
209
|
+
def export_corrections
|
|
210
|
+
@modified.to_a.sort.map do |idx|
|
|
211
|
+
error = @errors[idx]
|
|
212
|
+
suggestion = error.recommended_suggestion
|
|
213
|
+
|
|
214
|
+
{
|
|
215
|
+
line: error.location.line,
|
|
216
|
+
original: error.original,
|
|
217
|
+
replacement: suggestion.word,
|
|
218
|
+
error_type: error.error_type,
|
|
219
|
+
confidence: error.confidence
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Get user decision history.
|
|
225
|
+
#
|
|
226
|
+
# @return [Array<Hash>] List of decisions made
|
|
227
|
+
def history_summary
|
|
228
|
+
@history.map.with_index.map do |decision, idx|
|
|
229
|
+
{
|
|
230
|
+
id: idx + 1,
|
|
231
|
+
error_id: decision[:error_id],
|
|
232
|
+
action: decision[:action],
|
|
233
|
+
original: decision[:original],
|
|
234
|
+
replacement: decision[:replacement],
|
|
235
|
+
confidence: decision[:confidence],
|
|
236
|
+
timestamp: decision[:timestamp]
|
|
237
|
+
}
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
private
|
|
242
|
+
|
|
243
|
+
# Record a user decision.
|
|
244
|
+
#
|
|
245
|
+
# @param error_id [String] Error identifier
|
|
246
|
+
# @param action [Symbol] Action taken (:accept, :skip, :edit)
|
|
247
|
+
# @param original [String] Original text
|
|
248
|
+
# @param replacement [String] Replacement text
|
|
249
|
+
# @param confidence [Float] Confidence score
|
|
250
|
+
def record_decision(error_id:, action:, original:, replacement:, confidence:)
|
|
251
|
+
@history << {
|
|
252
|
+
error_id: error_id,
|
|
253
|
+
action: action,
|
|
254
|
+
original: original,
|
|
255
|
+
replacement: replacement,
|
|
256
|
+
confidence: confidence,
|
|
257
|
+
timestamp: Time.now
|
|
258
|
+
}
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Get status string for an error index.
|
|
262
|
+
#
|
|
263
|
+
# @param idx [Integer] Error index
|
|
264
|
+
# @return [String] Status string
|
|
265
|
+
def status_for(idx)
|
|
266
|
+
return "[DONE]" if @modified.include?(idx)
|
|
267
|
+
return "[SKIP]" if @skipped.include?(idx)
|
|
268
|
+
"[PENDING]"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|