Skip to content

ADR-0053: A published snapshot says what it shipped, in the shape the device stores

Status: Accepted Date: 2026-09-11

Decisions

The device's pull serves the documents the published snapshot covers, scoped by the snapshot row already read, and never every curated document in the store. "Which snapshot is live" is a marker, not the latest publishedAt. The link names rows and restates nothing they hold — no ordering, no per-document status — so a partial-Corpus mode (D-73) has no column to live in.

D-123 A snapshot carries the Curated Explanations that existed when it was published

Explanations are scoped by time, not by a link: those whose server-assigned createdAt is at or before the snapshot's publishedAt. The publisher's candidate reads every live explanation.

D-124 The server serialises the snapshot in the shape the device stores, and the device derives nothing from it

Every value a device renders per field — which spans a field holds, which caveats bear on it — is computed on the server. No caveat that bears on a field may be lost on the way: where more than one does, each reaches the device and renders verbatim. The contract between the two is asserted by a check that feeds the real serialiser's output to the real client, and fails if either side drifts.

Context

D-122 was the owner's ruling on blocker 10, built by ticket 33. The read side had lived with "latest publishedAt is live" as a TODO(spec); a publisher could not, because it must know which rows a snapshot covers. The link also makes a published version able to say what it shipped, a past snapshot inspectable, and a rollback expressible rather than an ordering accident. Before it, a half-selected document reached a device the moment it was written.

D-123 was the grill's Q5a. The link covers documents only, so explanations were read live on every pull: one authored after version N reached a device that believed it held N, and a rollback to N still served it. Documents needed a link because they have unfinished states — admitted but unreconciled, mid-selection. Explanations have none (both review columns are non-null since ticket 32), so "which shipped with N" is answered by "which existed when N was published", with no migration. createdAt is the server's clock; reviewedAt is curator-supplied and must not be the key. Exact only while nothing soft-deletes an explanation, and nothing can; the first path that retires one needs the link instead.

D-124 was found while writing ADR-0051 and ruled the same day (grill Q8b, handoff blocker 13). The API serialised a document-shaped payload — { version, publishedAt, documents[] } — and the client accepted only { snapshotVersion, chemicalRecords[], curatedExplanations }. A probe fed the client's sync a payload of exactly the server's shape and got Rejected: no device could load a Corpus from this server. Both suites were green, because each asserted its own side against its own fixtures and no test crossed between the repositories.

Closing the gap on the client would mean the device computing per-field caveats from document-level Restrictions and Reconciliations — the client-side derivation the API's own invariants forbid: the device's copy is a verbatim replica of server-curated content. So the server changes. The lossless clause is written into the ruling because the obvious mapping loses data: the client's model held one caveat per field, and a field can have several Reconciliations bearing on its spans. Joining them into one string composes text; keeping one drops a Restriction's resolution. Either is a D-44 failure at the last step before a Handler reads it.

Decision

A snapshot names the documents it covers, carries the explanations that existed when it was published, and arrives in the shape the device stores with every per-field value already decided.

Rejected options

  • Infer the live snapshot from the latest publishedAt — rejected. It cannot say what a version contained, and makes rollback an accident of ordering.
  • A link table for explanations now — rejected. A migration for a property the time scope already gives, while nothing can retire an explanation.
  • Leave explanations unversioned — rejected. A version would describe its documents only, and say so nowhere.
  • The client adapts the server's shape — rejected. Per-field caveats computed on the device are display-time safety logic.
  • Join several caveats into one string, or keep the first — rejected, as above.

This ruling may not be re-decided

If a change contradicts this ADR: stop and raise it. Do not implement over it.

Specifically: do not serve the device every curated document or every explanation; do not add a per-document status to the snapshot link; do not move any per-field computation onto the device; and do not remove the check that crosses the two repositories.

Consequences

What becomes true. A device can load the Corpus the server publishes, and a version describes everything it shipped. The seam between the repositories is under a gate for the first time.

What it costs. The serialiser grows the per-field assembly the device used to be assumed to have, and the checker that gates publishing reads the new shape. The snapshot link was a fifth unapplied migration; the label key since made it six.

What remains open. A version read and a snapshot pull are still two reads, so a client asking for the version and then pulling across a publish gets a later snapshot than it asked about. The client takes its sentinel from the payload's own version, which keeps the tear harmless — and that stays true only while it does.

Coverage

No upstream finding lands here. All three rulings came from building the publisher and reading the two sides of the sync (D-104).