Skip to content

Coffee Dictionary: home, term detail and the peek sheet over 72 terms in three states #95

Description

@maximsan

Specced from #20, which asked which dictionary surfaces ship and how 72 terms in three states are modelled. The content side is already bridged: the extractor (#77) emits dictionary_terms.json and validates every term's cat, related and lesson pointer.

Problem Statement

A learner meets words they do not know — washed, crema, channeling, TDS — inside lessons, on bags, and on menus. The app has nowhere to look any of them up. The Coffee Dictionary is absent from the app entirely: 72 terms across 8 categories are authored and now sit in bundled JSON, and not one of them is reachable.

Worse, the terms a learner most needs are the ones a lesson never covers. Eight terms are reference-only — nothing on the path teaches them — so waiting for the course to explain them is waiting forever.

Solution

A dictionary the learner can browse, search and read, reachable without leaving what they are doing.

Three surfaces:

  • Dictionary home — search across term names and their aliases, a category grid, and a status filter with live counts.
  • Term detail — the full entry: pronunciation, the short explanation, the deep explanation, a worked example, a self-check, related terms, cited sources, and where on the path it is taught.
  • Term peek — the same entry compressed into a sheet that does not take the learner off the screen they are on.

Every term carries one of three states, and the state is honest about what the course promises:

State When What the learner is told
Learned the lesson that teaches it is complete Where you learned it + the lesson
To learn a lesson teaches it, not yet complete Where you'll learn it + the lesson
Reference no lesson teaches it No lesson covers this one — it's here for when you meet it on a bag or a menu.

The rule carried over from the design verbatim: dashed means "not yet"; the dash means "not on the path at all". Showing a to-learn state for a term no lesson teaches is a promise the course cannot keep.

User Stories

  1. As a learner, I want to open the dictionary from the app's header, so that looking a word up never costs me my place.
  2. As a learner, I want to see every term grouped by category, so that I can browse a subject rather than only search a word I already know.
  3. As a learner, I want each category to show its name, its glyph and a one-line description, so that I know what is inside before I open it.
  4. As a learner, I want to search by typing part of a term, so that I can find it without knowing its exact spelling.
  5. As a learner, I want search to match a term's alternative names, so that typing "red bourbon" finds Bourbon and typing "canephora" finds Robusta.
  6. As a learner, I want search to ignore case and accents, so that "geisha" and "Gesha" both land somewhere useful.
  7. As a learner, I want to filter to terms I have learned, so that I can see how much vocabulary the course has actually given me.
  8. As a learner, I want to filter to terms I have yet to learn, so that I can see what is still ahead on the path.
  9. As a learner, I want each filter to show a live count, so that I can see the size of each set before I open it.
  10. As a learner, I want reference-only terms kept out of the "to learn" list and out of its count, so that the number in front of me is a promise the course can keep.
  11. As a learner, I want reference-only terms to still be findable under "all" and by search, so that a word I met on a bag is never unreachable.
  12. As a learner, I want to open a term and read a short plain-language explanation first, so that I get my answer in one line before deciding whether to read further.
  13. As a learner, I want a deeper explanation underneath when one exists, so that I can go further on the terms that repay it.
  14. As a learner, I want a worked example of the term in use, so that I recognise it next time I meet it in the wild.
  15. As a learner, I want a pronunciation respelling, so that I can say the word out loud without embarrassment.
  16. As a learner, I want a self-check question on terms that carry one, so that I can test whether I actually understood it.
  17. As a learner, I want the self-check to explain the answer after I choose, so that a wrong guess still teaches me.
  18. As a learner, I want related terms shown as chips I can tap, so that I can follow a thread through the vocabulary.
  19. As a learner, I want the sources a term draws on listed, so that I can trust what I am reading and go to the original.
  20. As a learner, I want a link to the lesson that teaches a term, so that I can go and learn it properly rather than just reading a definition.
  21. As a learner, I want a term I have already learned to say where I learned it, so that I can go back and revise.
  22. As a learner, I want a reference-only term to say plainly that no lesson covers it, so that I do not go hunting for a lesson that does not exist.
  23. As a learner, I want a term whose only content is the short explanation to look finished rather than broken, so that a brief entry does not read as a bug.
  24. As a learner, I want to peek at a term in a sheet without losing my place, so that checking a word does not cost me the screen I was on.
  25. As a learner, I want the peek sheet to offer a way through to the full entry, so that a quick check can become a proper read.
  26. As a learner, I want my learned terms to grow as I complete lessons, without marking anything by hand, so that the dictionary reflects my progress for free.
  27. As a learner, I want my learned terms to survive closing the app, so that progress is not re-earned every session.
  28. As a learner, I want my learned terms to arrive on a second device, so that the dictionary agrees with the rest of my progress.
  29. As a learner, I want Reset Progress to return every term to unlearned, so that a reset means what it says.
  30. As a learner who has completed nothing, I want the dictionary to be fully readable, so that it is useful as a reference from day one.
  31. As a learner, I want a search that matches nothing to say so clearly, so that I know the word is absent rather than the app broken.
  32. As a learner using a screen reader, I want each term's state announced, so that the distinction between learned, to-learn and reference is not carried by a ring shape alone.
  33. As a learner with reduced motion enabled, I want the peek sheet and filters to settle without animation, so that the app respects my system setting.
  34. As a developer, I want term status derived from progress rather than stored separately, so that there is one source of truth about what a learner knows.
  35. As a developer, I want the dictionary's rules decidable as pure functions, so that search, filtering, counting and status can be tested without pumping a widget.
  36. As a developer, I want the category bank generated from the prototype like every other bank, so that category names cannot drift from the design source.

