soma-som-core 0.1.0

Universal soma(som) structural primitives — Quad / Tree / Ring / Genesis / Fingerprint / TemporalLedger / CrossingRecord
Documentation
// SPDX-License-Identifier: LGPL-3.0-only
#![allow(missing_docs)]

//! Sibling extension manifest — the structural contract for ring-hosted siblings.
//!
//! ## Sibling extension mechanism
//!
//! Siblings are ring-native applications that run on the host ring application through the ring.
//! Each sibling declares its identity, ring extensions, resource requirements,
//! and structural routes via a `SiblingManifest` implementation.
//!
//! ## DOR-SIBLINGS-F1 / SIBLING-INTEGRATION-SPEC §2.2
//!
//! The manifest uses typed factory methods (not declarative declarations)
//! matching the existing ring extension registration API:
//! - `through_handler()` → `Arc<dyn ThroughRing>` (shared ownership for dispatch)
//! - `before_gate()` → `Box<dyn BeforeRing>` (exclusive ownership)
//! - `after_observer()` → `Box<dyn AfterRing>` (exclusive ownership)
//! - `around_extension()` → `Box<dyn AroundRing>` (exclusive ownership)
//!
//! ## Spec traceability
//!
//! | Item                  | Spec Reference                    |
//! |-----------------------|-----------------------------------|
//! | `SiblingManifest`     | DOR-SIBLINGS-F1 D3, SIBLING-INTEGRATION-SPEC §2.2 |
//! | `DomainRequest`       | DOR-SIBLINGS-F1 D4 (Stage 2)     |
//! | `CapabilityDeclaration` | DOR-SIBLINGS-F1 D5            |
//! | `SurfaceSection`      | DOR-SIBLINGS-F1 D7              |
//! | `RouteMount`          | SIBLING-INTEGRATION-SPEC §1.4   |
//! | `SiblingStatus`       | DOR-SIBLINGS-F1 D4 (lifecycle)  |

use std::sync::Arc;

use crate::extension::{AfterRing, AroundRing, BeforeRing, ThroughRing};

// ── Sibling lifecycle status ────────────────────────────────────────────────

/// Lifecycle state of a registered sibling.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SiblingStatus {
    /// Stage 1: Manifest accepted, validation passed.
    Validated,
    /// Stage 2: Persistence domain provisioned, resources allocated.
    Provisioned,
    /// Stage 3: Extensions registered in ring engine.
    Registered,
    /// Stage 4: Sibling active and receiving commands.
    Active,
    /// Registration failed at some stage.
    Failed,
}

impl std::fmt::Display for SiblingStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Validated => write!(f, "validated"),
            Self::Provisioned => write!(f, "provisioned"),
            Self::Registered => write!(f, "registered"),
            Self::Active => write!(f, "active"),
            Self::Failed => write!(f, "failed"),
        }
    }
}

// ── Resource request types ──────────────────────────────────────────────────

/// Request for a persistence domain (Stage 2: Provision).
///
/// Application example: maps to `SiblingDomainRequest` in soma-store, which
/// creates `/data/siblings/{sibling_id}/{sibling_id}.redb` with the requested capacity.
#[derive(Debug, Clone)]
pub struct DomainRequest {
    /// Unique sibling identifier (e.g., "qi", "git", "work").
    pub sibling_id: String,
    /// Human-readable display name (e.g., "Quality Intelligence").
    pub display_name: String,
    /// Sibling version (semver).
    pub version: String,
    /// Optional capacity limit in bytes. Defaults to 256 MiB if None.
    pub max_capacity_bytes: Option<u64>,
}

/// Declaration of a capability the sibling provides.
///
/// Capabilities are registered with the host ring application for RBAC evaluation
/// (the application assigns to GUARD). Each capability maps to a permission that can be granted to roles.
#[derive(Debug, Clone)]
pub struct CapabilityDeclaration {
    /// Machine-readable capability name (e.g., "qi.contract.verify").
    pub name: String,
    /// Human-readable description.
    pub description: String,
}

/// A section the sibling contributes to the SURFACE (ring UI).
///
/// Surface sections appear in the ring navigator and can render
/// sibling-specific panel content.
#[derive(Debug, Clone)]
pub struct SurfaceSection {
    /// Section identifier (kebab-case, e.g., "qi-contracts").
    pub id: String,
    /// Human-readable label for the navigator.
    pub label: String,
    /// Optional icon identifier.
    pub icon: Option<String>,
}

/// HTTP method for structural route declarations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpMethod {
    Get,
    Post,
    Put,
    Delete,
}

impl std::fmt::Display for HttpMethod {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Get => write!(f, "GET"),
            Self::Post => write!(f, "POST"),
            Self::Put => write!(f, "PUT"),
            Self::Delete => write!(f, "DELETE"),
        }
    }
}

