Datler treats a trackable’s entries the way a program treats an array of objects. The trackable’s definition, its fields and their kinds, says what properties each entry has; an entry is an object with those properties; and reading a trackable is list work over those objects: keep some, map each to a value along a path, group by day, reduce. The same definition generates everything else that has to know an entry’s shape, so a trackable has one description rather than one per surface.

The stored definition is Datler’s own schema language, kept in the trackable’s value_schema and edited by the trackable form. A Zod schema cannot be stored or edited, so Zod is compiled from the definition rather than being the definition: it is one of the things the definition generates.

flowchart LR
  defn["Definition (value_schema)"] --> shape["shapeOf: the properties"]
  shape --> check["Write check (Zod)"]
  shape --> form["Entry forms"]
  shape --> labels["Labels, columns, CSV"]
  row["Stored row"] -- entryObject --> obj["Entry object"]
  obj -- storedOf --> row
  obj --> reading["Readings: filter, map, group, reduce"]

The entry object

entryObject(trackable, row) in packages/core/src/entry-object.ts turns a stored entry into an object, and storedOf is its inverse, writing an object back as the columns it is stored in (value_number, value_text, value_json). A form edits the object and writes it through storedOf.

  • Its own properties are id, at (when it happened), recorded (when it was written down), note and nothing. No field may take one of these names (ENTRY_KEYS).
  • Its value’s properties sit beside them at the top level, so a path reads duration, book or pages.from. A yes/no reads as true or false, a choice as its code, a Category as its current values (through categoryNow, so a merged or deleted value reads as what it became), a progress field as { from, to }.
  • The trackable’s own value is the value property, read from its column; its other fields come from value_json.
  • An interval gives the entry start and end, its other fields beside them, and duration, which is empty while either end is missing.
  • An item field that carries the entry’s amount gives item (the item’s id), quantity, unit and base. An entry stored when its trackable was a whole reference, with its item under entity_id, reads the same way.
  • Computed properties are getters, not inputs. duration is one; what an amount of an item contained is another: handed contents, entryObject adds a getter for each value asked for, so Diet’s calories is the food’s calories per gram times the grams. A form skips computed properties; a Reading reaches them like any other.
  • A Nothing has no value properties, only its own.

shapeOf(trackable) lists the properties a form, a label or a column walks: the entry’s at and note, then its value’s, each with its kind (number, boolean, coded, enum, text, time, reference, category, progress, instant, duration), the computed ones marked, and each field’s declaration attached.

entryObject is the parser for every reader that goes through the shape. An interval’s ends have their own one parser, parseInterval, which entryObject uses and which a reader that wants only the ends calls directly, in core (interval.ts, hold.ts, routines.ts and the seeds) and in the renderer’s routine, schedule, hold, issue, timeline and day-visualization code.

Generated from the definition

The write check. storedEntrySchema in packages/core/src/entry-check.ts compiles a Zod schema from the definition: each kind’s encoding and column, a number’s limits in base units and whether it must be whole, a choice’s codes, a strict list’s options, a Category’s values, an interval’s instants. A tag or a derived trackable takes no entries. entryProblem runs it on every write: the entry repository’s create, bulk create, update and remap, which seeds go through too, and sync’s apply, where an entry its trackable does not accept yet waits, as a change whose parent has not arrived does, until the change that makes it valid arrives. Every field is optional at the boundary, so a running interval is written with only what it has, and a required field left empty is written too and reported as an issue rather than refused (Issues). A catalog item’s values are held to its catalog’s attributes the same way, by item-check.ts.

Labels, columns and export. entryLabel in apps/renderer/src/lib/labels.ts walks the shape with one formatter per kind of property (propertyText): a choice by its label, a Category by its values’ labels, an item by its name, a number in the unit it is shown in, a yes/no by its words. The entries table gives each property a column (a record’s fields; an interval’s start, end and duration; an item and its amount), and a cell shows the value without the field’s name, which its header carries. The CSV export’s Value column is the entry’s label, with its note beside it and the raw stored values after. The raw columns hold fields under their keys, not their labels; only the Value column is in words.

Forms. EntryFields renders a widget for each field by its kind, and the bespoke widgets register for their kinds beside the plain ones: an interval’s start-to-end row, the progress box, the item picker with quick-add, a moment’s time. A trackable’s own value goes through ValueEditor’s widgets by kind, and the entry panel’s bodies are registered by kind in the same way (Entry Panel). Where a field starts before anything is typed is declared in apps/renderer/src/components/entries/field-defaults.ts and run by useFieldValues: a position continues from where the last entry of its item stopped, and a fill sets a number from the trackable’s capture. A trackable’s own number starts from numberSeed. A number box reads a fraction or a mixed number as typed (“1/2”, “1 1/2”, parseNumberText), and a box holding what reads as no number hands on none, so a form never logs a number it does not show.

Templates fill any form. A template’s line is values for a trackable’s properties. Picking a one-line template fills the form it is picked in (a food’s item, amount and unit; a record’s fields; a book on Read) and leaves the entry’s start and stop to the form; a template of several lines writes several entries. A line need not name an item (template_line.entity_id is nullable), so a trackable with no item field, such as a blood pressure, takes templates too (Templates).

The schema of fields

packages/core/src/field-kinds.ts declares the kinds of field a form offers (Number, Text, Tags, Boolean, Category, Reference, Progress, Interval, Options, List) and the settings each takes: a measure or a free unit, whole numbers, limits, step, a slider between the limits, a Category’s values, the catalog a reference picks from and whether it logs an amount of the item, where a progress field ends, an interval’s words for its ends, how long it stays open before it is stale and which end dates the entry, and, for a catalog’s attribute, whether an absent value means none. Any field can be required. A field stored as a kind no form offers opens as Text and keeps its schema until its kind is changed. One FieldsEditor edits a trackable’s fields in the trackable form and a catalog’s attributes in the catalog form, so a kind and its settings are declared once for both. Each form names the kinds it offers: a catalog’s attribute is a Number, Text, Tags, Category or Boolean, and a List is offered only where a trackable already has one.

  • A field is named by its label. Its key, which entries hold its values under, is made once and never shown (Value Types). Every field needs a name, and no two of a trackable’s fields share one (fieldLabelsProblem: “Two fields are both called Fasted”). The trackable’s own value field is named by the trackable.
  • A number field with a measure is stored in the measure’s base unit, shown and typed in the person’s unit, and keeps its limits in the unit they were typed in. A stored field’s measure is fixed, since its logged numbers are in it.
  • A Category value merged or deleted is kept as an alias rather than rewritten into entries: CategoryValueSchema.retired maps each id the Category does not offer to what it became (another value, or none), flattened to one step, and a new value never reuses an old id.

Reading as a list

A Reading is the list half of the model: whole-list steps (coalesce, filter), a day’s pick (maxBy, rest), map along a path or arithmetic over paths, group by day and, where asked, by Event (byEvent), and reduce by one table of reduces, then fold the days. Every surface that shows a trackable reads it this way, and the derived evaluator works out every day in memory over entry objects. The steps, the reduces, how a day is answered and how a Reading is named are in Readings.

Readings · Value Types · Entry · Derived Trackables · Trackable Authoring Flow · Templates · Intervals