Composed and decomposed Korean, combining sequences in Japanese, and ZWJ emoji can look identical while differing internally. String length or direct equality could reject a correct answer or count one visible emoji as several characters.
The engine needed to stay independent from Flutter widgets and IME lifecycle and return the same result for the same text, policy, and time. Because the target is short learning prompts, I prioritized an explainable edit path and documented the O(n×m) time and memory boundary.
I normalize to NFC, split into extended grapheme clusters, and compute Levenshtein distance. Fixed tie-breaking makes edit paths deterministic; versioned policies handle whitespace and terminal punctuation, while accuracy and gross CPM use integer-scaled values.
- Covered composed/decomposed Korean, Japanese kana, combining marks, and ZWJ emoji.
- Strictly parsed policy JSON and rejected unknown versions, values, and fields.
- Published shared inputs and expected outputs as conformance/v1 fixtures.
Formatting, static analysis, 39 tests, and the publish dry run pass, with public CI covering Dart 3.8 and stable.
Unicode support required a contract for what counts as one character and how tied edit paths are explained—not only a normalization call.