An Achievement is a moment worth marking — a new highest day, a running total passing 100 km, a month of days without a headache — and the rule that decides when it happens. It is a moment detected from history, not a bar to grind toward: the date of each one is read off the data, so re-reading a year later finds the same moments on the same days, and nothing has to have been running at the time for it to count.

Nothing is celebrated unless you set it up

Whether a run of Yes or a run of No is the good one, and whether a new low is a triumph or a warning, depends on what is being tracked, and only the person tracking it can say. So every rule is stored and none is inferred: a trackable with no rules set up has no achievements, and the app says nothing about its values. The names are factual for the same reason — “new highest” and “new lowest”, never “best”.

The one achievement celebrated for everyone is the streak of Logged anything: days in a row you logged anything at all, at 7, 30, 100 and 365 days. Using the app is good whatever was logged, which no trackable’s own streak can say (a run of headaches is not). It reads every entry of yours, a Nothing included, and not the entries logged for you by an integration or a schedule. It has no row and no trackable (LOGGED_ANYTHING_STREAK in packages/core/src/achievement-defaults.ts).

A rule

A rule belongs to one trackable and has a measure, a threshold, and optionally a zone that says which days count.

measurewhat it reads after each daythresholdapplies to
streakthe run of days in a row that countladder or recordevery trackable that logs a value
bestthe day’s own valueladder or recorda number, an interval, a clock time, or a coded scale marked ordered
totalthe running sumladdera number or an interval
counthow many days have countedladdera number, an interval or a clock time, with a zone

The threshold is a ladder or a record, never one number. A ladder is a list of rungs ({ at: [7, 30, 100, 365] }) or a rule for producing them ({ every, scale }: a first rung, then each next one added evenly or multiplied by the scale, generated as far as the data has reached plus one, so it never runs out). A record fires whenever a day beats every day before it, and is offered only for measures that can be beaten: a count and a total only grow, so every day would be a record. A rung once reached is never taken back, because the ladder compares against the best value seen rather than the latest, so a streak that breaks does not revoke its 30-day mark.

Which measures apply is asked of the measure (measureApplies(measure, trackable)), not listed by the editor, so a new measure cannot be offered where it means nothing. A yes/no has no highest value (it would fire once, on the first Yes); an unordered choice is a set of categories wearing numbers; summing ordinal codes is arithmetic on labels; clock times have an earliest and a latest but do not add up; a tag logs nothing of its own, which its trackable’s kind says. A trackable logged with no value (a habit such as Journal) has the days it was logged and nothing more, so it takes a streak and only a streak. The editor also offers Days in a zone only on a trackable that has a zone, and a streak only where some days can count.

A best rule stores its direction, high or low, and a record waits for days with a value before it counts (minHistory, 5 by default, stored only when changed): with three days of data every day is a record. The wait counts days with a value rather than calendar days, so it scales with how often a thing is logged (five weigh-ins on a weekly trackable is five weeks), and the extreme keeps updating through it, so the first record announced is a real one. The editor words the wait as days with a value, since a Reading is a way of reading a trackable rather than a value logged.

A rule can read a Reading of its trackable rather than its own value (of on the rule): one field, reduced each day, for some of its items, with each Event as one where it says. An exercise’s new maximum is a new highest of Max of its weight for that exercise. It is set up beside that Reading on the exercise’s page, a bell on each of its single values, and named for both (“Bench press · Max weight — new highest”). The trackable’s own editor has no box for it and leaves it alone, so ticking or unticking the trackable’s new highest neither shows nor removes it.

Streaks

A streak counts the days in a row that count, and the rule says which: the days with some (Brush teeth’s Yes, more than 0, any choice but its none), the days with none (Smoked’s No, 0 cigarettes, Headache’s None, a Nothing), or the days in a zone. A stored 0 is none for a yes/no, a number and a choice alike (zeroIsFalse), so one rule reads all three; a value with no none (a text, an interval) offers only the days logged, or a zone. The editor names the choice in the trackable’s own words (“Days of Yes”, “Days of No”). Without a zone a streak reads the trackable as Any; with one, it reads the trackable’s own day value against the zone’s bounds. A derived trackable reads its own value: a derived yes/no, number or choice counts its 0 days as none (no caffeine after 15:00), while a derived clock time has a value every day and no none, so its streak needs a zone and is skipped without one.

A streak stands while its last day is today or yesterday, or every day since is on hold (liveRun): today may not be logged yet, but a run that ended last week is over however long it was, and the rule’s best is where it is remembered. A Home card shows a flame and the live count only for a streak set up on that trackable.

