//! 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);
}
}