jacquardSnapshot

← snapshot

8550 bytes
//! The registry port and its in-memory fake.

use jac_core::{IntroductionId, OrgId, PublicationId, Timestamp};
use jac_sketch::{ContentSketch, DecisionSketch, Similarity};

use crate::introduction::Introduction;

/// The only payload vocabulary the registry accepts.
///
/// Every variant's inner type is proven [`jac_core::DigestSafe`] by the
/// const assertions at the bottom of this module. Adding a variant whose
/// type is not `DigestSafe` fails to compile — extend the enum, extend the
/// assertion, or the build stops.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PublishedSketch {
    /// A sketch of repository content.
    Content(ContentSketch),
    /// A sketch of a design decision.
    Decision(DecisionSketch),
}

impl PublishedSketch {
    /// Similarity to another published sketch of the same kind.
    ///
    /// Cross-kind comparisons return [`Similarity::NONE`] rather than a
    /// number that means nothing: content resembling a decision is not a
    /// rendezvous.
    #[must_use]
    pub fn similarity(&self, other: &Self) -> Similarity {
        match (self, other) {
            (Self::Content(a), Self::Content(b)) => a.similarity(b),
            (Self::Decision(a), Self::Decision(b)) => a.similarity(b),
            _ => Similarity::NONE,
        }
    }

    /// Kind label for narration.
    #[must_use]
    pub const fn kind(&self) -> &'static str {
        match self {
            Self::Content(_) => "content",
            Self::Decision(_) => "decision",
        }
    }
}

/// One published sketch: who, what shape, when. This struct is the *totality*
/// of what an organisation discloses by publishing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Publication {
    /// The publishing organisation.
    pub org: OrgId,
    /// The sketch.
    pub sketch: PublishedSketch,
    /// When it was published.
    pub at: Timestamp,
}

/// A resemblance the registry noticed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SketchMatch {
    /// Which publication matched.
    pub publication: PublicationId,
    /// The organisation whose work resembles the probe.
    pub org: OrgId,
    /// How closely.
    pub similarity: Similarity,
}

/// The registry port.
///
/// Sans-IO; the shared in-memory fake is [`MemoryRegistry`]. A real adapter
/// is a network service, and everything it can ever hold is what
/// [`Publication`] holds.
pub trait Registry {
    /// Records a publication, returning its id.
    fn publish(&mut self, publication: Publication) -> PublicationId;

    /// Publications from *other* organisations whose sketches resemble the
    /// probe at or above `min`. The prober's own publications are excluded:
    /// an organisation does not need the registry to meet itself.
    fn find_similar(
        &self,
        probe: &PublishedSketch,
        min: Similarity,
        prober: OrgId,
    ) -> Vec<SketchMatch>;

    /// Brokers an introduction between two organisations.
    ///
    /// Both parties receive the same token; what they do with it happens
    /// entirely outside the registry.
    fn broker(&mut self, a: OrgId, b: OrgId, at: Timestamp) -> Introduction;
}

/// In-memory registry for tests and the narrated demo, where one instance
/// stands in for the shared network.
#[derive(Debug, Default)]
pub struct MemoryRegistry {
    publications: Vec<Publication>,
    introductions: Vec<Introduction>,
}

impl MemoryRegistry {
    /// An empty registry.
    #[must_use]
    pub const fn new() -> Self {
        Self {
            publications: Vec::new(),
            introductions: Vec::new(),
        }
    }

    /// Everything the registry knows, for the demo's "print exactly what
    /// left the org" scene. The type says it all: ids, sketches, timestamps.
    #[must_use]
    pub fn publications(&self) -> &[Publication] {
        &self.publications
    }

    /// Introductions brokered so far.
    #[must_use]
    pub fn introductions(&self) -> &[Introduction] {
        &self.introductions
    }
}

/// Derives the 1-based publication id for an index in the store.
///
/// # Panics
///
/// Cannot panic: index + 1 is never zero.
fn publication_id(index: usize) -> PublicationId {
    let raw = u64::try_from(index).unwrap_or(u64::MAX).saturating_add(1);
    PublicationId::new(raw).unwrap_or(PublicationId::MIN)
}