/// A structural HTTP route the sibling needs mounted at startup.
///
/// Structural routes are for OAuth callbacks, webhooks, and other
/// HTTP endpoints that cannot flow through the command gateway.
/// Commands should use ThroughRing dispatch, not structural routes.
#[derive(Debug, Clone)]
pub struct RouteMount {
    /// URL path (e.g., "/sibling/git/webhook").
    pub path: String,
    /// HTTP method.
    pub method: HttpMethod,
    /// Human-readable description.
    pub description: String,
}

// ── SiblingManifest trait ───────────────────────────────────────────────────

/// The structural contract for a ring-hosted sibling.
///
/// Every sibling implements this trait to declare
/// its identity, ring extensions, resource requirements, and structural
/// routes. The registration lifecycle (validate → provision → register →
/// activate) consumes this manifest.
///
/// ## Minimal implementation
///
/// A sibling only needs identity methods and `through_handler()`.
/// All other methods have sensible defaults (None or empty Vec).
///
/// ## Extension ownership
///
/// - `through_handler()` returns `Arc` (shared ownership for dispatch)
/// - `before_gate()`, `after_observer()`, `around_extension()` return `Box`
///   (exclusive ownership, consistent with `RingEngine::register_*`)
pub trait SiblingManifest: Send + Sync {
    // ── Identity ────────────────────────────────────────────────────────

    /// Unique sibling identifier (kebab-case, e.g., "qi", "git", "work").
    fn id(&self) -> &str;

    /// Human-readable display name (e.g., "Quality Intelligence").
    fn display_name(&self) -> &str;

    /// Sibling version (semver, e.g., "0.1.0").
    fn version(&self) -> &str;

    /// Minimum required ring API version (semver).
    fn min_ring_version(&self) -> &str;

    // ── Ring extensions (typed factory methods) ─────────────────────────

    /// Command handler for the sibling's FU.Data namespace.
    ///
    /// The returned ThroughRing should claim `sibling.{id}.*` prefix.
    /// This is the primary interaction model — commands flow through
    /// the ring, not through HTTP routes.
    fn through_handler(&self) -> Option<Arc<dyn ThroughRing>>;

    /// Pre-ring gate (rare — only for command rejection).
    fn before_gate(&self) -> Option<Box<dyn BeforeRing>> {
        None
    }

    /// Post-cycle observer (for ring output analysis).
    fn after_observer(&self) -> Option<Box<dyn AfterRing>> {
        None
    }

    /// Two Doors injection/observation.
    fn around_extension(&self) -> Option<Box<dyn AroundRing>> {
        None
    }

    // ── Resource declarations ───────────────────────────────────────────

    /// Persistence domains this sibling needs provisioned.
    fn persistence_domains(&self) -> Vec<DomainRequest> {
        vec![]
    }

    /// Capabilities this sibling provides (registered with the host ring application for RBAC).
    fn capabilities(&self) -> Vec<CapabilityDeclaration> {
        vec![]
    }

    /// Surface sections this sibling contributes to the ring UI.
    fn surface_sections(&self) -> Vec<SurfaceSection> {
        vec![]
    }

    /// Structural HTTP routes (OAuth callbacks, webhooks — NOT commands).
    ///
    /// Commands should flow through ThroughRing dispatch via POST /api/command.
    /// Only declare structural routes for endpoints that cannot use the
    /// command gateway (e.g., OAuth redirects, webhook receivers).
    fn structural_routes(&self) -> Vec<RouteMount> {
        vec![]
    }

    // ── RBAC ────────────────────────────────────────────────────────────

    /// Permission requirements this sibling declares for its commands.
    ///
    /// By default, delegates to the `through_handler`'s
    /// `permission_requirements()`. Siblings that need to declare
    /// permissions beyond their through handler (e.g., for before_gate or
    /// around_extension commands) override this.
    ///
    /// Requirements returned here are collected into the
    /// [`crate::authorization::PermissionRegistry`] during sibling
    /// registration.
    fn permission_requirements(&self) -> Vec<crate::authorization::PermissionRequirement> {
        self.through_handler()
            .map(|h| h.permission_requirements())
            .unwrap_or_default()
    }
}

// ── Registration error ──────────────────────────────────────────────────────

/// Error during sibling registration lifecycle, lexicon validation, and
/// ring-extension registration. (Unified from the previously-divergent
/// `extension::RegistrationError` and `sibling::RegistrationError` defs.)
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum RegistrationError {
    /// Sibling ID is invalid (empty, wrong format, reserved).
    InvalidId(String),
    /// Sibling ID is already registered.
    DuplicateId(String),
    /// A `ThroughRing` delegate already claims this namespace prefix.
    NamespaceConflict { prefix: String, claimed_by: String },
    /// Structural route collides with an existing route.
    RouteCollision(String),
    /// Sibling requires a newer ring version than currently running.
    IncompatibleVersion { required: String, running: String },
    /// Persistence domain provisioning failed.
    ProvisionFailed(String),
    /// Lexicon coordinate validation failed — a vocabulary entry's unit
    /// coordinate does not match the organ's declared unit.
    CoordinateValidation(String),
}

