kotoshu 0.3.0 → 0.4.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 +4 -4
- data/.rubocop.yml +335 -2
- data/CHANGELOG.md +341 -1
- data/CLAUDE.md +4 -0
- data/README.adoc +120 -0
- data/TODO.impl/36-cleanup-regressions-0.3.1.md +117 -0
- data/TODO.impl/37-hunspell-correctness-tier2.md +182 -0
- data/TODO.impl/38-onnx-semantic-gating.md +155 -0
- data/TODO.impl/39-tier3-and-beyond.md +196 -0
- data/data/en/grammar/rules.yaml +468 -0
- data/exe/kotoshu +1 -1
- data/lib/kotoshu/algorithms/lookup.rb +233 -127
- data/lib/kotoshu/algorithms/ngram_suggest.rb +117 -20
- data/lib/kotoshu/algorithms/permutations.rb +11 -11
- data/lib/kotoshu/algorithms/phonet_suggest.rb +20 -25
- data/lib/kotoshu/algorithms/suggest.rb +75 -37
- data/lib/kotoshu/algorithms.rb +12 -5
- data/lib/kotoshu/analyzers/semantic_analyzer.rb +160 -50
- data/lib/kotoshu/analyzers.rb +8 -0
- data/lib/kotoshu/cache/base_cache.rb +109 -17
- data/lib/kotoshu/cache/eviction_policy.rb +66 -0
- data/lib/kotoshu/cache/frequency_cache.rb +44 -4
- data/lib/kotoshu/cache/language_cache.rb +18 -8
- data/lib/kotoshu/cache/lookup_cache.rb +1 -1
- data/lib/kotoshu/cache/model_cache.rb +43 -2
- data/lib/kotoshu/cache/suggestion_cache.rb +2 -2
- data/lib/kotoshu/cache.rb +9 -4
- data/lib/kotoshu/cli/batch_reporter.rb +12 -6
- data/lib/kotoshu/cli/cache_command.rb +174 -258
- data/lib/kotoshu/cli/completions_command.rb +210 -0
- data/lib/kotoshu/cli/display_formatter.rb +15 -17
- data/lib/kotoshu/cli/interactive_reviewer.rb +3 -8
- data/lib/kotoshu/cli/navigation_manager.rb +1 -0
- data/lib/kotoshu/cli/personal_command.rb +113 -0
- data/lib/kotoshu/cli.rb +23 -15
- data/lib/kotoshu/commands/cache_command.rb +3 -6
- data/lib/kotoshu/commands/check_command.rb +13 -24
- data/lib/kotoshu/commands/model_command.rb +11 -15
- data/lib/kotoshu/commands.rb +12 -0
- data/lib/kotoshu/components/passthrough_spell_checker.rb +0 -2
- data/lib/kotoshu/components/whitespace_tokenizer.rb +2 -4
- data/lib/kotoshu/components.rb +13 -0
- data/lib/kotoshu/configuration/builder.rb +0 -2
- data/lib/kotoshu/configuration/resolver.rb +1 -1
- data/lib/kotoshu/configuration.rb +53 -13
- data/lib/kotoshu/core/exceptions.rb +24 -3
- data/lib/kotoshu/core/indexed_dictionary.rb +2 -5
- data/lib/kotoshu/core/models/affix_rule.rb +37 -35
- data/lib/kotoshu/core/models/result/document_result.rb +40 -181
- data/lib/kotoshu/core/models/result/word_result.rb +57 -147
- data/lib/kotoshu/core/models/word.rb +0 -11
- data/lib/kotoshu/core/trie/trie.rb +1 -1
- data/lib/kotoshu/core.rb +7 -7
- data/lib/kotoshu/data/common_words/de.yml +1 -1
- data/lib/kotoshu/data/common_words_loader.rb +2 -2
- data/lib/kotoshu/data.rb +8 -0
- data/lib/kotoshu/data_structures/bloom_filter.rb +8 -2
- data/lib/kotoshu/data_structures.rb +8 -0
- data/lib/kotoshu/debug_logger.rb +3 -3
- data/lib/kotoshu/defaults.rb +0 -2
- data/lib/kotoshu/dictionaries/catalog.rb +0 -3
- data/lib/kotoshu/dictionaries.rb +8 -0
- data/lib/kotoshu/dictionary/base.rb +33 -7
- data/lib/kotoshu/dictionary/cspell.rb +38 -44
- data/lib/kotoshu/dictionary/custom.rb +3 -5
- data/lib/kotoshu/dictionary/hunspell.rb +15 -32
- data/lib/kotoshu/dictionary/plain_text.rb +27 -29
- data/lib/kotoshu/dictionary/repository.rb +4 -6
- data/lib/kotoshu/dictionary/unified.rb +200 -224
- data/lib/kotoshu/dictionary/unix_words.rb +5 -7
- data/lib/kotoshu/dictionary.rb +15 -0
- data/lib/kotoshu/documents/document.rb +73 -192
- data/lib/kotoshu/documents/plain_text_document.rb +51 -128
- data/lib/kotoshu/documents/source_position.rb +37 -0
- data/lib/kotoshu/documents/source_range.rb +76 -0
- data/lib/kotoshu/documents/text_node.rb +49 -0
- data/lib/kotoshu/documents.rb +177 -0
- data/lib/kotoshu/embeddings/embedding_pipeline.rb +218 -222
- data/lib/kotoshu/embeddings/lru_cache.rb +226 -215
- data/lib/kotoshu/embeddings/onnx_runtime_model.rb +380 -367
- data/lib/kotoshu/embeddings/protocol.rb +92 -79
- data/lib/kotoshu/embeddings/protocols.rb +20 -14
- data/lib/kotoshu/embeddings/registry.rb +167 -165
- data/lib/kotoshu/embeddings/search.rb +187 -175
- data/lib/kotoshu/embeddings/similarity_engine.rb +236 -206
- data/lib/kotoshu/embeddings/similarity_search.rb +0 -3
- data/lib/kotoshu/embeddings/vocabulary.rb +238 -213
- data/lib/kotoshu/embeddings.rb +25 -35
- data/lib/kotoshu/grammar/pattern_matchers/base_matcher.rb +1 -1
- data/lib/kotoshu/grammar/pattern_matchers/double_negative_matcher.rb +98 -24
- data/lib/kotoshu/grammar/pattern_matchers/phrase_matcher.rb +86 -0
- data/lib/kotoshu/grammar/pattern_matchers/possessive_context_matcher.rb +0 -2
- data/lib/kotoshu/grammar/pattern_matchers/vowel_sound_matcher.rb +0 -2
- data/lib/kotoshu/grammar/rule.rb +3 -5
- data/lib/kotoshu/grammar/rule_engine.rb +21 -36
- data/lib/kotoshu/grammar/rule_loader.rb +0 -1
- data/lib/kotoshu/grammar.rb +11 -8
- data/lib/kotoshu/integrity/audit_log.rb +95 -16
- data/lib/kotoshu/integrity/manifest.rb +0 -1
- data/lib/kotoshu/integrity/net_http.rb +1 -1
- data/lib/kotoshu/integrity/rotation_policy.rb +88 -0
- data/lib/kotoshu/integrity.rb +1 -0
- data/lib/kotoshu/keyboard/layout.rb +1 -0
- data/lib/kotoshu/keyboard/layouts/azerty.rb +0 -2
- data/lib/kotoshu/keyboard/layouts/dvorak.rb +0 -2
- data/lib/kotoshu/keyboard/layouts/jcuken.rb +0 -2
- data/lib/kotoshu/keyboard/layouts/qwerty.rb +1 -3
- data/lib/kotoshu/keyboard/layouts/qwertz.rb +0 -2
- data/lib/kotoshu/keyboard/registry.rb +1 -8
- data/lib/kotoshu/keyboard.rb +11 -2
- data/lib/kotoshu/language/detector.rb +21 -21
- data/lib/kotoshu/language/identifier.rb +5 -7
- data/lib/kotoshu/language/languages/base.rb +15 -0
- data/lib/kotoshu/language/normalizer/arabic.rb +113 -0
- data/lib/kotoshu/language/normalizer/base.rb +8 -10
- data/lib/kotoshu/language/normalizer/hebrew.rb +72 -0
- data/lib/kotoshu/language/normalizer/persian.rb +36 -0
- data/lib/kotoshu/language/registry.rb +64 -2
- data/lib/kotoshu/language/segmenter.rb +122 -0
- data/lib/kotoshu/language/suika.rb +38 -0
- data/lib/kotoshu/language/tokenizer/french_tokenizer.rb +18 -16
- data/lib/kotoshu/language/tokenizer/german_tokenizer.rb +1 -1
- data/lib/kotoshu/language/tokenizer/japanese_tokenizer.rb +10 -15
- data/lib/kotoshu/language/tokenizer/latin_tokenizer.rb +3 -3
- data/lib/kotoshu/language/tokenizer/portuguese_tokenizer.rb +1 -1
- data/lib/kotoshu/language/tokenizer/russian_tokenizer.rb +2 -4
- data/lib/kotoshu/language/tokenizer/spanish_tokenizer.rb +1 -1
- data/lib/kotoshu/language.rb +33 -18
- data/lib/kotoshu/languages/ar/language.rb +84 -0
- data/lib/kotoshu/languages/de/language.rb +39 -31
- data/lib/kotoshu/languages/en/language.rb +36 -18
- data/lib/kotoshu/languages/es/language.rb +24 -11
- data/lib/kotoshu/languages/fa/language.rb +75 -0
- data/lib/kotoshu/languages/fr/language.rb +27 -15
- data/lib/kotoshu/languages/he/language.rb +83 -0
- data/lib/kotoshu/languages/ja/language.rb +10 -11
- data/lib/kotoshu/languages/pt/language.rb +22 -10
- data/lib/kotoshu/languages/ru/language.rb +23 -11
- data/lib/kotoshu/languages.rb +14 -36
- data/lib/kotoshu/metrics_collector.rb +2 -2
- data/lib/kotoshu/models/context.rb +3 -3
- data/lib/kotoshu/models/embedding_model.rb +4 -5
- data/lib/kotoshu/models/fasttext_model.rb +1 -9
- data/lib/kotoshu/models/nearest_neighbor.rb +6 -5
- data/lib/kotoshu/models/onnx_model.rb +5 -7
- data/lib/kotoshu/models/semantic_error.rb +55 -16
- data/lib/kotoshu/models/word_embedding.rb +4 -14
- data/lib/kotoshu/models.rb +27 -0
- data/lib/kotoshu/multi_language_checker.rb +124 -0
- data/lib/kotoshu/personal_dictionary.rb +24 -13
- data/lib/kotoshu/plugins/registry.rb +0 -2
- data/lib/kotoshu/plugins.rb +9 -0
- data/lib/kotoshu/readers/aff_data.rb +125 -26
- data/lib/kotoshu/readers/aff_reader.rb +26 -17
- data/lib/kotoshu/readers/condition_checker.rb +54 -71
- data/lib/kotoshu/readers/dic_reader.rb +124 -34
- data/lib/kotoshu/readers/file_reader.rb +39 -9
- data/lib/kotoshu/readers/lookup_builder.rb +347 -48
- data/lib/kotoshu/readers/readers.rb +15 -4
- data/lib/kotoshu/readers.rb +18 -2
- data/lib/kotoshu/resource_manager.rb +16 -14
- data/lib/kotoshu/results.rb +8 -0
- data/lib/kotoshu/source_registry.rb +16 -8
- data/lib/kotoshu/spellchecker.rb +4 -9
- data/lib/kotoshu/string_metrics.rb +9 -9
- data/lib/kotoshu/suggestions/context.rb +0 -12
- data/lib/kotoshu/suggestions/generator.rb +12 -17
- data/lib/kotoshu/suggestions/strategies/base_strategy.rb +28 -19
- data/lib/kotoshu/suggestions/strategies/composite_strategy.rb +3 -3
- data/lib/kotoshu/suggestions/strategies/edit_distance_strategy.rb +79 -67
- data/lib/kotoshu/suggestions/strategies/keyboard_proximity_strategy.rb +5 -5
- data/lib/kotoshu/suggestions/strategies/ngram_strategy.rb +2 -2
- data/lib/kotoshu/suggestions/strategies/phonetic_strategy.rb +1 -1
- data/lib/kotoshu/suggestions/strategies/semantic_strategy.rb +20 -28
- data/lib/kotoshu/suggestions/strategies/symspell_strategy.rb +6 -11
- data/lib/kotoshu/suggestions/suggestion.rb +57 -77
- data/lib/kotoshu/suggestions/suggestion_set.rb +10 -9
- data/lib/kotoshu/suggestions.rb +24 -0
- data/lib/kotoshu/version.rb +1 -1
- data/lib/kotoshu.rb +77 -97
- data/test_oop.rb +10 -10
- metadata +46 -12
- data/lib/kotoshu/documents/asciidoc_document.rb +0 -441
- data/lib/kotoshu/documents/location.rb +0 -139
- data/lib/kotoshu/documents/markdown_document.rb +0 -389
data/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,347 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.5.0] — 2026-06-30
|
|
9
|
+
|
|
10
|
+
Tier 2.5 release. Adds the personal-dictionary CLI, completes direct
|
|
11
|
+
spec coverage for every previously-untested namespace (~370 new
|
|
12
|
+
examples), eliminates every `respond_to?` / `send`-to-private /
|
|
13
|
+
`instance_variable_*` violation from `lib/`, and fixes a long list of
|
|
14
|
+
production bugs the refactors surfaced.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Cache eviction** (`Kotoshu::Cache::EvictionPolicy` +
|
|
19
|
+
`BaseCache#evict` + `kotoshu cache evict`). A pure value object
|
|
20
|
+
decides which entries to evict (LRU by `cached_at`, oldest first)
|
|
21
|
+
to fit a configured size cap; `BaseCache#evict` collects on-disk
|
|
22
|
+
entries (one record per discovered `metadata.json`) and executes
|
|
23
|
+
the plan. `--dry-run` returns the plan without touching disk. The
|
|
24
|
+
cap defaults to `max_cache_size` (1 GB, `KOTOSHU_MAX_CACHE_SIZE`)
|
|
25
|
+
and is now wired through `Configuration` instead of a hardcoded
|
|
26
|
+
constant. Corrupt metadata (missing `cached_at`) sorts oldest so it
|
|
27
|
+
is evicted first.
|
|
28
|
+
- **Audit log rotation** (`Kotoshu::Integrity::RotationPolicy`). When the
|
|
29
|
+
current `audit.log` exceeds `audit_max_bytes` (default 10 MB,
|
|
30
|
+
`KOTOSHU_AUDIT_MAX_BYTES`), the log rotates through `audit_rotations`
|
|
31
|
+
historical files (default 5, `KOTOSHU_AUDIT_ROTATIONS`). Total on-disk
|
|
32
|
+
footprint is bounded at `max_bytes * (rotations + 1)`. The rotation
|
|
33
|
+
policy is a pure value object — `AuditLog#record` consults it on every
|
|
34
|
+
write and executes the returned rename plan under an exclusive flock
|
|
35
|
+
on a sibling lockfile (`audit.log.lock`) so concurrent writers cannot
|
|
36
|
+
race the rename chain. `AuditLog#entries` now walks both the current
|
|
37
|
+
log and all rotations, newest-first.
|
|
38
|
+
- **Shell completion** (`kotoshu completions bash|zsh|fish`). Emits a
|
|
39
|
+
shell-specific completion script that completes top-level subcommands
|
|
40
|
+
and dynamically completes language codes for `setup` / `fetch` by
|
|
41
|
+
shelling out to `kotoshu completions languages`. README documents
|
|
42
|
+
install paths for bash, zsh, and fish.
|
|
43
|
+
- **Load-time model cache integrity** (`Cache::ModelCache`). A cached
|
|
44
|
+
ONNX model file is re-verified against the SHA-256 recorded in its
|
|
45
|
+
`metadata.json` every time it is loaded — not just at download time.
|
|
46
|
+
Tampering, truncation, or partial writes are caught before the file
|
|
47
|
+
reaches the ONNX runtime, and the resulting `Kotoshu::IntegrityError`
|
|
48
|
+
includes a `remediation:` hint pointing the user at
|
|
49
|
+
`kotoshu cache download :LANG --model`. Legacy caches whose metadata
|
|
50
|
+
predates the checksum field continue to load (graceful degradation).
|
|
51
|
+
- **`IntegrityError#remediation`** — the error now accepts an optional
|
|
52
|
+
`remediation:` keyword whose text is appended to the message and
|
|
53
|
+
exposed via a reader, so callers can surface the user-facing fix
|
|
54
|
+
(e.g. "Run `kotoshu cache download :en --model` to re-download").
|
|
55
|
+
Existing callers that omit it see no change in the message format.
|
|
56
|
+
- **Personal dictionary CLI** (`kotoshu personal SUBCOMMAND`). Thor
|
|
57
|
+
subcommand wrapping `Kotoshu::PersonalDictionary`: `add`, `remove`,
|
|
58
|
+
`list`, `import` (with `--dry-run`), `path`, and `clear` (with
|
|
59
|
+
`--yes`). Errors raise `Kotoshu::Cli::Errors::UsageError` (exit 2),
|
|
60
|
+
matching the existing CLI error contract.
|
|
61
|
+
- **Direct specs for previously-untested namespaces.** ~370 new
|
|
62
|
+
examples pinning the public contract of `Integrity`, `Embeddings`,
|
|
63
|
+
`Language`, `Dictionary`, and `Cache` namespaces — each spec uses
|
|
64
|
+
real instances and tmpdir/fixture IO, never `double()`.
|
|
65
|
+
- **`Kotoshu::Dictionary::Unified`** is now loadable. The class
|
|
66
|
+
was previously declared as `class Kotoshu::Dictionary`, colliding
|
|
67
|
+
with the existing `module Kotoshu::Dictionary` namespace and
|
|
68
|
+
raising `TypeError` on autoload. Renamed to `Unified` to match the
|
|
69
|
+
autoload entry. Two follow-on bugs (mismatched `Suggester.new`
|
|
70
|
+
kwargs and a custom `build_dic_structure` that produced the wrong
|
|
71
|
+
shape) fixed in the same change.
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
|
|
75
|
+
- **`Embeddings::LruCache#fetch`** now yields the *key* to the provided
|
|
76
|
+
block (matching Ruby's `Hash#fetch` convention) instead of invoking
|
|
77
|
+
the block with no arguments. Callers that compute the missing value
|
|
78
|
+
from the key no longer need a closure over an outer variable.
|
|
79
|
+
- **`Embeddings::SimilarityEngine`** exposes a `pre_normalize?`
|
|
80
|
+
predicate (replacing a dead `attr_reader :pre_normalize` that was
|
|
81
|
+
silently shadowed by a private method of the same name) and now
|
|
82
|
+
raises `ArgumentError` with a clear dimension-mismatch message
|
|
83
|
+
instead of crashing with a `TypeError` from `Array#zip` when given
|
|
84
|
+
vectors of differing lengths. `nil` and empty inputs still return
|
|
85
|
+
`0.0` (early-return path).
|
|
86
|
+
- **Eliminated `respond_to?` across `lib/`.** Replaced every
|
|
87
|
+
occurrence with `is_a?` / `case-when` / `method_defined?` / direct
|
|
88
|
+
calls. Duck-typing via `respond_to?` hid real type errors — the
|
|
89
|
+
production bugs surfaced by this refactor (see Fixed) had been
|
|
90
|
+
silently no-op'ing for as long as the code existed.
|
|
91
|
+
- **Eliminated `send` to private/protected methods** in `lib/`. The
|
|
92
|
+
`cache` CLI subcommand's `cache.send(:read_metadata, ...)` is now
|
|
93
|
+
`cache.read_metadata(...)` after promoting the method to public
|
|
94
|
+
(`public def`). `SemanticStrategy#embedding_for` was dead code that
|
|
95
|
+
called `@search.send(:get_embedding, ...)` on a method that didn't
|
|
96
|
+
exist — removed entirely.
|
|
97
|
+
- **Eliminated `instance_variable_set/get` across `lib/`.**
|
|
98
|
+
`BloomFilter#merge` now reads `other.bits` (public `attr_reader`)
|
|
99
|
+
instead of `other.instance_variable_get(:@bits)`.
|
|
100
|
+
`Dictionary::PlainText` and `Dictionary::CSpell` constructors now
|
|
101
|
+
accept a `words:` keyword instead of `from_words` poking ivars via
|
|
102
|
+
`allocate` + `instance_variable_set`. `Readers::LookupBuilder`
|
|
103
|
+
gained `aff_data:` / `words:` keyword args to replace the same
|
|
104
|
+
pattern in `from_data`.
|
|
105
|
+
- **Configuration dispatch is type-checked, not duck-typed.**
|
|
106
|
+
`Configuration#apply_*` previously called
|
|
107
|
+
`send("#{key}=", value) if respond_to?("#{key}=")`; now uses
|
|
108
|
+
`public_send(setter, value) if self.class.method_defined?(setter)`.
|
|
109
|
+
- **`Dictionary::Base` exposes a default `path` method** returning
|
|
110
|
+
`nil` (overridden by file-backed subclasses). `Spellchecker`
|
|
111
|
+
previously guarded with `dictionary.respond_to?(:path)`; now calls
|
|
112
|
+
unconditionally.
|
|
113
|
+
- **`Language::Registry.info` checks the type bound.** Was
|
|
114
|
+
`klass.respond_to?(:instance)`; now
|
|
115
|
+
`klass.is_a?(Class) && klass <= Kotoshu::Language::Base`.
|
|
116
|
+
|
|
117
|
+
### Fixed
|
|
118
|
+
|
|
119
|
+
- **`AuditLog#each` now iterates newest-first across current and
|
|
120
|
+
rotations.** The docstring promised newest-first but the
|
|
121
|
+
implementation yielded each file in write order (oldest line first)
|
|
122
|
+
then walked current → oldest file, producing a jumbled order. Each
|
|
123
|
+
file's lines are now read into memory and yielded in reverse before
|
|
124
|
+
walking to the next older rotation.
|
|
125
|
+
- **`Embeddings::Protocol` is no longer broken.** The module declared
|
|
126
|
+
`required_methods` and `required_methods(*names)` (a reader + a
|
|
127
|
+
writer with the same name). Ruby's last-def-wins shadowed the
|
|
128
|
+
reader, so every protocol registration silently dropped its
|
|
129
|
+
arguments and `compliance_errors` returned `[]` unconditionally —
|
|
130
|
+
every class appeared to conform to every protocol. Renamed the
|
|
131
|
+
writer to `required` (and `optional_methods` → `optional`); also
|
|
132
|
+
fixed `compliance_errors` to use `klass.method_defined?` instead of
|
|
133
|
+
`klass.respond_to?` (which checks class methods, not instance
|
|
134
|
+
methods).
|
|
135
|
+
- **`Embeddings::OnnxRuntimeModel` defines `loaded?`.** The protocol
|
|
136
|
+
requires `loaded?` but the class declared only `attr_reader :loaded`
|
|
137
|
+
(no question mark). `EmbeddingPipeline#stats` and `#to_s` called
|
|
138
|
+
`@model.loaded?` — would have raised `NoMethodError` on the real
|
|
139
|
+
model.
|
|
140
|
+
- **`Dictionary::Base` aliases dispatch dynamically.** Ruby's `alias`
|
|
141
|
+
keyword binds to the parent method body at definition time,
|
|
142
|
+
bypassing subclass overrides. `has_word?`/`include?`/`contains?`/
|
|
143
|
+
`lookup?`/`<<`/`all_words` on a concrete subclass (e.g.,
|
|
144
|
+
`UnixWords`) would invoke the abstract `Base#lookup` /
|
|
145
|
+
`Base#add_word` and raise `NotImplementedError`. Replaced with
|
|
146
|
+
one-line method definitions that call the abstract method (which
|
|
147
|
+
then dispatches dynamically to the subclass override).
|
|
148
|
+
- **`Dictionary::CSpell#add_word` no longer crashes.** Tried to
|
|
149
|
+
`@trie.insert` on a frozen `Trie` (frozen by `Builder#build`),
|
|
150
|
+
raising `FrozenError`. Aligned with `remove_word`'s existing
|
|
151
|
+
"always returns false (immutable after load)" contract.
|
|
152
|
+
- **`Cache::FrequencyCache#get_frequency` no longer raises
|
|
153
|
+
`ArgumentError`.** Called `get(language_code, force_download)`
|
|
154
|
+
positionally, but `BaseCache#get` declares `force_download:` as
|
|
155
|
+
keyword. Fixed to use the keyword form.
|
|
156
|
+
- **`Cache::FrequencyCache#cached_resources` no longer reports the
|
|
157
|
+
`tmp/` scratch slot.** `BaseCache#initialize` creates
|
|
158
|
+
`<cache_path>/tmp/` as a working directory; the directory-walking
|
|
159
|
+
implementation in `FrequencyCache` listed it as a cached language.
|
|
160
|
+
Added an explicit `basename != "tmp"` exclusion.
|
|
161
|
+
- **`Cache::FrequencyCache#install_local` exists.** `ResourceManager`
|
|
162
|
+
called `freq_cache.install_local(...)` behind a `respond_to?` guard
|
|
163
|
+
that always returned `false`, so local frequency installs were
|
|
164
|
+
silently skipped — `setup(from:)` reported `frequency_status: :local`
|
|
165
|
+
even though no install happened. Added the method, mirroring
|
|
166
|
+
`LanguageCache#install_local` but taking a single `path:` kwarg.
|
|
167
|
+
- **`PhonetTable::Rule#match?` is no longer dead code.**
|
|
168
|
+
`Algorithms::PhonetSuggest` had its own `match_rule` that
|
|
169
|
+
duplicated the rule-matching logic, ignoring `Rule#match?`
|
|
170
|
+
entirely. Consolidated into `Rule#match_length` (returning
|
|
171
|
+
`Integer|nil` — the metaphone walker needs the length to advance
|
|
172
|
+
its position) with `Rule#match?` as a boolean wrapper;
|
|
173
|
+
`PhonetSuggest#match_rule` now delegates.
|
|
174
|
+
- **`DoubleNegativeMatcher` recognizes the "not only...but also"
|
|
175
|
+
correlative.** The previous check fired only on the narrow shape
|
|
176
|
+
`not [current_negative] only`, missing the actual idiom (where
|
|
177
|
+
"not" and "only" are adjacent). Replaced with a region-based
|
|
178
|
+
matcher that finds every exception phrase in the token stream and
|
|
179
|
+
suppresses any negative whose index falls inside a region. Supports
|
|
180
|
+
both `"prefix...suffix"` (open-ended) and `"literal n-gram"` (no
|
|
181
|
+
ellipsis) phrase shapes, driven by the rule's `exceptions.phrases`
|
|
182
|
+
config.
|
|
183
|
+
- **`SemanticAnalyzer` no longer returns `[]` for OOV words.**
|
|
184
|
+
`suggest_corrections` called `@model.nearest_neighbors(word)`,
|
|
185
|
+
which returns `[]` for OOV inputs — but OOV is precisely what the
|
|
186
|
+
analyzer exists to handle. Added an edit-distance fallback: when
|
|
187
|
+
the model returns `[]`, walk the vocabulary and return words within
|
|
188
|
+
Levenshtein distance 2, scored by `1/(1+distance)`. Drive-by fix:
|
|
189
|
+
`Models::Suggestion.new` was called with `word:` as keyword but
|
|
190
|
+
the constructor takes `word` positional — exposed once the OOV
|
|
191
|
+
fallback actually reached the `Suggestion.new` call.
|
|
192
|
+
|
|
193
|
+
### Removed
|
|
194
|
+
|
|
195
|
+
- **Orphan files emptied to no-op stubs.**
|
|
196
|
+
`lib/kotoshu/embeddings/protocols.rb` (plural) had
|
|
197
|
+
`require_relative 'protocols/embedding_model'` pointing at
|
|
198
|
+
nonexistent files (loading it raised `LoadError`).
|
|
199
|
+
`lib/kotoshu/readers/readers.rb` (nested) duplicated the autoload
|
|
200
|
+
setup in `lib/kotoshu/readers.rb` with `require_relative` calls
|
|
201
|
+
(violating the project rule against `require_relative` in lib/).
|
|
202
|
+
Both reduced to documentation-only stubs; no callers were affected.
|
|
203
|
+
The real protocol definitions live in
|
|
204
|
+
`lib/kotoshu/embeddings/protocol.rb`; the real reader autoloads
|
|
205
|
+
live in `lib/kotoshu/readers.rb`.
|
|
206
|
+
|
|
207
|
+
## [0.4.0] — 2026-06-29
|
|
208
|
+
|
|
209
|
+
Tier 2 release. Completes the Hunspell correctness work (morphological
|
|
210
|
+
rules, REP/phonet/ngram ranking, compound handling), migrates the entire
|
|
211
|
+
library from `require_relative` to Ruby `autoload`, and tightens the
|
|
212
|
+
cache interface alignment that 0.3.1 sketched.
|
|
213
|
+
|
|
214
|
+
### Added
|
|
215
|
+
|
|
216
|
+
- **Hunspell morphological correctness** (T2 Phases 2A–2D):
|
|
217
|
+
- `FLAG long|num|utf-8` parsing and `AF` alias resolution.
|
|
218
|
+
- `COMPOUNDRULE`, `CHECKCOMPOUNDPATTERN` (with empty-flag fix),
|
|
219
|
+
`CIRCUMFIX` handling.
|
|
220
|
+
- `ICONV`/`OCONV` `ConvTable` with stable sort — preserves Nepali
|
|
221
|
+
`ZWNJ`/`ZWJ` ordering.
|
|
222
|
+
- `KEEPCASE` suggestion flag with `CHECKSHARPS` exception, plus the
|
|
223
|
+
German eszet rule (`ss` ↔ `SS`).
|
|
224
|
+
- `REP` table, phonet ordering, ngram ranking wired end-to-end.
|
|
225
|
+
- Edge cases: `IJ` digraph, `i58202` case preservation,
|
|
226
|
+
`opentaal_forbiddenword2`, `breakdefault` `BREAK` pattern.
|
|
227
|
+
- **Hunspell Suggester** is now wired into the Hunspell dictionary; the
|
|
228
|
+
default affix directives are honoured when generating suggestions.
|
|
229
|
+
- **`BaseCache#max_cache_size`** attribute (1 GB default) — the
|
|
230
|
+
forward-looking hook for cache eviction work (see
|
|
231
|
+
`TODO.impl/34-cache-eviction.md`).
|
|
232
|
+
- **`LanguageCache#language_path(lang, type)`** public helper; resource
|
|
233
|
+
and metadata path resolution is now DRY'd through it.
|
|
234
|
+
- **`Kotoshu::Language` suika soft-load module** with specs — keeps
|
|
235
|
+
tokenization lazy and optional.
|
|
236
|
+
- **Multi-language EditDistanceStrategy spec coverage** — pins
|
|
237
|
+
per-language keyboard selection (QWERTZ/AZERTY/QWERTY/JCUKEN) and
|
|
238
|
+
verifies German and French typo correction end-to-end. The previous
|
|
239
|
+
`skip 'Multi-language support not yet implemented'` hook is removed;
|
|
240
|
+
the strategy has been language-aware via `Keyboard::Registry` +
|
|
241
|
+
`Cache::FrequencyCache` for several releases.
|
|
242
|
+
|
|
243
|
+
### Changed
|
|
244
|
+
|
|
245
|
+
- **Library-wide `autoload` migration.** Every `require_relative` (and
|
|
246
|
+
in-library `require`) under `lib/kotoshu/` is replaced with a Ruby
|
|
247
|
+
`autoload` declared in the immediate parent namespace's file. This
|
|
248
|
+
fixes load-order races, makes the eager/opt-in split explicit, and
|
|
249
|
+
matches the contributor guidance in `CLAUDE.md`. Behaviour is
|
|
250
|
+
unchanged for callers who only `require "kotoshu"`, but extenders who
|
|
251
|
+
reached past the public facade may need to update their load entry
|
|
252
|
+
points.
|
|
253
|
+
- **`Embeddings` namespace consolidation.** All embedding classes are
|
|
254
|
+
nested under `Kotoshu::Embeddings` (was scattered with several
|
|
255
|
+
`require_relative` shims).
|
|
256
|
+
- **Grammar rules bundled in the gem.** English `rules.yaml` ships
|
|
257
|
+
inside the gem package so a `gem install kotoshu` is self-contained
|
|
258
|
+
for grammar checking without a separate download.
|
|
259
|
+
- **Plain-text edit distance is now case-insensitive**, matching the
|
|
260
|
+
Hunspell backend's behaviour for `PlainText` dictionaries.
|
|
261
|
+
|
|
262
|
+
### Fixed
|
|
263
|
+
|
|
264
|
+
- **`Cli::CacheCommand` wired to the real `LanguageCache` API.** The
|
|
265
|
+
previous implementation called nonexistent methods (`cache_status`,
|
|
266
|
+
`get_frequency_data`, `get_language_info`, `purge_all`); `kotoshu cache`
|
|
267
|
+
now correctly routes through `available?`, `cached_resources`,
|
|
268
|
+
`clear_all`, `clean`, `stats`, `get_spelling`, `get_grammar`, and the
|
|
269
|
+
standalone `Cache::FrequencyCache` for frequency downloads.
|
|
270
|
+
- **`Keyboard::Registry` lazy reload.** `clear` now marks
|
|
271
|
+
`@languages_loaded = true` to prevent an infinite lazy-reload loop
|
|
272
|
+
when the registry is reset at runtime.
|
|
273
|
+
- **German common-words YAML.** `data/common_words/de.yml` line 466 had
|
|
274
|
+
`-ecke` (missing space after dash), a syntax error that prevented the
|
|
275
|
+
file from loading and silently broke German strategy instantiation.
|
|
276
|
+
- **`Readers::AffReader` Hunspell edge cases** — encoding fallback for
|
|
277
|
+
Latin-1 fixtures, `REP` table splitting, `MultiWord` dash variants,
|
|
278
|
+
default affix directives honoured when the `.aff` file omits them.
|
|
279
|
+
- **RuboCop 3.x / plugins migration.** `rubocop-rspec` is bumped to
|
|
280
|
+
3.x and the config uses the new plugins syntax; safe auto-corrections
|
|
281
|
+
applied across `lib/` and `spec/` (`Layout/ExtraSpacing`,
|
|
282
|
+
`Layout/EmptyLine`).
|
|
283
|
+
- **Cross-platform CI.** `bundle` is invoked via `Gem.ruby -S` for
|
|
284
|
+
Windows compatibility; Ruby 4.0 compatibility fixes applied.
|
|
285
|
+
|
|
286
|
+
### Internal
|
|
287
|
+
|
|
288
|
+
- `TODO.impl/` planning documents (00–40) are the tiered execution
|
|
289
|
+
plan. Tier 2 is complete; Tier 3 (`audit-log-rotation`,
|
|
290
|
+
`cache-eviction`, `shell-completion`) and content-repo tasks
|
|
291
|
+
(`onnx-vocab-json-generation`) remain deferred past 0.4.0.
|
|
292
|
+
|
|
293
|
+
## [0.3.1] — 2026-06-28
|
|
294
|
+
|
|
295
|
+
Bug-fix release. Closes the regressions introduced by the 0.3.0
|
|
296
|
+
lutaml-model migration and tightens up spec gating so the default suite
|
|
297
|
+
is green on the non-network, non-ONNX subset.
|
|
298
|
+
|
|
299
|
+
### Fixed
|
|
300
|
+
|
|
301
|
+
- `WordResult#suggestions` is `Array<Suggestion>` after the lutaml
|
|
302
|
+
migration; the integration spec now reads it as
|
|
303
|
+
`suggestions.map(&:word)` instead of the removed `SuggestionSet#words`.
|
|
304
|
+
- `WordResult#to_h` was removed by lutaml; the integration spec now uses
|
|
305
|
+
the framework-supplied `to_hash`.
|
|
306
|
+
- `Readers::AffReader#detect_encoding` now handles a nil path so the
|
|
307
|
+
reader can be constructed from an in-memory `StringReader` (used by
|
|
308
|
+
the unit tests and by programmatic callers that build affix data
|
|
309
|
+
without a file on disk).
|
|
310
|
+
- `Embeddings::SimilaritySearch` is now eagerly loaded alongside the
|
|
311
|
+
rest of the embeddings namespace; previously the constant was
|
|
312
|
+
uninitialized when `SemanticStrategy` tried to use it.
|
|
313
|
+
- `Embeddings::Vocabulary.from_cache` is implemented (was missing). It
|
|
314
|
+
resolves the `vocab.json` sibling of the cached ONNX model and
|
|
315
|
+
returns nil when the model or vocab is unavailable, so callers can
|
|
316
|
+
degrade gracefully.
|
|
317
|
+
- `Embeddings::OnnxRuntimeModel.from_cache` now accepts the `cache:`
|
|
318
|
+
keyword (matching `Vocabulary.from_cache` and
|
|
319
|
+
`SimilaritySearch.from_cache`); the previous positional signature
|
|
320
|
+
silently swallowed the cache as a Hash.
|
|
321
|
+
- `Suggestions::Strategies::SemanticStrategy` now surfaces its named
|
|
322
|
+
constructor kwargs (`min_semantic_similarity`,
|
|
323
|
+
`semantic_boost_weight`, `max_context_window`) through `get_config`,
|
|
324
|
+
so the strategy's configurable knobs are queryable the same way as
|
|
325
|
+
the base strategy's `**config` bag.
|
|
326
|
+
|
|
327
|
+
### Changed
|
|
328
|
+
|
|
329
|
+
- `spec/spec_helper.rb` excludes `:onnx`-tagged examples unless
|
|
330
|
+
`ONNX_TESTS=1` is set, mirroring the existing `:network` and `:slow`
|
|
331
|
+
opt-in pattern. `spec/kotoshu/suggestions/strategies/semantic_strategy_spec.rb`
|
|
332
|
+
is the sole consumer; its `:integration` tags are now `:onnx`.
|
|
333
|
+
- The SymSpell benchmark spec and the edit-distance / symspell
|
|
334
|
+
performance describe blocks are tagged `:slow`, removing
|
|
335
|
+
environment-dependent timing flakes from the default run.
|
|
336
|
+
- Arabic language detection is marked `pending` with a pointer to
|
|
337
|
+
`TODO.impl/30-language-auto-detection.md` (FastText LID missing the
|
|
338
|
+
Arabic vector).
|
|
339
|
+
- `edit_distance_strategy_spec` now uses the `from_source?` predicate
|
|
340
|
+
instead of asserting `source == :edit_distance`, matching the
|
|
341
|
+
lutaml-era contract that `Suggestion#source` is a string.
|
|
342
|
+
|
|
343
|
+
### Internal
|
|
344
|
+
|
|
345
|
+
- Added `TODO.impl/36-cleanup-regressions-0.3.1.md` (this release),
|
|
346
|
+
`TODO.impl/37-hunspell-correctness-tier2.md`,
|
|
347
|
+
`TODO.impl/38-onnx-semantic-gating.md`, and
|
|
348
|
+
`TODO.impl/39-tier3-and-beyond.md` as the tiered execution plan.
|
|
9
349
|
|
|
10
350
|
## [0.3.0] — 2026-06-27
|
|
11
351
|
|
data/CLAUDE.md
CHANGED
|
@@ -21,6 +21,8 @@ bundle exec rspec -e "matches a word" # Run examples matching a name
|
|
|
21
21
|
bundle exec rspec --only-failures # Rerun just failing examples (uses .rspec_status)
|
|
22
22
|
|
|
23
23
|
NETWORK_TESTS=1 bundle exec rspec # Opt INTO tests that download dictionaries
|
|
24
|
+
ONNX_TESTS=1 bundle exec rspec # Opt INTO tests that need onnxruntime + cached models
|
|
25
|
+
SLOW_TESTS=1 bundle exec rspec # Opt INTO benchmarks and full-dictionary sweeps
|
|
24
26
|
bundle exec rubocop # Lint
|
|
25
27
|
bundle exec rubocop -A # Lint with safe auto-fix
|
|
26
28
|
bundle exec rake # default task = spec + rubocop
|
|
@@ -32,6 +34,8 @@ gem build kotoshu.gemspec && gem install kotoshu-*.gem
|
|
|
32
34
|
|
|
33
35
|
Notes that aren't obvious from the Rakefile:
|
|
34
36
|
- `spec/spec_helper.rb` excludes anything tagged `:network` unless `NETWORK_TESTS=1` is set — those specs download dictionaries from GitHub and are slow/flaky.
|
|
37
|
+
- `spec/spec_helper.rb` excludes anything tagged `:onnx` unless `ONNX_TESTS=1` is set — those specs need `onnxruntime` installed **and** the language model cached via `kotoshu setup :en --model`.
|
|
38
|
+
- `spec/spec_helper.rb` excludes anything tagged `:slow` unless `SLOW_TESTS=1` is set — those are timing-sensitive benchmarks and full-dictionary sweeps.
|
|
35
39
|
- SimpleCov runs on every `rspec` invocation (configured in `spec_helper.rb`).
|
|
36
40
|
- `spec/spylls_test_helper.rb` is mixed into every spec. It ports Hunspell's reference test fixtures from [Splylls](https://github.com/neolithos/spylls) (the Python Hunspell port); many specs assert behavior against those fixtures.
|
|
37
41
|
|
data/README.adoc
CHANGED
|
@@ -715,6 +715,39 @@ Total:
|
|
|
715
715
|
======================================================================
|
|
716
716
|
----
|
|
717
717
|
|
|
718
|
+
=== Shell Completion
|
|
719
|
+
|
|
720
|
+
Kotoshu ships tab-completion scripts for bash, zsh, and fish. The scripts
|
|
721
|
+
complete subcommands for `kotoshu <TAB>` and language codes for
|
|
722
|
+
`kotoshu setup <TAB>` (the language list is read dynamically via
|
|
723
|
+
`kotoshu completions languages`, so newly registered languages appear
|
|
724
|
+
without re-running the install step).
|
|
725
|
+
|
|
726
|
+
Install for your shell:
|
|
727
|
+
|
|
728
|
+
[source,bash]
|
|
729
|
+
----
|
|
730
|
+
# bash (system-wide)
|
|
731
|
+
kotoshu completions bash > /etc/bash_completion.d/kotoshu
|
|
732
|
+
|
|
733
|
+
# bash (single user)
|
|
734
|
+
kotoshu completions bash > ~/.local/share/bash-completion/completions/kotoshu
|
|
735
|
+
|
|
736
|
+
# zsh — drop into any directory on your $fpath
|
|
737
|
+
kotoshu completions zsh > "${fpath[1]}/_kotoshu"
|
|
738
|
+
|
|
739
|
+
# fish
|
|
740
|
+
kotoshu completions fish > ~/.config/fish/completions/kotoshu.fish
|
|
741
|
+
----
|
|
742
|
+
|
|
743
|
+
To see the supported language codes without installing a completion
|
|
744
|
+
script:
|
|
745
|
+
|
|
746
|
+
[source,bash]
|
|
747
|
+
----
|
|
748
|
+
kotoshu completions languages
|
|
749
|
+
----
|
|
750
|
+
|
|
718
751
|
=== Language Support Matrix
|
|
719
752
|
|
|
720
753
|
Kotoshu provides multi-language support with varying feature availability.
|
|
@@ -937,6 +970,93 @@ kotoshu setup en --want spelling,model
|
|
|
937
970
|
git clone https://github.com/kotoshu/dictionaries.git ~/src/kotoshu/dictionaries
|
|
938
971
|
----
|
|
939
972
|
|
|
973
|
+
== Writing a Document Plugin
|
|
974
|
+
|
|
975
|
+
Kotoshu ships only `Kotoshu::Documents::PlainTextDocument` for the
|
|
976
|
+
plain-text case. Format-specific parsers (Markdown, AsciiDoc,
|
|
977
|
+
reStructuredText, LaTeX, …) live in plugins — see the
|
|
978
|
+
`kotoshu-document-plugin-boundary` design note. Kotoshu auto-discovers
|
|
979
|
+
plugin files via `Gem.find_files("kotoshu_plugin/document/*.rb")` — install
|
|
980
|
+
the gem and the parser registers itself on first use.
|
|
981
|
+
|
|
982
|
+
A plugin has three pieces:
|
|
983
|
+
|
|
984
|
+
1. *Parser class* — responds to
|
|
985
|
+
`.from_string(text, language_code:)` (and optionally
|
|
986
|
+
`.from_file(path, language_code:)`). Returns a
|
|
987
|
+
`Kotoshu::Documents::Document` whose `text_nodes` carry proper
|
|
988
|
+
`Documents::SourceRange`s.
|
|
989
|
+
2. *Plugin file* — placed at
|
|
990
|
+
`lib/kotoshu_plugin/document/<format>.rb` in the gem. The file body
|
|
991
|
+
calls `Kotoshu::Documents.register(format, ParserClass)` at load
|
|
992
|
+
time.
|
|
993
|
+
3. *Gem dependency* — `kotoshu` (this gem) and whatever
|
|
994
|
+
format-parsing library the parser wraps (e.g. `kramdown` for
|
|
995
|
+
Markdown, `asciidoctor` for AsciiDoc).
|
|
996
|
+
|
|
997
|
+
Example skeleton (`my_markdown_parser` gem):
|
|
998
|
+
|
|
999
|
+
[source,ruby]
|
|
1000
|
+
----
|
|
1001
|
+
# lib/my_markdown_parser/document_parser.rb
|
|
1002
|
+
module MyMarkdownParser
|
|
1003
|
+
class DocumentParser
|
|
1004
|
+
# Build one or more TextNodes from the Markdown AST, each
|
|
1005
|
+
# carrying a SourceRange that points at the original source.
|
|
1006
|
+
def self.from_string(text, language_code: nil)
|
|
1007
|
+
# ... walk the AST, collect (text, source_offset) per text run,
|
|
1008
|
+
# build TextNodes with SourceRanges ...
|
|
1009
|
+
Kotoshu::Documents::Document.new(
|
|
1010
|
+
text_nodes: nodes,
|
|
1011
|
+
source: text,
|
|
1012
|
+
format: :markdown,
|
|
1013
|
+
language_code: language_code
|
|
1014
|
+
)
|
|
1015
|
+
end
|
|
1016
|
+
end
|
|
1017
|
+
end
|
|
1018
|
+
----
|
|
1019
|
+
|
|
1020
|
+
[source,ruby]
|
|
1021
|
+
----
|
|
1022
|
+
# lib/kotoshu_plugin/document/markdown.rb
|
|
1023
|
+
require "kotoshu"
|
|
1024
|
+
require "my_markdown_parser/document_parser"
|
|
1025
|
+
Kotoshu::Documents.register(:markdown, MyMarkdownParser::DocumentParser)
|
|
1026
|
+
----
|
|
1027
|
+
|
|
1028
|
+
After installing the gem, callers get Markdown parsing for free:
|
|
1029
|
+
|
|
1030
|
+
[source,ruby]
|
|
1031
|
+
----
|
|
1032
|
+
require "kotoshu"
|
|
1033
|
+
|
|
1034
|
+
doc = Kotoshu::Documents.parse(source, format: :markdown,
|
|
1035
|
+
language_code: "en")
|
|
1036
|
+
errors = Kotoshu::Spellchecker.new(
|
|
1037
|
+
resource_bundle: Kotoshu::ResourceManager.resolve(language: "en")
|
|
1038
|
+
).check(doc.flattened_text)
|
|
1039
|
+
errors.each do |error|
|
|
1040
|
+
puts "#{error.source_range}: #{error.original} → #{error.recommended_suggestion}"
|
|
1041
|
+
end
|
|
1042
|
+
----
|
|
1043
|
+
|
|
1044
|
+
The structure-aware contract: the analyzer scans
|
|
1045
|
+
`doc.flattened_text` (concatenation of every node's `text`), but every
|
|
1046
|
+
error carries a `source_range` from the original source. An editor can
|
|
1047
|
+
use that range to highlight the user's actual markup, not the
|
|
1048
|
+
stripped text. For example, on the source
|
|
1049
|
+
|
|
1050
|
+
I'm an **friend** of Tom
|
|
1051
|
+
|
|
1052
|
+
a grammar error "an friend" → "a friend" carries a source_range
|
|
1053
|
+
pointing at the original `an **friend**` range, not the flattened
|
|
1054
|
+
"an friend" range.
|
|
1055
|
+
|
|
1056
|
+
Plugin gems should publish to RubyGems with a runtime dependency on
|
|
1057
|
+
`kotoshu >= 0.5.0`. Format-specific parsers are the plugin's
|
|
1058
|
+
responsibility — kotoshu never owns document parsing.
|
|
1059
|
+
|
|
940
1060
|
== License
|
|
941
1061
|
|
|
942
1062
|
BSD 2-Clause — see the link:LICENSE[LICENSE] file for details.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# 36 — Cleanup Regressions + 0.3.1 Bug-Fix Release
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Ship `kotoshu-0.3.1` — a focused bug-fix release that closes the
|
|
6
|
+
regressions introduced by the 0.3.0 lutaml-model migration and a small
|
|
7
|
+
set of pre-existing spec failures. This is the **only** Tier-1 work;
|
|
8
|
+
its acceptance criterion is a clean spec run on the non-network,
|
|
9
|
+
non-ONNX subset.
|
|
10
|
+
|
|
11
|
+
## Scope (locked)
|
|
12
|
+
|
|
13
|
+
| # | Failure cluster | Root cause | Fix shape |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| 1 | `spec/integration/walking_skeleton_spec.rb:22` — `result.suggestions.words` | Lutaml migration changed `WordResult#suggestions` from `SuggestionSet` to `Array<Suggestion>`. | Update spec to `result.suggestions.map(&:word)`. |
|
|
16
|
+
| 2 | `spec/integration/walking_skeleton_spec.rb:265` — `result.to_h` | Lutaml-model replaced hand-rolled `to_h`. The instance method is gone; framework supplies `to_hash`. | Update spec to call `result.to_hash` (lutaml-supplied). |
|
|
17
|
+
| 3 | `spec/kotoshu/suggestions/strategies/semantic_strategy_spec.rb` (22 failures) | Specs assume `onnxruntime` is installed and a model is loaded. Without ONNX, every example errors. | Tag the spec file `:onnx` and exclude by default; document the opt-in env (`ONNX_TESTS=1`) in CLAUDE.md. Mirror the `:network` opt-in pattern. |
|
|
18
|
+
| 4 | `spec/unit/hunspell/read_aff_spec.rb` (7 failures) | Real parser bugs in REP, MAP, PFX directive parsing and UTF-8 / numeric / long flag formats. | Fix each parser branch with a targeted spec; do **not** rewrite the whole reader. |
|
|
19
|
+
| 5 | `spec/kotoshu/language/detector_spec.rb:56` — Arabic detection | Pre-existing FastText LID gap. | Mark `pending` with a TODO referencing the upstream model; do not block the release on it. |
|
|
20
|
+
| 6 | `spec/benchmark/symspell_benchmark_spec.rb` (1 failure) | Likely environment-dependent timing. | Audit; if real bug, fix; if env noise, mark `:slow`. |
|
|
21
|
+
|
|
22
|
+
**Out of scope:** the 90+ `spec/integrational/{lookup,suggest}_spec.rb`
|
|
23
|
+
Hunspell-fixture failures. Those are T2 (`TODO.impl/37-hunspell-correctness-tier2.md`).
|
|
24
|
+
|
|
25
|
+
## Phase A — Lutaml regression cleanups
|
|
26
|
+
|
|
27
|
+
### A1. walking_skeleton_spec
|
|
28
|
+
|
|
29
|
+
- Line 28: `expect(result.suggestions.words).to include("hello")`
|
|
30
|
+
→ `expect(result.suggestions.map(&:word)).to include("hello")`
|
|
31
|
+
- Line 268: `hash = result.to_h`
|
|
32
|
+
→ `hash = result.to_hash` (lutaml-model supplies this)
|
|
33
|
+
|
|
34
|
+
### A2. semantic_strategy_spec gating
|
|
35
|
+
|
|
36
|
+
- Add `describe ..., :onnx do` or `RSpec.describe ... do; before { skip "set ONNX_TESTS=1" unless ENV["ONNX_TESTS"] } end`.
|
|
37
|
+
- Update `spec/spec_helper.rb` to exclude `onnx: true` unless `ONNX_TESTS=1`.
|
|
38
|
+
- Update `CLAUDE.md` "Notes" section to mention the opt-in.
|
|
39
|
+
|
|
40
|
+
### A3. Audit other lutaml fallout
|
|
41
|
+
|
|
42
|
+
- `grep -rn "\.suggestions\.words\|\.suggestions\.to_a\|\.to_h\b" spec/`
|
|
43
|
+
(only the two walking_skeleton hits — confirmed.)
|
|
44
|
+
- `grep -rn "\.as_json\b" spec/` — already replaced in cli.rb; verify
|
|
45
|
+
no spec calls the removed instance method.
|
|
46
|
+
|
|
47
|
+
**Acceptance:** `bundle exec rspec --tag ~network --tag ~onnx` shows
|
|
48
|
+
**0 failures** attributable to lutaml migration.
|
|
49
|
+
|
|
50
|
+
## Phase B — Hunspell .aff parser fixes
|
|
51
|
+
|
|
52
|
+
For each failing directive, add a focused spec covering the exact
|
|
53
|
+
shape from the failing fixture, then fix the parser branch.
|
|
54
|
+
|
|
55
|
+
### B1. REP directive (read_aff_spec:12)
|
|
56
|
+
|
|
57
|
+
Repairs — substitution pairs used by the suggester. Currently parsed
|
|
58
|
+
incorrectly.
|
|
59
|
+
|
|
60
|
+
### B2. MAP directive (read_aff_spec:31)
|
|
61
|
+
|
|
62
|
+
Character equivalence classes (e.g. `MAP uü`, `MAP aàáâã`). Sets up
|
|
63
|
+
related-character substitution in suggestions.
|
|
64
|
+
|
|
65
|
+
### B3. PFX directive (read_aff_spec:47)
|
|
66
|
+
|
|
67
|
+
Prefix rules. Currently fails to parse multi-rule PFX blocks. Likely
|
|
68
|
+
the same bug as SFX since they share the parser.
|
|
69
|
+
|
|
70
|
+
### B4. Long flag format (read_aff_spec:62)
|
|
71
|
+
|
|
72
|
+
`FLAG long` mode — two-character flags.
|
|
73
|
+
|
|
74
|
+
### B5. UTF-8 flag format (read_aff_spec:106)
|
|
75
|
+
|
|
76
|
+
`FLAG UTF-8` mode — flags are single Unicode codepoints.
|
|
77
|
+
|
|
78
|
+
### B6. Numeric flag format (read_aff_spec:125)
|
|
79
|
+
|
|
80
|
+
`FLAG num` mode — integer flags.
|
|
81
|
+
|
|
82
|
+
### B7. AF directive / flag aliases (read_aff_spec:125)
|
|
83
|
+
|
|
84
|
+
`AF` directive defines aliases used to keep `.dic` files compact.
|
|
85
|
+
|
|
86
|
+
**Acceptance:** all 7 `read_aff_spec` examples green; no regression in
|
|
87
|
+
existing `algorithms/` specs.
|
|
88
|
+
|
|
89
|
+
## Phase C — Pre-existing pending/skips
|
|
90
|
+
|
|
91
|
+
- Arabic detection: `pending "FastText LID missing Arabic vector — see TODO.impl/30-language-auto-detection.md"`.
|
|
92
|
+
- Symspell benchmark: audit; if real, fix; if env noise, `:slow`.
|
|
93
|
+
|
|
94
|
+
## Phase D — Cut 0.3.1
|
|
95
|
+
|
|
96
|
+
1. Bump `lib/kotoshu/version.rb` → `0.3.1`.
|
|
97
|
+
2. Update CHANGELOG (or create if missing).
|
|
98
|
+
3. Update README quickstart if it references any 0.3.0-broken API.
|
|
99
|
+
4. Run full suite (`bundle exec rspec --tag ~network --tag ~onnx`)
|
|
100
|
+
green; `bundle exec rubocop` clean.
|
|
101
|
+
5. PR (not direct-to-main — see global rule).
|
|
102
|
+
6. After merge: tag `v0.3.1` (the **user** does this; never the agent).
|
|
103
|
+
|
|
104
|
+
## Release-level acceptance criteria
|
|
105
|
+
|
|
106
|
+
- `bundle exec rspec --tag ~network --tag ~onnx` → 0 failures
|
|
107
|
+
(excluding the explicitly-pending FastText Arabic case).
|
|
108
|
+
- `bundle exec rubocop` → clean.
|
|
109
|
+
- Lutaml migration regressions: 0.
|
|
110
|
+
- Pre-existing Hunspell `.aff` parser bugs: 0.
|
|
111
|
+
- No new public API changes — 0.3.1 is strictly a bug-fix release.
|
|
112
|
+
|
|
113
|
+
## Dependencies
|
|
114
|
+
|
|
115
|
+
- **Blocked by:** nothing.
|
|
116
|
+
- **Blocks:** `TODO.impl/37-hunspell-correctness-tier2.md` (the next
|
|
117
|
+
tier of Hunspell-fixture work).
|