frame-conv 0.2.0

Conversation patterns — request-response, subscription, pub/sub, and workflow over liminal
Documentation
//! The handle's typed anomaly surface and named counters (F-3a R2's pinned
//! home): every protocol anomaly is BOTH queued as a typed value the caller
//! drains AND counted on a named, readable counter — never a log line
//! wearing a type, never a silent drop.

use crate::id::{ConversationSeq, CorrelationId};

/// A typed protocol anomaly observed on the handle.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Anomaly {
    /// A second reply arrived for a correlation that was already answered.
    /// The first reply's delivery is unaffected (F-3a R6).
    DuplicateReply {
        /// The exchange that was answered twice.
        correlation: CorrelationId,
        /// Sequence position of the duplicate.
        seq: ConversationSeq,
    },
    /// A reply arrived after its request's deadline had already elapsed.
    /// The caller has already observed exactly one outcome for the exchange;
    /// the late reply is recorded here (F-3a R2 — defined fate).
    LateReply {
        /// The elapsed exchange.
        correlation: CorrelationId,
        /// Sequence position of the late reply.
        seq: ConversationSeq,
    },
    /// A reply whose correlation this handle never issued. In a
    /// multi-participant conversation this is another requester's exchange,
    /// observed because deliveries fan out to every enrolled peer; it is
    /// recorded, not treated as an error.
    ForeignReply {
        /// The foreign correlation.
        correlation: CorrelationId,
        /// Sequence position of the observed reply.
        seq: ConversationSeq,
    },
    /// An ordinary record whose bytes were refused by the envelope codec —
    /// surfaced with its closed refusal slug, never dropped silently.
    MalformedInbound {
        /// Sequence position of the refused record.
        seq: ConversationSeq,
        /// The closed `CONV_*` refusal slug.
        slug: &'static str,
        /// Exact refusal detail.
        detail: String,
    },
    /// A frame outside the expected receive grammar (an uncorrelated
    /// operation answer, or a substrate refusal frame), preserved verbatim.
    UnexpectedFrame {
        /// Exact frame detail.
        detail: String,
    },
}

/// Named, readable counters over the anomaly surface — the counter half of
/// R2's anomaly home. Counters only ever increase and survive draining the
/// queued anomalies.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct AnomalyCounters {
    /// Count of [`Anomaly::DuplicateReply`].
    pub duplicate_replies: u64,
    /// Count of [`Anomaly::LateReply`].
    pub late_replies: u64,
    /// Count of [`Anomaly::ForeignReply`].
    pub foreign_replies: u64,
    /// Count of [`Anomaly::MalformedInbound`].
    pub malformed_inbound: u64,
    /// Count of [`Anomaly::UnexpectedFrame`].
    pub unexpected_frames: u64,
    /// Count of detected delivery-sequence gaps (surfaced as subscription
    /// items; counted here so a non-subscribing caller can still read them).
    pub gaps: u64,
    /// Count of suppressed duplicate publication presentations (F-3b R1's
    /// exactly-once dedup — the O(1) last-presented-sequence comparison;
    /// suppression is counted, never silent).
    pub duplicate_publications: u64,
}