ADR-005: The snapshot is a derived contract rather than a world mirror

Date: 2026-07-19

Status: Accepted

Context

PBI-024 defines what a presentation consumer may observe about a world. The world is a public structure, so the cheapest contract would expose it directly or copy it field for field. Both make the snapshot grow with the Core.

The world already holds state that no consumer should see. activity_since_ms and expression_since_ms exist so that the transition tables can measure a dwell. They are the representation of a mechanism, not a fact about the pet. update_count records how the host divided its time rather than anything the pet did. Two worlds that lived the same sixty seconds in one update and in ten updates are the same pet, and a consumer that could tell them apart would be reading the host rather than the world.

Section 8.3 of the version architecture already forbids a mutable internal pointer in a snapshot. That rules out one failure. It does not say which fields belong there.

Decision

PetSnapshot is a derived presentation contract. It is not a mirror of PetWorld or PetEntity, and no rule requires a world field to have a snapshot counterpart.

v0.1.0 exposes the copied pet name, its length, the visible activity, the visible expression, and elapsed_time_ms. The name is copied into snapshot-owned storage of PET_NAME_CAPACITY, so no pointer into a world escapes and no consumer can reach authoritative state.

elapsed_time_ms is included and update_count is excluded. Lived world time is a fact about the pet that a frontend can present. The number of updates is a property of the host loop, and excluding it is what makes equal worlds produce byte-equal snapshots however the host divided its time.

Every field added to a world from now on is classified as one of four things, and the classification is recorded where the field is added:

  1. internal only, present for a mechanism and never exposed
  2. presentable copy, exposed as an owned copy of the authoritative value
  3. presentable derived value, computed at extraction and never stored in a world
  4. deferred, presentable in principle but out of scope for the current release

The default is internal only. A field reaches a snapshot because someone decided it is presentable, not because it exists.

Consequences

Adding authoritative state is no longer a change to the public presentation contract. Mood, memories, traits, and progression can arrive in the world without the snapshot growing, which is what keeps a future frontend from depending on the shape of Core storage.

A snapshot is a value with no validity marker and no link to its world. It records the world as it was, and a later update or action is observable only through a new request. Its lifetime is the lifetime of the storage the caller declared. This is simpler to document than a handle, and it is the reason mutation isolation is a property of the type rather than of a promise.

Two things must move together when a presentable field arrives. The extraction must write every byte of its destination and the snapshot must carry no padding, or the byte equality that the determinism tests assert stops being meaningful. A test asserts the absence of padding for that reason.

The price is duplication. The name exists twice while a snapshot is alive, and PET_NAME_CAPACITY bytes are copied on every request even though most of them are usually zero. At 56 bytes for a snapshot and 32 bytes for the copy this is not worth optimising, and the copy is what makes the isolation unconditional.

update_count being absent means the headless host cannot show its own loop through a snapshot. That is correct. The host owns its loop and already knows how many times it called the Core.

Alternatives considered

Return a const PetWorld *. Zero cost, and the world is already a public type. Rejected because const is removable in C, the caller would depend on the layout of authoritative storage, and every internal field including both region timers would become part of the presentation contract.

Copy PetWorld into the snapshot. Trivially complete and always in sync. Rejected for the same reason: it makes every future world field public presentation state by default, which is exactly the coupling this decision exists to prevent.

Expose a const char * to the name inside the world. Avoids the copy and the 32 bytes. Rejected because the pointer outlives nothing safely. A reset, a reuse of the storage, or an ordinary assignment leaves the snapshot pointing at a different pet, and the snapshot stops being a value.

Include update_count. Useful for diagnostics and already authoritative. Rejected because it is host cadence rather than product meaning, and it would make two identical pets snapshot differently. A host that wants it reads its own counter.

Include the region timers as remaining dwell. A frontend could animate a settle. Rejected because no supported behaviour needs it in v0.1.0, and because a derived remaining-time value would encode the guard constants into the public contract, so changing a dwell would become a contract change.

A pet_snapshot_is_valid marker. Would let a consumer tell an extracted snapshot from cleared storage. Rejected because a snapshot has no lifecycle to be wrong about. Extraction either returns PET_OK and writes everything or returns a status and writes nothing, so the return value already carries the answer.

References