Back to projects

Published on pub.dev · adopted in product

Unicode Typing Grader

A Unicode-aware Pure Dart grading engine extracted from a real app and published as an independent OSS package.

I separated a typing grader first needed in a personal app into an independent Pure Dart package and published it on pub.dev. NFC normalization, grapheme-level edit distance, versioned policies, and conformance fixtures define reproducible results, and the package is now used by a real app.

Period
2026.08.24 - Present
Ownership
Package design, implementation, tests, release, and product adoption
Team
Solo OSS project
Repository
Public repository

Turning product grading rules into a public contract

While building a grader for a personal app, I found that NFC normalization, grapheme-level comparison, whitespace and punctuation policies, and active-time CPM were not responsibilities of one screen.

I moved the calculation into Pure Dart and published it on pub.dev with English, Korean, and Japanese documentation, a browser example, and conformance fixtures. I then adopted it in the app to verify what should remain outside the package.

Product-to-OSS boundary

Preserving the product contract while centralizing grading in the public engine

  1. 01

    Practice screen

    Passes committed text

  2. 02

    Product policy

    Existing storage and display shapes

  3. 03

    Thin adapter

    Maps versions and field names

  4. 04

    Public OSS engine

    NFC, graphemes, edit path

  5. 05

    Product result

    Stores fixed-point metrics

A visible character is not the same as a string length

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.

Adopting the extracted engine back into its source product

If the consuming app kept its internal copy after release, fixes would split across two implementations and Unicode rules or metric rounding could drift. Reusability would also remain a design claim without a real consumer.

Replacing the app's models directly with the public package types could break compatibility with existing stored data. The OSS boundary needed to stay independent while the consuming app kept its own representation.

The consuming app now uses version 0.1.2 directly from pub.dev and keeps only a small adapter between its existing models and the package models. Shared calculations live in the package; app-specific display and storage shapes remain at the conversion point.

  • Removed duplicate comparison and metrics calculations.
  • Consumed pub.dev version 0.1.2 without a local fork.
  • Limited app-specific compatibility work to a small adapter.

A real personal app adopted the public package and removed duplicate comparison and metrics calculations without replacing its existing stored data.

Keeping the shared engine distinct from the product contract—and the conversion point small—preserved both reuse and compatibility.

unicode_typing_grader 0.1.2 is public on pub.dev and GitHub with 39 tests and CI, and a real app consumes the published package.

I no longer treat moving reusable-looking code into another repository as sufficient for OSS. It becomes an explainable engineering asset only after closing the loop through a public API, versioned contract, edge-case tests, licensing, and adoption by a real consumer.

This is a 0.1.x package first published in August 2026; external adoption, long-term compatibility, and representative-length benchmarks are not yet established.

  • Add time and memory benchmarks for representative prompt lengths
  • Collect external-consumer feedback and decide the API boundary for 1.0.0
  • Make Dart and Go fixture validation a required check for package upgrades