Skip to content

Universe Write Contract — Design

Five verbs live in every universe: create, update, touch, affirm, delete. What differs between a subject that keeps an occurrence log and one that does not is the act — and the difference is carried by a type parameter rather than by a default value.

“Act” here is the entry written to a subject’s occurrence log, not the domain’s Actions — the actuations and observations that behavior descriptions are built from. The two are unrelated and the word is shared.

One write base carries the five verbs, parameterized on the act A, over a separate read base. It has two bindings:

  • Nothing? — a subject with no log. The act is null, and a convenience overload means basic callers keep today’s signatures exactly.
  • Occurrence<K, R> — a subject with a log. A is non-nullable here, so an act-less call has no overload to bind to.

That last sentence is the whole point. The illegal call does not fail a guard; it fails to compile, because nothing exists for it to resolve against.

The table’s declaration selects the binding. OccurrenceUniverse already carries a where TBL : UniverseTable, TBL : OccurrenceColumns bound, so the shape follows from what the table declares. The mechanism is in the tree today, applied to the universe rather than to the write signatures.

Each verb takes a carrier — Create, Update, Touch, Delete, Affirm — each parameterized on A so that each degrades the same way, with convenience bindings as default implementations so existing call sites compile unchanged.

The reason is not symmetry. update is the only verb that can carry idempotency or effectiveTimeOrdering today, and a delete writes a tombstone successor row with no way to say how it should be ordered. Giving each verb its own carrier lets each say what it can actually mean.

CarrierCarriesNotes
Createpayload, metadata, asOf, actNo ordering — there is no predecessor
Updatepayload, metadata, asOf, act, idempotency, effective-time orderingSerializable; crosses the wire
ToucheId, asOf, act, idempotency, effective-time orderingToday it can carry neither ordering field
Deleteorigin eId, metadata, asOf, effective-time orderingAct fixed to the retire kind
AffirmeId, affirmed record id, asOfAct fixed to the affirm kind; the staleness guard is itself the monotonic condition

delete and affirm supply their own act; create, update and touch take one.

So delete needs no act parameter — the universe supplies it — and retireKind sits on the interface for the same reason the affirm kind does: the universe must know its own act in order to fix it.

retireKind is mandatory on every occurrence universe. An earlier proposal put it on a sub-interface so that a log with no delete-shaped kind could have no delete at all; that proposal is withdrawn, along with the RetirableOccurrenceUniverse it needed. The decision log records why, because the way it failed is more useful than the proposal.

retireKind names which act delete fixes. It does not define what counts as a retire. A log may have several retire-shaped kinds, and the model’s lifecycle family has no notion of the retire kind. The consequence matters more than the rule: terminality attaches to the act, not to the verb — the guard forbidding commits after a retire keys on the kind being retire-shaped, however it was written.

affirm on a basic universe is touch with a staleness guard.

With no log, “I checked and it is unchanged” has nowhere to be recorded as an act, so it is a re-stamp — and a re-stamp on a subject with no log is not a false log entry. Basic rows still carry record ids, so a stale claim can still be refused, which is why the guard is kept rather than making it a bare alias: an affirmation that cannot be stale asserts nothing.

The change reaches below the universe interfaces. In scope: OccurrenceTable and ScopedOccurrenceTable (no child variant), with Persistence.kt and AbstractUniverse.kt split along the same seam, and both throwing branches of fillOccurrence deleted rather than shrunk.

No migrations are touched and no second flag day is needed. The LargeSourceFile suppression comes off as a by-product.

This removes two of the three runtime guards, not three. The one that stays is the subject of Illegal Call Classes.

Measured rather than estimated: zero of 1,788 call sites in operations hold a bare universe reference.

DataAuthorityService is the only consumer generic over every subject, and it is the reason the shape is one parameterized base rather than two disjoint interfaces. Under two interfaces it would have to be split, or made generic over which interface it holds; under one base it is generic over A, and that is all. The entire generic-consumer cost of the alternative would have landed on the single class least able to absorb it.