@@ -147,6 +147,26 @@ pub enum EnvelopeError {
147147/// the already-resident backing bytes and *what cycle stamp* the store carries.
148148/// The read-only view here mirrors `MailboxSoaView` vs `MailboxSoaOwner`:
149149/// mutation lives on the owner type, never on this trait.
150+ ///
151+ /// # Ownership inheritance — up and down (operator, 2026-07-02)
152+ ///
153+ /// The LE contract IS the ownership carrier, in both directions:
154+ ///
155+ /// - **Structurally (compile time):** every view this trait hands out
156+ /// ([`as_le_bytes`](SoaEnvelope::as_le_bytes) / [`row_le`](SoaEnvelope::row_le)
157+ /// / [`column_le`](SoaEnvelope::column_le)) is a `&self` borrow OF the
158+ /// implementing owner — a view cannot outlive or alias the mailbox that
159+ /// owns the backing store, and a batch-writer cast (`&mut` on the owner)
160+ /// cannot begin while any view is live. Rust's borrow rules ARE the
161+ /// inheritance; nothing is checked at runtime.
162+ /// - **Nominally (provenance):** [`mailbox_owner`](SoaEnvelope::mailbox_owner)
163+ /// stamps WHICH mailbox owns these bytes, so the ownership survives the
164+ /// two places borrows cannot reach — DOWN into Lance (the tombstone
165+ /// records whose mailbox wrote the packet) and UP to consumers (every
166+ /// consuming crate writes ON BEHALF OF this id, never directly — the
167+ /// fleet-wide write-on-behalf rule; a write cast naming a different
168+ /// mailbox than the envelope's stamp is the delegation case the batch
169+ /// writer's cache logic resolves at cast time).
150170pub trait SoaEnvelope {
151171 /// Layout version this implementor's geometry conforms to.
152172 const LAYOUT_VERSION : u8 = ENVELOPE_LAYOUT_VERSION ;
@@ -166,6 +186,16 @@ pub trait SoaEnvelope {
166186 /// coherent "packet at cycle N".
167187 fn cycle ( & self ) -> u32 ;
168188
189+ /// The mailbox that OWNS this envelope's backing store — the nominal half
190+ /// of the up/down ownership inheritance (see the trait docs). `0` is the
191+ /// bootstrap/unowned stamp per the zero-fallback ladder (RESERVE, DON'T
192+ /// RECLAIM: default = not consulted, never absent); a real mailbox
193+ /// overrides with its [`MailboxId`](crate::collapse_gate::MailboxId).
194+ /// Consumers write on behalf of THIS id, never directly.
195+ fn mailbox_owner ( & self ) -> crate :: collapse_gate:: MailboxId {
196+ 0
197+ }
198+
169199 /// The whole packet as contiguous LE bytes, zero-copy. Length MUST be
170200 /// `row_stride() * n_rows()`.
171201 fn as_le_bytes ( & self ) -> & [ u8 ] ;
@@ -315,6 +345,38 @@ mod tests {
315345 }
316346 }
317347
348+ #[ test]
349+ fn mailbox_owner_defaults_to_bootstrap_and_is_overridable ( ) {
350+ // Zero-fallback ladder: the default stamp is 0 (bootstrap/unowned —
351+ // reserved, never absent), so every existing implementor is already
352+ // conformant; a real mailbox overrides with its id.
353+ let e = two_col_envelope ( 1 ) ;
354+ assert_eq ! ( e. mailbox_owner( ) , 0 , "default = bootstrap/unowned" ) ;
355+
356+ struct Owned ;
357+ impl SoaEnvelope for Owned {
358+ fn columns ( & self ) -> & [ ColumnDescriptor ] {
359+ & [ ]
360+ }
361+ fn row_stride ( & self ) -> usize {
362+ 0
363+ }
364+ fn n_rows ( & self ) -> usize {
365+ 0
366+ }
367+ fn cycle ( & self ) -> u32 {
368+ 0
369+ }
370+ fn as_le_bytes ( & self ) -> & [ u8 ] {
371+ & [ ]
372+ }
373+ fn mailbox_owner ( & self ) -> crate :: collapse_gate:: MailboxId {
374+ 42
375+ }
376+ }
377+ assert_eq ! ( Owned . mailbox_owner( ) , 42 , "owner stamp inherits up/down" ) ;
378+ }
379+
318380 #[ test]
319381 fn kind_widths ( ) {
320382 assert_eq ! ( ColumnKind :: U8 . elem_bytes( ) , 1 ) ;
0 commit comments