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