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/// One named contribution to a rule evaluation, with the evidence it read.
506///
507/// Term IDs and their meaning are application content. The contribution is an
508/// integer in the rule's own fixed unit.
509#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
510pub struct EvaluationTerm {
511    pub term_id: String,
512    pub contribution: i64,
513    /// Persisted evidence the term read, strictly sorted and unique.
514    pub evidence: Vec<EvidenceRef>,
515}
516
517/// Explains how one application rule produced one integer result for one
518/// subject at one boundary.
519///
520/// A trace is boundary evidence, never simulation state: nothing reads it back
521/// to decide an outcome. Rule IDs, versions, and term semantics are
522/// application content; the engine checks only shape, limits, subject
523/// identity, and evidence availability. The result is recorded as the rule
524/// computed it and need not equal the sum of the term contributions.
525#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
526pub struct EvaluationTraceRecord {
527    pub rule_id: String,
528    pub rule_version: String,
529    pub subject: EntityRef,
530    pub terms: Vec<EvaluationTerm>,
531    pub result: i64,
532    /// Boundary whose settlement evaluated the rule.
533    pub boundary: BoundaryId,
534}
535
536/// `SplitMix64` is compact, deterministic, serializable, and sufficient for the
537/// initial movement slice.
538#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
539pub struct DeterministicRng {
540    state: u64,
541}
542
543impl DeterministicRng {
544    const STEP: u64 = 0x9E37_79B9_7F4A_7C15;
545
546    #[must_use]
547    pub const fn from_seed(seed: u64) -> Self {
548        Self { state: seed }
549    }
550
551    #[must_use]
552    pub const fn state(self) -> u64 {
553        self.state
554    }
555
556    #[must_use]
557    pub const fn state_after(seed: u64, draws: u64) -> u64 {
558        seed.wrapping_add(Self::STEP.wrapping_mul(draws))
559    }
560
561    #[must_use]
562    pub const fn seed_before(state: u64, draws: u64) -> u64 {
563        state.wrapping_sub(Self::STEP.wrapping_mul(draws))
564    }
565
566    pub fn next_u64(&mut self) -> u64 {
567        self.state = self.state.wrapping_add(Self::STEP);
568        let mut value = self.state;
569        value = (value ^ (value >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
570        value = (value ^ (value >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
571        value ^ (value >> 31)
572    }
573
574    /// Returns a uniformly distributed value in `[0, upper_exclusive)`.
575    ///
576    /// Zero returns zero. Rejection sampling is used so non-power-of-two
577    /// bounds do not introduce modulo bias.
578    pub fn range(&mut self, upper_exclusive: u64) -> u64 {
579        if upper_exclusive == 0 {
580            return 0;
581        }
582        let rejection_threshold = upper_exclusive.wrapping_neg() % upper_exclusive;
583        loop {
584            let value = self.next_u64();
585            if value >= rejection_threshold {
586                return value % upper_exclusive;
587            }
588        }
589    }
590
591    /// Returns the historical modulo-reduction value for the V1 random-stream
592    /// contract.
593    #[must_use]
594    pub fn range_modulo(&mut self, upper_exclusive: u64) -> u64 {
595        if upper_exclusive == 0 {
596            return 0;
597        }
598        self.next_u64() % upper_exclusive
599    }
600}
601
602/// Error returned when a schema registration would replace an existing type.
603#[derive(Clone, Debug, Eq, PartialEq)]
604pub enum SchemaRegistryError {
605    DuplicateType(String),
606}
607
608impl Display for SchemaRegistryError {
609    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
610        match self {
611            Self::DuplicateType(type_name) => {
612                write!(formatter, "schema type {type_name} is already registered")
613            }
614        }
615    }
616}
617
618impl std::error::Error for SchemaRegistryError {}
619
620#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
621pub struct FieldSchema {
622    pub name: String,
623    pub value_type: String,
624    pub description: String,
625    pub reference_type: Option<String>,
626    pub writable_via_debug_command: bool,
627}
628
629#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
630pub struct TypeSchema {
631    pub type_name: String,
632    pub description: String,
633    pub fields: Vec<FieldSchema>,
634}
635
636#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
637pub struct SchemaRegistry {
638    types: BTreeMap<String, TypeSchema>,
639}
640
641impl SchemaRegistry {
642    /// Registers a schema without replacing a different existing definition.
643    ///
644    /// Re-registering an identical definition is accepted for idempotent
645    /// plugin registration.
646    ///
647    /// # Errors
648    ///
649    /// Returns [`SchemaRegistryError::DuplicateType`] when the type name is
650    /// already registered with a different definition.
651    pub fn register(&mut self, schema: TypeSchema) -> Result<(), SchemaRegistryError> {
652        if let Some(existing) = self.types.get(&schema.type_name) {
653            if existing == &schema {
654                return Ok(());
655            }
656            return Err(SchemaRegistryError::DuplicateType(schema.type_name));
657        }
658        self.types.insert(schema.type_name.clone(), schema);
659        Ok(())
660    }
661
662    #[must_use]
663    pub fn get(&self, type_name: &str) -> Option<&TypeSchema> {
664        self.types.get(type_name)
665    }
666
667    pub fn iter(&self) -> impl Iterator<Item = &TypeSchema> {
668        self.types.values()
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use super::*;
675
676    struct Office;
677
678    impl DomainRecordType for Office {
679        type Payload = String;
680        type Class = DomainEntityKindClass;
681
682        const NAMESPACE: &'static str = "fixture.governance";
683        const NAME: &'static str = "office";
684    }
685
686    struct Obligation;
687
688    impl DomainRecordType for Obligation {
689        type Payload = String;
690        type Class = DomainValueKindClass;
691
692        const NAMESPACE: &'static str = "fixture.governance";
693        const NAME: &'static str = "obligation";
694    }
695
696    struct Assessment;
697
698    impl KnowledgeRecordType for Assessment {
699        type Payload = String;
700
701        const NAMESPACE: &'static str = "fixture.knowledge";
702        const NAME: &'static str = "assessment";
703        const SCHEMA_VERSION: u32 = 2;
704    }
705
706    #[test]
707    fn typed_domain_identity_preserves_wire_shape_and_kind_boundary() {
708        let typed = TypedDomainRecordRef::<Office>::new("secretariat");
709        let raw = DomainRecordRef::new("fixture.governance", "office", "secretariat");
710
711        assert_eq!(
712            serde_json::to_value(&typed).expect("typed identity should serialize"),
713            serde_json::to_value(&raw).expect("raw identity should serialize")
714        );
715        let round_trip: TypedDomainRecordRef<Office> = serde_json::from_value(
716            serde_json::to_value(&typed).expect("typed identity should serialize"),
717        )
718        .expect("typed identity should deserialize");
719        assert_eq!(round_trip.as_untyped(), &raw);
720        assert_eq!(EntityRef::from(round_trip), EntityRef::Domain(raw.clone()));
721
722        let wrong_kind =
723            DomainRecordRef::new("fixture.governance", "obligation", "secretariat-duty");
724        assert_eq!(
725            TypedDomainRecordRef::<Office>::from_untyped(wrong_kind.clone()),
726            Err(wrong_kind)
727        );
728        assert!(TypedDomainRecordRef::<Obligation>::from_untyped(raw).is_err());
729        assert!(
730            serde_json::from_value::<TypedDomainRecordRef<Office>>(serde_json::json!({
731                "kind": {
732                    "namespace": "fixture.governance",
733                    "name": "obligation"
734                },
735                "id": "secretariat-duty"
736            }))
737            .is_err()
738        );
739    }
740
741    #[test]
742    fn knowledge_identity_and_holder_wire_shapes_are_stable() {
743        let kind = KnowledgeRecordKind::new(Assessment::NAMESPACE, Assessment::NAME);
744        let schema = KnowledgeSchemaId::new(kind.clone(), Assessment::SCHEMA_VERSION);
745
746        assert_eq!(kind.to_string(), "fixture.knowledge.assessment");
747        assert_eq!(schema.version, 2);
748        assert_eq!(schema.kind, kind);
749        assert_eq!(
750            KnowledgeHolderPolicy::default(),
751            KnowledgeHolderPolicy::Disallowed
752        );
753
754        assert_eq!(
755            serde_json::to_value(KnowledgeHolderRef::Person(PersonId::new(7)))
756                .expect("person holder should serialize"),
757            serde_json::json!({ "type": "person", "value": 7 })
758        );
759        let invalid_shape = KnowledgeHolderRef::Entity(EntityRef::Person(PersonId::new(7)));
760        assert!(invalid_shape.is_person_entity());
761        let institution =
762            KnowledgeHolderRef::Entity(EntityRef::Organization(OrganizationId::new(3)));
763        assert!(!institution.is_person_entity());
764        assert_eq!(
765            serde_json::to_value(institution).expect("institution holder should serialize"),
766            serde_json::json!({
767                "type": "entity",
768                "value": { "type": "organization", "id": 3 }
769            })
770        );
771    }
772
773    #[test]
774    fn exact_domain_record_evidence_has_a_stable_wire_identity() {
775        let evidence = EvidenceRef::DomainRecordVersion(DomainRecordVersionRef {
776            record: DomainRecordRef::new("fixture.information", "dispatch", "dispatch-7"),
777            version: 2,
778            established_by: DomainRecordVersionSource::BoundaryChange {
779                boundary: BoundaryId::new(12),
780                change_index: 3,
781            },
782        });
783
784        assert_eq!(
785            serde_json::to_value(evidence).expect("evidence should serialize"),
786            serde_json::json!({
787                "type": "domain_record_version",
788                "value": {
789                    "record": {
790                        "kind": {
791                            "namespace": "fixture.information",
792                            "name": "dispatch"
793                        },
794                        "id": "dispatch-7"
795                    },
796                    "version": 2,
797                    "established_by": {
798                        "type": "boundary_change",
799                        "boundary": 12,
800                        "change_index": 3
801                    }
802                }
803            })
804        );
805    }
806
807    #[test]
808    fn deterministic_range_stays_bounded_for_non_power_of_two_limits() {
809        for seed in 0..32 {
810            let mut rng = DeterministicRng::from_seed(seed);
811            assert_eq!(rng.range(0), 0);
812            for _ in 0..128 {
813                assert!(rng.range(7) < 7);
814            }
815        }
816    }
817
818    #[test]
819    fn schema_registration_is_idempotent_but_rejects_definition_replacement() {
820        let schema = TypeSchema {
821            type_name: "fixture.office".to_owned(),
822            description: "An office".to_owned(),
823            fields: vec![FieldSchema {
824                name: "name".to_owned(),
825                value_type: "string".to_owned(),
826                description: "The office name".to_owned(),
827                reference_type: None,
828                writable_via_debug_command: false,
829            }],
830        };
831        let mut registry = SchemaRegistry::default();
832        registry
833            .register(schema.clone())
834            .expect("the first schema registration should succeed");
835        registry
836            .register(schema.clone())
837            .expect("an identical schema registration should be idempotent");
838
839        let mut conflicting = schema;
840        conflicting.description = "A different office".to_owned();
841        assert_eq!(
842            registry
843                .register(conflicting)
844                .expect_err("a different definition must not replace the registered schema"),
845            SchemaRegistryError::DuplicateType("fixture.office".to_owned())
846        );
847    }
848}