Repository navigation
ADR 0050 English Synonym Sets
Source of truth: this wiki page.
-
Status: Accepted (owner, 2026-10-30, on #924) for: bundled delivery,
typo tolerance, the match scope, the
not_synonymsfield and the reviewer proposal fields. Details still open (plan questions 6, 7, 8, 10, 11) stay Proposed and have recommendations. - Date: 2026-10-30
- Builds on: ADR-0034 (Hear types the meaning), ADR-0038 (review proposals), ADR-0040 (decks are CC BY-SA). Plan: Plan-Synonym-Dictionary. Issue: #924.
The owner's Hear drill showed జాగ్రత్త! with the meaning "Take care!
Careful!". Typing "take care" was marked wrong, because only ;
alternatives are accepted (lib/core/grading/meaning_rules.dart). The
owner: "This is extremely bad experience." Part 1 of #924 rewrites the decks'
unseparated alternatives. This ADR is part 2: a typed word that means the
same as the card's answer should be right, without the deck author having
listed it.
The owner's decisions of 2026-10-30, on #924:
- Scope: English answers first, shared by every course. Target-language synonyms come later, per course.
- Source: all three: a shared file, per-card alternates in the decks, and reviewers adding through the review flow (#508, #922).
- Grading: a synonym is right, not "close", not silent. The reveal shows the card's own answer, with a short hint ("Also: take care").
- The data is sets (not a dictionary): each set is a list of English words or phrases, and every member accepts every other member.
- Only absolute synonyms, words interchangeable in all situations. No sense-dependent pairs (bank / shore; big / large, which differ in some contexts). Regional spelling variants (colour / color, grey / gray, favourite / favorite, organise / organize) are included, and are the first and safest kind of set, seeded first (owner, 2026-10-30).
- Every list is poked for holes by an independent Sonnet pass before it ships.
The owner's answers on this ADR (2026-10-30, on #924):
- Delivery: bundled in the app first.
- Typos: yes, the same tolerance as the card's own answer.
- Match scope: "Whole answer, or when preceded by an article or verb and without anything succeeding (add the phrasal verbs as exceptions). Think of what else. Think only in context of English."
-
Schema: both
not_synonyms(0.6.x) and the reviewer proposal fieldsalt_native_addandnot_synonyms_add(0.7.0) are approved.
-
One file,
assets/synonyms/en.yaml, a YAML list of lists, in a restricted subset a small parser reads (below). Bundled in the app, so it works offline from the first launch. Target size: a few thousand sets, well under 300 KB; the builder is told to stop at the course vocabulary's most frequent meanings, not to fill the language.
# Synonym sets for typed English meanings (ADR-0050).
# Licence: CC BY-SA 4.0. Attribution: see the Sources screen.
version: 1
sets:
- ["colour", "color"]
- ["take care", "be careful", "look out"]-
Each member is quoted (AGENTS.md rule 2) and written in its
comparison form: lower case, no punctuation, no bracketed qualifier, no
leading "to", contractions expanded, "ok" for okay: the form
meaningKeyproduces. The validator refuses a member whose key differs from what is written. - Sorted: members within a set, then sets by first member, so a diff shows what changed and a merge conflict is a line.
- No word is in two sets. A word that two sets both want means they are one set (merge them) or one of them is wrong (it is not absolutely synonymous). Sets are never transitive by accident: only members of the same set accept each other.
- No set of fewer than two members, no empty member, no repeat.
- A phrase is a member ("take care"). Inflected forms are not listed: see matching. An irregular verb's forms are sets of their own (["began", "started"]), kept to the verbs that have sets.
-
Header: a comment saying what the file is, its licence and where to
read the attribution, and
version. A set carries no per-set comment or source, so that the file stays a list of lists.
-
lib/core/grading/synonym_sets.dart(new, no Flutter import, AGENTS.md rule 3):SynonymSets.parse(String text)reads the restricted subset (asets:line, then one- ["a", "b"]per line, comments and blank lines skipped; anything else is aFormatException, which the loader treats as "no sets", never as a crash). It exposesSet<String> equivalents(String key). The packageyamlis not needed, and no dependency is added (rule 6). - The bundle is read in the app layer (
lib/app/), once, on the first typed-meaning answer, and given to theAnswerGraderthrough its constructor as an optionalsynonymsargument. Without it, grading is as before. The web build reads the same asset. -
pubspec.yamlgains oneassets:line, in its own small PR with the loader (the file is a contention point; no dependency changes).
The synonym pass sits after the existing exact and diacritic passes (a card's own answers always win), and its typo pass after the card's own typo pass. The owner's scope (2026-10-30, on #924): "Whole answer, or when preceded by an article or verb and without anything succeeding (add the phrasal verbs as exceptions). Think of what else. Think only in context of English." The precise rules are in Plan-Synonym-Dictionary, with the behaviour table; in short:
-
Keys. Both sides are put in
meaningKeyform (rules 1 to 7 stay as they are: case, punctuation, contractions, brackets, a leading "to", "ok", "please"). Every accepted form of the card counts (thenativetext's;parts andalt_native, brackets dropped), each tried alone. -
Frame. A typed answer is
[lead] core, with at most one lead word and nothing after the core. The lead is an article (a, an, the) or the infinitive "to", free on either side; or a possessive, demonstrative or copula (my, your, this, that, is, are, be and the like), which must be the same on both sides. The core must be a whole member of a set (or an inflection of one, below). A synonym is never a word inside a longer sentence, so nothing after the core is ever tolerated: "give up" is not "give", "put off" is not "delay", "very big" is not "big", "not big" is not "small". A phrasal verb matches only when a set lists the phrasal verb itself. -
Inflection. The core is split into a lemma and a regular ending
(
-s,-es,-ies,-ed,-ing,-er,-est, doubled consonant, droppede) and matches when the endings are equal and the lemmas are in one set; a form that is itself a member is read as that member first. A phrasal verb is split on its verb, not on its particle. Irregular forms are sets of their own. British and American -l-/-ll- spellings (cancelled / canceled, travelling / traveling, labelled / labeled) are one word by rule, for a closed list of lemmas truly spelled both ways (bevel, cancel, channel, chisel, counsel, dial, duel, enamel, equal, fuel, funnel, grovel, jewel, label, level, libel, marvel, medal, model, panel, pedal, pencil, quarrel, rival, shovel, signal, snorkel, swivel, total, travel, trowel, tunnel, yodel): a doubled l before -ed, -ing, -er, -ers, -or or -ors is optional on both sides, whatever the sets hold, so no lemma set is needed for them. A word whose lemma is not on the list is never matched by the rule (never pulled, filled, rebel, control), so a slip such astelingfortellingis graded by the card's own typo pass, a close typo, not exact. Other regional spellings (organise / organize, colour / color) are lemma sets, and their inflections follow from rule 3:organisedisorganisewith -ed, so it matchesorganizedwhen both sides carry the same ending. -
Typos: yes, the same tolerance as the card's own answer (owner,
2026-10-30; this replaces the earlier "no typo distance on synonyms").
After the card's own typo pass fails, the typed core is compared with
each member of the matching sets using the grader's own thresholds
(
typoDistance,longTypoDistance,longThreshold,shortThreshold), counted in the letters of the member's lemma: none under three, one for three to seven, two from eight. Two safeguards keep the short-word risk (car / cat) at the card's own level: a typed word that is itself a member of any set is a real word and is never typo-matched to another set, and a typo match is only evercloseTypo, never exact (below). Nothing else stacks: a typo is tried on the core after the lead is removed, the ending must be equal, and the lead rules are not relaxed. -
Result. An exact match is
AnswerOutcome.exact(grade 5) with a new flag onGradedAnswer,viaSynonym, the waydroppedArticleis a flag and not an outcome, so no switch over the outcomes changes. A typo match iscloseTypo(grade 3, as for the card's own typos) with the same flag.matchedis the card's own answer, not the typed word. - Which side leads. The "to" lead counts only when the card's own answer also starts with "to" (a verb card); an article lead on one side and "to" on the other is a part-of-speech clash and refuses ("a run" is not "to sprint").
The sets hold only absolute synonyms, so a wrong match is rare and is a
defect in a set. The rare case still needs an escape that does not wait for
a release of the file: a card's native side (its English layer card, where
alt_native lives) may carry not_synonyms, a list of typed answers the
sets must not accept for this card. Example: a card for a river's bank
whose set partner is wrongly accepted: not_synonyms: ["shore"].
- Quoted strings, compared in
meaningKeyform; the synonym pass skips a typed key listed there. They never block the card's own answers or itsalt_native. - A card older apps cannot read: a released parser rejects an unknown
field, so decks use
not_synonymsonly a release after the app that reads it (as forwarning, #691). The schema change needs the owner's nod (AGENTS.md, "What an agent must not decide alone"), given on 2026-10-30; the field ships in 0.6.x. - A set that many cards escape is the real fix: the validator reports it (below) and the set is removed or split.
; in native and alt_native (ADR-0034; docs/DECK-FORMAT.md) are
accepted outright by rule 4 and the grader's alternates. They stay the
first tool of a deck author and are unchanged; the sets only add to them.
Part 1 of #924 moves unseparated alternatives into ;.
A reviewer who sees a correct typed answer marked wrong, or a wrong one
accepted, proposes it through the review flow (ADR-0038), so the owner's
automation carries it: field gains two values for the layer card,
alt_native_add (text is one extra accepted answer, appended to
alt_native when agreed) and not_synonyms_add (appended to
not_synonyms). This changes the proposal format and tools/proposals.py,
tools/review_bot.py and reviewer mode, and the owner approved
both (2026-10-30); it ships with the reviewer-mode overhaul (0.7.0). A
reviewer never edits the shared file: a word proposed for the same card
answer on several cards becomes a candidate set, reported to the owner by a
tool, and goes through the seeding workflow below.
Built (2026-10-31, plan step 9, tools only; the app's reviewer mode is the
overhaul's): the format is as above, with now always "" (the proposal
adds, it replaces nothing). The review file names the parts alt_native and
not_synonyms with action: "add"; the bot puts a not_synonyms word in
comparison form. Applying appends to the list, closes only proposals of the
same item, and serialises the rest (two agreements cannot both edit one
line). The bot refuses what CI's validate_synonyms.py would reject: a
refused word that is one of the card's own answers, is in no set with
another of its answers, or whose set another card already escapes.
tools/synonym_candidates.py is the candidate-set report.
-
tools/english_meanings.pylists the English meanings of every course (nativeandalt_nativeof the layers), normalised by the same rules, most frequent first (cards across courses, then the deck priority of CLAUDE.md). - Haiku drafts sets, regional spelling variants first (batch 0), then the commonest meanings, in batches of about 200 meanings, writing only groups it is sure are interchangeable everywhere, leaving a meaning alone when unsure. Drafting may consult Wiktionary and Open English WordNet, both CC BY-SA 4.0-compatible (CC BY-SA 4.0 and CC BY 4.0), and nothing else (no thesaurus under a more restrictive licence).
- A Sonnet pass pokes holes in every set of the batch: for each, a counterexample sentence where the members cannot swap, register differences (formal, slang, rude, dated), grammar differences (countable, transitive, preposition), and any set it cannot defend is struck. Only surviving sets merge. This is the batch PR's one review (CLAUDE.md, one review per PR); the PR body lists the sets struck. Regional spellings (colour / color) survive.
-
tools/validate_synonyms.pyruns in CI ("Validate decks" job) on the file: format and sorting, quoting and comparison form, no set under two members, no word in two sets (it names the sets to merge), no repeat. It also reads the decks: everynot_synonymsentry must be a member of a set that has another member among the card's answers (else it does nothing and is an error), and a set that is escaped on two or more cards is reported as a failure, because "absolutely synonymous" is then false (open question 6). - A second tool listing, as warnings, pairs of cards in one deck whose
answers are in one set (a deck teaching sofa and couch as two cards),
so authors can add
not_synonymswhere the contrast is the point (open question 7).
On a synonym match the reveal shows the card's own answer, as on any right
answer, and one hint: Also: {typed}, the learner's own word in the hint,
for a typed answer of one or two words; "Also right" for a longer one. No
full stop, like every hint (wiki Design-Guidelines, #913). The text is an
ICU token in lib/l10n/app_en.arb; no literal in the widget (rule 10). The
screen reader reads the hint. The grade is the same as an exact answer. For a typo match
(closeTypo) the hint shows the member spelled right, not the typed misspelling,
and the grade is the card's own typo grade.
The file is CC BY-SA 4.0, like the decks (ADR-0040). The Sources screen
gets a line beside the Wiktionary one: "Synonym sets: adapted from
Wiktionary and Open English WordNet, licensed CC BY-SA 4.0" (a sources
token like sourcesPhoneticLine), and the file's header says the same.
Whether the credit is owed when sets are written by a model from its own
knowledge and only checked against those sources is open question 8; the
line is added by default, the cheaper wrong.
Behaviour, with made-up words, never real language (AGENTS.md, "Tests check
behaviour"): the Dart tests build a SynonymSets from a string with words
such as "florp" and "glim", and check what a learner sees: a member accepts
another and the reveal says so; the card's own answer still wins and is
not marked as a synonym; an inflection matches only with its own ending; a
not_synonyms entry blocks; a typo of a member is closeTypo and flagged, and not
accepted under three letters or when the typed word is a member of another
set; a leading article is free and a possessive must match; a phrasal verb
or a trailing word refuses; a malformed file grades as before. The Python
tests give the validator fixtures with made-up words and check each refusal
(a word in two sets, an unsorted file, a dead not_synonyms). Roughly ten
Dart and five Python tests in all; no per-line tests of the parser.
- A learner is no longer marked wrong for "take care" on a card that means "Careful!" once the sets hold the pair. Coverage is as large as the sets are: small at first, growing with each batch.
- Quality depends on a gate that costs tokens: every batch is drafted and poked. The owner chose that over a large, noisy file.
- "Absolute synonym only" leaves out many true near-synonyms (big / large).
Those stay with the deck author's
alt_native, which is exact and per card. - A shared file can be wrong for a card;
not_synonymsfixes one card without a release, but only after the deck format carries the field, so decks wait a release for it. - The python validator and
meaningKeynormalise separately. They must agree; a shared list of cases (a JSON fixture both test suites read) keeps them so. - Bundling means a new set reaches learners with the next app release; the owner chose this first (2026-10-30); following the decks can come once the file grows.
- Nothing about target-language synonyms is decided here.
- A dictionary (words mapped to senses, with transitive groups): the owner chose sets; a dictionary invites sense-dependent matches, which the owner ruled out.
- WordNet synsets taken whole: large, and synsets hold near-synonyms and hypernyms; far too loose for "absolutely synonymous".
- Accepting synonyms as "close": the owner chose right, with a hint.
- No typo tolerance on synonyms (the first recommendation): the owner chose the card's own tolerance instead, with the safeguards above.
- Word-by-word substitution inside sentences: rejected by the owner's scope (a lead and nothing after).
- Letting the model grade meanings (the AI-conversation plan): not offline, and not deterministic.
- JSON or one word per line: a list of lists in YAML matches the decks' and the owner's wording, reads well in a diff, and the subset needs no package.
See also: Plan-Synonym-Dictionary · ADR-0034 · ADR-0038 · ADR-Index · Home
1. Learner guides
- Learner Guide
- Getting Started
- Today and Daily Lessons
- Drills and Skills
- Courses and Paths
- Proficiency Levels
- Scripts and Readings
- Readings ISO 15919
- Speaking and Listening
- Progress and Stats
- Profiles and Settings
- Backup and Restore
- Reporting a Problem
2. Deck building guides
- Deck Building Guide
- Deck Format
- Romanising Indic Languages
- Deck Licences and Quoted Books
- Deck Tools
3. Review guides
4. Translation guide
5. Building on iOS
6. Roadmap
7. Sources and attributions
8. Contribution and agentic coding