impl std::fmt::Display for RegistrationError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::InvalidId(id) => write!(f, "invalid sibling ID: {id}"),
            Self::DuplicateId(id) => write!(f, "sibling '{id}' already registered"),
            Self::NamespaceConflict { prefix, claimed_by } => {
                write!(f, "namespace '{prefix}' already claimed by '{claimed_by}'")
            }
            Self::RouteCollision(path) => write!(f, "route collision: {path}"),
            Self::IncompatibleVersion { required, running } => {
                write!(f, "sibling requires ring {required}, running {running}")
            }
            Self::ProvisionFailed(reason) => write!(f, "provisioning failed: {reason}"),
            Self::CoordinateValidation(msg) => {
                write!(f, "lexicon coordinate validation failed: {msg}")
            }
        }
    }
}

impl std::error::Error for RegistrationError {}

// ── Tests ───────────────────────────────────────────────────────────────────

// inline: exercises module-private items via super::*
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sibling_manifest_is_object_safe() {
        fn _assert(_: &dyn SiblingManifest) {}
    }

    #[test]
    fn sibling_status_display() {
        assert_eq!(SiblingStatus::Validated.to_string(), "validated");
        assert_eq!(SiblingStatus::Provisioned.to_string(), "provisioned");
        assert_eq!(SiblingStatus::Registered.to_string(), "registered");
        assert_eq!(SiblingStatus::Active.to_string(), "active");
        assert_eq!(SiblingStatus::Failed.to_string(), "failed");
    }

    #[test]
    fn sibling_status_equality() {
        assert_eq!(SiblingStatus::Active, SiblingStatus::Active);
        assert_ne!(SiblingStatus::Active, SiblingStatus::Failed);
    }

    #[test]
    fn registration_error_display() {
        let e = RegistrationError::InvalidId("bad!id".into());
        assert_eq!(e.to_string(), "invalid sibling ID: bad!id");

        let e = RegistrationError::DuplicateId("qi".into());
        assert_eq!(e.to_string(), "sibling 'qi' already registered");

        let e = RegistrationError::NamespaceConflict { prefix: "sibling.qi.*".into(), claimed_by: "qi".into() };
        assert_eq!(e.to_string(), "namespace 'sibling.qi.*' already claimed by 'qi'");

        let e = RegistrationError::RouteCollision("/oauth/callback".into());
        assert_eq!(e.to_string(), "route collision: /oauth/callback");

        let e = RegistrationError::IncompatibleVersion {
            required: "0.2.0".into(),
            running: "0.1.0".into(),
        };
        assert_eq!(e.to_string(), "sibling requires ring 0.2.0, running 0.1.0");

        let e = RegistrationError::ProvisionFailed("disk full".into());
        assert_eq!(e.to_string(), "provisioning failed: disk full");
    }

    #[test]
    fn http_method_display() {
        assert_eq!(HttpMethod::Get.to_string(), "GET");
        assert_eq!(HttpMethod::Post.to_string(), "POST");
        assert_eq!(HttpMethod::Put.to_string(), "PUT");
        assert_eq!(HttpMethod::Delete.to_string(), "DELETE");
    }

    #[test]
    fn domain_request_creation() {
        let req = DomainRequest {
            sibling_id: "qi".into(),
            display_name: "Quality Intelligence".into(),
            version: "0.1.0".into(),
            max_capacity_bytes: Some(128 * 1024 * 1024),
        };
        assert_eq!(req.sibling_id, "qi");
        assert_eq!(req.max_capacity_bytes, Some(128 * 1024 * 1024));
    }

    #[test]
    fn capability_declaration_creation() {
        let cap = CapabilityDeclaration {
            name: "qi.contract.verify".into(),
            description: "Verify QI contracts".into(),
        };
        assert_eq!(cap.name, "qi.contract.verify");
    }

    #[test]
    fn surface_section_creation() {
        let section = SurfaceSection {
            id: "qi-contracts".into(),
            label: "QI Contracts".into(),
            icon: Some("shield".into()),
        };
        assert_eq!(section.id, "qi-contracts");
        assert_eq!(section.icon, Some("shield".into()));
    }

    #[test]
    fn route_mount_creation() {
        let route = RouteMount {
            path: "/sibling/git/webhook".into(),
            method: HttpMethod::Post,
            description: "Git webhook receiver".into(),
        };
        assert_eq!(route.path, "/sibling/git/webhook");
        assert_eq!(route.method, HttpMethod::Post);
    }
}