impl Registry for MemoryRegistry {
    fn publish(&mut self, publication: Publication) -> PublicationId {
        self.publications.push(publication);
        publication_id(self.publications.len() - 1)
    }

    fn find_similar(
        &self,
        probe: &PublishedSketch,
        min: Similarity,
        prober: OrgId,
    ) -> Vec<SketchMatch> {
        self.publications
            .iter()
            .enumerate()
            .filter(|(_, p)| p.org != prober)
            .filter_map(|(i, p)| {
                let similarity = probe.similarity(&p.sketch);
                (similarity >= min && similarity > Similarity::NONE).then(|| SketchMatch {
                    publication: publication_id(i),
                    org: p.org,
                    similarity,
                })
            })
            .collect()
    }

    fn broker(&mut self, a: OrgId, b: OrgId, at: Timestamp) -> Introduction {
        let raw = u64::try_from(self.introductions.len())
            .unwrap_or(u64::MAX)
            .saturating_add(1);
        let token = IntroductionId::new(raw).unwrap_or(IntroductionId::MIN);
        let introduction = Introduction {
            token,
            parties: (a, b),
            at,
        };
        self.introductions.push(introduction);
        introduction
    }
}

// The boundary's trait half, proven at compile time for every payload
// variant. A new variant whose type cannot pass this assertion cannot ship.
const _: () = {
    crate::assert_digest_safe::<ContentSketch>();
    crate::assert_digest_safe::<DecisionSketch>();
};

#[cfg(test)]
#[expect(
    clippy::unwrap_used,
    reason = "test assertions read better with unwrap"
)]
mod tests {
    use jac_sketch::SketchBuilder;

    use super::*;

    fn org(n: u64) -> OrgId {
        OrgId::new(n).unwrap()
    }

    fn decision_sketch(text: &str) -> PublishedSketch {
        let mut b = SketchBuilder::new();
        b.feed_text(text);
        PublishedSketch::Decision(DecisionSketch(b.finish()))
    }

    fn content_sketch(text: &str) -> PublishedSketch {
        let mut b = SketchBuilder::new();
        b.feed_text(text);
        PublishedSketch::Content(ContentSketch(b.finish()))
    }

    #[test]
    fn similar_work_is_found_across_orgs() {
        let mut registry = MemoryRegistry::new();
        let text = "auth retry fails closed on 401 and 403, never with cached credentials";
        registry.publish(Publication {
            org: org(1),
            sketch: decision_sketch(text),
            at: Timestamp::from_millis(1),
        });

        let matches = registry.find_similar(&decision_sketch(text), Similarity::NONE, org(2));
        assert_eq!(matches.len(), 1);
        assert_eq!(matches[0].org, org(1));
        assert_eq!(matches[0].similarity, Similarity::IDENTICAL);
    }

    #[test]
    fn an_org_never_matches_itself() {
        let mut registry = MemoryRegistry::new();
        let text = "the same decision text";
        registry.publish(Publication {
            org: org(1),
            sketch: decision_sketch(text),
            at: Timestamp::from_millis(1),
        });

        assert!(
            registry
                .find_similar(&decision_sketch(text), Similarity::NONE, org(1))
                .is_empty()
        );
    }

    #[test]
    fn kinds_do_not_cross() {
        let mut registry = MemoryRegistry::new();
        let text = "identical words in both sketches";
        registry.publish(Publication {
            org: org(1),
            sketch: content_sketch(text),
            at: Timestamp::from_millis(1),
        });

        assert!(
            registry
                .find_similar(&decision_sketch(text), Similarity::NONE, org(2))
                .is_empty(),
            "a decision probe must not match content publications"
        );
    }

    #[test]
    fn broker_mints_distinct_tokens() {
        let mut registry = MemoryRegistry::new();
        let i1 = registry.broker(org(1), org(2), Timestamp::from_millis(5));
        let i2 = registry.broker(org(1), org(3), Timestamp::from_millis(6));
        assert_ne!(i1.token, i2.token);
        assert_eq!(registry.introductions().len(), 2);
    }
}