= Minerva for Downstream Consumers
:toc: macro
:toclevels: 3
_Supported surfaces and caller-gated capabilities._
This guide is the consumer companion to xref:../DOCS.adoc[`DOCS.adoc`]. It
identifies stable, changing, and deferred capabilities. It also states the
caller evidence that can activate a deferred capability. The recorded
consumers are `plethos` and `pinax`, `ethos`, `stokes`, `alma`, and `polis`.
This document is a guide, not a contract. The capability PRDs, module notes,
code, and tests are authoritative. If this guide disagrees with those sources,
this guide is stale.
toc::[]
== How to read this
Minerva `0.1.0` contained only `kairos`. Version `0.2.0` adds the `metis`
decision layer, `Kairos::to_bytes`, `Clock::observe`, the gate seam, the
`Dot` identity type, and the recorded-membership implementation.
The version 2 membership frames are dormant siblings of the frozen version 1
family. A machine without admissions emits version 1. A version 1 reader
refuses a complete version 2 frame. Only a joint registry decision activates
the version 2 family. A release does not activate it.
Use `minerva = "0.2"` for normal SemVer resolution. Use
`minerva = "=0.2.0"` when you require one exact artifact. Before the owner
completes a publication, use a path dependency or an immutable Git revision.
No external consumer requires backward compatibility by default. These rules
apply:
* *Additive* changes can add a method, type, or default-preserving generic
parameter. Consumers adopt them when needed.
* *Breaking* changes are permitted before external adoption. A breaking change
must be deliberate, consistent across its boundary, and recorded with a
migration note.
* *Deferred* capabilities have an accepted design but no implementation. A
caller request must identify the application boundary that activates one.
Status legend, used throughout:
[horizontal]
*Stable*:: Shipped and tested. Build on it.
*Deferred*:: Designed, not built. Caller-gated. Request to pull.
*Hazard*:: A sharp edge in the stable surface to design around now.
== The stable surface
The following sections summarize the supported layers. Signatures are
abbreviated. See the module notes and rustdoc for complete signatures.
=== `kairos` (the stamp and the clock)
[horizontal]
`Kairos`:: A 128-bit causal stamp, `(physical, logical, kairotic, station_id)`, whose derived
`Ord` _is_ the causal-then-spatial total order. `to_bytes` / `from_bytes` give a fixed
17-byte big-endian frame that sorts causally; `KAIROS_WIRE_LEN` is public so you can split an
envelope. The `kairotic` tag is yours: a caller-defined qualitative dimension ranked above
`station_id`. A constant tag (`kairotic = 0`) neutralizes it if you do not want it
(`pinax` / `plethos` ADR-0009 takes this opt-out). A varying tag is a caller protocol,
not merely a number: define its admitted rank space, equal-tag meaning, neutral opt-out,
and unknown-value refusal. Stokes' first implemented admission-side scheme maps nonzero
command classes `1..=4` in precedence order, rejects unknown tags, and validates each
payload class against its stamp before admission; its production emitter is not built yet.
See xref:glossary.adoc[the glossary's kairotic-scheme entry].
`Clock<TS: TimeSource>`:: Lock-free HLC clock over one `AtomicU128`, owning
its source by concrete type and allocating nothing for that source.
`DynClock` is the explicit `Box<dyn TimeSource>` form; `LocalClock<TS>` and
`DynLocalClock` are the single-threaded twins. `now(kairotic)` mints (send), `after(prev)`
fuses receive-then-send, `observe(remote)` is the pure receive rule (advance past a remote
stamp without minting), and `try_observe(remote, max_forward_skew)` is its bounded,
trust-aware form (reject a too-far-future remote; see below). Generation is infallible; only
`Clock::new`, `Clock::with_default_config`, `LocalClock::new`, and
`LocalClock::with_warning_threshold` return `Result<_, InvalidStationId>`
when `station_id` is zero. `Clock` is `Send + Sync` and can be shared as
`Arc<Clock<TS>>`.
`TimeSource` / `Backoff`:: The injected effect seams. `Backoff` is time-free (a bounded spin
count, not a measured delay).
=== `metis` (the decision layer)
[horizontal]
`VersionVector`:: The lattice that recovers what a scalar stamp cannot: _ordered
versus concurrent_. `get` / `increment` / `observe` / `merge` (the join) / `meet` (the greatest
lower bound, S78) / `iter`, plus
`PartialOrd` (never `Ord`, the order is genuinely partial). `restrict(roster)` (S89) reads
the vector relative to a sub-roster, and `try_glue(&other, overlap)` is the exact merge of
partial views, refusing overlap disagreement with a typed `Disagreement` (see the relative
reads section below). `difference(&other)` (S95) is the owed set, the least vector whose
merge into the other side covers this one (see the difference reads section below).
Canonical wire frame through
`encoded_len` / `to_bytes` / `from_bytes` / `from_prefix`, with exact
allocation-free length preflight and decode validating canonical form and
bounding allocation on untrusted input, with a `DecodeError`.
`Event<T, D = VersionVector>`:: A stamped event `(stamp, deps, payload)`. `D` is the gate's
dependency descriptor; the default is a `VersionVector` (causal), so `Event<T>` still names
the causal event.
`Gate` (`Causal`, `Fifo`):: The buffer's stability rule, the seam the delivery machine runs
over. `Causal` is the happens-before `VersionVector` rule; `Fifo` is the scalar per-source
watermark (`Dep = u64`). They are *peers*; neither is a silent default.
`Released<T, G>`:: An affine, gate-branded witness minted only after `Ideal<T, G>` removes a ready
pending occurrence and advances `G`. Borrow its event data for audit or downstream policy;
consume it with `into_event` or `into_payload` only when deliberately erasing provenance. It is
neither authority nor durable evidence.
`Ideal<T, G: Gate>` (`CausalIdeal<T>`, `FifoIdeal<T>`):: The delivery buffer. `insert` an
event, drain with `pop_ready` until `None`; release respects causality and linearizes the
concurrent frontier by `Kairos`. `pop_ready_event` is the same release handing back a
`Released<T, G>` over the whole `Event` (`stamp` and `deps`), not just the payload, so you can tell a causally-forced release
(`a -> b`) from a `Kairos` tiebreak between concurrent events; `pop_ready` is its payload
projection. `frontier()` exposes that frontier read-only (the ready antichain under `Causal` /
`Fifo`). `pop_ready_event_where` filters the live ready set and atomically releases its
`Kairos`-minimal eligible member; a refusal advances nothing. `pending_len()` for backlog
visibility; `delivered()` (causal) / `progress()` (any gate) for per-source progress.
`Producer<C = DynClock>`:: The send half. `C` is a sealed `ClockCarrier`:
an owned `Clock<TS>`, `&Clock<TS>`, or `Arc<Clock<TS>>`.
`produce(kairotic, payload)` mints a causal `Event`;
`observe(&event)` learns from a received one (receipt, not application delivery: the buffer's
`pop_ready` is delivery); `try_observe(&event, max_forward_skew)` is the bounded, all-or-nothing
receipt (the `Clock::try_observe` skew bound lifted over the knowledge merge). Share the
carrier when the station has other stamping sites (see the shared-clock
section below). Composes with an `Ideal` rather than owning one, so you wire the two: feed each
arrival to your producer (to observe) and your buffer (to reorder).
`Stability`:: The causal-stability tracker (S78): a fixed, caller-declared roster of stations,
`report(station, &delivered)` join-folding each member's delivered cut (non-roster reporters
refused with `UnknownStation`), and `watermark()` the n-ary `meet`: the greatest cut every
member has passed, hence safe to forget. Mechanism only; what you reclaim below it, and when
rosters change, is yours (see the causal-stability section below).
`DotSet`:: The exact have-set (S83, PRD 0012): records per station exactly which dots (1-based
event indexes; a 0-based sequence maps `dot = sequence + 1`) have been received, however
disordered the arrival. `insert` / `contains` / `merge` (the union join) / `from_witnessed` /
`restrict` (S89, the sub-roster read, exact against every fold here), and the
two cuts that bracket disordered receipt: `floor()`, the greatest gap-free prefix, a genuine
cut by construction and the honest `Stability` report; `high_water()`, the per-station
maximum, a liveness bound only (over holes it is an upper bound of a cut, never a stability
report). `holes()` names the missing dots (the repair fetch list), with `holes_for(station)`
its per-station view and `hole_count()` the repair-debt norm (both S98, PRD 0016);
`difference(&other)` (S95) enumerates the exact dots you hold that a peer lacks (the serve
list, the other direction of the same read), `intersect(&other)` (S96) is the meet (what both
hold), `exceptions_len()` is the reorder-exposure count, and `exceptions()` walks those sparse
dots in canonical order. `station_count`, `station`, and `stations` expose
allocation-free `DotSetStation` row summaries carrying station, floor, high water, and forward
gap. To ship a have-set across a
boundary, `to_bytes` / `from_bytes` / `from_prefix` is the canonical run-length frame (S98,
PRD 0016; a v1 byte-identity contract like the vector frame, decode validating canonical form
and bounding allocation on untrusted input with a `HaveSetDecodeError`). An embedding
protocol with a validated exception ceiling uses `HaveSetDecodeBudget` with
`from_bytes_with_budget` or `from_prefix_with_budget` (S242); the unqualified
decoders retain their input-length secure default; `encoded_len()` preflights the exact
canonical size without allocating. Polis S2 is the first
disordered-receipt adopter: its fact possession, bounded serve list, and paged holes use this
surface directly, and Polis S3 (`5d10141`) embeds v1 in its authenticated
possession protocol. That downstream protocol pins byte identity under R-8;
changing the inner frame requires a coordinated Polis protocol version. As of S96 `DotSet`
is also the causal *context* type and the simplest causal store (see the causal store section
below).
`Dotted<S: DotStore>` / `DotMap<K, S>`:: The causal store algebra (S96, PRD 0015): a store
of dot-tagged content bracketed by the context of everything ever seen, whose `merge`
(the only write) honors removals without tombstones and keeps unseen concurrent writes.
`DotStore` is an open axis like `Gate`: Minerva ships `DotSet`, `DotFun<V>`,
`DotMap<K, S>`, and its structured stores; implement it for a different shape. See the
causal store section below.
== Composing gates: the algebra
*Status: Stable (S237). The shipped leaf gates are `Causal` and `Fifo`; `And<A, B>` is the
shipped product over `ReleaseDrivenGate` components. `MapDep` remains caller-side until a
carried-dependency adapter has a renter.*
The gate axis has structure, not just a catalog. It is closed under two combinators and provably
not under a third, recorded in xref:metis-gate-algebra.adoc[the gate-algebra note]:
[horizontal]
`And<A, B>` (product):: Deliver only when *both* rules say stable. `Dep = (A::Dep, B::Dep)`,
`deliverable` is conjunction, `advance` is componentwise, `stale` is *either*. Sound and
contract-preserving when both components implement `ReleaseDrivenGate`: release must be their
only progress authority. This is how you require "causally complete *and* endorsed" in one rule
(`And<Causal, Participation>`). Progress is `AndProgress<A::Progress, B::Progress>`, whose
`PartialOrd` is componentwise; use `left()` / `right()` to read components and `into_parts()`
to consume them. `EpochGate` is deliberately not composable: `adopt_epoch` is separate checked
authority, for which a product-aware API does not exist.
`MapDep<G, P>` (reindex):: Reuse a gate `G` over a *different carried dependency* through a pure
projection `P: Wire -> G::Dep`. Adapts the dependency only, never the progress.
`Or<A, B>`:: A deliberate *non-goal*, twice over. A general `Or` breaks stable up-closure (let one
component go stale while the other still waits, and the composite falls back to *waiting*,
stranding the event), and even one that did not would break the implicit dynamic contract: on a
disjunctive release only one component fired, so advancing both corrupts a stateful progress and
advancing one fractures it. Do not build it.
The load-bearing rules when you author your own gate. First, the static laws, terminal `stale`,
disjoint from `deliverable`, *stable up-closure* (`deliverable || stale` is itself up-closed, which
does **not** follow from the other two), and monotone `advance`, are necessary. Check them with
`check_gate_laws`, which tests all four.
Second, they are not sufficient. `advance` must fold only a *released* event, so `progress` stays
the faithful record of what your gate released. A monotone fold (a contributor set, a delivered
count, a watermark) satisfies this; a rule that advances progress for an event it did not gate does
not.
Run caller-generated comparable progress cases through
`check_gate_laws::<YourGate>(&lower, &upper, sender, &dep)`. The dependency-free checker reports
descending or incomparable inputs separately from monotone-advance, disjointness,
terminal-stale, and stable-up-closure violations, so it fits proptest, quickcheck, fuzz corpora, or
deterministic tables without owning a generator. It cannot prove that a combinator advances only a
component that admitted the release; keep that dynamic proof at the combinator boundary.
Implement `ReleaseDrivenGate` only when those releases are the gate's complete progress
authority. A gate opened by an external witness can still implement `Gate`, but must not opt into
product composition without a checked product-aware transition.
That rule has a consumption-side twin, for any threshold-shaped gate (one whose `deliverable`
fires when a tally crosses a bound): the machine releases a gated event at the *moment its
threshold crosses*, so a `Progress`-derived tally read at release time is the minimal crossing
prefix: every release looks threshold-weight, and a ranking built on release-time tallies
silently degenerates to your tiebreak. If you rank releases by accumulated weight, drain the
`Ideal` fully first (`pop_ready` to `None`, delivering the trailing contributions), then read;
fully drained, the tally is again a deterministic function of the event multiset. This is not a
buffer defect but the release-at-crossing semantics any holdback machine owes; `ethos` hit it in
testing (see its recipe below).
== Bounded delivery: `Ideal::try_insert` and causal-safe replacement
*Status: Stable (S30; S235 corrected by S236). S236 narrows replacement to
`Ideal<T, Causal>` and removes `BoundedInsert`'s dependency type parameter.*
`Ideal::insert` buffers unconditionally. An event whose dependencies never arrive (a lost
message, a crashed or silent peer) waits forever, so `pending` can grow without bound. That is
inherent to causal delivery over a lossy channel, and until now the only mitigation was to watch
`pending_len()` yourself. This slice gives the buffer the *mechanism* to cap its own backlog,
while leaving the *policy* with you.
Current surface:
[source,rust]
----
impl<T, G: Gate> Ideal<T, G> {
/// Buffer an event, refusing to grow the live backlog past `capacity`.
/// `Ok(())` -> buffered, or dropped as a stale duplicate (counts against nothing).
/// `Err(AtCapacity { event, capacity })` -> the live backlog was full; buffer unchanged.
pub fn try_insert(
&mut self,
event: Event<T, G::Dep>,
capacity: NonZeroUsize,
) -> Result<(), AtCapacity<T, G::Dep>>;
}
impl<T> Ideal<T, Causal> {
/// Replace the first caller-approved causally maximal resident in `Kairos` order.
pub fn try_insert_evicting_where<F>(
&mut self,
event: Event<T>,
capacity: NonZeroUsize,
evictable: F,
) -> Result<BoundedInsert<T>, AtCapacity<T>>
where
F: FnMut(&Event<T>) -> bool;
}
pub struct AtCapacity<T, D = VersionVector> {
pub event: Event<T, D>, // handed back so you can re-route without rebuilding it
pub capacity: NonZeroUsize,
}
pub enum BoundedInsert<T> {
Buffered,
DroppedStale,
Evicted { evicted: Event<T> },
}
----
How to adopt:
[source,rust]
----
let cap = NonZeroUsize::new(4096).expect("non-zero backlog cap");
match buf.try_insert(event, cap) {
Ok(()) => {} // buffered (or dropped as a stale replay)
Err(AtCapacity { event, .. }) => {
// your give-up policy: retry later, shed, escalate, or evict-and-reinsert
on_overflow(event);
}
}
while let Some(payload) = buf.pop_ready() { deliver(payload); }
----
Three things to understand:
. *The bound is a property of the call, not of the buffer.* There is no `with_capacity`
constructor and no stored capacity. `insert` (infallible, unbounded) and `try_insert`
(bounded, fallible) are two ways to insert into the same buffer; you choose per call. This
mirrors the `observe` / `try_observe` escape-hatch pattern in the error model: the infallible default
is untouched, and an additive `try_*` surfaces a limit you may care about. Pass the same
`capacity` consistently; a varying cap is a caller bug the type cannot catch.
. *The cap counts _live_ backlog.* When full, `try_insert` first reclaims provably-dead
entries (a pre-delivery duplicate whose twin was already released, which `insert` otherwise
lets linger forever) before rejecting, so the bound is not spent on garbage. That reclamation
scan is `O(pending)` and is paid only on the full path, never the common one.
. *We own causal safety; you own value policy.* `try_insert_evicting_where` exists only on
`Ideal<T, Causal>`. On the full path Minerva filters out the arrival's predecessors and
residents that any other pending event depends on. Your predicate sees only the remaining
causally maximal candidates and may rank them by class, deadline, age, or application value.
Refusal, retry, quarantine, and shedding remain caller policy.
Use replacement only when the admission policy is explicit:
[source,rust]
----
match buf.try_insert_evicting_where(event, cap, is_lower_priority) {
Ok(BoundedInsert::Buffered | BoundedInsert::DroppedStale) => {}
Ok(BoundedInsert::Evicted { evicted }) => quarantine(evicted),
Err(AtCapacity { event, .. }) => on_overflow(event),
}
----
The operation is one-for-one and atomic: the backlog never grows, no eviction advances gate
progress, and the retained set acquires no missing predecessor. This is a safety guarantee, not
unconditional liveness: an already-missing predecessor or permanently false local eligibility
can still prevent progress. The returned victim remains a local command or message to retry,
reroute, quarantine, or audit. It has not been retracted from any replica.
What does _not_ change: `insert`, `new`, `Default`, `pop_ready`, `frontier`, `pending_len`,
release order, and every ordering invariant. If you do nothing, nothing changes.
This is the mechanism half of the long-standing "pending bound / give-up policy" item. The
policy half stays on the deferred menu below.
== Trust-aware receive: `Clock::try_observe`
*Status: Stable (additive, S24). No migration required.*
`Clock::observe` is infallible and unconditionally dominating: it advances `physical` to pass
any remote stamp. That is correct for honest peers (the skew is recorded in `stats()`, never
silently corrected), but once a transport admits *untrusted* stamps it is an attack surface, a
far-future `remote.physical` drags your clock forward without bound (hazard 1 below). This slice
gives the clock the *mechanism* to refuse such a stamp, while leaving the *policy* with you. It
landed when the identity model it was gated on was defined (`pinax` ADR-0012: per-station
Ed25519 keyed by `station_id`), and that model is now *deployed practice, not design*: both
named consumers run `try_observe` at their trust boundary (`ethos`'s `δέχεσθαι` through
`Producer::try_observe`; `pinax`'s `apply` under its shard lock, mapping `SkewExceeded` to a
typed `LogError` carrying both fields, its S53).
New, additive surface:
[source,rust]
----
impl Clock {
/// Fold `remote` in as `observe` does, but only if its forward skew over the
/// current reading is within `max_forward_skew`. A stamp beyond the bound is
/// rejected and the clock is left unchanged.
pub fn try_observe(
&self,
remote: Kairos,
max_forward_skew: u64,
) -> Result<(), SkewExceeded>;
}
pub struct SkewExceeded {
pub observed_forward_skew: u64, // how far ahead of local the remote ran
pub max_forward_skew: u64, // the bound you passed
}
----
How to adopt, at the replication ingress (verify identity first, then bound the skew):
[source,rust]
----
// after the Ed25519 signature verifies against the key for remote.station_id():
match clock.try_observe(remote, max_skew) {
Ok(()) => { /* folded; proceed to merge */ }
Err(SkewExceeded { observed_forward_skew, .. }) => {
// your policy: drop, quarantine, or eject the peer
reject_event(observed_forward_skew);
}
}
----
Three things to understand:
. *The bound is a property of the call, not of the clock.* `observe` (infallible, unconditional)
and `try_observe` (bounded, fallible) are two ways to receive into the same clock; you choose
per call, the same infallible-default / `try_*` escape-hatch shape as `try_insert`. The bound value is
yours, a runtime argument, so a deployment sets how far ahead a verified peer may legitimately be.
. *Forward skew only, measured against one reading.* The checked quantity is `remote.physical()`
minus a single reading of the time source, shared by the check and the fold. A remote at or
behind your reading has zero forward skew and is always admitted (it cannot drag you forward).
The bound is inclusive. It caps what the *remote* contributes; it does not re-bound skew your
clock already carries.
. *We own the mechanism; you own the policy, and the trust.* `try_observe` rejects and leaves the
clock untouched (the receive is all-or-nothing). It does not clamp, log, or eject. Crucially, a
skew bound only defends when `remote.station_id()` is *authenticated*, else an adversary evades
it by rotating the claimed identity; minerva counts and gates, it does not authenticate.
*The field recipe (two live citations, a recipe deliberately not a type).* Both consumers
independently hand-rolled the same check order at their receive boundary, which makes it the
documented pattern for the next trust-aware ingress:
. *Authenticity first*: verify the per-event signature against the *claimed* `station_id`'s
registered key (key-to-station at `ethos`'s deed feed, station-to-key at `pinax`'s ledger
ingress; opposite lookup directions because opposite boundaries, one `station_id` space).
. *Register / dedup second*: a duplicate is a no-op regardless of provenance, so an exact-match
pre-check can skip the expensive verification on retries (`pinax`'s `holds_exact`).
. *Bounded observe third*: `try_observe` (clock- or producer-level) in place of `observe`; a
rejection is inert, no clock movement, no slot spent, no I/O triggered.
. *Capacity / commit last*: `try_insert` or your store's write, so a stamp that will be refused
never spends a slot.
The ordering is load-bearing: authenticating after the fold would let an unverifiable stamp move
your clock, and committing before the bound would let a refused event leave a trace. A shared
default for the bound value is a *consumer* convention, not ours (`pinax` adopted `60_000`
deliberately from `ethos`'s `ReceptionBounds` so one station's two receive boundaries agree);
minerva keeps the bound a per-call argument.
What does _not_ change: `observe`, `now`, `after`, and every ordering invariant. If you do
nothing, nothing changes.
This is the *reject* mechanism. The *clamp* variant (advance-but-cap instead of reject) is the
deployment liveness/safety policy on the deferred menu below, additive when you choose it.
== Shared-clock `Producer` and the bounded receipt
*Status: Stable. S73 added shared-clock carriers; S291 changes source and carrier types. See
the S291 migration entry below.*
`Producer::new` used to take its `Clock` by value only. That contradicted the station invariant
the crate itself states (one station, one logical clock): a station whose clock also stamps
*non-producer* sites (a seal, a ledger append, all sharing one `Arc<Clock<TS>>`) could not construct
a `Producer` at all without minting a second clock under the same `station_id`, which is exactly
the colliding-stamps hazard the invariant exists to prevent. `ethos` is the caller that hit this
(its `κοινωνία` layer re-rolled `produce` by hand while waiting; see the per-consumer recipe).
Current surface, combining S73's shared-clock capability with S291's concrete-source model:
[source,rust]
----
pub struct Producer<C = DynClock> { /* clock: C, knowledge: VersionVector */ }
impl<C: ClockCarrier> Producer<C> {
pub const fn new(clock: C) -> Self; // owned Clock<TS>, Arc<Clock<TS>>, or &Clock<TS>
/// observe(&event), but behind Clock::try_observe's skew bound.
/// Ok -> exactly observe (knowledge merged, clock advanced).
/// Err -> neither changed; the rejected event leaves no partial trace.
pub fn try_observe<P>(
&mut self,
event: &Event<P>,
max_forward_skew: u64,
) -> Result<(), SkewExceeded>;
}
----
How to adopt, for a station with one shared clock:
[source,rust]
----
let clock: Arc<Clock<MySource>> = station_clock(); // the one clock every site stamps with
let mut producer = Producer::new(Arc::clone(&clock));
let event = producer.produce(kairotic, payload); // broadcast this
// at the ingress, after the sender's signature verifies:
match producer.try_observe(&event_in, max_skew) {
Ok(()) => buf.try_insert(event_in, cap).map_err(on_overflow)?,
Err(SkewExceeded { .. }) => reject_peer(), // producer untouched
}
----
Three things to understand:
. *`C` abstracts ownership, nothing else.* Every operation reaches the clock through the
sealed `ClockCarrier`, so semantics do not depend on `C`; callers cannot
substitute a different minting machine under the producer contract. A bare
`Producer` means `Producer<DynClock>`; inferred construction preserves a
concrete source instead. `station_id()` is not a `const fn` because carrier
access is not const.
. *Share the clock across stamping sites, not the producer role.* One station still runs one
producer. Two producers over one shared clock would each self-count `knowledge[self]`
independently and mint colliding dots; the type cannot catch this, so it is yours to uphold.
. *`try_observe` is all-or-nothing by design, not convenience.* There is deliberately no
knowledge-merge accessor that would let you compose `Clock::try_observe` with a manual merge:
folding a rejected event's `deps` without its stamp would let a later `produce` snapshot
dependencies its minted stamp does not dominate, breaking the embedding of causal order in
stamp order. If you need the bounded receipt, take it as one operation.
Runtime semantics do not change: `produce`, `observe`, `knowledge`, composition with `Ideal`, and
every ordering invariant remain the same. Source compatibility does change under S291: inferred
construction produces `Producer<Clock<TS>>`, while a bare `Producer` annotation names
`Producer<DynClock>`. Choose the concrete or erased form, and replace custom `Borrow<Clock>`
wrappers, as described in the S291 migration entry below.
== Causal stability: `VersionVector::meet` and the `Stability` watermark
*Status: Stable after first contact (S78; Alma `93ca2744` / `0a779100`,
independently confirmed by Stokes `1b37f98` / `4615cdd`). No migration
required.*
Any consumer that retains delivered history (retransmission buffers, audit replay, rebuilding a
crashed peer) eventually has to reclaim it, and no retention bound is _safe_ without answering
"what has every peer already passed?" That answer is the *stability watermark* `min_n D_n`, the
meet of the members' delivered cuts: an event at or below it is delivered everywhere, so no member
can ever again need it re-sent. This slice ships the *mechanism*, the third face of the
bounded-resources boundary beside `try_insert` (backlog) and `try_observe` (skew); the forgetting
*policy* stays yours.
New, additive surface:
[source,rust]
----
impl VersionVector {
/// The greatest lower bound: pointwise min, the lattice meet (dual of merge).
pub fn meet(&self, other: &Self) -> Self;
}
pub struct Stability { /* roster of station_ids -> join-folded reported cuts */ }
impl Stability {
pub fn new(roster: impl IntoIterator<Item = u32>) -> Self; // fixed, declared family
/// Claim-only door for a bare vector whose evidence stays caller-side.
pub fn report(&mut self, station: u32, delivered: &VersionVector)
-> Result<(), UnknownStation>;
/// Preferred door: join-fold a witnessed, gap-free cut.
pub fn report_cut(&mut self, station: u32, delivered: &Cut)
-> Result<(), UnknownStation>;
/// The n-ary meet of every member's vouched cut. Monotone non-decreasing.
pub fn watermark(&self) -> VersionVector;
/// A witnessed watermark when every report used `report_cut`.
pub fn watermark_cut(&self) -> Option<Cut>;
/// The per-member slots, ascending; the straggler pinning the watermark is visible.
pub fn reports(&self) -> impl Iterator<Item = (u32, &VersionVector)>;
}
----
How to adopt, at whatever cadence you already gossip durable application cuts:
[source,rust]
----
let mut stability = Stability::new(members); // your declared membership set
// Authenticate the peer's stable-before-emit claim, then retain its cut witness.
let peer_cut = Cut::floor_of(&peer_durable_have);
stability.report_cut(peer_id, &peer_cut)?; // stale/duplicate reports absorbed
let local_cut = Cut::delivered_by(&my_ideal);
persist_local_through(local_cut.as_vector())?; // caller-owned durability fence
stability.report_cut(my_id, &local_cut)?;
let safe = stability
.watermark_cut()
.expect("every report retained its cut witness");
my_store.compact_below(safe.as_vector()); // your policy, not minerva's
----
Three things to understand:
. *The roster is the meaning, and it is yours.* A meet is only as trustworthy as the family it
ranges over, so the roster is fixed at construction (a constructor argument, like `capacity` and
`max_forward_skew`) and non-roster reporters are refused. The two errors this makes
unrepresentable are the ones that lose data: meeting over "peers heard from so far"
(over-approximates stability whenever an unheard member is behind) and the empty family's
vacuous "everything is stable" (the empty roster answers bottom instead). Membership _change_ is
a policy: a joiner must regress the watermark, a leaver un-pins it, and both re-define "stable",
so you handle them by building a new tracker over the new roster.
. *The watermark never regresses, which is your license to act.* Reports fold by join, so
duplicated, reordered, or stale gossip is absorbed and `watermark()` is monotone non-decreasing.
The cost is the meet's availability profile: a member that never reports pins the watermark at
bottom, and one that stops reporting pins it at its last cut. That is inherent (stability _is_
unanimity), and `reports()` exposes who is pinning; paging or evicting the straggler is your
call.
. *Mind hazard 2: report delivered cuts, not observed knowledge.* The meaningful input is an
application-delivery cut (an `Ideal`'s `delivered()` / `progress()`, or your own consumer's
equivalent), because "safe to forget" means *applied everywhere*, not *received everywhere*. A
`Producer`'s `knowledge` is receipt-level and generally runs ahead; feeding it to `report` would
declare stability for events a peer has received but not yet delivered. And a report must be a
*genuine cut*: a gap-free per-station prefix (PRD 0011 R8). Minerva's own vectors are gap-free by
construction, but a max-style high-water over an out-of-order store can run *past holes* (hold
`{0, 5}`, publish `6`); that is an upper bound of a cut, and meeting upper bounds over-claims
stability, so close the gaps (or track an exact have-set) before reporting. The exact have-set
is in-tree since S83: `DotSet` (PRD 0012) records disordered receipt exactly, its `floor()` is
a cut by construction (the honest report), and its `high_water()` is precisely the quantity
that must never be reported.
. *A report is a durability claim too (S264, ruling R-51).* The same join that absorbs stale
gossip absorbs a crash-restart regression silently: if you report a delivered cut, crash
before persisting it, and restart lower, every peer's tracker keeps your higher pre-crash
claim while you can no longer honor it, and the watermark may already have licensed
irreversible action on it. Report the floor of what you have durably stored, not of what you
have delivered; the executable recovery recipe (fence before emit, checkpoint at the seal,
replay plus anti-entropy) is the fleet replica's, `src/metis/tests/fleet/`.
A derived read worth knowing: the same reports also fold the other way. The pair
`(meet, join)` of the roster's cuts brackets the system (everything below the meet is
everywhere; nothing above the join is anywhere), and the per-station gap between them is a
natural replication-lag window, computable today with the two folds you already have. Minerva
ships no metric type for it, since which norm over the window matters is yours; if you find
yourself computing it, say so, because that interest is a signal.
First contact arrived through Alma's committed certified-view transport on
2026-07-20. A `ViewCertificate` defines the fixed roster, the caller durably
persists its own `Cut` report table before publishing, one `InstalledView` owns
one tracker across every document carrier, and restart rebuilds it through
`report_cut`. The consumer uses `watermark_cut` for Minerva's epoch lifecycle
license and keeps certificate authority, incarnations, persistence failure,
transport, roster replacement, and refoundation policy outside Minerva. The
surface held without a bulk constructor, tracker codec, or merge operation, so
PRD 0011 R7 is satisfied.
Stokes independently reached the same boundary through a different application
shape. Its draining lineage runtime privately owns the fixed-roster tracker,
persists bounded membership-bound witnessed-cut rows, rebuilds through
`report_cut` on restart, and permits durable terminal retirement only after the
witnessed watermark covers the proposal frontier. Stokes keeps credentials,
codec policy, persistence, retries, and retirement semantics. It also needed no
tracker-level persistence or merge API.
What does _not_ change: every existing type and method, the wire frames, and every ordering
invariant. The tracker has no wire form of its own (gossip your cuts as PRD 0007 `VersionVector`
frames); it is local state you feed. If you do nothing, nothing changes.
This is the mechanism half of the long-standing cross-node-meet menu item (S29b, PRD 0011, built
under owner ruling R-6). The policy halves, reclamation and membership change, stay on the
deferred menu below.
== Relative reads: restriction and the exact glue
*Status: Stable (S89, xref:prd/0013-metis-relative-base-change.adoc[PRD 0013], owner ruling
R-10). Additive reads on the existing types; if you do nothing, nothing changes.*
Both lattice types now answer questions *relative to a sub-roster*, and vectors can be glued
from partial views *exactly* instead of laxly:
[horizontal]
`VersionVector::restrict(roster)` / `DotSet::restrict(roster)`:: The same causal record, read
as far as the named stations are concerned; everything else reads absent. Restriction
commutes exactly with every fold and read you already use (`merge`, `meet`, `floor`,
`high_water`, `holes`, `from_cut`), so it composes freely: restrict-then-fold equals
fold-then-restrict, always.
`VersionVector::try_glue(&other, overlap)`:: The `merge`, guarded: checks that both sides
agree on every station in `overlap` first, and refuses with a `Disagreement { station,
left, right }` instead of folding when they do not.
Two disciplines make these safe, and both are yours to hold:
* *Restricted order is order over the sub-roster, never globally.* If two restrictions are
*concurrent*, the originals genuinely are (trust that verdict). But two concurrent vectors
can restrict to *ordered* ones, because the roster you dropped may be exactly where they
disagreed. Never compare restrictions and act as if the verdict were global; that
fabricates a happens-before out of forgetting (the created-order hazard, and the reason
`restrict` exists as a documented read rather than a pattern you hand-roll).
* *Glue authority; merge knowledge.* `merge` resolves every disagreement by maximum, which
is right when a bigger count is simply newer news. It is wrong when two partial views each
*claim to speak for* a station: merging their disagreement fabricates a cut nobody
reported, and a fabricated cut fed to `Stability::report` over-claims in the unsafe
direction (the same trap as reporting a high-water, one step removed). Declare the overlap
(which stations both views claim; a deployment fact, like the roster) and `try_glue`; on
`Disagreement`, deciding who over-claims, or falling back to `merge` deliberately, is
yours.
The natural first uses: scoping any vector question to a shard or working set
(`restrict` then compare, holding the discipline above); and assembling one honest cut from
views that arrive in sub-roster pieces (`try_glue` piecewise, then `floor`-and-report). If
your reports genuinely cover sub-rosters as a matter of course (a relay topology), say so:
the n-ary fold over a declared cover is chartered and waiting on exactly that signal
(PRD 0013 R7).
== Difference reads: what do I owe you
*Status: Stable (S95, xref:prd/0014-metis-difference-reads.adoc[PRD 0014], owner ruling
R-11). Additive reads on the existing types; if you do nothing, nothing changes.*
The anti-entropy question between the folds you already have: `merge` is what we jointly
know, `meet` is what we agree on, and `difference` is what I owe you.
[horizontal]
`VersionVector::difference(&other)`:: The *owed set*: the least vector whose `merge` into
`other` covers `self` ("the least gossip that makes you cover me"). Empty exactly when
`other` already dominates you; merging it into `other` lands exactly on the join, no
more. It commutes with `restrict` ("what do I owe you about the stations you track").
`DotSet::difference(&other)`:: The same question at dot granularity: a lazy, ascending
`(station, dot)` enumeration of exactly the dots you hold that `other` lacks, the serve
list dual to `holes()`'s fetch list. Serving them converges the peer to the union;
between genuine cuts it enumerates exactly the spans the vector read names.
One shape discipline is the whole hazard, and it is worth stating because the wrong code
type-checks: *the residual carries claims, never subtracted counters*. Where you lead 5 to
3, `difference` answers 5 (the count the peer must be raised to), not 2; the owed events
are the dots in the span `(3, 5]`. If you hand-roll `a(s) - b(s)` anywhere, the numbers
still look like a vector, but they name the wrong dots, and a repair loop built on them
re-serves your oldest events forever while telling you it is converging. The read exists so
that trap does not (the adjoint ledger's shape warning, pinned by example test upstream).
What stays yours: everything around the read. Who asks whom, when, over what transport,
batch sizes and caps (bound the enumeration with `take` or your own budget), and what a
count *means*. The vector read computes over claims: differencing against a liveness
high-water tells you what the peer's *horizon* lacks, which over holes is less than what
the peer actually lacks; difference against a floor (or an exact have-set) when repair
below the horizon matters. That is the same cut-versus-upper-bound discipline as the
stability section's, one read over.
== The causal store: replicated state that can remove
*Status: Stable (S96, xref:prd/0015-metis-causal-store-algebra.adoc[PRD 0015], owner ruling
R-12). Alma supplied first contact through `Composer<Rhapsody>`; Polis confirms the in-tree
`DotMap<K, DotFun<V>>` composition, and Stokes `0a14aae` supplies the first external custom
`DotStore`, an inverse-indexed effect store checked against the canonical `DotMap` result.*
If your replicas only ever add, the joins you already have converge you. The moment you
*remove*, a plain union resurrects everything any stale peer still holds, and the classic
patches are both bad materials (tombstones grow forever; last-writer-wins drops concurrent
writes, which in a collaborative editor is a user's keystrokes). The causal pair is the
lattice answer:
[horizontal]
`Dotted<S>`:: Your state *and* your deltas, one type: a store `S` of dot-tagged content
plus the context of every dot ever seen. `merge` is the only write. A dot survives a
merge iff both stores hold it, or one holds it and the other's context never saw it;
"in the context, not in the store" means seen-and-removed, and the merge honors it.
`DotStore`:: The open axis (the `Gate` posture). Minerva ships `DotSet` (bare presence),
`DotFun<V>` (a value per dot under the equal-by-basis discipline), and `DotMap<K, S>`
(per-key composition, nesting to any depth, your `K` and `V` never interpreted). Implement
`DotStore` for a different store shape against the five trait laws; your payload semantics
never enter minerva. Stokes's indexed effect store is the field example: its payload and
inverse-support invariant stay local, while `causal_merge_from` overrides only the cost of the
same survivor law.
The write flow (the merge records the dot; nothing else does):
. `let dot = live.next_dot(my_station);` assignment reads the *context*, so it stays
fresh across removals. Merge each write's delta before assigning the next.
. Build the delta: `Dotted::from_store(DotMap::singleton(key, {dot}))` for an add;
`Dotted::from_context(observed_dots)` for an observed remove (supersede exactly what
you saw, so an unseen concurrent add survives: add-wins is *your delta's observation*,
not a merge mode); `Dotted::try_new(store, context)` when a write also supersedes (a
register-style update), refused with an `UncoveredDot` witness if the parts lie.
. `live = live.merge(&delta);` then ship the delta (or the full state; they are the same
type and any merge order converges). Persist before you send: a delta that escapes
before it is durably merged locally can, after a crash, cost you a dot reassigned to
different content.
Two disciplines, both yours: what a *removal observes* is your semantics (observed dots
for add-wins; unobserved dots is a remove-wins posture; minerva never chooses), and
*display order* among concurrent survivors is yours to rank (the natural discriminator
is the `Kairos` stamp you already carry, kairotic tag included). For an editor
specifically: your sequence elements' identifiers *are* dots, so the kernel gives you
identity, causality, and removal, and you keep ordering, payloads, and the sequence
structure itself in your own `DotStore` impl.
== Breaking changes already landed
If you are pinned to an older `minerva` revision, apply these migrations on
upgrade. Each change is deliberate. Each row states its behavior impact.
[cols="1,3,3",options="header"]
|===
| Slice | What changed | Migration
| S348 / 0.2.0 | The single-variant `ClockError` enum is replaced by the
`InvalidStationId` unit struct. All four clock constructors return this
error when `station_id` is zero. Validation, state transitions, and the
constructor-produced error's `Display` text are unchanged. The old enum let
callers construct the variant with a custom static string. The unit struct
removes that presentation hook.
| Replace `ClockError` imports and `Result<_, ClockError>` annotations with
`InvalidStationId`. Replace `Err(ClockError::InvalidStationId(_))` patterns
with `Err(InvalidStationId)`.
| S291 / 0.2.0 | `Clock` and `LocalClock` now carry their owned
`TimeSource` as a type parameter. Concrete construction is allocation-free;
`DynClock` and `DynLocalClock` name the erased forms. `Producer` accepts the
sealed `ClockCarrier` capability so concrete, borrowed, and `Arc` clocks all
preserve one station clock without erasing its source.
| Prefer `Clock::with_default_config(MySource::new(), station)` and let `TS`
infer. Where one field must hold heterogeneous sources, spell
`DynClock` and pass `Box<dyn TimeSource>`. Apply the same choice to
`LocalClock`. Existing `Clock` type annotations continue to mean the erased
default; make `Clock<MySource>` explicit when storing a concrete clock. A
custom wrapper that previously implemented `Borrow<Clock>` is no longer a
producer carrier: pass an owned clock, `&Clock<TS>`, or `Arc<Clock<TS>>`
directly. `ClockCarrier` is sealed so wrappers cannot redefine the producer's
clock capability.
| S266 | The seal bequest is renamed the seal *consignment* (an owner naming
decision; Latin _consignare_, to mark under seal, the door's own witnessed
precondition): `EpochShadow::bequeath` is now `EpochShadow::consign`,
`EpochBequest` is `EpochConsignment`, and `EpochBequestError` is
`EpochConsignmentError`. Signatures, semantics, refusal variants, and wire
contracts are unchanged, and no consumer had adopted epochs at the rename.
| Rename the identifiers at your call sites; nothing else moves. If you had
pattern-matched `EpochBequestError` variants, only the enum's name changes,
never the variants.
| S199 | `Rhapsody::locus` and `Recension::locus` return `Option<Locus>` by value
instead of `Option<&Locus>`: the identity plane run-coalesced (arc 11 phase two,
ruling R-26), so a chain-interior element stores an inline rank step rather than a
`Locus` and the read materializes its answer. `Locus` is `Copy`; wire bytes,
ordering, and every other read are unchanged.
| Usually nothing: a binding read like `let Some(locus) = store.locus(s, d)` followed
by field access compiles identically (verified at alma's one call site, its
`weave_char`: `cargo check` and its collab suites ran green against the S199 tree
unchanged). Only a pattern that dereferences (`Some(&locus)`) or stores the
reference itself needs the `&` dropped.
| S32c | The producer's learn method `Producer::deliver(&event)` was renamed
`Producer::observe(&event)`: it is *receipt*, not application delivery (in
Birman-Schiper-Stephenson "deliver" is the buffer's `pop_ready`). Same signature and behavior.
| Rename the call site `producer.deliver(&e)` -> `producer.observe(&e)`. No behavior change.
| S27b | The gate seam. `Ideal<T>` is now `Ideal<T, G: Gate>` with *no default gate*;
`Event<T>` is now `Event<T, D = VersionVector>` (the default keeps `Event<T>` valid).
`Ideal::delivered` is `Causal`-only.
| Name the gate: `Ideal<T>` -> `CausalIdeal<T>` (or `Ideal<T, Causal>`). For per-source FIFO
delivery use `FifoIdeal<T>` (`Event<T, u64>`). Read generic progress through `progress()`;
`delivered()` stays for the causal case. No behavior change on the causal path.
| S26 | The delivery buffer was renamed. `CausalBuffer<T>` -> `Ideal<T>`; its frontier reader
`antichain()` -> `frontier()`. "Causal" was de-emphasized as branding (the mechanism is still
causal broadcast).
| Rename the type and the method at your call sites. No behavior change (byte-for-byte the same
gate, linearization, and frontier set).
| S21 | `Producer::latest()` (getter and field) was removed; the clock is now the single
high-water. An `observe` now ticks the clock once (the HLC receive rule), so a later produced
stamp can land a logical step higher than before.
| Drop any use of `latest()`. Assert stamp _relations_ (monotonicity, dominance), not absolute
logical values; the free-running logical tiebreak carries no meaning and may shift.
|===
The `Kairos` field layout (`kairotic` above `station_id`) and the wire frames (PRD 0002, PRD
0007) are the load-bearing compatibility surfaces. A change to either is a wire-and-ordering
break and would be called out as such; none is planned. Since 2026-07-02 the stamp frame carries
a second, stricter obligation: both field consumers embed `Kairos::to_bytes()` in Ed25519
signature *preimages* and recompute the frame at verification (`pinax`'s
`pinax/event/ed25519/v1` provenance message, `ethos`'s seal's `κανονικὸν_μήνυμα`), so byte
*identity*, not just byte order, is contract. A future frame version would invalidate every
stored provenance and seal minted over v1, record-carried authenticity ecosystem-wide, not a
codec migration; any v2 ships only with advance notice here and in both consumers' feedback
files, so stored records can pin v1 bytes in their preimages or re-sign first.
The `VersionVector` frame carries the same class of obligation since `pinax`'s anti-entropy
session v1 (its S57, 2026-07-02): every session body opens with this frame, split off by
`from_prefix` from network bytes, so the session protocol transitively pins frame v1 and a
frame v2 would be a session *wire break*, not just a new durable format. The absorption path
exists by construction (the session frame carries its own version byte, so a coordinated bump
rides a session v2), but the notice discipline is identical. Both pins are instances of the
standing rule R-8 in `owner-comments.adoc`: a frame a consumer embeds in a preimage or a wire
protocol is byte-identity contract, the crate's fuzz coverage follows the embedding's trust
class (S85 added `envelope_from_prefix` for exactly this seam), and a frame version bump is a
coordinated ecosystem event, never a unilateral one.
`DotSet` v1 joined that registry with Polis S3 (`5d10141`): Polis possession
protocol v1 length-delimits the canonical have-set beneath its authenticated
envelope and passes a receiver-owned exception ceiling to
`from_bytes_with_budget`. A `DotSet` v2 would therefore be a Polis possession
protocol break. Minerva pins the canonical inner bytes and allocation bound;
Polis pins the outer length, signature, charter, and admission behavior.
*Second frame-embedding shape observed (2026-07-17, alma TRANSPORT-2
worktree, ruling R-55), the Polis S3 handling applied again:* alma
transport v1 would be the first protocol embedding of the *rhapsody v2
skeleton frame* anywhere, plus the have-set frame as claim and context
sections and the 17-byte `Kairos` stamp inside its movement rows, all
beneath a BLAKE3 keyed-MAC envelope (MAC verified before any structural
read, minerva's exception-dot budgets at every have-set, an outer frame
cap refusing oversized declarations off the header alone). The
inspected alma tree is uncommitted atop `3afd6c4e`, so exactly as with
Polis S3, R-8's protocol pin, the coordinated-bump duty, and the
composed-parser coverage obligation *wait for its landed commit* rather
than being claimed early; the reply letter (local, untracked) asks alma
to name it. What did land now, as standalone decoder coverage rather
than the composed-path obligation: the v2 frame decoder's
adversarial-bytes fuzz target (`rhapsody_from_bytes`) beside its
standing never-panics proptests.
*Those pins fired (2026-07-18, S280, ruling R-62): the embeddings are
landed.* Alma `83fb666c` lands the authenticated carrier with the
rhapsody v2, have-set, and `Kairos` stamp embeddings exactly as
observed, and alma `764d8b03` adds the *snapshot v1 frame* (PRD 0023)
under an authenticated selector byte, admissible only against a
correlated whole-store fresh claim (empty text context, woven witness,
and visible witness), with the S277 `snapshot_encoded_len` preflight as
the pre-serialization cap. Rhapsody v2, snapshot v1, `DotSet` v1, and
`Kairos` v1 are therefore *protocol contract for alma transport* under
R-8: a version bump on any of them is a coordinated alma wire event,
the composed-parser coverage rides alma's transport falsifier suite
(which mutates canonical frames through the authenticated envelope),
and minerva's standalone fuzz targets remain the inner-decoder floor.
At this S280 cut the `Metatheses` v1 and epoch-ledger v1 entries still
waited: the deltas-v3 movement adoption and authenticated checkpoint
wrapper were uncommitted. Their later landed pins are recorded below.
*The movement-record frame those rows anticipated now ships as
minerva's own codec (S272, ruling R-56):* `Metatheses::to_bytes` /
`from_bytes` / `from_bytes_with_budget` under `METATHESES_WIRE_V1`,
rows in strictly ascending testimony-dot order spelling `(testimony
dot, target dot, sided anchor, 17-byte kairos rank)`, alma's field
order under minerva's big-endian discipline (the two spellings agree on
fields and diverge on byte order; alma retires its little-endian
version-zero section on adoption). Canonical form is enforced (byte
identity is value identity; zero-index targets and anchors are
representable payload and round-trip), the testimony budget is the
caller's (`MetathesesDecodeBudget`, the have-set precedent), refusals
are layered (`MetathesesDecodeError`), and coverage is the full R-9
split: the golden layout fixture, shaped proptests, and the
`metatheses_from_bytes` fuzz target. Since S280 (ruling R-62) the
codec also owns its exact length: `Metatheses::encoded_len()` is the
allocation-free pre-serialization preflight (the R-59
`snapshot_encoded_len` shape), so a carrier enforcing an authenticated
payload cap never re-derives the row widths downstream; the wire
property suite pins it equal to the emitted frame.
*The metatheses v1 pins fired (S281, ruling R-63): alma `b150a2b8`
lands deltas kind v3 on this frame.* Alma's provisional movement body,
version tag, parser, and byte order are retired; structural refusal is
delegated to `Metatheses::from_bytes` with the typed cause surfaced
through their `DeltaDecodeError::Metatheses`; their nonzero-dot
narrowing runs after minerva's exact decode as embedding policy; and
`5aebe4d5` adopts `encoded_len`, deleting their duplicate length law.
Metatheses v1 is therefore alma-transport protocol contract: a version
bump is a coordinated wire event, and the composed-parser coverage
rides alma's envelope falsifiers (which mutate the canonical frame
through the authenticated MAC). The *refound-map v1* frame is
tuple-pinned at the same commit: the frozen protocol tuple and the
lineage recipe's `M` field name it, so its bytes are contract, with
the first byte-level embedding arriving with the boundary-lineage
code. The frozen joint registry itself is tracked as PRD 0025
(minerva's half) beside alma's `epoch-transport.adoc`. The epoch-ledger
v1 entry fired later when TRANSPORT-15 placed its canonical bytes in
Alma's durable owner record; that storage boundary is described below.
*The five lifecycle record frames landed as S284 (ruling R-67; PRD
0025 section 6, kinds 5 through 9):* `Declaration::to_bytes` /
`from_bytes` / `encoded_len` under `DECLARATION_WIRE_V1`, and the four
new report-record types `ConfirmationRecord`, `AdoptionReportRecord`,
`SealRecord` (self-delimiting: `from_prefix` beside `from_bytes`, a
`SealDecodeBudget` capping each retained set in the candidate unit),
and `LineageProofRecord` / `LineageProofEntry` (consecutive
generations enforced at `try_new` and decode alike, a
`LineageProofDecodeBudget` carrying the generation ceiling plus the
per-seal candidate budget threaded into every embedded frame), each
with its own version namespace, typed error, and exact `encoded_len`
preflight. Decoded forms are report records, never witnesses (the
S273 rule): a decoded `Declaration` feeds `Epochs::deliver`, which
re-validates everything; a decoded `SealRecord` is a peer claim
compared against `SealRecord::from_sealed` of your own seal; nothing
decodes into `Adopted`, `SealedEpoch`, or a live machine. An
`EpochAddress` identifies a winning declaration, not the full record:
comparing addresses across members does not compare records. Arrival
endorsements compare their caller-supplied commitments over the exact seal
frame when record identity is required. Every embedded cut rides as a
canonical gap-free have-set whose gap-freedom
the receiver proves (a gapped embedding refuses as
`WireCutError::Gapped`; the decoded cut enters through
`Cut::floor_of`, a peer claim in witnessed shape). The seal frame
carries the window's *two distinct retained sets* (retired candidate
addresses and the protocol declaration-dot ledger), and the golden
fixtures pin a displaced-declaration window whose ledger keeps a dot
the candidates lost (the PRD 0025 R4 obligation). Coverage is the R-9
sampled-plus-fuzzed split: golden fixtures, shaped proptests, and five
fuzz targets. The kind 5 through 7 registry pins fired at alma's
landed embedding commit `4060ab6b`; kinds 8 and 9 fired at `806d4ecf`,
where TRANSPORT-17 admits the checkpoint pair only after Session Hello
v3's bilateral fence and retires the predecessor carrier after
successor installation. All five Minerva lifecycle rows are protocol
contract. Alma `1454e905` adds the distinct Byzantine profile without
respelling them: crash-only retains Hello v3 and kinds 1 through 9;
Byzantine Hello v4 binds the exact `ConfigurationId` and adds kind 10 v1,
a fixed 205-byte checkpoint attestation. Kind 10 is Alma-owned and is not a
sixth Minerva lifecycle frame. The epoch-ledger v1 storage pin fired
separately at alma `f37c3245`:
TRANSPORT-15 persists and restores Minerva's bounded canonical ledger
in a private, atomically replaced owner record. On a pending boundary,
Alma separately verifies the certificate-bearing checkpoint, requires its
delta generation to equal the ledger generation, and binds its lineage to
the separately stored proof. The local ledger bytes are not covered by the
checkpoint certificate, so neither section is authority alone. Alma
`a813c4ee` compares the proof's retained seals with the rehydrated ledger on
every owner-state write and restore before replay or authority escape.
Checkpointed states require exact bounded-history equality. The pending
durable-seal window permits exactly one newest ledger seal beyond the proof,
with generation and horizon trimming checked explicitly. Restore refuses a
mismatch as `EpochWindowInstallError::LedgerProofMismatch`. Alma `9d035920`
uses one relation classifier on both paths and pins exact and pending
canonical-section splices. TRANSPORT-20 at `a6b9e29d` then advances Alma's
private aggregate from `ALMAEW04` to `ALMAEW05`: one closed lifecycle replaces
independent pending/checkpoint fields, and a successor handoff retains the
exact predecessor batch plus one durable receipt per immutable-roster slot.
The complete successor image is encoded and bounded before the installed view
advances; view-first recovery reconstructs the same handoff. This is an
Alma-owned aggregate migration. Epoch-ledger v1 and every wire frame remain
byte-identical. Wire and storage pins do not follow the crate version; each
requires its own coordinated migration.
*The joint boundary fixture is bilateral (S327, ruling R-77):* alma
`806d4ecf` owns the BLAKE3 tuple calculation and pins the worked
two-station boundary's `P`, `S`, `M`, `A`, and `N` rows. Minerva checks
in their provenance-stamped export, compares all five rows to independent
byte arrays, and compares its lineage encoder against a manually assembled
frame with `N` at the exact `LINEAGE_PROOF_WIRE_V1` offset before decoding
that independent frame. This proves the owned codec preserves the caller's
lineage coordinate without importing Alma's glyph, digest, or attestation
domain. Alma `1454e905` leaves the worked crash-only fixture source unchanged
and separately runs a real-socket Byzantine window: both sides complete the
fence, persist before successor installation, carry the ordered seal,
attestation, checkpoint triple, verify the station claim under the exact
session roster against locally durable truth, and reopen durably. This B4
evidence is carriage, not a quorum certificate or freshness decision; those
remain Alma B5 and B6. Alma's first non-empty transported window then landed at
`49c11873`: all nine event kinds crossed, one duplicate replayed, and
the boundary stayed off the interaction thread. Alma `a6b9e29d` and
`728aa945` subsequently complete the RECEIPT-backed crash schedule. A fresh
process reopens the installed views and `ALMAEW05` owners, each exact owner
mints a move-only `DurableCheckpointRecovery`, and the headless adapter
consumes it without replaying checkpoint text as local authorship. Successor
Hello releases nothing; authenticated successor state durably receipts the
exact peer before semantic admission, roster completion opens both owners,
and new edits from both recovered stations converge with dots above their
restored histories. S330 records this caller-owned capability and carrier
proof without adding a recovery wire kind, second snapshot codec, or Minerva
API.
*The kind-9 receiver has its machine door (S292, ruling R-74):*
`Epochs::bootstrap(roster, horizon, &LineageProofRecord)` constructs a
joiner's lifecycle machine from certificate-verified proof entries,
installing exactly the duplicate-recognizer horizon (the retained
lineage plus the next window generation, the newest seal's successor)
and nothing else. The trust decision is yours and comes first: verify
your checkpoint certificate before this door, because the door
re-proves only the machine invariants (roster-minted retained sets
and sealed-join support, winner among its candidates, ledger covered
by the sealed join, candidate generation homogeneity) and refuses
with the typed `EpochBootstrapError` vocabulary (the empty proof, a
proof past your horizon, the ceiling-adjacent generations, and the
per-entry violations). What
you get is agreement: a bootstrapped machine equals one that lived
through the sealed generations, so `recognize`, replay absorption, and
the next lifecycle round behave identically, while adoption authority
is deliberately not restored, `Adopted` is re-earned live through
`adopt`. Fresh join keeps its one door (`Epochs::new`); trimming an
over-horizon proof to its newest suffix is your visible act.
*The live machine now mirrors the snapshot's read surface (S294, ruling
R-76):* four O(1) reads answer the questions a returning or recovering
member used to have to materialize a checkpoint for. `Epochs::generation`
(the next window's lineage generation, advanced only at seal),
`Epochs::roster` (ascending station ids), and `Epochs::horizon`
(the retained-recognizer bound) are the live twins of the snapshot's
configuration reads, so a *config-versus-proof* check reads the machine
directly: validate an incoming `LineageProofRecord` or certificate against
your configured family without decoding a snapshot to learn three numbers.
`Epochs::newest_sealed` returns the newest retained `SealedEpoch` by
reference, and carries two duties on one door. First, the *freshness floor*
for a returning member, stated in checkpoint terms because an installable
checkpoint names its *successor* generation while a seal names its
completed one: before authenticating a checkpoint and passing its verified
`LineageProofRecord` to `Epochs::bootstrap`, refuse any checkpoint whose
successor generation is at or below your newest seal's, equivalently below
live `generation()`. An equal-labeled pre-seal checkpoint must never roll
you back across your durable seal. Second, the
*crash-between-seal-and-checkpoint* re-entry pivot: if you crash after
persisting your sealed ledger but before minting your bootstrap checkpoint,
rehydrate (Minerva re-proves the retained lineage whole), read
`newest_sealed`, and re-wrap that recognizer under your fresh incarnation
behind your own checkpoint-pending guard, then recompute the boundary state
deterministically and re-enter your existing mint. Minerva supplies only the
read: `try_seal` stays the one live door, and the affine `Adopted` witness
is re-earned through `adopt`, never reconstructed from bytes. `SealedEpoch`
also now exposes `candidates` (every retired candidate address, ascending),
restoring symmetry with its wire twin so a certificate layer binds over both
retained sets from the live entry without cloning a record. One codec edge
hardened with these reads: snapshot decode now refuses a sealed join
covering a station outside the roster (`InvalidState`): impossible for a
live seal and refused by the bootstrap door, but previously installable by a
crafted local checkpoint, where it would have handed `recognize` verdicts to
foreign traffic.
*The rename map has its canonical frame too (S278, ruling R-60):*
`RefoundMap::to_bytes` / `from_bytes` / `from_bytes_with_budget` under
`REFOUND_MAP_WIRE_V1`, one row per ceiling station spelling `(station,
cut ceiling, live count, then the old-epoch indices whose one-based
position is the new index they compact onto)`. This is the byte
carrier for the epoch boundary's old-to-new identity commitment, the
piece the S275 state-identity research found missing from every
published system: if your payload re-keys through the map (glyphs,
cursor anchors, undo records), the lineage node's rename digest will
bind these bytes, so two stacks that disagree on the map become
auditable instead of silently misassociating payload. Two things to
internalize before re-implementing: entry order is *semantic* (the
position carries the new index, so permuted entries are a different
map, both canonical), and a fully swept station still rides as a
zero-count row because its ceiling bounds the affine window
translation. Budgets are yours (`RefoundMapDecodeBudget`: station rows
and cumulative entries), refusals are layered
(`RefoundMapDecodeError`), and coverage is the full R-9 split (golden
fixture, shaped proptests over maps earned from real `refound` folds,
the `refound_map_from_bytes` fuzz target). The digest function,
preimage composition, and this frame's registry entry are the S274
joint session's to settle; the bytes are stable now.
*The family's byte discipline is big-endian, permanently (ruling
R-58).* If you re-implement a frame in another stack, expect network
byte order everywhere, and expect it to stay: byte order is not a
preference here but a property. Big-endian fixed-width integers make
memcmp order equal numeric order, so an encoded `Kairos` stamp sorts
*causally* as an opaque byte string in any byte-keyed store without
linking the codec (the stamp rustdoc's founding claim, frozen into
signature preimages at two consumers), and numeric dot rows encoded in
canonical ascending order remain memcmp-ascending for free.
Your little-endian host pays one `bswap` per field inside a validating
parser that touches every field anyway; there is nothing to win by
asking for a native-order variant, and no knob will ship (a
configurable order would give one value two encodings, which kills the
canonical bijection your content hashes, MACs, and the future lineage
digests rest on). Practically: take byte order from the golden
fixtures, which read left-to-right as place value, and if your
platform genuinely needs a native-order *at-rest* record format some
day, that is a new sibling codec conversation under R-8, not a
parameter on these frames.
Since S86 the assurance behind these pins divides by tool fit (ruling R-9). The `Kairos`
frame's laws, the ones your signature preimages rest on, are *proven totally*: round-trip and
sorts-causally (PRD 0002 R6/R2) are discharged by Kani for every pair of stamps, so the bytes
you sign over are backed by a proof, not a test corpus. The `VersionVector` parser's
map-touching laws (accept-path bijection, canonical-form rejection) are owned by *shaped*
property tests plus the two fuzz targets, with Kani kept to the parser's heap-free fragment
(length arithmetic, sub-header rejection): symbolic map structure is out of a bit-blasting
solver's affordable scope, and Creusot is the chartered deductive rung if a consumer ever
needs the stronger claim there.
== Per-consumer integration
Concrete recipes for the consumers on the tree today. Each depends on `minerva` *directly*;
none depends on another through `minerva` (an `ethos` endorsement ledger backed by `pinax` is
an `ethos`<->`pinax` integration, with `minerva` as the common substrate, not an intermediary).
Each recipe is an instance of "pulling a menu item": the use case, the surface that serves it, and
the trigger that un-gates the rest.
=== `plethos` / `pinax` (the durable ledger and replication)
*Keep the ledger `metis`-free.* The append-only ledger surface (`Archive`, `EventHandle`,
`Event`, `Anagraph`, `LogError`) need not touch a `VersionVector`; only the replication path
(`Horizon`, `ReplicationPort::synopsis` / `delta`) reads `metis`. That split is worth preserving in
your types, because a ledger sequence and a causal vector carry different meanings over the same
`station_id` space (hazard 2 below).
*Per-source FIFO is the `Fifo` gate, held in reserve.* Read against your live source (2026-07-02),
your shipped apply path deliberately needs no holdback: `merge_remote` admits events in any order
(dedup by exact `Kairos`, order recovered by re-sort at read), and your `Horizon` is a liveness
high-water rather than an exact have-set. Do not adopt a buffer you do not need. The moment a
refinement *does* force per-source ordered application (your R9 discussion names the fork: a
reorder buffer or the DVV), the reorder buffer already exists and is exactly `FifoIdeal<T>` over
`Event<T, u64>` with `dep = sequence + 1` (your ADR-0010 interlock): `deps` is the sender's bare
`u64` sequence number, `progress()` is the per-sender watermark map, and release is per-source FIFO
with the same `Kairos` linearization of the concurrent frontier as the causal path.
[source,rust]
----
let mut buf: FifoIdeal<Record> = Ideal::default();
buf.insert(Event { stamp, deps: seq /* u64 */, payload: record });
while let Some(record) = buf.pop_ready() { apply(record); }
----
The send-side mirror (`metis`-blessed minting / framing of `Event<P, u64>`) is *deferred* (S27c,
PRD 0009 R7): rolling a per-source `u64` counter yourself is trivial, so frame the scalar `deps`
yourself for now (the `VersionVector` wire codec is causal-specific).
*The synopsis / delta is the `VersionVector` wire codec.* Frontier reconciliation is
`VersionVector::merge` (the join) and `partial_cmp` (are two frontiers equal, ordered, or
concurrent). On the wire, `to_bytes` / `from_bytes` / `from_prefix` is the canonical,
bounded-allocation, fuzz-tested digest frame: equal knowledge encodes to identical bytes (a sound
dedup key), and decode validates canonical form on untrusted input. `from_prefix` splits a digest
off the front of a framed message and hands the tail back untouched, the same seam the `Event`
envelope uses.
*One trigger has fired; one is still gated.* Your replication design moved two menu items:
[horizontal]
Trust-aware receive (S24):: *Now built and field-adopted* (your S53, verified at
`replication.rs`): at `apply`, after per-event Ed25519 verification against the claimed
`station_id`'s registered key, `Clock::try_observe(kairos, max_forward_skew)` runs under the
shard lock in place of `observe`, before the reservation and anagraph write, with
`SkewExceeded` mapped to your typed `LogError::SkewExceeded` carrying both fields. That makes
you the second of two field consumers of the mechanism (with `ethos`'s `δέχεσθαι`), and your
wiring confirmed the load-bearing properties in anger: the all-or-nothing inert rejection, the
inclusive bound, and the single-reading coupling of check and fold. The bound stayed
caller-side where it belongs (your per-archive tunable, defaulting to `60_000` adopted from
`ethos`'s `ReceptionBounds`). You had already *decided reject and declined clamp* (ADR-0012): a
clamp would record a far-future stamp your clock never reaches, breaking the
clock-dominates-stream invariant that lets a concurrent `append` `push`. Nothing further is
owed here; the check order you and `ethos` independently converged on is now the documented
field recipe in the trust-aware-receive section above.
Cross-node meet / GC (S29b):: *Mechanism now built* (S78, PRD 0011, owner ruling R-6):
`VersionVector::meet` and the roster-fixed `Stability` tracker shipped ahead of your wiring, the
same way the reject mechanism did (justified by the crate-wide bounded-resources boundary, not a
consumer ask). Your two boundaries survive intact as the *adoption* gate, and reading
`merge_remote` / `synopsis` against source (2026-07-02) added a third your R11 falsifier list
does not yet name: (a) *membership*, the roster is a constructor argument, so the tracker is
only meaningful once you own a declared, finite member set (your local-hash shards still are not
one); (b) *reclamation*, the watermark earns its keep only when you *act* below it (truncate,
compact), reversing your strictly-append-only posture; and (c) *gap-freedom*, because your
`Horizon` is a liveness high-water that runs past holes (order-free apply can hold `{0, 5}` and
publish `6`), an *upper bound* of a cut rather than a cut, and a meet over upper bounds
over-claims stability: reclamation below an over-claimed watermark can destroy the last fillable
copy of an event a replica still has a hole for, a permanent gap (PRD 0011 R8). The meet needs
genuine cuts, which for you means the R9 exact have-set (DVV) or contiguity-enforced apply;
minerva's half of that fork now exists held-in-reserve (`DotSet`, PRD 0012, `dot = sequence + 1`
in your basis) while your ranked first discharge stands. One
shape note for that day: `Horizon` deliberately exposes no inner `&VersionVector` (byte-only,
basis-asserting construction), so the natural adoption path is a pinax-side wrapper that reports
decoded synopsis frames, keeping the tracker's whole family Horizon-basis by construction, your
own newtype discipline extended one type up. When all three are owned, the sections are the
`D_n` cuts you already gossip and adoption is the causal-stability section above. Alma has since
closed the base tracker's first-contact review without a shape change; a bulk constructor would
now need to pay rent as `pinax`-specific convenience.
Your 2026-07-02 notes confirmed the roster shape from the consumer side: membership, when it
arrives, will enter `pinax` as a trait seam (the `Politeuma` precedent, the register staying
with an identity-owning consumer), and a caller-declared fixed roster as a constructor argument
is exactly what that seam would feed; no friction anticipated on the shape, so the gates above
are what remain.
*DVV adoption stays deferred, and that is correct; minerva's half now exists in reserve.*
Per-source FIFO has no same-source concurrency, so the
gaps Dotted Version Vectors solve cannot arise: `Event.deps` already carries the per-event dot at
`deps[sender]`, and the contiguity invariant forbids the non-contiguous holes a DVV disambiguates.
Since S83 the *delivery-side* half of your R9 fork is in-tree as `DotSet` (PRD 0012, owner ruling
R-7): the exact have-set whose `floor()` is a gap-free cut by construction (the honest stability
report), whose `high_water()` is your `Horizon` quantity, and whose `holes()` is the named fetch
list, `dot = sequence + 1` in your basis. It changes none of your decisions: your ranked first
discharge (contiguity-enforced apply, your `owner-comments.adoc` R-6) stands, and the
*production-side* sibling-tagging DVV becomes relevant only if one `station_id` ever hosts
genuinely concurrent writes (a server proxying many clients under one key, i.e. partial
replication with a resolver-side causal context); that half remains unbuilt and ungated.
Until a falsifier fires, the plain station-keyed vector is the honest record.
=== `ethos` (the deliberation layer)
*The ἔνδοξα watermark is PRD 0009's participation gate, consumed as a fold.* A Πρᾶξις becoming
κυρία once enough distinct authorized endorsers weigh in is exactly the participation watermark
PRD 0009 names, and `ethos` is the caller it names. Per R6 the participation gate *stays a caller's
gate*: you build the monotone weighted-threshold fold yourself (the `ΚανὼνΚυρώσεως::κυρῶσαι` you
have), and minerva ships only the test-only `Quorum` that proves the seam carries the shape. That is
the intended division of labor, not a missing feature.
*Your tier weighting is a legal, well-behaved gate.* Distinct-endorser dedup plus a saturating
weighted sum against a fixed threshold is monotone (the contributor set only grows), so it is an
R1-legal `Gate`. The finite tier weights (σοφοί 3x, πολλοί 1x) with a finite threshold `k` stay
Scott-continuous: PRD 0010's side condition only breaks for weights with infimum `0` or a limit
threshold, which finite tiering is not. So the fold is sound today and stays sound if you ever
promote it to a machine gate.
*You took the enactment path, and it needed nothing from us (2026-07-01).* The prediction the
2026-06-21 exchange recorded ("wrap the same fold as a `Gate` and run a `metis::Ideal` over it")
held exactly: your `ἐπικύρωσις::Ἐκκλησία` runs `Ideal<(), ΠύληΚυρώσεως>` over your own gate, and
every κυρία proposal releases once, in ledger-`Kairos` order. Two things about that gate are worth
recording as the *caller-typed-`Dep` pattern*, since the next gate author will want them:
* *The `Dep` is yours, entirely.* Since S27b, `Event<T, D>` is generic over the dependency
descriptor and a `Gate` impl chooses both `Dep` and `Progress`. Your `Dep` is a fused enum (an
endorsement carries the endorser's 32-byte `ΔημοσίαΚλείς` and advances a per-proposal
distinct-endorser tally; a proposal carries the assembly's threshold rule, gates on it, and goes
terminally stale once enacted), and your `Progress` keys itself by proposal seal. The `u32`
sender argument simply goes unused. This is why the anticipated "keyed `Progress`" request
(rec #2 of your 2026-06-20 disposition) was *withdrawn rather than granted*: the seam already
carried it, and nothing was owed upstream.
* *You did not need `And<Causal, Participation>` either.* Your enactment determinism comes from
merge-stable ledger stamps, not causal dependency completeness, so the composition stays an
available option (the gate algebra above), not a requirement. Do still not reach for `Or` (a
proven non-goal).
*The read discipline your testing found: drain before ranking.* Your ordinal conflict pick reads
each released proposal's κύρωσις, and the general release-at-crossing rule (the gate-algebra
section above) applies with full force: read at release, every proposal looks threshold-weight and
"rank by κῦρος" silently becomes "rank by time." Drain `ἐπιτελεῖν` to `None` first, then read each
seal's tally.
*So the deinotēs seam stays uncarved, now with a consumer on record (the corrected merge level).*
The 2026-06-21 note recorded your merge level as *fold*; building the consumer showed that
conflated the verdict with enactment. The honest ledger is three-layered: the *verdict* folds (your
revisable κυρία LWW view over the G-Set, the only genuine merge); *release* is not a merge at any
level (every κυρία proposal, once each, in `Kairos` order, the machine default); and *conflict*
among concurrent κύριαι is *ordinal*, caller policy above the release stream (rank by weighted
κῦρος, tiebreak lex-posterior by `Kairos`, which is PRD 0008's composition-with-the-kairotic note
in the wild). Your `ἦθος::νόμος` register then makes that ordinal pick a strict total order, so the layer
above the release stream is again an order-independent fold: the whole stack is fold -> once-only
release -> fold, with the non-merge layer exactly one deep. That register is the first *real*
conflict-consumer on record, and it needed *no selective release*, so the one capability a carved
`Resolver` seam would add that caller code cannot express (choose which deliverable event releases
next, against the machine's `Kairos`-minimal rule) remained gated until stokes supplied exactly
that irreversible-effect consumer. S234 now provides the narrow
`Ideal::pop_ready_event_where` operation; it does not carve the broader `Resolver` framework.
*The seal stamp: use `Kairos::to_bytes`, do not hand-roll the layout.* The byte frame is
`[0x01, physical(8), logical(2), kairotic(2), station_id(4)]` big-endian, so the *kairotic tag is
more significant than `station_id`* (that ranking is the point of the kairotic dimension). But the
constructor argument order is `Kairos::new(physical, logical, station_id, kairotic)`, with
`station_id` *before* `kairotic`. A seal that serializes fields in constructor-argument order
therefore puts them in the wrong significance order, the `station_id` / `kairotic` "swap" you saw.
The fix (your M2) is to seal over `Kairos::to_bytes()` directly, the canonical 17-byte frame, never
a hand-rolled field concatenation; `KAIROS_WIRE_LEN` is public so you can split the stamp prefix
from the rest of an envelope.
*Your shared-clock `Producer` request is answered, plus the half the swap also needed
(2026-07-01, S73).* Your `κοινωνία` addendum asked for a `Producer` over the station's shared
`Arc<Clock<TS>>`; reading `Κοινωνός` against `producer.rs` showed the promised swap needed one more
thing the addendum did not name: your receive path (`δέχεσθαι`) folds through the *bounded*
`Clock::try_observe` plus a knowledge merge, a shape `Producer::observe` (unbounded) cannot
express, and routing the check and the fold separately would tick the clock twice per receipt.
Both halves shipped (see the shared-clock section above): `Producer::new` now takes a sealed
`ClockCarrier`, and `Producer::try_observe(&event, max_forward_skew)` is your
`δέχεσθαι` fold as one all-or-nothing operation. The swap is mechanical:
`Κοινωνός { γνῶσις, ὑποδοχή }` becomes `{ παραγωγός: Producer<Arc<Clock<TS>>>, ὑποδοχή }`,
`κοινοῦν`'s three hand-rolled lines become `παραγωγός.produce(kairotic, πεπραγμένον)`, and
`δέχεσθαι`'s check-then-merge becomes `παραγωγός.try_observe(&συμβάν, ὅρια.max_forward_skew)`
before the existing `try_insert`. Your seal-verify-first ordering and your capacity policy are
untouched; keep them caller-side. This deliberately does *not* generalize `Producer` across
gates (your own rec #7): it is the causal minter you asked for, over a shared clock.
*Migration, when you adopt the metis-era minerva.* The breaking `kairos` changes are small and
localized (see "Breaking changes already landed"): `now` / `after` take the kairotic and return a
bare `Kairos` (contention moved to `Clock::stats`), and `with_default_config(time_source,
station_id)` dropped the `initial_kairotic` argument. The deinotēs resolver (your M4) is PRD 0008,
the most deliberately held menu item. Your enactment consumer remains on record needing no seam
(above); stokes subsequently fired the narrower selective-release gate, while general resolution
stays caller-side.
=== `alma` (the collaborative editor)
*The chartered consumer arrived (2026-07-07, first field adoption of the causal
store).* The multiplayer editor the R-12 through R-15 chain built ahead of is now
live consumer source: alma's `src/collab/` (decision record COLLAB-1 in its
`docs/owner-comments.adoc`) drives a raw `Dotted<Rhapsody>` pair for shared text:
one element per character, glyph payloads caller-side exactly on the R-12 boundary,
dotted delete as a born-dead marker beside the observed-remove set, run-grade insert
through `weave_run` with the rank-above-children observe discipline, and
`Composer<DotMap<u32, DotFun<_>>>`
register writes through `compose_super`/`observed` for per-participant cursors
whose payloads are *element-identity anchors*, the identity-not-offset shape this
guide's causal-store section recommends. Its movement pair remains a
`Composer<Metatheses>` and also serves as the shared event-allocation ledger (the
COLLAB-17 conversion recorded below). Exchange is pull-based
`owed_to_witnessed` / `absorb` (since S191 the text exchange carries the peer's
`woven()` recording witness beside its context, its COLLAB-3, so a steady-state
round is delta-sized instead of re-shipping the ordering skeleton; the cursor
registers are support-only and take no witness); the deterministic `TickCounter`
clock keeps orderings a pure function of write history. Its convergence proof
(`tests/collab_convergence.rs`) runs two- and three-session fleets through real
editor input.
*What this adoption pins.* The genuinely-concurrent-writes condition is now met
at a field consumer (the production-side dotted-vector charter's own gate
condition); the interleaving cure (PRD 0018) and the survivor law are
load-bearing in someone else's test suite; and the integration pin is a
*sibling-checkout path pin* by recorded owner decision (alma's
`docs/supply-chain.adoc`: alma deliberately does not build from a bare
clone until this crate releases, a vendored copy explicitly declined on
R-2's own words), so the first build environment that cannot carry the
sibling fires ruling R-2's import-shaped trigger verbatim. Since its
COLLAB-4 (2026-07-08) the *retirement half* is field-consumed too: alma
gathers each participant's applied-removals report (read off the
replica's own merged state, so it cannot over-claim), meets them through
the roster-fixed `Retirement` tracker, and condenses every session
against the typed `Retired` evidence, with glyph hygiene riding the same
round: the first genuine ack round through the machinery (the bench's
`Retired::trust` stand-in is no longer the only usage shape), and the
first field exercise of the retention face end to end.
*The membership and retention story is now field-complete (its COLLAB-5 and
COLLAB-6, 2026-07-07).* A late joiner bootstraps through the owed read
against the empty claim (this guide's own "a full state is just a delta a
fresh peer has never seen", consumed as policy: no snapshot type exists at
the consumer), join-after-condense is pinned end to end, a graceful leave
retracts the leaver's cursor register so its retention pins release, and
crash eviction is a survivor's retract of the vanished register (safe by
the survivor law: a participant evicted in error recovers by publishing).
Retention runs on a cadence (a tombstone-debt threshold) and the
long-session profile the arc rulings were waiting on has run at the
consumer (`tests/collab_profile.rs`, 1,600 rounds of two-author interior
churn, sync per round): grow-only retention held 19,440 loci over a
1,440-char living document (13.5x) with a late-joiner bootstrap of 817,331
bytes at the frame's ~42 B/locus; the cadence held 5,878 loci (4.1x) and a
247,727-byte bootstrap, reclaiming 75% of all deletions; and the remaining
4,438 tombstones (roughly 25% of everything deleted, growing linearly with
churn) are *fertile* residue live text anchors through, which no roster
round can reclaim: the uncollectable-residue shape arc 5 recorded for path
compression, now with a field rate.
*The epoch boundary is now field-consumed too (COLLAB-17 and COLLAB-18,
2026-07-17).* Alma adopted the full declaration, confirmation, adoption,
seal, stratum, shadow, and consignment cycle over a quiescent roster. At the
boundary the 4,438 fertile tombstones fall to zero on both replicas, 8,876
loci are swept, the skeleton resets to the 1,440-character living document,
and the late-joiner bootstrap falls from 820,323 to 60,547 bytes. The
translation inventory is complete: glyphs re-key through the retired map;
cursor values whose anchors were swept degrade by byte to the same order
slot; marks and byte-span undo hold no cross-generation identity today. The
retired map is therefore consumed during the fold and retained for zero
generations. Identity-anchored undo is the first named surface that would
change that policy. Alma reports that `EpochShadow::consign` held its shape
at first contact and requests no change to the refusal-hands-back seam.
*The boundary stall is a schedule you own, and its safety is now a type
(S270, ruling R-54; S272, ruling R-56).* The transition is one consuming
door since S272: `EpochStratum::into_transition()` moves the pristine
native plane and the shadow out together (`EpochTransition { native,
shadow }`, no clone: no door discards the native plane before it
reaches you, so re-deriving it is never needed; `EpochShadow::open` is
retired, the breaking change alma requested and consented to). Your native allocator basis is the
native store's own per-station high water. Replay the window traffic you
have already delivered through `EpochShadow::deliver_text_batch` /
`deliver_moves_batch` rather than the per-delta doors: validation and
refusals are identical, and the maintained decision surface refreshes
once per plane instead of once per delta, which the S270 phase split
measured as the difference between a multi-second and a sub-100 ms
boundary at cadence 20,000. The remaining term, re-merging the
generation log at the adopted cut, is schedulable, and the schedule's
safety laws live in `EpochPreparation` since S272:
`EpochPreparation::begin(&declaration, base_text, base_moves)` pins the
candidate (refusing a base already above the declared cut), each
`absorb_text` / `absorb_moves` folds one covered log entry on whatever
idle tick you own (the chunk you feed is the budget; a window entry
returns the `Window` verdict and is left for the shadow replay). Each
absorb names the entry's own event dots, your log's annotation under
the dotted-deletes half of the one-event-space duty: the events are
the classifier, because a deletion is a bare context delta whose bytes
cannot say which side of the cut it fell on, and an entry naming no
events (an undotted retraction, invisible to every cut) conservatively
stays window-side; structural context-and-skeleton scans back the
declared events as defense against mislabeled entries. Then
`finish(&adopted)` proves the exact winner-and-cut match before running
the full stratum validation, with the losing-candidate and drifted-cut
misses typed (`EpochPreparationMiss`) so the full-rebuild fallback is
demanded, never remembered. One obligation stays yours under staging
and rebuild alike, stated on the type: the proof is structural
(coverage, bounds, foldability), not a content audit, so feeding the
generation's actual covered log is your log discipline; a base whose
context covers the cut but whose store is hollow re-founds hollow on
either path. If you declared, your dry-run already built
the pairs: `begin` from them and absorb only what arrives after,
instead of paying the rebuild twice, and carry forward as window
traffic the entries your dry-run skipped as above-cut (they were
classified by the same event-dot rule; the shadow replay owns them at
adoption). Which candidate to stage in a contested window, and
whether to stage several, stays your policy. The reference recipe is the
retention arm of `bench/src/bin/longevity_epochs.rs` (its staging now
wraps this type; what remains caller-side there is exactly the schedule
and the timers); measured numbers are in the closing report's S270
addendum.
*One event space per station is an adoption precondition, not a style
preference.* Epoch declarations, both adopted document planes, the sealed
join, the frozen map, and the consigned context must range over one dot space
per station. Independent self-allocating `Composer` instances for text and
movement can mint the same `(station, counter)` in different planes; their
images then collide at re-foundation. Alma found this while converting its
former three-facade shape. Its repair uses the movement pair's context as the
allocation ledger: every weave run, delete marker, movement testimony, and
declaration reserves there first, so the union context is a gap-free prefix
and `Cut::floor_of` is the honest witnessed report. `Composer` itself remains
a one-pair facade and does not hide shared mutable allocation. A future
ergonomic surface, if a caller needs to leave the raw pair, should be an
explicit station-scoped ledger or reservation capability above composers,
with durability and reporting visible at that owner boundary.
*The bootstrap bytes now have their codec (S197, PRD 0023, ruling R-25).*
The numbers above fired arc 5's snapshot half:
`Rhapsody::snapshot_encoded_len` / `to_snapshot_bytes` /
`from_snapshot_bytes` is the
run-compressed sibling of the canonical frame, 6.3x smaller on honest
two-author traffic at 65k chars (~42 B/locus down to under 7), decode
proven total (the second dual-homed Creusot core) and fuzzed. The length
preflight is allocation-free and uses the codec's own chain law, so an
embedding can reject an over-budget frame before serialization. When alma
grows a transport or a persistence checkpoint, the whole-store form is
this frame, not the v2 frame; the owed read stays the steady-state
exchange (a snapshot is for joining and saving, never for gossip), and
the first *embedding* of the frame in a protocol or a stored artifact
fires ruling R-8's pin obligations, so file it when it happens.
*What its flat-text surface now asks, kept precise.* Alma is still not a
`Node` consumer, and the structured pages/blocks surface remains the named
prospective ask for the subtree half of stage C. But both structured-stage
signals now have flat-sequence consumer evidence on the record
(2026-07-07): its COLLAB-8 pins the stage C hazard in the field (its
`:move` is a hull replace, so a concurrent edit converges *stranded* at
the old identity's order slot; the exhibit test is written to fail the day
moves preserve identity), and its COLLAB-7 hand-rolls stage D's point half
caller-side (marks captured as identity anchors across an absorb, the
cursor-register recipe applied to marks) while filing the range asks
(shared marks, proposal spans, comments: two sided ends whose overlap and
convergence are a store's problem). *That filed ask fired stage D* (S195,
ruling R-23, PRD 0021): the range half now ships as the `Verge` boundary
vocabulary, the `Rhapsody::extent` projection (verdicts total: covered,
empty, inverted, dangling; a deleted boundary element still bounds through
its tombstone slot, the degrade its own recipe proved), and the
`Scholia<T>` mark store (tags opaque; sharing and editing are the register
recipes; `anchor_dots()` is the pin set its retention cadence subtracts
before minting claims). Expansion at a span's edge is the side choice at
mint (After-start / Before-end expand; Before-start / After-end do not),
so its selection, comment, and proposal-span models choose behavior by
anchoring, and no schema entered the crate. Adoption is its call through
its own gates.
*The reorder half of stage C fired on the COLLAB-8 defect the same way*
(S196, ruling R-24, PRD 0022): movement now ships as testimony
(`Metathesis` / `Metatheses`, in its own causal pair beside the text's)
plus the `Rhapsody::recension` replay, so a moved element keeps its identity
and its stranded-edit exhibit flips (a concurrent edit anchored to the moved
element follows it, never stranding), with concurrent-move cycles refused
deterministically and surfaced in `refused()`. Undo is a `retract` of the
testimony's dot. The *subtree* half (blocks reorganizing a structured page)
stays the named ask on the pages/blocks surface alma does not yet run.
*The adoption blocker fired next (S203, ruling R-30, PRD 0022 R7):* a
per-keystroke editor that adopts moves must read the document through a
`Recension` on every event, and the eager replay at 64k chars
(~26.8 ms) sat four orders of magnitude above alma's every other
per-event term, so the cure for COLLAB-8 was unadoptable in practice.
`Recension::collate` is the door: hold one `Recension` beside the pair,
call `collate(text, moves)` after every compose, absorb, retract, or
retirement round, and read through it; the call costs the changed
regions' replay plus per-call possession scans (since S205, ruling
R-32: one `O(pages)` word-comparison walk over the visible occupancy planes plus flipped-dot extraction (the visible scan's cost no longer depends on its have-set fragmentation), beside the woven have-set diff (floor-compact on honest traffic) and the surviving-testimony record scan;
~1.4-4.9 us per keystroke under a live record on the bench fixture,
~0.3-3.4 us caught-up, ~8-17 us per block-shaped move event) and is
always exactly equal to a fresh `recension()`.
*The owed number ran the same day (alma COLLAB-9, 2026-07-10), the
adoption landed, and the instrument bit twice.* Alma's `:move` now rides
the machine end to end (announced relocation intent paired at its owner
core, the two-phase root-and-straggler recipe read against the evolving
collation, every relocation normalized to its upward complement so a
typed document's downward destination never sits inside the mover's own
subtree, testimony hygiene in its retirement round) and the COLLAB-8
exhibit flipped: a concurrent edit follows the moved line through real
editor input. The measured per-event profile (release, replica layer, a
2 KB document under interior churn: 512 keystrokes, 124 line moves, 447
surviving testimonies, ~500 fertile interior tombstones): keystroke
~137 us, line move ~83 ms, absorb ~6.9 ms per round. Two seams now have
field numbers where the bench fixtures had none:
* *The collation's possession scans cost their representation, not the
change* (~135 us per keystroke with an EMPTY movement record, so the
term is the mark diffs, not the move machinery; per-diff attribution
between the woven and visible scans is still owed, both riding the
same `DotSet::difference` mechanism): each scan pays a tree lookup
per station and a tree-membership probe per retained EXCEPTION (the
have-set entries above the floors) beside enumerating the actual
differences, a churned document's visible have-set is nearly all
exceptions above a low floor, fertile interior residue cannot be
retired away, and the `collate_typing` fixture never deletes, so the
term was invisible upstream. This is the collation's next number,
unnamed until now: the cure direction is a possession pass costing
the pair's actual difference, its own slice against this profile as
the instrument (with the per-diff isolation its first measurement).
*Cured the same day (S205, ruling R-32)*: the visibility diff is now
one paged occupancy walk (a 64-bit mask per station and 64-dot page,
word compares plus the flipped dots, exact for any pair), and the
new `collate_churn` instrument, built to your COLLAB-9 shape, fell
from ~57 us / ~2.6 ms per churned keystroke at 4k/64k chars to
~2.5 / ~4.9 us, parity with an undeleted document. Alma's S225
re-profile confirms the cure at field shape: the empty-movement control
fell from COLLAB-9's roughly 135 us to 8.38 us per keystroke, with a
465 us absorb.
* *The fragment-splice residual (R-30's named next cure) has its field
number*: ~83 ms per line move on a 2 KB typed chain, the chain-shaped
region re-placed at slot granularity once per testimony. *Cured the
same day (S204, ruling R-31)*: the placed-to-placed re-placement now
splices the region through the order thread as whole fragments, so a
chain-shaped region costs its runs, never its characters. The
`collate_move_suffix` instrument fell from ~642 us / ~3.45 ms /
~14.4 ms to ~3.5 / ~14.4 / ~46.8 us across 4k/16k/64k (183x to 307x,
a same-session stash A/B on one machine state), with typing, the
fresh move, the idle call, and the eager replay unchanged within
noise. Alma's S225 re-profile confirms a material but incomplete field
improvement: the line move fell from roughly 83 ms to 46.66 ms. The
operation remains frame-breaking because alma expresses one line move
as a root testimony followed by boundary-straggler testimonies, each
collated against the intermediate reading.
*The predicted record-scan sequel did not fire (S225, ruling R-43).*
With 447 surviving testimonies the live arm measured 11.52 us per
keystroke, 46.66 ms per line move, and 3.91 ms per absorb. Two internal
prototypes tested the roadmap's next-cure hypothesis directly: exact
copy-on-write testimony pages removed the recension's whole-record diff,
then a maintained target index also removed `moves_of(target)`'s scan.
The line move stayed at 47.35 ms and 48.04 ms in the same release window,
so neither prototype shipped. The dominant remaining term is repeated
region replay across alma's multi-testimony recipe, not record lookup. A
batch movement/replay door now needs a joint semantic design: later
stragglers choose anchors from intermediate readings, so merely deferring
`collate` would change placement. The 1,600-round retention profile
reproduced its earlier counts and bytes exactly, leaving the memory and
epoch premises unchanged.
*That door shipped as S226 (ruling R-44).* Call
`Recension::with_movement_batch(text, &mut moves, |batch| ...)` only for a
local, strictly forward movement recipe over a fully placed reading. Minerva
retains ownership of the scope while the callback borrows a capability over
the concrete `Composer<Metatheses>` beside the recension and
exposes only `locus`, `children_of`, `anchor_for_visual_insert`, and
`move_to`. Both the canonical text topology and the current effective reading
must be fully placed; a move-repaired dangling birth stays on `collate`, since
superseding its repair could expose the dangling region again. Every accepted
testimony updates the effective topology before the
next placement read; tidy re-moves unwind the superseded play-log suffix first,
because alma exhibited a move that looks cyclic until its prior testimony is
removed. The positional thread is materialized explicitly before the owner
returns, with `Drop` only the unwind backstop, and full order/offset reads are
unavailable while it is deferred. Typed refusals occur before record mutation; delayed,
dangling, nonmonotonic, or station-exhausted work stays on total `collate`. The clean alma
prototype reduced the 64-line churn arm from 50.47 ms to 32.68 ms per line
move and passed its structural and convergence suites. Alma adopted the
boundary as COLLAB-11 (`89f0895d`); three release runs on the live consumer
measured 17.94 to 18.08 ms per line move, 12.01 to 12.45 us per keystroke, and
4.11 to 4.18
ms per absorb with the same 447 surviving testimonies. Alma observes every
received movement rank before merging, maintaining the batch's global-rank
precondition even when the next destination bucket has a lower local maximum.
The remaining move cost still needs phase attribution before a positional
planner is justified. Do not apply the batch mechanically: the clean
two-testimony chain control is slower at 596 us versus 198 us sequential.
For first-party attribution, enable `timing` and use
`MovementBatch::move_to_profiled`. Its `ProfiledMovement` carries the ordinary
witness and causal delta beside causal-record composition and a structured
topology profile: one outer duration, disjoint play-search, suffix-unwind,
suffix-edit, and suffix-replay durations, and exact search, suffix, cycle-walk,
refusal, and locus-replacement counts. Cycle validation is nested under suffix
replay with its own duration and exact exclusive guard classes, bounded-walk
nodes, and terminals. The capability implies `std`; bounded
structural counts remain available through the separate `instrumentation`
feature without changing the core's `no_std` floor.
The structured `topology` field replaces the provisional flat
`topology_placement` duration introduced one slice earlier. The old field is a
deprecated compatibility alias populated from `topology.total`; new code
should not depend on it.
The cycle refinement intentionally breaks the unreleased flat work fields
rather than retain two mutable-looking sources of truth. Migrate
`topology.work.cycle_guards_skipped` to
`topology.cycle_validation.work.guards_skipped`;
`cycle_guards_entered` to the sum of `self_guard_entries`,
`live_guard_entries`, and `stale_guard_entries`; `cycle_chain_nodes_visited` to
`chain_nodes_visited`; and `cycle_refusals` to `refusals`. The remaining
topology-work fields keep their paths.
The allocation-free follow-on removes the provisional
`repeated_node_terminals`, `visited_sets_created`,
`visited_set_insert_attempts`, and `visited_set_insertions` fields.
`anchor_cycle_terminals` retains the repeated-node terminal's meaning without
exposing the discarded set implementation. A nonzero count means an entered
walk ended in a legal non-target cycle of unplaced birth anchors; it does not
refuse the play. `invariant_refusals` counts only exhaustion of the larger
defensive bound after allocation-free Brent detection has had enough steps to
terminate on any finite skeleton. Such a play refuses fail closed.
On Alma's 64-line field profile, the topology structure isolates suffix replay
as the residual: 10.18 to 10.30 ms of 10.30 to 10.43 ms topology application,
about 668 replayed plays and 210,049 cycle-chain nodes per move. Search and
suffix editing are negligible. Nearly every replay enters the monotone cycle
guard, so the next experiment belongs to validation rather than a target index
or positional planner.
That validation experiment finds the guard mostly live, not stale. Three
unchanged release runs classify 598.3 live-child and only 27.9 stale-monotone
entries per move, beside 40.5 self entries and 1.2 skips. Cycle validation costs
10.71 to 10.90 ms and 210,049 visited nodes produce 209,939 distinct fresh-set
insertions. Pruning stale guard membership cannot remove the dominant work.
The allocation-free bounded walk validates that representation. Three stable
release runs preserve the exact workload shape and report no non-target anchor
cycles or invariant refusals while cycle validation falls to 5.020 to 5.113
ms, topology application to 5.294 to 5.418 ms, and the whole line move to
11.709 to 11.940 ms. The repeated two-testimony control measures 165.54 us
sequential and 575.07 us batched, so the explicit batching policy remains
necessary. Cycle validation and the consumer's 4.911 to 4.953 ms
straggler-tail ancestry walk are now co-dominant. A shared exact ancestry
capability is justified, and S228 (ruling R-46) settles its contract without
selecting a representation. It is an inclusive relation over a fully placed
effective reading: `Same`, `Descendant`, or `NotDescendant`, with unknown dots,
unplaced state, and invariant exhaustion kept as typed failures. `Recension`
owns the derived coordinate; a movement batch temporarily holds that same
coordinate and updates it atomically with every reverse-unwind and
forward-replay locus transition. Reverse unwind restores logged loci without a
cycle query and may privately enter `Unplaced` for a historically unknown
anchor; bounded fallback and incremental repair must restore `Ready` before
`move_to` returns. If changed suffix verdicts leave the tentative result
unplaced, Minerva restores the exact pre-call topology and ancestry state and
all per-call batch bookkeeping, including `anchored`, `highest_rank`, and the
changed flag, then returns `MovementBatchError::UnplacedResult` before
composing a testimony.
The error carries the requested target and first unplaced dot; earlier
successful calls in the batch remain committed.
Forward origin moves bypass the dot relation and link the synthetic root. The
coordinate may not be cloned or rebuilt per batch, and queries or admitted
batch updates may not allocate coordinate storage. Replay fails closed by
refusing an indeterminate play; a consumer must abort before composing its
first testimony. See xref:metis-exact-ancestry.adoc[the exact ancestry
capability]. Total collation keeps the bounded parent-walk fallback for lawful
unknown or unplaced destination anchors, preserving the unborn non-refusal and
dangling-arrival re-evaluation; capability unavailability cannot change those
movement verdicts. Only an invariant failure after both operands are known and
placed refuses fail closed. An index, topology overlay, or lower-plane reuse
still needs a measured concrete comparison before production code changes.
Batch admission returns `MovementBatchError::AncestryUnavailable` for
coordinate capacity or validation failure without invoking the callback;
`UnplacedReading` remains the fully-placed topology gate.
S229 (ruling R-47) selects the private representation but ships no API.
A detached rooted link-cut spike put one recension-owned coordinate on Alma's
consumer and replay query paths. The unchanged field arm fell to 2.085 to
2.133 ms per line move; the tiny sequential and batched controls fell to 25.48
to 37.51 us and 422.75 to 430.73 us. The spike's seven delayed/unplaced
failures are the implementation gate, not accepted behavior: Minerva must ship
R-46's parked state, repair, rollback, typed errors, and reference-walk laws
before Alma may adopt the capability. See
xref:metis-exact-ancestry-representation.adoc[the representation decision].
S230 (ruling R-48) implements the private compact-addressed node arena,
transactional state construction, bounded link-cut core, and reference-walk
oracle. S231 (ruling R-49) activates it under sole `Recension` ownership.
`Recension::with_exact_ancestry` and `MovementBatch::relation` lend only a
borrow-scoped exact query; admission and queries retain separate typed failures.
Every effective-locus transition now synchronizes the coordinate, batches
transfer it without clone or rebuild, and tentative unplaced suffixes roll back
before record composition. S232 completes Alma's adoption: straggler-tail
discovery borrows `ExactAncestry` once across all candidates, release builds
perform no parent walk, and debug builds retain the old walk only as an
independent oracle. The intentional instrumentation migration reports exact
query counts instead of parent-chain visits.
The residuals alma recorded on its side: undo of a move replays the
reverse hull as a plain edit and re-forks (the retract-shaped cure
joins its identity-anchored undo ask), and the end-of-buffer
trailing-newline shapes are not element rotations and keep the plain
capture.
Its transport existed only in-process when this paragraph was first
written. Its TRANSPORT-1/TRANSPORT-2 slice (S271, ruling R-55) was
audited in its *uncommitted* worktree: alma transport v1 there carries
the rhapsody v2 frame, the have-set frame, and the kairos stamp beneath
an authenticated envelope (the registry section above records the
observed shape under the Polis S3 waiting rule), and spells its
movement section as `(testimony dot, target dot, sided anchor, kairos
rank)` per testimony. The movement-record codec activated on alma's
same-day activation request (S272, ruling R-56, refining R-55's timing
gate): the canonical `Metatheses` frame ships now (the registry section
above records the shape and what still waits), the layout being
minerva's own store spelling rather than a copy of alma's bytes, while
the R-8 registry pin of alma's embedding, the coordinated-bump duty,
and their duplicate parser's retirement wait on their landed commit
exactly as before. The generation-qualified address frame family's
"consumer that serializes dots" trigger remains treated as observed,
not met, with the frame charter deliberately held for the coordinated
design session where alma brings its inventory (their event inventory
arrived with the same letter; the charter still waits for the landed
evidence commit and the versioned table it promised). The arc 10 number the profile was named
for was measured and cured (S191, ruling R-21, adopted the same session);
arc 11's phase-one ruling has its consumer profile beside the S192
instrument numbers.
*The positional reads its viewport math was priced against now exist*
(S200, ruling R-27, additive): `Rhapsody::order_len` (`O(1)`),
`order_at(offset)` / `offset_of(station, dot)` (the offset coordinate in
both directions, `O(log)`, tombstone slots included with the extent
read's degrade), and `order_walk_rev` / `order_walk_rev_before` (the
scroll-up window), all delegated by `Recension` for a moved reading. An
editor that renders by offset or does caret arithmetic no longer pays an
`order()` pass per question (~4.5 ms at 65k chars; the reads measure
nanoseconds, flat). Adoption is its call through its own gates.
*The paste door now exists* (S211, ruling R-33, additive): a paste,
import, or file-open stops being ten thousand individual weaves. Reserve
the run's ranks in one clock commitment (`clock.now_run(len, tag)`),
weave it in one call (`Rhapsody::weave_run(first, anchor, run)` (the reservation is consumed: affine, spent at most once),
all-or-nothing, through your existing `Composer::compose` closure exactly
like a keystroke), and ship the one delta; the receiving fold absorbs a
run-shaped delta at run grade on its own, with no adoption step on the
receive side. On the recorded window a 100,000-character paste fell from
~116 ms to ~4.1 ms locally (fresh document; ~12.5 ms into the 64k churned
fixture) and its absorb from ~37.5 ms to ~2.8 ms (fresh; ~5.4 ms
churned), every arm inside the 16 ms frame budget. The members chain by
the clock fold's own successor rule, so a pasted run coalesces in the
identity plane, the order thread, and the snapshot codec exactly as
patient typing would (byte-identical snapshot bytes, pinned). The paste
exchange's fixed removal-context term on a churned document (the
have-set carrying every prior removal, ~524 KB on the S210 fixture) is
deliberately untouched: that is the epoch/retention track's territory,
not the ingest door's.
=== `polis` (the replicated coordination ledger)
*The committed core is a causal fact store, not a delivery queue.* At Polis
`45b17aa`, one immutable coordination fact occupies one dot in
`Dotted<DotMap<ThreadId, DotFun<Fact>>>`, and `Composer` is the sole local
assignment path. Thread state is a total caller-owned projection over partial
receipt. Concurrent dispositions remain concurrent facts; Minerva merges them
and Polis derives disagreement without asking timestamp order to decide it.
*Keep same-dot disagreement ahead of total merge.* `DotFun` keeps self when two
copies carry one dot because its basis invariant says honest copies carry equal
content. Polis owns the boundary that can falsify that assumption: authenticate
the station, reject duplicate candidate dots, compare the whole candidate batch
with committed content, and only then construct a covered delta. Do not make the
causal-store lattice fallible to absorb equivocation policy.
*Exact repair is adopted.* Polis S2 (`45b17aa`) derives possession from the
causal context, serves `local.difference(peer).take(...)`, pages `holes_for`,
reports `hole_count`, and preflights a bounded candidate context before one merge. Its fact-count,
payload-byte, station, hole, exception, and forward-gap limits remain Polis
policy. In particular, a one-dot bounded insert would not protect the actual
batch merge through `Composer::absorb`; keep the clone-and-check path until a
landed profile proves a reusable Minerva mechanism is needed.
*The have-set frame has its first landed protocol embedding.* Polis S3
(`5d10141`) places
`DotSet` v1 bytes inside an authenticated, allocation-bounded possession
envelope. Its `max_exceptions` limit is exactly the expansion ceiling Minerva
needs before materializing wire runs, so S242 exposes `HaveSetDecodeBudget` and
the explicit-budget exact and prefix decoders. Pass the validated exception
limit into `Possession::from_wire_bytes`; only then apply Polis's station, hole,
and forward-gap checks. The v1 bytes and unqualified `from_bytes` secure default
do not change. Ruling R-8 now pins the inner v1 bytes to Polis possession
protocol v1. Minerva's property and fuzz target cover the exact-slice,
explicit-budget seam; Polis separately covers the complete authenticated frame.
Persistence, authentication, payload framing, retry, station leasing, and
repository authority stay in Polis. `Stability` waits for its later fixed-roster
deployment, durable application-delivery reports, and an actual retention
operation. A path dependency that still resolves against `../minerva` is not the
0.2.0 release trigger; a clean environment that cannot provide the sibling is.
== The caller-gated menu
These are designed and deliberately unbuilt. They are not a roadmap we will deliver on our own
schedule; they are options you can pull. The "what we need" column is the boundary we will not
guess at. Pulling one means: tell us the use case, and where the listed decision lives in your
crate.
[cols="1,2,3,2",options="header"]
|===
| Item | What it gives you | What we need from you | Spec
| Skew clamp policy | The _policy_ half of the trust-aware receive above: an *advance-but-cap*
variant beside `try_observe`'s reject, for a deployment that prefers to admit a too-far-future
stamp with bounded effect rather than drop it. The reject mechanism shipped in S24.
| *`plethos` / `pinax` declined clamp* (ADR-0012: it fights pinax's clock-dominates-stream invariant),
so this now waits on a *different* consumer that wants advance-but-cap, and on its clamp semantics
(cap the clock advance only, or also rewrite the admitted stamp). We will not pick the tradeoff for
you; compose your policy on `try_observe`'s `SkewExceeded` until one does.
| PRD 0008 (adversarial reading)
| Stability roster membership change | The _policy_ half of the causal-stability watermark above:
join / leave / crash semantics over a live `Stability` roster (regress-on-join, un-pin-on-leave,
epoch'd trackers), or a richer cross-node type (a CRDT-of-trackers that gossips itself, a
bulk-report constructor). The meet mechanism shipped in S78.
| A caller's membership model: who declares the family, and what a joiner or leaver means for
state already reclaimed below the old watermark. We will not invent membership semantics; build
a new tracker per roster epoch until one exists.
| PRD 0011 (open questions)
| Frontier resolution (_deinotēs_) | The narrow selective-release mechanism now ships as
`pop_ready_event_where`: filter the live ready set while `Ideal` retains readiness and progress
ownership. The broader typed seam for what a concurrent frontier _means_ (keep siblings, pick a
winner, or fold a join) remains caller-side.
| The resolution algebra and where converged state lives: an open `Resolver<T>` trait or a
closed `Merge<T>` enum, per-key or per-frontier. The most deliberately held item: we will not
carve this without a caller drawing it, and the first real conflict-consumer (`ethos`'s
enactment stack: its own gate with a caller-typed `Dep` and per-proposal `Progress`, its ordinal
conflict pick a strict total order folded *above* the release stream) is now on record needing
no seam; its anticipated combined request was withdrawn. The narrow signal later fired when
stokes needed to skip releases ahead of irreversible external effects. That caller drew only
frontier filtering, so no `Resolver<T>` framework or merge catalog followed it.
| PRD 0008
| `Producer` / codec under a generic gate | `metis`-blessed minting and framing for a
non-causal gate (e.g. `Fifo`): produce `Event<P, u64>` and frame it, the send-side mirror of
the generic `Ideal`.
| A scalar (FIFO) caller that wants minting from `metis` rather than rolling a per-source
counter itself (which is trivial, hence deferred).
| PRD 0009 R7
| Unified `Station` endpoint | One type owning a `Producer` + an `Ideal`, with the wiring done
for you, instead of composing the two materials by hand.
| A caller that wants the bundle. We default to keeping them orthogonal (you wire send and
receive), so this is convenience, not capability.
| PRD 0006 (non-goal)
| Dependency index | An index over `pending` to beat the linear `O(pending)` scan in the
buffer.
| A real workload where that scan is the bottleneck. Pure optimization; we will not add the
complexity speculatively.
| PRD 0005 (open question)
| Bounded `DotSet` admission follow-through | The canonical run-length
wire frame (`to_bytes` / `from_bytes` / `from_prefix`) and the per-station repair reads
(`holes_for(station)`, `hole_count()`) shipped as PRD 0016 (S98) and are in the stable surface
above. Polis S2 shows why the predicted bounded one-dot insertion is not the
actual boundary: a whole fact batch must be preflighted before causal-store
merge. S242 separately closes the dense-frame decode question with the typed
`HaveSetDecodeBudget`; it is not an insertion-capacity mechanism.
| Polis S2 (`45b17aa`) fires bounded reorder exposure and adopts the repair reads. Its whole-batch
cloned-context preflight is correct and bounded; a direct one-dot insertion limit would not guard
`Composer::absorb`. S84 is therefore re-pointed to a landed measurement showing the clone needs
a reusable merge-side mechanism. Polis S3 (`5d10141`) supplies the landed
frame call and exact decode-budget shape; S243 records its R-8 pin and
composed coverage.
| PRD 0016 (open questions)
| Epoch adoption (the consumer-payload half) | The retention exit whole: the epoch rounds
(`Epochs`, declaration through seal over your `Stability` reports), the sealed-stratum shadow
judging in-flight old traffic, and since S263 (ruling R-50) the *seal consignment*:
`EpochShadow::consign`, given the seal witness alone, checks the sealed join is covered
exactly (typed `Incomplete` / `BeyondSeal` refusals that hand the shadow back for the cure,
never a silent drop) and hands you the next generation's opening base pair with the two
document planes carried across the boundary whole (window births, moves, and deletes at their
judged places, floors born gap-free) and the retired map riding along as your translation
seam. One duty rides the carry: defer native movement testimonies to the seal (the consignment
re-spells the topology movement acceptance depends on, so a window-time verdict can flip; the
window is bounded at two watermark rounds). The
document planes cross on their own; what remains is yours: re-translating external references
(cursors, held mark identities, content hashes) through the retired map, the
generation-qualified address frame (a coordinated R-8 event), the map's retention window, and
the marker-conversion inventory (text deletes and future undo retracts become marker-dot
mints; the fleet replica, `src/metis/tests/fleet/`, is the executable recipe). One further
conversion is load-bearing: every plane and protocol event adopted together must draw from
one event space per station. Separate self-allocating `Composer` facades collide counters
across planes and make the affine image unsound. Keep one explicit allocation ledger, reserve
every weave, delete marker, movement testimony, and declaration through it, and report the
floor of the adopted planes' union. Alma COLLAB-17 uses the movement pair's context as that
ledger; minerva does not yet ship a multi-plane composer facade. Since S264
(ruling R-51) the recipe also carries the crash-restart duties: fence your durable store
before anything you emit (your mints and your acknowledgements are the hard points; a
re-derived allocator that forgot an emitted dot forks identity forever), report only your
durable floor (the tracker's join masks a post-restart regression by design), checkpoint the
consigned base, the sealed lineage, and a clock high-water at the seal (the re-foundation
re-bases ranks, so the sealed window's spoken stamps survive in no plane you could walk), and
recover by replaying your journal through your ordinary delivery path, observing every
journal-carried stamp before you mint again, plus bidirectional anti-entropy that re-serves
under original stamps only (re-serve what your journal says you have said, not only pull what
you missed: a crash can land between the fence and the send, and a freshly minted repair
stamp would itself be spoken-but-unfenced), never by a second code path. One honesty note: the fleet's journal is an in-memory
*model* of those duties (what must be durable before which emission), not a shipped
persistence surface; spelling your durable store's outer bytes stays yours. What minerva
ships since S273 (ruling R-57) is the lifecycle machine's own checkpoint: `Epochs::snapshot()`
captures the complete record (generation, roster, horizon, the open window whole with its
candidates, both report rounds, the winner latch and adoption bit, and the retained
recognizer lineage) as an inert `EpochLedgerSnapshot` with a canonical frame (`to_bytes` /
`from_bytes` under a caller-owned `EpochLedgerDecodeBudget`, golden fixture, proptests, and
the `epoch_ledger_from_bytes` fuzz target). The trust seam is structural: decoded bytes are
never a machine, an `Adopted` or a `SealedEpoch`; `Epochs::rehydrate(&snapshot, roster,
horizon)` is the one explicit local-trust decision and refuses a checkpoint that does not
match your current configuration, in-flight rounds ride whole so a clean restart is exact,
and the affine adoption witness is re-earned by calling `adopt` against live stability
evidence, never deserialized. Peer bytes stay an inert snapshot you may compare or retain;
never rehydrate from an ordinary peer. Wrap the frame in your own authenticated checkpoint
record (that embedding is this row's coordinated R-8 event); atomic persistence, fencing,
digest policy, and recovery orchestration stay yours.
| Field-consumed by alma COLLAB-17/18. A further adopter should bring the inventory of references
it holds across generations, how long it needs the retired map answerable, and which one
station-scoped ledger allocates every adopted-plane and protocol dot.
| PRD 0024 (stage four)
|===
== Hazards to design around now
Sharp edges in the stable surface. None is a bug; each is a boundary the crate deliberately
does not police, so you must.
. *Unbounded forward skew on `Clock::observe` under untrusted stamps.* Minerva tolerates and
reports clock skew; it does not correct it and does not synchronize physical clocks. For
honest peers this is benign. Once your transport admits _untrusted_ stamps, a far-future
remote stamp passed to `observe` (directly, or through `Producer::observe`) drags your clock
forward with no bound. The defense now ships at both levels:
`Clock::try_observe(remote, max_forward_skew)` rejects a stamp past your bound (see the
trust-aware-receive section), and `Producer::try_observe(&event, max_forward_skew)` is the
same bound over the producer's whole receipt (stamp *and* knowledge, all-or-nothing; see the
shared-clock section). Authenticate the remote's `station_id` first, then `try_observe`
instead of `observe` at your trust boundary; a skew bound on an unauthenticated peer is
theater (it rotates identities). As of 2026-07-02 this is deployed practice at every ingress
that exists, from both directions over the one `station_id` space: `ethos`'s `Πολίτευμα` binds
key-to-station at the deed feed, `pinax`'s `Politeuma` seam binds station-to-key at the ledger
ingress. See the field recipe in the trust-aware-receive section.
. *A `VersionVector`'s counter meaning is yours, and two meanings do not mix.* The type is a
per-`station_id` `u64` count lattice; what a count _means_ is caller-defined. `metis` itself
already reads it two ways (a `Producer`'s delivered-knowledge versus an `Ideal`'s
released-count). Vectors carrying different meanings over the same `station_id` space compare
and merge structurally but are *not* interchangeable. If you run both a `Producer` and your
own consumer (a replication sequence watermark, say), hold two distinct vectors and never
feed one where the other is expected.
. *Index basis: 1-based dot versus 0-based sequence.* `metis` is 1-based: an event's dot is
`deps.get(sender) >= 1`, so "next owed from `s`" is the _strict_ `dot > D.get(s)`. A consumer
keying on a 0-based sequence asks the _non-strict_ `sequence >= watermark.get(s)`; the
translation is `sequence == dot - 1`. Crossing the two bases is a silent off-by-one the type
cannot catch (a 1-based dot tested with `>=` re-delivers the boundary event; a 0-based
sequence tested with `>` skips it). If you mix `metis`-minted vectors with your own, make the
basis unrepresentable-to-confuse: a `Watermark(VersionVector)` newtype, or `next_sequence` /
`count_seen` accessors, not a bare `get`.
. *The `Kairos` constructor argument order is not the wire significance order.* `Kairos::new` takes
`(physical, logical, station_id, kairotic)`, but the stamp ranks `kairotic` *above* `station_id`
(the kairotic dimension is more significant), so the canonical frame is
`[0x01, physical(8), logical(2), kairotic(2), station_id(4)]`. Hand-rolling a stamp's bytes from
the constructor argument order silently swaps the `kairotic` and `station_id` fields, breaking
both the byte layout and the sort. Never reconstruct the layout yourself: encode with
`Kairos::to_bytes()` and split with `KAIROS_WIRE_LEN`. (This is the seal divergence `ethos` hit;
see per-consumer integration above.)
. *The knowledge join trusts the claim: an event's `deps` are an assertion, not an observation.*
`Producer::observe` folds a received event's dependency vector into observed-knowledge by an
unbounded monotone join (`knowledge.merge(&event.deps)`), and `Producer::try_observe` bounds
only the *stamp's* physical skew, not the vector. So one accepted event whose `deps` claim a
counter far past anything that exists (an authenticated-but-compromised peer can sign that lie;
hazard 1's identity defense does not reach it) poisons your knowledge *permanently*, since a
join cannot be undone, and every event you mint afterwards carries the poisoned entry in its
`deps`: at every correct causal gate your own output now waits on an event that will never
exist, neither deliverable nor stale, held until capacity. This is the vector half of hazard 1,
and unlike the stamp there is no bound to ask for: a physical reading gives forward skew a local
reference point, but a dependency counter has no local ground truth, so any numeric bound would
be theater with a type signature. The defense is structural, a composition discipline. On a
*trusted* mesh, receipt-level `observe` keeps its order-robustness (feed arrivals in any order;
knowledge only grows). At an *untrusted* boundary, feed the producer from the `Ideal`'s releases
instead of raw receipt: `pop_ready_event` then `observe(released.event())`. A poisoned-deps event never
becomes deliverable, so it never reaches your knowledge, and delivery-fed knowledge is exactly
the Birman-Schiper-Stephenson send rule (your minted `deps` reference only what you actually
applied, where receipt-level knowledge *overstates* your causal past by everything received but
not yet applied, which is safe but delays your events' deliverability at every receiver).
Distrusting the lying peer itself stays the adversarial deinotēs reading (PRD 0008), caller
policy over minerva's reads.
== Requesting a change
The posture is deliberate: we build ahead of a caller only for cross-cutting production
invariants the whole crate already honors. The bounded `try_insert`, the skew-bounded
`try_observe`, and the `Stability` watermark are the three instances, each justified by the
bounded-resources capability boundary rather than any single consumer, and each shipped in the
same four-part shape (mechanism, typed refusal, caller policy, observability read; the target
architecture names it the resilience pattern). The test we apply is recorded as ruling R-6: the
boundary must already own the concern *and* the hand-rolled alternative must fail unrecoverably,
not merely redundantly. Everything that encodes _application_ semantics waits for you.
To pull a menu item, open the conversation with: the use case, the specific decision from the
"what we need" column and where it lives in your crate, and whether you need it as a stable API
or just a seam to prototype against. That is enough to un-gate the design and schedule the
slice. Breaking changes to accommodate you are on the table while there is no other external
consumer; we will call them out here when they land.
== Where the authoritative detail lives
[cols="2,3",options="header"]
|===
| Document | For
| xref:../DOCS.adoc[`DOCS.adoc`] | The documentation index and its maintenance rules.
| xref:target_arch/index.adoc[`docs/target_arch/`] | Invariants, capability boundaries, non-goals.
| xref:glossary.adoc[`docs/glossary.adoc`] | The load-bearing vocabulary (kairos, mētis, gate, deinotēs).
| `docs/prd/NNNN-*.adoc` | Per-capability requirements and the decisions behind them.
| xref:../src/metis/mod.adoc[`src/metis/mod.adoc`], xref:../src/kairos/mod.adoc[`src/kairos/mod.adoc`] | The implementer's view, co-located with the code.
| xref:kairos-minerva-reconciliation.adoc[`docs/kairos-minerva-reconciliation.adoc`] | The `kairos` / `plethos` convergence and its ADRs.
| xref:metis-gate-algebra.adoc[`docs/metis-gate-algebra.adoc`] | The gate algebra: composing gates (`And` / `MapDep`), why `Or` is excluded, the dynamic contract for authoring a gate.
| https://github.com/OwlRoute/minerva/blob/main/todos.adoc[`todos.adoc`] (repository only) | The living slice tracker: unresolved work only; completed slices live in git history.
|===