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 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#[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 #[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#[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#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
134pub struct DomainRecordRef {
135 pub kind: DomainRecordKind,
136 pub id: String,
137}
138
139#[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#[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#[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#[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#[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#[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#[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
234pub 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
263pub 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
280pub trait DomainKindClass: domain_kind_class::Sealed {
282 const IS_ENTITY: bool;
283}
284
285pub enum DomainEntityKindClass {}
287
288impl domain_kind_class::Sealed for DomainEntityKindClass {}
289
290impl DomainKindClass for DomainEntityKindClass {
291 const IS_ENTITY: bool = true;
292}
293
294pub enum DomainValueKindClass {}
296
297impl domain_kind_class::Sealed for DomainValueKindClass {}
298
299impl DomainKindClass for DomainValueKindClass {
300 const IS_ENTITY: bool = false;
301}
302
303pub trait DomainEntityType: DomainRecordType<Class = DomainEntityKindClass> {}
305
306impl<T: DomainRecordType<Class = DomainEntityKindClass>> DomainEntityType for T {}
307
308pub trait DomainValueType: DomainRecordType<Class = DomainValueKindClass> {}
310
311impl<T: DomainRecordType<Class = DomainValueKindClass>> DomainValueType for T {}
312
313#[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 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#[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#[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 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}