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,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kotoshu
4
+ module Models
5
+ # Immutable value object for word embeddings.
6
+ #
7
+ # Represents a word and its vector representation in a semantic space.
8
+ # Used for semantic similarity calculations and nearest neighbor searches.
9
+ #
10
+ # @example Creating an embedding
11
+ # embedding = WordEmbedding.new("hello", [0.1, -0.2, 0.3], "en")
12
+ # embedding.similarity(other_embedding) # => 0.85
13
+ #
14
+ # @see https://fasttext.cc/docs/en/crawl-vectors.html FastText crawl vectors
15
+ class WordEmbedding
16
+ attr_reader :word, :vector, :language_code, :dimension
17
+
18
+ # Create a new word embedding.
19
+ #
20
+ # @param word [String] The word
21
+ # @param vector [Array<Float>] The word's vector representation
22
+ # @param language_code [String] ISO 639-1 language code
23
+ # @param dimension [Integer] Vector dimension (default: 300 for FastText)
24
+ # @raise [ArgumentError] if vector doesn't match dimension
25
+ def initialize(word, vector, language_code, dimension: 300)
26
+ raise ArgumentError, "Vector dimension mismatch" unless vector.size == dimension
27
+
28
+ @word = word
29
+ @vector = vector.freeze
30
+ @language_code = language_code
31
+ @dimension = dimension
32
+
33
+ freeze
34
+ end
35
+
36
+ # Calculate cosine similarity with another embedding.
37
+ #
38
+ # Cosine similarity measures the cosine of the angle between two vectors.
39
+ # Returns 1.0 for identical vectors, 0.0 for orthogonal vectors.
40
+ #
41
+ # @param other [WordEmbedding] Another embedding
42
+ # @return [Float] Similarity score (0.0 to 1.0)
43
+ # @raise [TypeError] if other is not a WordEmbedding
44
+ def similarity(other)
45
+ raise TypeError, "Must be WordEmbedding" unless other.is_a?(WordEmbedding)
46
+
47
+ return 0.0 if @dimension != other.dimension
48
+
49
+ dot_product = @vector.zip(other.vector).map { |a, b| a * b }.sum
50
+ magnitude_a = vector_magnitude
51
+ magnitude_b = other.vector_magnitude
52
+
53
+ return 0.0 if magnitude_a.zero? || magnitude_b.zero?
54
+
55
+ dot_product / (magnitude_a * magnitude_b)
56
+ end
57
+
58
+ # Calculate Euclidean distance from another embedding.
59
+ #
60
+ # @param other [WordEmbedding] Another embedding
61
+ # @return [Float] Euclidean distance
62
+ # @raise [TypeError] if other is not a WordEmbedding
63
+ def distance(other)
64
+ raise TypeError, "Must be WordEmbedding" unless other.is_a?(WordEmbedding)
65
+
66
+ return Float::INFINITY if @dimension != other.dimension
67
+
68
+ Math.sqrt(@vector.zip(other.vector).map { |a, b| (a - b)**2 }.sum)
69
+ end
70
+
71
+ # Check if this embedding is equal to another.
72
+ #
73
+ # @param other [Object] Another object
74
+ # @return [Boolean] True if words and languages match
75
+ def ==(other)
76
+ return false unless other.is_a?(WordEmbedding)
77
+
78
+ @word == other.word && @language_code == other.language_code
79
+ end
80
+ alias_method :eql?, :==
81
+
82
+ # Hash code for hash table usage.
83
+ #
84
+ # @return [Integer] Hash code
85
+ def hash
86
+ [@word, @language_code].hash
87
+ end
88
+
89
+ # String representation.
90
+ #
91
+ # @return [String] Human-readable representation
92
+ def to_s
93
+ "#{self.class.name}[#{@word}, #{@language_code}, #{@dimension}D]"
94
+ end
95
+ alias_method :inspect, :to_s
96
+
97
+ private
98
+
99
+ # Calculate vector magnitude (Euclidean norm).
100
+ #
101
+ # @return [Float] Magnitude
102
+ def vector_magnitude
103
+ @magnitude ||= Math.sqrt(@vector.map { |x| x * x }.sum)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Kotoshu
6
+ # Centralized XDG Base Directory paths.
7
+ #
8
+ # Resolves cache, config, and data paths per the XDG Base Directory
9
+ # Specification. Kotoshu-specific env vars (KOTOSHU_CACHE_PATH etc.)
10
+ # take precedence over the generic XDG_*_HOME vars, which take
11
+ # precedence over the hardcoded defaults.
12
+ #
13
+ # Default layout:
14
+ # ~/.cache/kotoshu/ downloaded dicts, models, frequency lists
15
+ # ~/.config/kotoshu/ user-edited config, personal dictionary
16
+ # ~/.local/share/kotoshu/ append-only data (audit log)
17
+ module Paths
18
+ class << self
19
+ def cache_path
20
+ ENV.fetch("KOTOSHU_CACHE_PATH", nil) || xdg("CACHE", "cache")
21
+ end
22
+
23
+ def config_path
24
+ ENV.fetch("KOTOSHU_CONFIG_PATH", nil) || xdg("CONFIG", "config")
25
+ end
26
+
27
+ def data_path
28
+ ENV.fetch("KOTOSHU_DATA_PATH", nil) || xdg("DATA", "local/share")
29
+ end
30
+
31
+ def audit_log_path
32
+ ENV.fetch("KOTOSHU_AUDIT_LOG", nil) || File.join(data_path, "audit.log")
33
+ end
34
+
35
+ def personal_dictionary_path
36
+ ENV.fetch("KOTOSHU_PERSONAL_DIC", nil) || File.join(config_path, "personal.dic")
37
+ end
38
+
39
+ def ensure_exist!
40
+ FileUtils.mkdir_p(cache_path)
41
+ FileUtils.mkdir_p(config_path)
42
+ FileUtils.mkdir_p(data_path)
43
+ end
44
+
45
+ private
46
+
47
+ def xdg(suffix, default_subdir)
48
+ base = ENV.fetch("XDG_#{suffix}_HOME", nil) || File.join(Dir.home, ".#{default_subdir}")
49
+ File.join(base, "kotoshu")
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Kotoshu
6
+ # Personal dictionary for user-specific words.
7
+ #
8
+ # Stored in ~/.config/kotoshu/personal.dic (Hunspell format) under the
9
+ # XDG config directory. Override via KOTOSHU_PERSONAL_DIC.
10
+ class PersonalDictionary
11
+ PERSONAL_FILE = Kotoshu::Paths.personal_dictionary_path
12
+
13
+ class << self
14
+ # Add a word to personal dictionary.
15
+ #
16
+ # @param word [String] Word to add
17
+ # @return [Boolean] True if added
18
+ def add_word(word)
19
+ return false if word.nil? || word.empty?
20
+
21
+ ensure_directory
22
+ words = load_words
23
+
24
+ unless words.include?(word.downcase)
25
+ words << word.downcase
26
+ save_words(words)
27
+ end
28
+
29
+ true
30
+ end
31
+
32
+ # Get all personal words.
33
+ #
34
+ # @return [Array<String>] All personal words
35
+ def words
36
+ load_words
37
+ end
38
+
39
+ # Remove a word from personal dictionary.
40
+ #
41
+ # @param word [String] Word to remove
42
+ # @return [Boolean] True if removed
43
+ def remove_word(word)
44
+ return false if word.nil? || word.empty?
45
+
46
+ words = load_words
47
+ if words.delete(word.downcase)
48
+ save_words(words)
49
+ true
50
+ else
51
+ false
52
+ end
53
+ end
54
+
55
+ # Check if word is in personal dictionary.
56
+ #
57
+ # @param word [String] Word to check
58
+ # @return [Boolean] True if present
59
+ def include?(word)
60
+ return false if word.nil? || word.empty?
61
+
62
+ load_words.include?(word.downcase)
63
+ end
64
+
65
+ private
66
+
67
+ # Ensure personal dictionary's parent directory exists.
68
+ def ensure_directory
69
+ FileUtils.mkdir_p(File.dirname(PERSONAL_FILE))
70
+ end
71
+
72
+ # Load words from personal dictionary file.
73
+ #
74
+ # @return [Array<String>] List of words
75
+ def load_words
76
+ return [] unless File.exist?(PERSONAL_FILE)
77
+
78
+ File.readlines(PERSONAL_FILE, chomp: true)
79
+ .reject { |line| line.empty? || line.start_with?("#") }
80
+ .map(&:strip)
81
+ end
82
+
83
+ # Save words to personal dictionary file.
84
+ #
85
+ # @param words [Array<String>] Words to save
86
+ def save_words(words)
87
+ ensure_directory
88
+ File.open(PERSONAL_FILE, "w") do |f|
89
+ words.sort.uniq.each { |word| f.puts word }
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kotoshu
4
+ module Plugins
5
+ # Base class for plugins.
6
+ #
7
+ # Plugins provide extensible functionality with dependency injection.
8
+ #
9
+ # @example Creating a plugin
10
+ # class MyPlugin < Kotoshu::Plugins::Plugin
11
+ # def self.plugin_name
12
+ # :my_plugin
13
+ # end
14
+ #
15
+ # def self.dependencies
16
+ # [:dictionary]
17
+ # end
18
+ #
19
+ # def self.provides
20
+ # [:suggestions]
21
+ # end
22
+ #
23
+ # def initialize(dictionary:)
24
+ # @dictionary = dictionary
25
+ # end
26
+ # end
27
+ class Plugin
28
+ # @return [Symbol] Plugin name
29
+ def self.plugin_name
30
+ raise NotImplementedError, "#{name} must define .plugin_name"
31
+ end
32
+
33
+ # @return [Array<Symbol>] Dependencies
34
+ def self.dependencies
35
+ []
36
+ end
37
+
38
+ # @return [Array<Symbol>] Provided services
39
+ def self.provides
40
+ []
41
+ end
42
+
43
+ # Lifecycle hook called before plugin starts.
44
+ #
45
+ # Override in subclass to add startup logic.
46
+ def before_start
47
+ # Override in subclass
48
+ end
49
+
50
+ # Lifecycle hook called after plugin stops.
51
+ #
52
+ # Override in subclass to add cleanup logic.
53
+ def after_stop
54
+ # Override in subclass
55
+ end
56
+ end
57
+
58
+ # Error raised when a dependency cannot be resolved.
59
+ class DependencyError < StandardError; end
60
+ end
61
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "plugin"
4
+
5
+ module Kotoshu
6
+ module Plugins
7
+ # Registry for managing plugins and their dependencies.
8
+ #
9
+ # @example Registering a plugin
10
+ # registry = Registry.new
11
+ # registry.register(:dictionary, MyDictionary)
12
+ # registry.register(:suggestions, MySuggestions)
13
+ #
14
+ # @example Creating an instance with DI
15
+ # suggestions = registry.create_instance(MySuggestions)
16
+ class Registry
17
+ # @return [Hash] Registered services
18
+ attr_reader :services
19
+
20
+ # @return [Hash] Registered plugins
21
+ attr_reader :plugins
22
+
23
+ # Create a new registry.
24
+ def initialize
25
+ @services = {}
26
+ @plugins = {}
27
+ @singletons = {}
28
+ end
29
+
30
+ # Register a service.
31
+ #
32
+ # @param name [Symbol] Service name
33
+ # @param klass [Class] Service class or instance
34
+ # @return [self] Self for chaining
35
+ def register(name, klass)
36
+ @services[name] = klass
37
+ self
38
+ end
39
+
40
+ # Register a plugin.
41
+ #
42
+ # @param plugin [Class] Plugin class
43
+ # @return [self] Self for chaining
44
+ def register_plugin(plugin)
45
+ @plugins[plugin.plugin_name] = plugin
46
+ self
47
+ end
48
+
49
+ # Get a service instance.
50
+ #
51
+ # @param name [Symbol] Service name
52
+ # @return [Object] Service instance
53
+ def get_service(name)
54
+ service = @services[name]
55
+
56
+ raise DependencyError, "Unknown service: #{name}" unless service
57
+
58
+ # Return singleton if already created
59
+ return @singletons[name] if @singletons.key?(name)
60
+
61
+ # Create new instance
62
+ instance = service.is_a?(Class) ? service.new : service
63
+
64
+ # Store singleton
65
+ @singletons[name] = instance if service.is_a?(Class)
66
+ instance
67
+ end
68
+
69
+ # Create an instance with dependency injection.
70
+ #
71
+ # @param klass [Class] Class to instantiate
72
+ # @return [Object] Created instance
73
+ def create_instance(klass)
74
+ return klass.new unless klass.is_a?(Class)
75
+
76
+ # Get dependencies if it's a Plugin
77
+ if klass < Plugin
78
+ dependencies = resolve_dependencies(klass)
79
+ klass.new(**dependencies)
80
+ else
81
+ klass.new
82
+ end
83
+ rescue ArgumentError => e
84
+ raise DependencyError, "Failed to create instance of #{klass}: #{e.message}"
85
+ end
86
+
87
+ # Check if a service is registered.
88
+ #
89
+ # @param name [Symbol] Service name
90
+ # @return [Boolean] True if registered
91
+ def registered?(name)
92
+ @services.key?(name)
93
+ end
94
+
95
+ # Clear all singletons (force re-instantiation).
96
+ #
97
+ # @return [self] Self for chaining
98
+ def clear_singletons
99
+ @singletons.clear
100
+ self
101
+ end
102
+
103
+ private
104
+
105
+ # Resolve dependencies for a plugin.
106
+ #
107
+ # @param plugin [Class] Plugin class
108
+ # @return [Hash] Keyword arguments for initialization
109
+ def resolve_dependencies(plugin)
110
+ deps = {}
111
+
112
+ plugin.dependencies.each do |dep|
113
+ deps[dep] = get_service(dep)
114
+ end
115
+
116
+ deps
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kotoshu
4
+ # Project configuration for .kotoshu file.
5
+ #
6
+ # Auto-discovers .kotoshu file by searching up directory tree.
7
+ class ProjectConfig
8
+ CONFIG_FILE = ".kotoshu"
9
+
10
+ class << self
11
+ # Load project config for given path.
12
+ #
13
+ # @param start_path [String] Starting directory
14
+ # @return [Hash, nil] Configuration hash or nil
15
+ def load(start_path = Dir.pwd)
16
+ path = find_config_file(start_path)
17
+ return nil unless path
18
+
19
+ parse_config_file(path)
20
+ end
21
+
22
+ # Check if project config exists.
23
+ #
24
+ # @param start_path [String] Starting directory
25
+ # @return [Boolean] True if config exists
26
+ def exists?(start_path = Dir.pwd)
27
+ !find_config_file(start_path).nil?
28
+ end
29
+
30
+ # Get ignore patterns from project config.
31
+ #
32
+ # @param start_path [String] Starting directory
33
+ # @return [Hash] Configuration with ignore patterns
34
+ def ignore_patterns(start_path = Dir.pwd)
35
+ config = load(start_path) || {}
36
+ {
37
+ words: config["ignore_words"] || [],
38
+ patterns: (config["ignore_patterns"] || []).map { |p| Regexp.new(p) }
39
+ }
40
+ end
41
+
42
+ private
43
+
44
+ # Find .kotoshu file by searching up directory tree.
45
+ #
46
+ # @param start_path [String] Starting directory
47
+ # @return [String, nil] Path to config file or nil
48
+ def find_config_file(start_path)
49
+ path = Pathname.new(start_path)
50
+
51
+ while path
52
+ config_file = path.join(CONFIG_FILE)
53
+ return config_file.to_s if config_file.file?
54
+
55
+ parent = path.parent
56
+ break if parent == path # Reached root
57
+
58
+ path = parent
59
+ end
60
+
61
+ nil
62
+ end
63
+
64
+ # Parse .kotoshu YAML file.
65
+ #
66
+ # @param path [String] Path to config file
67
+ # @return [Hash] Parsed configuration
68
+ def parse_config_file(path)
69
+ require "yaml"
70
+ YAML.load_file(path) || {}
71
+ rescue ArgumentError, Psych::SyntaxError
72
+ {}
73
+ end
74
+ end
75
+ end
76
+ end