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