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:
- internal only, present for a mechanism and never exposed
- presentable copy, exposed as an owned copy of the authoritative value
- presentable derived value, computed at extraction and never stored in a world
- 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
- Architecture for v0.1.0
- General architecture, section 5.9
- Product specification,
SPEC-FR-025,SPEC-FR-029 - ADR-001
- ADR-004
PBI-024,PBI-025,PBI-026