Implementation Decisions

Surfaces

  • Three surfaces ship: dictionary home, term detail, term peek sheet. Term-of-Day, Flashcards and the Vocab game are each deferred to their own ticket (see Out of Scope) — none of them had a ticket before this spec.
  • Dictionary routes are registered inside the navigation shell so the tab bar persists, and are navigated by route name, never by a hardcoded path, per the repo's navigation policy.
  • Entry point is a header action on the Learn tab, matching the design's top-right placement. The design pairs it with a Saved entry; Saved is not built here and stays with its own ticket.

Term status is derived, never stored

  • Status is a three-valued enum — learned / to-learn / reference — but the value is derived, not persisted and not a field on the content model. The term model carries an optional lesson pointer and nothing about the learner.
  • The derivation, and it is the whole rule:
    • no lesson pointer → reference
    • lesson pointer present and that lesson is in the learner's completed set → learned
    • otherwise → to learn
  • learned stays derived. This was the open question in Coffee Dictionary: which surfaces ship, and how 72 terms in three states are modelled #20 and the design has since removed the reason to store it: the Espresso defect that made derivation unsafe is fixed at source (a real Espresso lesson was written, the pointers are now true, and the design's own audit returns clean). Deriving costs no storage, needs no migration, and converges across devices for free because completed lessons already sync.
  • The progress snapshot's existing learned-terms field must not be written by this work. It is a reserved slot for a future explicit signal — a term marked known by hand, or counted as met. Writing it while status is also derived would create two sources of truth about the same fact, which is the bug this decision exists to avoid. A follow-up that introduces an explicit signal owns that field.
  • Because status derives from completed lessons, Reset Progress clears learned terms for free — completed lessons already sit in the reset scope. No new reset wiring, and no way for a reset to forget the dictionary.
  • Reference terms are excluded from the to-learn set and from its count. Under the "all" filter they appear normally.

Content and the extractor

  • Terms load from the generated dictionary bank the extractor already emits, through the existing content repository, exactly as modules, lessons and collectibles do.
  • The extractor gains a sixth bank: dictionary categories. It already reads the category declaration to validate every term's category pointer but does not emit it; the home screen needs each category's label, glyph and one-line description. Emitting it is a small extension at an established seam, and it keeps category names from being retyped into Dart where they can drift.
  • Term shape on the wire is the prototype's vocabulary verbatim — the extractor renames nothing — with idiomatic Dart names applied through serialization annotations, matching how the card union already handles this.
  • A term's self-check reuses the existing choice value type built for the card union rather than declaring a parallel one: the shape is identical (option text plus an optional correct flag), and one type means one set of rules about what a correct answer looks like.
  • Stubs ship as-is and the split is not surfaced. 26 of 72 terms carry only the short explanation; they render without the deep, example, self-check and sources blocks and carry no "incomplete" marker. The learner's question is what does this word mean, and the short explanation answers it — labelling a quarter of the dictionary as thin advertises a gap that is not one.
  • Verified counts against the prototype source, since two docs disagree: 72 terms, 8 categories, 46 with deep text (26 stubs), 29 with a self-check, 23 with a pronunciation, 48 with aliases, 25 distinct lesson pointers, and 8 reference-only termsmasl, wet-hulled, tds, cold-brew, cupping, gooseneck, sca, origin-boards. Coffee Dictionary: which surfaces ship, and how 72 terms in three states are modelled #20 and the design reference both list 9, wrongly including washing-station, which does have a lesson. The design reference's own summary table says 8 and is right; its prose list is wrong.

