A derived trackable’s formula is made in the trackable form (Trackable Authoring Flow): choosing Derived under Computation shows the formula builder (DerivedBuilder in apps/renderer/src/components/trackables/TrackableForm.tsx). The builder writes the stored formula tree directly; there is no second representation of a formula. It offers templates rather than free composition, because a handful of shapes cover what people build and each can be shown and edited with ordinary controls.

The templates

The Formula control offers four:

  • Any: “Yes on days anything was logged for…” a trackable or a tag, a tag shown as “Everything tagged Fitness” and written as a members ref, anything else as a trackable ref. A tag’s own summary is a derived member nominated under Summarised by, not a choice here: a derived trackable holds no members, so a bare members ref of its own reads nothing, and one stored before the seed named its tag opens with nothing picked. The result is a yes/no.
  • Aggregate: a reduce of one trackable (a tag is not offered), “Sum of Water”. Over a trackable whose entries point at catalog items it can read one number of theirs instead: Daily Calories is Sum of Diet reading Calories. Over an interval it can read its duration or either end under the interval’s own words for them (Sleep reads Bedtime or Waketime), and over another trackable logged with a time its value or the time logged; a trackable that keeps its day alone, or a derived one, offers no such choice. The reduces offered are those the source takes, and a clock reading offers the ones that keep a time a time (CLOCK_REDUCES: Mean, Median, Min for the earliest, Max for the latest, First and Last).
  • Combine: two reduced trackables joined by + − × ÷, each optionally multiplied by a constant. Hydration is (Water × 250000) ÷ Weight: millilitres per glass, in the base unit values are stored in, so the multiplier is asked for rather than guessed.
  • Conditions: a list of tests a day has to pass, all of them. Each row is a trackable and what has to be true of it, “was logged” or “was in” one of its zones; the choices are built from that trackable’s own zones, so a condition on a zone it does not have cannot be written, and the zone’s own label is the wording. It is written as a left-folded mul of Any and zone nodes, and the result is a yes/no; a day that only some rows have a reading for has no value rather than a No.

Which entries count narrows Any and Aggregate before they reduce:

  • a time of day, “only between” two clock times, for a trackable logged with a time: “Ate before 17:00” is Any of Diet between 04:00 and 17:00;
  • the items’ Category, for entries that point at catalog items: “only items whose” Category holds (or holds none of) the values picked, so Calories from sweets is Sum of Diet reading Calories, only items whose Food type is Sweets;
  • the items themselves, for entries that name a catalog item, with an amount of it or not: any item, only these, or all but these, picked one by one (ItemNarrow), so Cardio’s distance can read Runs and Jogs and leave Walks out;
  • a Routine, its entries inside one or outside all of them (Events);
  • for an interval, which of a day’s entries: every one, the longest, the longest of at least N hours, the one running at a clock time, or all but the longest, with pieces less than N minutes apart joined first. A view of an interval, such as Main sleep, offers its pick or every other one.

Each of these keeps or drops entries rather than days, so a day of other food reads 0, or No, while a day with no entry at all stays blank. A chart narrows the same way, with the same controls (Narrowings in components/trackables/Narrowings.tsx): a series of the editor for several, and a trackable’s own Chart, Calendar and Distribution (Visualizations), so a narrowed chart needs no derived trackable made for it. They are written as the list steps Readings describes (refOfChoices). A Category or Routine narrow whose field the catalog dropped, or whose routine is no longer one, still applies as written, so the form says it is gone and offers Clear rather than showing another field or routine in its place, or nothing.

A rolling window

A rolling window is a modifier on any of the four, not a fifth template: “the seven-day mean of Sum of Run” is the Aggregate with a window around it. It is a checkbox below the formula with its own reduce, a span, days or readings, and “Wait until the window is full”. parseDerived takes the window off before it matches a template, and buildDerivedSpec wraps whatever the template built, so four templates stay four rather than eight.

Opening a formula again

Editing reads the stored formula back into the template that shows it (parseDerived), and saving writes it again (buildDerivedSpec). Reading and writing back is the identity for every formula, byte for byte, so opening a derived trackable to change its colour can never change its formula; derived-round-trip.test.ts pins it.

  • A template is a view of the tree, not a kind of formula. The parser picks the simplest view that gives what is stored, so a one-row “was logged” condition list reopens as Any. Conditions are tried before Combine, since a mul of conditions is also an arithmetic node, and a mul of two aggregates stays a Combine because a condition row must be an Any or a zone.
  • A template claims a formula only if it can show all of it: Aggregate takes a ref whose steps and map its controls can show; a Combine operand takes only a plain trackable ref, or one multiplied by a constant on the right, the only form the builder writes. mul(const, ref) means the same and is left alone, since reading it would write back a different tree.
  • Anything else is kept whole (opaque). The builder is withheld rather than disabled, because any template it drew would be what a save wrote: it says the formula was not written by the builder, shows it as JSON (capped and scrollable), and offers Replace the formula as a deliberate act. The rest of the form stays editable. A new derived trackable starts as an empty Aggregate, not as an unreadable formula.

The builder cannot write, and so shows read-only: arithmetic deeper than one Combine, per-entry arithmetic over a record’s fields (Daily Volume, sets × reps × weight, which its module writes), an explicit choice of a tag’s direct members rather than its whole subtree, and any mix of templates.

What it produces

The builder names what the formula produces, so the value reads in the terms of what it aggregates (aggregateSchema):

  • Any, Conditions, and an Aggregate read by Any are a yes/no.
  • An Aggregate keeps its source’s dimension or unit through every reduce but Count: “Max of Weight” reads 78.6 kg, not grams. An interval’s length is a duration. A clock reading, or a moment’s time, stays a time under the reduces that keep one. A Sum or a Mean of a yes/no, and any Count, is a plain number.
  • An Aggregate reading an item’s number keeps the trackable’s own unit while the number’s dimension is unchanged (Daily Alcohol in standard drinks).
  • A rolling window is an aggregate of what it wraps, by the same rule.
  • Combine is a plain number, since the unit of mass over mass or volume times a count is unit algebra the engine does not do.

This names the output; the engine itself reads stored base values. In derived mode the Zones section reads its unit from the formula’s output, and a time output turns its bounds into clock times placed in the reading’s frame (Time of Day).

Cycles are refused by the trackable repository when the formula is saved (Derived Trackables), so the builder needs no guard of its own. Derived values are a day’s; there is no formula over hours.

Derived Trackables · Trackable Authoring Flow · Readings · Tags · Value Types