Product
A local-first tracker for the structures you define yourself.
What it is for
Most tracking apps decide what a workout, a reading session or a habit looks like. This one does not. You define the structure: a reading session with a book, a duration and notes; a workout that contains sets; anything with parts.
Entries are facts you logged. The app derives everything else from them and never pretends a view is a fact. Your data lives on your device, in a store that keeps every version of what you wrote.
It is built in Rust on a small engine, described below for readers who want the mechanism. The app itself is in development: the engine's contracts are issued, and the browser app is growing the editor that lets you define your own modules. It succeeds Logger, an earlier tracker of mine (see Earlier work).
One vocabulary for personal structure
Imagine defining a reading session with a book, a duration and several notes, or a workout containing sets. In the Product's engine, those domain names come from definitions. The core vocabulary is smaller: Type describes a value, Entry describes an occurrence, Write adds or changes canonical facts, and Read derives answers. An effect is an invocation binding over modules, not a fifth module kind[1, §1][2, §1]. Entries and their nested values use one graph representation[1, §2.1].
The example is an illustration of that model. A composition can contain Type values and nested Entry occurrences; the identity rule distinguishes them even when their visible data looks identical. That distinction lets the model represent both a reusable value and the fact that something happened again[1, §§1–2.1].
Content has an address; an occurrence has a history
Each immutable node has a content-derived address: a Merkle hash. A leaf's address incorporates its exact module version and value bytes; a composite incorporates its module version and ordered child references. Those references include the slot and ordinal distinctions, so the parent's address commits to its complete edge set. Children are addressed by hash rather than owned through a mutable parent pointer. This is the content-addressed directed acyclic graph, or DAG[1, §§2.1–2.2].
For Type values, equal content under the same module version can share one node. For Entry occurrences, the hash also includes a deterministic salt derived from the creating event. Logging the same reading duration twice therefore need not create two copies of the Type value, but it must create two distinct Entry occurrences[1, §2.1]. Replaying the same action preserves its identity inputs, and a retried request recovers its original receipt[3, §5.3, §7.2 items 1–2]; that is different from recording a new occurrence.
There is a third useful identifier: a stable Entry identity that spans corrections[1, §§1, 4.2]. Correcting an entry appends a new revision and a supersession link while preserving that stable identity. “Log this again” creates a fresh stable identity and does not supersede the original[1, §3.1]. A revision address, an occurrence-producing event and a stable identity answer different questions.
Suppose one duration in a larger entry is wrong. The correction writes a replacement value and new nodes along the ancestor path to the root, reusing unchanged subtrees. The previous graph remains intact. This saves copying unchanged canonical content; it does not promise that all index maintenance costs only the path's depth, since a new root's reachability index may examine its whole closure[1, §3.1].
History also makes disagreement explicit. Concurrent corrections remain separate terminal branches until a Write resolves them by naming the predecessors it supersedes. The contract does not pick a winner by arrival time or hash order[1, §3.2]. Deletion appends an explicit tombstone; an empty or dataless entry is not itself a deletion[1, §3.3]. A reader can distinguish a live entry, a tombstoned one, a conflict, incomplete evidence and an integrity error[1, §4.3].
A language whose work is bounded before it runs
User-defined structure extends to the interpretation of a leaf's bytes. A Type can describe fixed-width values, bounded text, enums, nested structs and bounded arrays. This format describes one leaf payload; a composite still consists of graph nodes and edges[2, §3.1]. Decoding produces either a complete typed value or an explicit error, never a partly decoded success or a silent repair of stored data[2, §3.2].
The module language also describes logic using typed expressions and finite step sequences. It supports bounded maps and folds, conditions, typed queries and outputs[2, §§2.1–2.2]. It has no user recursion, backward jumps, unbounded collections or arbitrary host callbacks. These restrictions make termination a property of the accepted language[2, §§1, 2.1, 8.6]. YAML is an authoring presentation of its typed syntax tree; the spelling of a YAML file is not the semantic identity of a module[2, §§1, 9].
Termination is only part of the contract. Before activation, the compiler derives worst-case work and checks it against both the definition's limit and the kernel ceiling. Body execution and non-body positions have separate accounting. A missing charge, bound or required access certificate causes refusal rather than a zero-cost assumption. Work units are protocol accounting units, not a latency benchmark[2, §§6, 8.1].
Reactive chains need their own bound. The kernel preserves an immutable root budget while causal depth increases; individual steps remain bounded and the active binding snapshot is finite. Depth alone does not bound total work tightly because a chain can branch. The contract accounts for the finite reaction tree and separately owned maintenance and preparation work. A runtime meter is a backstop for implementation defects, not the proof that an arbitrary program will terminate[2, §8.6].
This is a deliberate boundary on programmability: portable module logic uses the admitted language, rather than arbitrary downloaded host code. It does not mean that any declaration captures the user's intent, or that bounded work is necessarily fast enough for a particular device. Those are separate validation and measurement questions; numeric schedule approval still requires calibration[2, §§2.1, 8.1, 9].
Facts survive the views built from them
The store separates canonical facts from current-state indexes and projection caches. Only Write appends canonical mutations. Reads and bindings may persist derived results, but those results do not become another source of truth. After a complete rebuild using the same retained facts and semantic versions, the derived answers must preserve their meaning. If a required index is unavailable, the contract calls for an explicit unavailable result rather than an empty answer presented as complete[1, §8].
The same separation reaches the application boundary. The Rust domain and store and the bounded runtime have no runtime dependency on the UI framework. Native and WASM share the semantic implementation, with platform IO and transport in the host. Both interfaces run locally and offline. One host owns a writable store; other processes or tabs are clients of its interfaces. Browser clients use worker and WASM ports, while native bindings reach the native host[3, §§4–5.2].
The conformance corpus is part of the explanation
A contract is useful only if an implementation can be checked against it. The Product's interface contract keeps one shared corpus of JSON vectors: it drives serialization round-trips, deterministic stubs and real-binding conformance. A support manifest, checked before any fixture lookup, determines which operations are available. Unsupported operations return the named refusal; hypothetical success fixtures are excluded from current-stage lookup and earn no implementation credit[3, §7.1].
The required cases include two clients racing on the same head, a lost reply recovered through the original durable outcome, replay without a duplicate occurrence, expired snapshots, hash mismatches, concurrent revisions and rebuild equality[3, §7.2 items 1–6]. Common native and browser fixtures must carry identical serialized payloads[3, §7.2 item 10]. Real persistence and app journeys need evidence on the actual bindings; a convincing stub or an honest refusal does not prove that a feature works[3, §7.2, final paragraph].
This gives a reader two things to inspect together: the model's invariants and the examples that distinguish correct behavior from plausible substitutes[3, §§7.1–7.2]. For a module author, the same discipline includes exact-version format fixtures for round trips and expected failures, with old bytes read under their pinned version[2, §3.3].
Where the demo stands
This article is an engine-contract narrative. The first release bar is a real-browser journey in which you define an Entry module with fields and nested child Entry modules, log an entry with it and see it in the log. That journey is in work and has not yet passed, and no copy of the app is running anywhere for visitors. No browser qualification was run for this article.
This page presents the technical narrative before it links a qualified demo. User-defined rewards and assisted ingestion are further work, not capabilities claimed here. When the app is available, this page will say so, with the contracts and the evidence behind the claim.
Your data
The app is local-first: it runs in your browser and stores your entries in your browser's own storage, on your device. There is no account and no server holding your entries. While the app is in preview, a new version may reset that storage, so do not keep anything in it that you cannot lose. When the preview ends, the app will export your data in a documented format before any change that could lose it.
Try it
The preview will be shared by invitation while it is being qualified, and there is nothing to open yet. To ask for an invitation, write to me at ed@ed-li.com. The public preview follows the first release bar above: define your own Entry module, log it, and see it in the log.
Follow the work
New notes appear in the feed. The way this project is built, by many model sessions and one person deciding, is described on the Process page.
References
Superscript numbers in the text point to the design sheets below; a section within a sheet follows its number, as in [1, §2.1], and each entry lists the sections cited. The sheets are not yet published; these pins identify the exact versions the text was checked against.
- CANONICAL_STORE v6, issued . — §1 · §2.1 · §3.1 · §3.2 · §3.3 · §4.3 · §8SHA-256
ebfc5e24f5c179cb311a6ea0083f32b6eac548c824c205334e0d3e90a5d1023a - MODULE_LANGUAGE v8, issued . — §1 · §2.1 · §3.1 · §3.2 · §3.3 · §6 · §8.6SHA-256
197efc7352b844e30acc5a408b898b25f736c78d7ac458dc0ad5e770fd6870ee - PRODUCT_INTERFACES v11, issued . — §4 · §5.3 · §7.1 · §7.2SHA-256
c399dee453f5141d3a789a68f52a08094a8adb342320e00757f940bcbdd61883