Skip to main content

canwu_core/
lib.rs

1//! Stable identifiers, deterministic utilities, and lightweight schema metadata.
2
3use serde::{Deserialize, Serialize};
4use std::cmp::Ordering;
5use std::collections::BTreeMap;
6use std::fmt::{Display, Formatter};
7use std::hash::{Hash, Hasher};
8use std::marker::PhantomData;
9
10macro_rules! define_id {
11    ($name:ident) => {
12        #[derive(
13            Clone,
14            Copy,
15            Debug,
16            Default,
17            Deserialize,
18            Eq,
19            Hash,
20            Ord,
21            PartialEq,
22            PartialOrd,
23            Serialize,
24        )]
25        #[serde(transparent)]
26        #[doc = concat!("Stable numeric identifier for [`", stringify!($name), "`].")]
27        pub struct $name(pub u64);
28
29        impl $name {
30            /// Creates an identifier from its wire value.
31            #[must_use]
32            pub const fn new(value: u64) -> Self {
33                Self(value)
34            }
35
36            /// Returns the identifier's wire value.
37            #[must_use]
38            pub const fn get(self) -> u64 {
39                self.0
40            }
41        }
42
43        impl Display for $name {
44            fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
45                Display::fmt(&self.0, formatter)
46            }
47        }
48    };
49}
50
51define_id!(ArmyId);
52define_id!(BoundaryId);
53define_id!(CommandAttemptId);
54define_id!(CommandId);
55define_id!(CommandRequestId);
56define_id!(DecisionRequestId);
57define_id!(DecisionTicketId);
58define_id!(DecisionTraceId);
59define_id!(EventId);
60define_id!(GovernmentId);
61define_id!(IngressId);
62define_id!(HolderKnowledgeRecordId);
63define_id!(LetterId);
64define_id!(OrganizationId);
65define_id!(PersonId);
66define_id!(RandomDrawId);
67define_id!(KnowledgeRecordId);
68define_id!(ResourceId);
69define_id!(RouteId);
70define_id!(TerritoryId);
71
72/// Generic simulation granularity used by host applications to map aggregate,
73/// group, and individual actors onto the same authoritative engine.
74///
75/// The engine deliberately does not call these levels "population", "special
76/// group", or "character". Those are content terms owned by a reference
77/// integration such as Celestial Mandate. A host may map its population model
78/// to [`Self::Aggregate`], its special groups to [`Self::Group`], and its
79/// characters to [`Self::Actor`] without changing the kernel's identity wire
80/// format.
81#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
82#[serde(rename_all = "snake_case")]
83pub enum SimulationGranularity {
84    Aggregate,
85    Group,
86    Actor,
87}
88
89impl SimulationGranularity {
90    /// Returns the stable public label used in manifests and diagnostics.
91    #[must_use]
92    pub const fn as_str(self) -> &'static str {
93        match self {
94            Self::Aggregate => "aggregate",
95            Self::Group => "group",
96            Self::Actor => "actor",
97        }
98    }
99}
100
101/// Stable application-defined record kind. Namespaces and names are validated
102/// by the simulation package registry before authoritative use.
103#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
104pub struct DomainRecordKind {
105    pub namespace: String,
106    pub name: String,
107}
108
109impl DomainRecordKind {
110    #[must_use]
111    pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
112        Self {
113            namespace: namespace.into(),
114            name: name.into(),
115        }
116    }
117
118    #[must_use]
119    pub fn for_type<T: DomainRecordType>() -> Self {
120        Self::new(T::NAMESPACE, T::NAME)
121    }
122
123    #[must_use]
124    pub fn matches_type<T: DomainRecordType>(&self) -> bool {
125        self.namespace == T::NAMESPACE && self.name == T::NAME
126    }
127}
128
129impl Display for DomainRecordKind {
130    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
131        write!(formatter, "{}.{}", self.namespace, self.name)
132    }
133}
134
135/// Stable string identity for an application-defined entity or record.
136#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
137pub struct DomainRecordRef {
138    pub kind: DomainRecordKind,
139    pub id: String,
140}
141
142/// Persisted identity of the operation that established one domain-record
143/// version. Version zero is reserved and rejected by runtime validation.
144#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
145#[serde(tag = "type", rename_all = "snake_case")]
146pub enum DomainRecordVersionSource {
147    InitialScenario,
148    BoundaryChange {
149        boundary: BoundaryId,
150        change_index: u64,
151    },
152}
153
154/// Exact historical identity for an application-defined record version.
155#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
156pub struct DomainRecordVersionRef {
157    pub record: DomainRecordRef,
158    pub version: u64,
159    pub established_by: DomainRecordVersionSource,
160}
161
162/// Shared persisted-evidence identity used by knowledge, decisions, random
163/// operations, replay, and compact archive receipts.
164#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
165#[serde(tag = "type", content = "value", rename_all = "snake_case")]
166pub enum EvidenceRef {
167    Command(CommandId),
168    CommandAttempt(CommandAttemptId),
169    Event(EventId),
170    Ingress(IngressId),
171    Boundary(BoundaryId),
172    RandomDraw(RandomDrawId),
173    DomainRecordVersion(DomainRecordVersionRef),
174}
175
176/// Stable namespace and kind for a holder-relative knowledge record.
177#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
178pub struct KnowledgeRecordKind {
179    pub namespace: String,
180    pub name: String,
181}
182
183impl KnowledgeRecordKind {
184    #[must_use]
185    pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
186        Self {
187            namespace: namespace.into(),
188            name: name.into(),
189        }
190    }
191}
192
193impl Display for KnowledgeRecordKind {
194    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
195        write!(formatter, "{}.{}", self.namespace, self.name)
196    }
197}
198
199/// Exact version of one registered knowledge schema.
200#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
201pub struct KnowledgeSchemaId {
202    pub kind: KnowledgeRecordKind,
203    pub version: u32,
204}
205
206impl KnowledgeSchemaId {
207    #[must_use]
208    pub fn new(kind: KnowledgeRecordKind, version: u32) -> Self {
209        Self { kind, version }
210    }
211}
212
213/// Stable holder identity shared by people and eligible institutional entities.
214#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
215#[serde(tag = "type", content = "value", rename_all = "snake_case")]
216pub enum KnowledgeHolderRef {
217    Person(PersonId),
218    Entity(EntityRef),
219}
220
221impl KnowledgeHolderRef {
222    #[must_use]
223    pub const fn is_person_entity(&self) -> bool {
224        matches!(self, Self::Entity(EntityRef::Person(_)))
225    }
226}
227
228/// Whether a domain entity schema may receive holder-relative knowledge.
229#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
230#[serde(rename_all = "snake_case")]
231pub enum KnowledgeHolderPolicy {
232    #[default]
233    Disallowed,
234    Allowed,
235}
236
237/// Compile-time identity for one versioned holder-relative knowledge schema.
238pub trait KnowledgeRecordType {
239    type Payload;
240
241    const NAMESPACE: &'static str;
242    const NAME: &'static str;
243    const SCHEMA_VERSION: u32;
244}
245
246impl DomainRecordRef {
247    #[must_use]
248    pub fn new(
249        namespace: impl Into<String>,
250        kind: impl Into<String>,
251        id: impl Into<String>,
252    ) -> Self {
253        Self {
254            kind: DomainRecordKind::new(namespace, kind),
255            id: id.into(),
256        }
257    }
258}
259
260impl Display for DomainRecordRef {
261    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
262        write!(formatter, "{}:{}", self.kind, self.id)
263    }
264}
265
266/// Compile-time identity for one namespaced application-defined record kind.
267///
268/// The associated payload stays outside the kernel's type graph. Domain
269/// packages use this trait to bind stable identities and payload codecs while
270/// Canwu persists the existing schema-validated [`DomainRecordRef`] shape.
271pub trait DomainRecordType {
272    type Payload;
273    type Class: DomainKindClass;
274
275    const NAMESPACE: &'static str;
276    const NAME: &'static str;
277}
278
279mod domain_kind_class {
280    pub trait Sealed {}
281}
282
283/// Sealed type-level classification for application-defined record kinds.
284pub trait DomainKindClass: domain_kind_class::Sealed {
285    const IS_ENTITY: bool;
286}
287
288/// Type-level class for domain kinds whose instances are entity identities.
289pub enum DomainEntityKindClass {}
290
291impl domain_kind_class::Sealed for DomainEntityKindClass {}
292
293impl DomainKindClass for DomainEntityKindClass {
294    const IS_ENTITY: bool = true;
295}
296
297/// Type-level class for domain kinds whose instances are non-entity records.
298pub enum DomainValueKindClass {}
299
300impl domain_kind_class::Sealed for DomainValueKindClass {}
301
302impl DomainKindClass for DomainValueKindClass {
303    const IS_ENTITY: bool = false;
304}
305
306/// Marker implemented automatically for entity-class domain record types.
307pub trait DomainEntityType: DomainRecordType<Class = DomainEntityKindClass> {}
308
309impl<T: DomainRecordType<Class = DomainEntityKindClass>> DomainEntityType for T {}
310
311/// Marker implemented automatically for non-entity domain record types.
312pub trait DomainValueType: DomainRecordType<Class = DomainValueKindClass> {}
313
314impl<T: DomainRecordType<Class = DomainValueKindClass>> DomainValueType for T {}
315
316/// Typed façade over a stable application-defined record identity.
317///
318/// Its serialized representation is exactly the wrapped [`DomainRecordRef`];
319/// the marker exists only at compile time.
320#[derive(Serialize)]
321#[serde(transparent, bound = "")]
322pub struct TypedDomainRecordRef<T: DomainRecordType> {
323    reference: DomainRecordRef,
324    #[serde(skip)]
325    marker: PhantomData<fn() -> T>,
326}
327
328impl<T: DomainRecordType> Clone for TypedDomainRecordRef<T> {
329    fn clone(&self) -> Self {
330        Self {
331            reference: self.reference.clone(),
332            marker: PhantomData,
333        }
334    }
335}
336
337impl<T: DomainRecordType> std::fmt::Debug for TypedDomainRecordRef<T> {
338    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
339        formatter
340            .debug_tuple("TypedDomainRecordRef")
341            .field(&self.reference)
342            .finish()
343    }
344}
345
346impl<T: DomainRecordType> PartialEq for TypedDomainRecordRef<T> {
347    fn eq(&self, other: &Self) -> bool {
348        self.reference == other.reference
349    }
350}
351
352impl<T: DomainRecordType> Eq for TypedDomainRecordRef<T> {}
353
354impl<T: DomainRecordType> PartialOrd for TypedDomainRecordRef<T> {
355    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
356        Some(self.cmp(other))
357    }
358}
359
360impl<T: DomainRecordType> Ord for TypedDomainRecordRef<T> {
361    fn cmp(&self, other: &Self) -> Ordering {
362        self.reference.cmp(&other.reference)
363    }
364}
365
366impl<T: DomainRecordType> Hash for TypedDomainRecordRef<T> {
367    fn hash<H: Hasher>(&self, state: &mut H) {
368        self.reference.hash(state);
369    }
370}
371
372impl<'de, T: DomainRecordType> Deserialize<'de> for TypedDomainRecordRef<T> {
373    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
374    where
375        D: serde::Deserializer<'de>,
376    {
377        let reference = DomainRecordRef::deserialize(deserializer)?;
378        Self::from_untyped(reference).map_err(|reference| {
379            serde::de::Error::custom(format!(
380                "domain record reference {reference} does not match typed kind {}",
381                DomainRecordKind::for_type::<T>()
382            ))
383        })
384    }
385}
386
387impl<T: DomainRecordType> TypedDomainRecordRef<T> {
388    #[must_use]
389    pub fn new(id: impl Into<String>) -> Self {
390        Self {
391            reference: DomainRecordRef {
392                kind: DomainRecordKind::for_type::<T>(),
393                id: id.into(),
394            },
395            marker: PhantomData,
396        }
397    }
398
399    #[must_use]
400    pub const fn as_untyped(&self) -> &DomainRecordRef {
401        &self.reference
402    }
403
404    #[must_use]
405    pub fn into_untyped(self) -> DomainRecordRef {
406        self.reference
407    }
408
409    /// Converts an untyped reference when its namespaced kind matches `T`.
410    ///
411    /// # Errors
412    ///
413    /// Returns the original reference when it belongs to another kind.
414    pub fn from_untyped(reference: DomainRecordRef) -> Result<Self, DomainRecordRef> {
415        if !reference.kind.matches_type::<T>() {
416            return Err(reference);
417        }
418        Ok(Self {
419            reference,
420            marker: PhantomData,
421        })
422    }
423
424    #[must_use]
425    pub fn id(&self) -> &str {
426        &self.reference.id
427    }
428}
429
430impl<T: DomainRecordType> Display for TypedDomainRecordRef<T> {
431    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
432        Display::fmt(&self.reference, formatter)
433    }
434}
435
436impl<T: DomainRecordType> From<TypedDomainRecordRef<T>> for DomainRecordRef {
437    fn from(reference: TypedDomainRecordRef<T>) -> Self {
438        reference.into_untyped()
439    }
440}
441
442impl<T: DomainEntityType> From<TypedDomainRecordRef<T>> for EntityRef {
443    fn from(reference: TypedDomainRecordRef<T>) -> Self {
444        Self::Domain(reference.into_untyped())
445    }
446}
447
448#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
449#[serde(rename_all = "snake_case")]
450pub enum CoreEntityKind {
451    Army,
452    Government,
453    Organization,
454    Person,
455    Resource,
456    Route,
457    Territory,
458}
459
460/// Serializable entity reference used by events, queries, and generic tools.
461#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
462#[serde(tag = "type", content = "id", rename_all = "snake_case")]
463pub enum EntityRef {
464    Army(ArmyId),
465    Domain(DomainRecordRef),
466    Government(GovernmentId),
467    Organization(OrganizationId),
468    Person(PersonId),
469    Resource(ResourceId),
470    Route(RouteId),
471    Territory(TerritoryId),
472}
473
474impl Display for EntityRef {
475    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
476        match self {
477            Self::Army(id) => write!(formatter, "army:{id}"),
478            Self::Domain(reference) => write!(formatter, "domain:{reference}"),
479            Self::Government(id) => write!(formatter, "government:{id}"),
480            Self::Organization(id) => write!(formatter, "organization:{id}"),
481            Self::Person(id) => write!(formatter, "person:{id}"),
482            Self::Resource(id) => write!(formatter, "resource:{id}"),
483            Self::Route(id) => write!(formatter, "route:{id}"),
484            Self::Territory(id) => write!(formatter, "territory:{id}"),
485        }
486    }
487}
488
489impl EntityRef {
490    #[must_use]
491    pub const fn core_kind(&self) -> Option<CoreEntityKind> {
492        match self {
493            Self::Army(_) => Some(CoreEntityKind::Army),
494            Self::Domain(_) => None,
495            Self::Government(_) => Some(CoreEntityKind::Government),
496            Self::Organization(_) => Some(CoreEntityKind::Organization),
497            Self::Person(_) => Some(CoreEntityKind::Person),
498            Self::Resource(_) => Some(CoreEntityKind::Resource),
499            Self::Route(_) => Some(CoreEntityKind::Route),
500            Self::Territory(_) => Some(CoreEntityKind::Territory),
501        }
502    }
503}
504
505/// `SplitMix64` is compact, deterministic, serializable, and sufficient for the
506/// initial movement slice.
507#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
508pub struct DeterministicRng {
509    state: u64,
510}
511
512impl DeterministicRng {
513    const STEP: u64 = 0x9E37_79B9_7F4A_7C15;
514
515    #[must_use]
516    pub const fn from_seed(seed: u64) -> Self {
517        Self { state: seed }
518    }
519
520    #[must_use]
521    pub const fn state(self) -> u64 {
522        self.state
523    }
524
525    #[must_use]
526    pub const fn state_after(seed: u64, draws: u64) -> u64 {
527        seed.wrapping_add(Self::STEP.wrapping_mul(draws))
528    }
529
530    #[must_use]
531    pub const fn seed_before(state: u64, draws: u64) -> u64 {
532        state.wrapping_sub(Self::STEP.wrapping_mul(draws))
533    }
534
535    pub fn next_u64(&mut self) -> u64 {
536        self.state = self.state.wrapping_add(Self::STEP);
537        let mut value = self.state;
538        value = (value ^ (value >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
539        value = (value ^ (value >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
540        value ^ (value >> 31)
541    }
542
543    /// Returns a uniformly distributed value in `[0, upper_exclusive)`.
544    ///
545    /// Zero returns zero. Rejection sampling is used so non-power-of-two
546    /// bounds do not introduce modulo bias.
547    pub fn range(&mut self, upper_exclusive: u64) -> u64 {
548        if upper_exclusive == 0 {
549            return 0;
550        }
551        let rejection_threshold = upper_exclusive.wrapping_neg() % upper_exclusive;
552        loop {
553            let value = self.next_u64();
554            if value >= rejection_threshold {
555                return value % upper_exclusive;
556            }
557        }
558    }
559
560    /// Returns the historical modulo-reduction value for the V1 random-stream
561    /// contract.
562    #[must_use]
563    pub fn range_modulo(&mut self, upper_exclusive: u64) -> u64 {
564        if upper_exclusive == 0 {
565            return 0;
566        }
567        self.next_u64() % upper_exclusive
568    }
569}
570
571/// Error returned when a schema registration would replace an existing type.
572#[derive(Clone, Debug, Eq, PartialEq)]
573pub enum SchemaRegistryError {
574    DuplicateType(String),
575}
576
577impl Display for SchemaRegistryError {
578    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
579        match self {
580            Self::DuplicateType(type_name) => {
581                write!(formatter, "schema type {type_name} is already registered")
582            }
583        }
584    }
585}
586
587impl std::error::Error for SchemaRegistryError {}
588
589#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
590pub struct FieldSchema {
591    pub name: String,
592    pub value_type: String,
593    pub description: String,
594    pub reference_type: Option<String>,
595    pub writable_via_debug_command: bool,
596}
597
598#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
599pub struct TypeSchema {
600    pub type_name: String,
601    pub description: String,
602    pub fields: Vec<FieldSchema>,
603}
604
605#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
606pub struct SchemaRegistry {
607    types: BTreeMap<String, TypeSchema>,
608}
609
610impl SchemaRegistry {
611    /// Registers a schema without replacing a different existing definition.
612    ///
613    /// Re-registering an identical definition is accepted for idempotent
614    /// plugin registration.
615    ///
616    /// # Errors
617    ///
618    /// Returns [`SchemaRegistryError::DuplicateType`] when the type name is
619    /// already registered with a different definition.
620    pub fn register(&mut self, schema: TypeSchema) -> Result<(), SchemaRegistryError> {
621        if let Some(existing) = self.types.get(&schema.type_name) {
622            if existing == &schema {
623                return Ok(());
624            }
625            return Err(SchemaRegistryError::DuplicateType(schema.type_name));
626        }
627        self.types.insert(schema.type_name.clone(), schema);
628        Ok(())
629    }
630
631    #[must_use]
632    pub fn get(&self, type_name: &str) -> Option<&TypeSchema> {
633        self.types.get(type_name)
634    }
635
636    pub fn iter(&self) -> impl Iterator<Item = &TypeSchema> {
637        self.types.values()
638    }
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644
645    struct Office;
646
647    impl DomainRecordType for Office {
648        type Payload = String;
649        type Class = DomainEntityKindClass;
650
651        const NAMESPACE: &'static str = "fixture.governance";
652        const NAME: &'static str = "office";
653    }
654
655    struct Obligation;
656
657    impl DomainRecordType for Obligation {
658        type Payload = String;
659        type Class = DomainValueKindClass;
660
661        const NAMESPACE: &'static str = "fixture.governance";
662        const NAME: &'static str = "obligation";
663    }
664
665    struct Assessment;
666
667    impl KnowledgeRecordType for Assessment {
668        type Payload = String;
669
670        const NAMESPACE: &'static str = "fixture.knowledge";
671        const NAME: &'static str = "assessment";
672        const SCHEMA_VERSION: u32 = 2;
673    }
674
675    #[test]
676    fn typed_domain_identity_preserves_wire_shape_and_kind_boundary() {
677        let typed = TypedDomainRecordRef::<Office>::new("secretariat");
678        let raw = DomainRecordRef::new("fixture.governance", "office", "secretariat");
679
680        assert_eq!(
681            serde_json::to_value(&typed).expect("typed identity should serialize"),
682            serde_json::to_value(&raw).expect("raw identity should serialize")
683        );
684        let round_trip: TypedDomainRecordRef<Office> = serde_json::from_value(
685            serde_json::to_value(&typed).expect("typed identity should serialize"),
686        )
687        .expect("typed identity should deserialize");
688        assert_eq!(round_trip.as_untyped(), &raw);
689        assert_eq!(EntityRef::from(round_trip), EntityRef::Domain(raw.clone()));
690
691        let wrong_kind =
692            DomainRecordRef::new("fixture.governance", "obligation", "secretariat-duty");
693        assert_eq!(
694            TypedDomainRecordRef::<Office>::from_untyped(wrong_kind.clone()),
695            Err(wrong_kind)
696        );
697        assert!(TypedDomainRecordRef::<Obligation>::from_untyped(raw).is_err());
698        assert!(
699            serde_json::from_value::<TypedDomainRecordRef<Office>>(serde_json::json!({
700                "kind": {
701                    "namespace": "fixture.governance",
702                    "name": "obligation"
703                },
704                "id": "secretariat-duty"
705            }))
706            .is_err()
707        );
708    }
709
710    #[test]
711    fn knowledge_identity_and_holder_wire_shapes_are_stable() {
712        let kind = KnowledgeRecordKind::new(Assessment::NAMESPACE, Assessment::NAME);
713        let schema = KnowledgeSchemaId::new(kind.clone(), Assessment::SCHEMA_VERSION);
714
715        assert_eq!(kind.to_string(), "fixture.knowledge.assessment");
716        assert_eq!(schema.version, 2);
717        assert_eq!(schema.kind, kind);
718        assert_eq!(
719            KnowledgeHolderPolicy::default(),
720            KnowledgeHolderPolicy::Disallowed
721        );
722
723        assert_eq!(
724            serde_json::to_value(KnowledgeHolderRef::Person(PersonId::new(7)))
725                .expect("person holder should serialize"),
726            serde_json::json!({ "type": "person", "value": 7 })
727        );
728        let invalid_shape = KnowledgeHolderRef::Entity(EntityRef::Person(PersonId::new(7)));
729        assert!(invalid_shape.is_person_entity());
730        let institution =
731            KnowledgeHolderRef::Entity(EntityRef::Organization(OrganizationId::new(3)));
732        assert!(!institution.is_person_entity());
733        assert_eq!(
734            serde_json::to_value(institution).expect("institution holder should serialize"),
735            serde_json::json!({
736                "type": "entity",
737                "value": { "type": "organization", "id": 3 }
738            })
739        );
740    }
741
742    #[test]
743    fn exact_domain_record_evidence_has_a_stable_wire_identity() {
744        let evidence = EvidenceRef::DomainRecordVersion(DomainRecordVersionRef {
745            record: DomainRecordRef::new("fixture.information", "dispatch", "dispatch-7"),
746            version: 2,
747            established_by: DomainRecordVersionSource::BoundaryChange {
748                boundary: BoundaryId::new(12),
749                change_index: 3,
750            },
751        });
752
753        assert_eq!(
754            serde_json::to_value(evidence).expect("evidence should serialize"),
755            serde_json::json!({
756                "type": "domain_record_version",
757                "value": {
758                    "record": {
759                        "kind": {
760                            "namespace": "fixture.information",
761                            "name": "dispatch"
762                        },
763                        "id": "dispatch-7"
764                    },
765                    "version": 2,
766                    "established_by": {
767                        "type": "boundary_change",
768                        "boundary": 12,
769                        "change_index": 3
770                    }
771                }
772            })
773        );
774    }
775
776    #[test]
777    fn deterministic_range_stays_bounded_for_non_power_of_two_limits() {
778        for seed in 0..32 {
779            let mut rng = DeterministicRng::from_seed(seed);
780            assert_eq!(rng.range(0), 0);
781            for _ in 0..128 {
782                assert!(rng.range(7) < 7);
783            }
784        }
785    }
786
787    #[test]
788    fn schema_registration_is_idempotent_but_rejects_definition_replacement() {
789        let schema = TypeSchema {
790            type_name: "fixture.office".to_owned(),
791            description: "An office".to_owned(),
792            fields: vec![FieldSchema {
793                name: "name".to_owned(),
794                value_type: "string".to_owned(),
795                description: "The office name".to_owned(),
796                reference_type: None,
797                writable_via_debug_command: false,
798            }],
799        };
800        let mut registry = SchemaRegistry::default();
801        registry
802            .register(schema.clone())
803            .expect("the first schema registration should succeed");
804        registry
805            .register(schema.clone())
806            .expect("an identical schema registration should be idempotent");
807
808        let mut conflicting = schema;
809        conflicting.description = "A different office".to_owned();
810        assert_eq!(
811            registry
812                .register(conflicting)
813                .expect_err("a different definition must not replace the registered schema"),
814            SchemaRegistryError::DuplicateType("fixture.office".to_owned())
815        );
816    }
817}