1use 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 #[must_use]
32 pub const fn new(value: u64) -> Self {
33 Self(value)
34 }
35
36 #[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#[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 #[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#[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#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
137pub struct DomainRecordRef {
138 pub kind: DomainRecordKind,
139 pub id: String,
140}
141
142#[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#[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#[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#[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#[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#[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#[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
237pub 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
266pub 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
283pub trait DomainKindClass: domain_kind_class::Sealed {
285 const IS_ENTITY: bool;
286}
287
288pub enum DomainEntityKindClass {}
290
291impl domain_kind_class::Sealed for DomainEntityKindClass {}
292
293impl DomainKindClass for DomainEntityKindClass {
294 const IS_ENTITY: bool = true;
295}
296
297pub enum DomainValueKindClass {}
299
300impl domain_kind_class::Sealed for DomainValueKindClass {}
301
302impl DomainKindClass for DomainValueKindClass {
303 const IS_ENTITY: bool = false;
304}
305
306pub trait DomainEntityType: DomainRecordType<Class = DomainEntityKindClass> {}
308
309impl<T: DomainRecordType<Class = DomainEntityKindClass>> DomainEntityType for T {}
310
311pub trait DomainValueType: DomainRecordType<Class = DomainValueKindClass> {}
313
314impl<T: DomainRecordType<Class = DomainValueKindClass>> DomainValueType for T {}
315
316#[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 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#[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#[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 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 #[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#[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 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}