The pure seam

Everything interesting about the dictionary is a derivation over content plus progress, so it lives in a pure module with no widget and no database, following the repo's existing rule that animation math, mapping and derivations move to a sibling pure-Dart file:

  • resolving a term's three-valued status from the completed-lesson set
  • searching terms by name, alias and category label, case- and diacritic-insensitive
  • filtering to a status and producing the counts, with reference terms excluded from the to-learn set and its count
  • grouping terms under their categories for the home grid

The screens read these functions and render; they do not restate the rules.

Deliberately not taken on

  • Text-to-speech is deferred. Adding a speech dependency under the SPM-only iOS constraint is a real platform decision, and it is not needed to answer what does this word mean. The pronunciation respelling ships as text.
  • Loading, empty and error states carry semantics labels and respect the reduced-motion setting, per the repo's rules.

Testing Decisions

A good test here asserts what a learner would notice — a reference term never appears in the to-learn count, searching an alias finds the term — and never how the code is arranged internally. Tests are written against the pure functions and the repository, not against widget internals, so that rearranging the screens does not rewrite the suite.

  • The pure derivation module is where the behaviour is tested, as plain Dart with no ProviderScope and no pumpWidget. Cases to cover: all three statuses including the boundary where a term has a lesson pointer the learner has not completed; alias and diacritic matching; a search matching nothing; the to-learn filter and its count both excluding reference terms; category grouping covering every term exactly once. Prior art: the snapshot merge tests, which prove laws over pure functions with no database.
  • The content repository is tested for loading both banks off the bundled assets and for referential health — every term's category resolves, every related term resolves, every lesson pointer resolves. Prior art: the existing content repository test, which does exactly this for modules, lessons and cards, and the extractor's own graph validation.
  • The extractor's new category bank is tested at the existing extractor seam — the subprocess test that already asserts a clean run writes every bank and that a seeded broken reference is refused with nothing written. The bank count assertion moves from five to six, and the committed-output drift check covers the new file for free.
  • Widget tests cover wiring only, with a fake content repository overriding the content provider: the home renders categories and reacts to a query, detail renders the reference-only block for a reference term and the lesson link otherwise, and the peek sheet opens and dismisses. Prior art: the existing lesson-completion and module-summary widget tests, which override the content provider with an in-memory fake.
  • One test asserts the stub path renders without the deep, example, self-check and sources blocks and without an error — the 26 short-only terms are a quarter of the dictionary and must not read as broken.

Out of Scope

Each of these was deferred by this spec and has been given its own ticket, because none existed before and deferring without one drops the work on the floor:

  • Term-of-Day — the banner and its screen. A deterministic pick by date; cheap once the pure seam exists, but not needed to make the dictionary useful.
  • Flashcards — a drill over saved terms. Blocked on the Saved shelf, which is still unresolved.
  • Vocab game — a generated multi-round vocabulary drill. Also owes an answer on whether reference-only terms belong in the practice pool, which the design's dictionary surfaces do not state.
  • Term linkification in lesson body copy — auto-linking glossary terms inside card text so the peek sheet opens without leaving the lesson. It is the one piece coupling the dictionary to the card renderers, which are still being built. The peek sheet still ships here, opened from the related-term chips.

Also out of scope: text-to-speech; saving or bookmarking terms; the global header's Saved entry; and the v2 Atlas and Duel surfaces that also carry vocabulary.

Further Notes

  • The dictionary is the first feature to read a content bank that no screen has ever read, so it is also the first real test of whether the extractor's output is shaped for the app rather than for the prototype. Friction found here is worth recording against the extractor rather than worked around in the screens.
  • Reference-only terms are the most interesting thing in this feature and the easiest to get wrong. They are the terms a learner is most likely to look up — words met on a bag or a menu rather than in a lesson — and they are precisely the ones the course never explains. Every rule that touches them should be read twice.
  • The prototype freezes the dictionary to a different date than the rest of the app, which drives Term-of-Day. That inconsistency belongs to the Term-of-Day ticket, not here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestready-for-agentFully specified, ready for an AFK agent

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions