tidaldb/tidal/src/governance/scope.rs
jx12n 3bcfb3c576 feat: Bazel build, crate docs/ai-lookup, docker images, and engine hardening
- Add BUILD.bazel across tidal, tidal-net, tidal-server, tidalctl for bzlmod build
- Add tidal/ crate docs (README, CHANGELOG, CONTRIBUTING, AGENTS, CLAUDE, API, ARCHITECTURE) and ai-lookup reference
- Add docker standalone/cluster/deploy images, compose, and prometheus config
- Harden WAL (batch format, writer, dedup, diagnostics), text syncer/collectors, and vector registry
- Expand tidalctl CLI and tests; restructure WAL/visibility integration test suites
- Refine tidal-net transport/client/server and tidal-server cluster/scatter-gather
2026-06-07 18:29:38 -06:00

194 lines
6.4 KiB
Rust

//! Signal scope: where a signal lives and whether it may ever leave the node.
//!
//! [`SignalScope`] is the load-bearing M9 primitive. The default is
//! [`SignalScope::Local`], and local-scoped signals **never ship** to a
//! community and are **never purged** by community operations — this is the
//! "local profile remains intact" guarantee that every M9/M10 surface upholds.
//!
//! On the WAL wire the scope is persisted as a single discriminant byte (see
//! [`SignalScope::discriminant`]). The community id carried by
//! [`SignalScope::Community`] is *not* stored in the base event record: which
//! community a share-eligible event flows into is decided at ship time from the
//! writer's [memberships](crate::governance::membership), so the persisted byte
//! records only the scope *class*. Decoding a community byte therefore yields
//! `Community(CommunityId::NONE)`; the concrete id lives in the in-memory API
//! value and in the community ledger's keys, not in the per-event envelope.
use crate::wal::error::WalError;
/// Identifies a community personalization overlay.
///
/// `CommunityId(0)` ([`CommunityId::NONE`]) is reserved to mean "no community"
/// and is what a community scope decodes to when read back from the WAL (the
/// concrete id is membership-driven, not stored per event).
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub struct CommunityId(pub u64);
impl CommunityId {
/// Sentinel for "no community".
pub const NONE: Self = Self(0);
/// The raw community id.
#[must_use]
pub const fn as_u64(self) -> u64 {
self.0
}
/// Whether this is the reserved [`CommunityId::NONE`] sentinel.
#[must_use]
pub const fn is_none(self) -> bool {
self.0 == 0
}
}
impl std::fmt::Display for CommunityId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "c{}", self.0)
}
}
/// Scope of a signal: where it lives and whether it may ever ship.
///
/// - [`Local`](SignalScope::Local) — default; never ships, never community-purged.
/// - [`Community`](SignalScope::Community) — eligible to contribute to community
/// overlays subject to the writer's [`SharePolicy`](super::share_policy::SharePolicy).
/// - [`Session`](SignalScope::Session) — scoped to one agent session.
/// - [`Agent`](SignalScope::Agent) — attributed to an agent; removable by agent scope.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
)]
pub enum SignalScope {
/// The default. Stays on the local node forever.
#[default]
Local,
/// Contributes to a community overlay when share-eligible.
Community(CommunityId),
/// Scoped to an agent session.
Session,
/// Attributed to an agent.
Agent,
}
impl SignalScope {
/// The 1-byte wire discriminant for this scope class.
#[must_use]
pub const fn discriminant(self) -> u8 {
match self {
Self::Local => 0,
Self::Community(_) => 1,
Self::Session => 2,
Self::Agent => 3,
}
}
/// Decode a scope class from its wire discriminant.
///
/// A community byte decodes to `Community(CommunityId::NONE)` — the concrete
/// id is membership-driven and not stored in the base event (see module docs).
///
/// # Errors
///
/// Returns [`WalError::Corruption`] for any discriminant outside `0..=3`.
pub fn from_discriminant(b: u8) -> Result<Self, WalError> {
match b {
0 => Ok(Self::Local),
1 => Ok(Self::Community(CommunityId::NONE)),
2 => Ok(Self::Session),
3 => Ok(Self::Agent),
other => Err(WalError::Corruption {
message: format!("invalid SignalScope discriminant: {other}"),
}),
}
}
/// Whether an event of this scope is ever eligible to leave the node.
///
/// `Local` is never shippable; everything else *may* ship subject to a
/// [`SharePolicy`](super::share_policy::SharePolicy).
#[must_use]
pub const fn is_shippable(self) -> bool {
!matches!(self, Self::Local)
}
/// Whether this scope is a community scope.
#[must_use]
pub const fn is_community(self) -> bool {
matches!(self, Self::Community(_))
}
/// The concrete community id, if this is a community scope.
#[must_use]
pub const fn community_id(self) -> Option<CommunityId> {
match self {
Self::Community(id) => Some(id),
_ => None,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn default_is_local() {
assert_eq!(SignalScope::default(), SignalScope::Local);
}
#[test]
fn discriminant_roundtrip_all_classes() {
for scope in [
SignalScope::Local,
SignalScope::Community(CommunityId(7)),
SignalScope::Session,
SignalScope::Agent,
] {
let b = scope.discriminant();
let decoded = SignalScope::from_discriminant(b).unwrap();
// Community loses its id on the wire (decodes to NONE), so compare classes.
assert_eq!(decoded.discriminant(), b);
}
}
#[test]
fn community_decodes_to_none() {
let decoded = SignalScope::from_discriminant(1).unwrap();
assert_eq!(decoded, SignalScope::Community(CommunityId::NONE));
}
#[test]
fn invalid_discriminant_is_corruption() {
assert!(SignalScope::from_discriminant(4).is_err());
assert!(SignalScope::from_discriminant(255).is_err());
}
#[test]
fn local_is_never_shippable() {
assert!(!SignalScope::Local.is_shippable());
assert!(SignalScope::Community(CommunityId(1)).is_shippable());
assert!(SignalScope::Session.is_shippable());
assert!(SignalScope::Agent.is_shippable());
}
#[test]
fn community_accessors() {
let c = SignalScope::Community(CommunityId(42));
assert!(c.is_community());
assert_eq!(c.community_id(), Some(CommunityId(42)));
assert!(!SignalScope::Local.is_community());
assert_eq!(SignalScope::Local.community_id(), None);
}
#[test]
fn community_id_display_and_none() {
assert_eq!(CommunityId(5).to_string(), "c5");
assert!(CommunityId::NONE.is_none());
assert!(!CommunityId(1).is_none());
assert_eq!(CommunityId(9).as_u64(), 9);
}
}