Days on hold carry a streak: a day a Disruption touches, or a day nothing was logged, neither breaks a run nor adds to it, and every measure reads through the same runs. Disruptions says which days are on hold, and why Logged anything’s streak carries across disrupted days only.

Computed, with one thing kept

There is no table of unlocks. Every moment is computable from the entries and its date comes from the data, so storing it would be storing a derived value. The one fact the data cannot supply is whether you have already been told, and that is the only thing kept: a set of moment keys (rule id, day and rung) in the achievements:seen setting.

Evaluation follows logging, the only thing that changes history. After a log the app re-reads that trackable’s rules and Logged anything’s streak over the entries already loaded, marks every new moment seen, and shows the last of them as a toast, since several rungs can land on one log and a queue of popovers is a punishment. The rule’s series is built from the measure rather than stored: the trackable’s own day value for best and count, its Sum for total, and its Any for an unzoned streak.

Units. Values are stored in their dimension’s base unit, while a zone keeps the unit its author typed and a running-total step is typed in the trackable’s display unit. Both are converted to base before the fold (zoneInBase; the editor’s step conversion), or kilograms are compared against grams and no day ever qualifies. See Units & Dimensions.

Storage

Rules are rows in the achievement table: a trackable id, an optional zone id, the rule as JSON, and a sort order. Both foreign keys cascade: a rule goes with its trackable, and a zone-qualified rule goes with its zone, since a rule about a range that no longer exists has nothing to say. A rule’s identity is its row id, with the direction in the key for a best rule and the days for a streak of none, so flipping highest to lowest does not inherit the other’s announced moments. The editor therefore plans a reconcile — create what is newly ticked, update what changed, delete what was unticked — rather than rebuilding a list, because recreating a row would re-announce every moment it ever produced, and a rule the editor has no box for (a hand-written ladder) is left alone. See Database Schema.

Where it shows

  • The Achievements page (/achievements): Rules, a picker of trackables and the editor for the one picked; Coming up, the next rung of each rule and how far along it is; Earned, every moment grouped by day, because they arrive in clumps. Rules sit first because Earned is unbounded.
  • A toast after a log that earned something.

Both word a moment one way (measureText): a streak or a count of days in days, and a running total or a day’s value in the trackable’s own unit, read the way the rule read it, so a total of metres reads “10 km” and a record on Weight reads kilograms rather than grams. A record shows its value alone, since the rule’s name already says highest or lowest.

  • A flame on a Home card and beside a row of the Trackables list, for a trackable with a streak set up, hidden by the streak switch below. Both read one count (useSetUpStreak), so a list row never shows a run the card does not.
  • Getting started’s finale points at the page (Getting Started).

Turning it off

Streaks, achievements and the Getting Started checklist are three switches for one choice: whether the app cheers you on. Onboarding asks it once and sets all three; Settings has them apart under Encouragement. All three default to on, and a stored preference that does not mention a switch reads it as on. The streak switch hides the flame on a Home card and in the Trackables list, and nothing else: a streak is a fact about the data, so stats and analysis keep reading it. With achievements off, the page says so and nothing is watched for.

The question is put even-handedly, that some find this motivating and some find it nagging, since neither answer is the better one. It is asked once rather than three times, because three questions about being cheered on would be the nagging. Everything starts on because the person a nudge helps most is the one who skipped the question, and a switch added later starts on rather than silently off for everyone who set the others.

What a rule cannot say

  • A change between two points (“lose 5 kg”) is not a fold over days, and no measure expresses one.
  • More than one trackable. A rule names only its own trackable, so “logged both X and Y today” is a rule over a derived trackable that combines them (Derived Trackables).
  • A trailing period. A rule is all-time; “your best month” is a rule over a windowed derived trackable (Rolling Windows).

Code

  • packages/core/src/achievement.ts — the rule types, measureApplies, ladderRungs, and the pure fold (readAchievement, evaluateAchievement) over an already-computed day series.
  • packages/core/src/achievement-defaults.ts — achievementsFor: stored rows into evaluable specs, names, the zone in base units.
  • packages/core/src/achievement-repository.ts, packages/core/src/day-folds.ts (runningDays, liveRun), packages/core/src/streak.ts (runsEnding, streaks).
  • apps/renderer/src/lib/earned.ts, apps/renderer/src/lib/measure-text.ts, apps/renderer/src/hooks/useEarnedWatch.ts, apps/renderer/src/components/achievements/, apps/renderer/src/routes/achievements.index.tsx.

Zones · Disruptions · Getting Started · Derived Trackables · Chart · Glossary