Skip to main content

arkhe_forge_core/
lib.rs

1//! # ArkheForge Runtime — L1 Primitives (`arkhe-forge-core`)
2//!
3//! Core 5 primitive types (User / Actor / Space / Entry / Activity), sealed
4//! `ArkheComponent` / `ArkheAction` / `ArkheEvent` traits, `ShellBrand<'s>`
5//! invariant-lifetime isolation, deterministic entity-id derivation.
6//!
7//! Pure compute — no I/O, no time, no randomness (L0 A11 succession). Only
8//! depends on `arkhe-kernel` (L0) and `arkhe-macros` (derive provider).
9//! `arkhe-forge-platform` must not be pulled in (layer-independence directive).
10
11#![forbid(unsafe_code)]
12#![warn(missing_docs)]
13
14// Self-alias so macro-generated `::arkhe_forge_core::...` paths resolve when
15// the derive is applied inside this crate itself.
16extern crate self as arkhe_forge_core;
17
18pub mod action;
19pub mod activity;
20pub mod actor;
21pub mod brand;
22pub mod bridge;
23pub mod component;
24pub mod context;
25pub mod entry;
26pub mod event;
27pub mod pii;
28pub mod pipeline;
29// Internal sealed-trait machinery — `__sealed::__Sealed` is exposed only so
30// the runtime derive macros can emit `impl __Sealed for UserType {}`.
31// Downstream crates must never reference this module. It is a convention seal,
32// not a compile-time hard seal: a derive macro in the sibling
33// `arkhe-forge-macros` crate emits the impl into downstream code, so the path
34// is necessarily name-reachable downstream and cannot be language-sealed. See
35// `sealed.rs` for the full rationale.
36#[doc(hidden)]
37#[path = "sealed.rs"]
38pub mod __sealed;
39pub mod space;
40pub mod typecode;
41pub mod user;
42
43/// Re-exports of the Runtime-layer derive + attribute macros from
44/// `arkhe-forge-macros`. `#[arkhe_pure]` is the E14.L1 Subset-Rust
45/// purity attribute for `Action::compute` bodies.
46pub use arkhe_forge_macros::{arkhe_pure, ArkheAction, ArkheComponent, ArkheEvent};
47
48use arkhe_kernel::abi::{EntityId, InstanceId, Tick, TypeCode};
49
50/// ArkheForge Runtime semver triple — matches the repo release.
51pub const RUNTIME_SEMVER: (u16, u16, u16) = (0, 14, 0);
52
53/// Maximum retry count for zero-digest collision avoidance.
54pub const MAX_ID_DERIVE_RETRIES: u32 = 16;
55
56/// Deterministic entity-id derivation. Pure — no hidden state.
57///
58/// Given a 256-bit non-exportable `world_seed`, a per-runtime `instance_id`,
59/// the primitive's `type_code`, and the spawning `tick` / `seq`, returns a
60/// collision-resistant `EntityId`. Zero-valued outputs are retried by
61/// incrementing `seq` up to [`MAX_ID_DERIVE_RETRIES`]; exhaustion returns
62/// `None` so the caller can emit an `IdExhaustion` Failure event.
63#[must_use]
64pub fn derive_entity_id(
65    world_seed: &[u8; 32],
66    instance_id: InstanceId,
67    type_code: TypeCode,
68    tick: Tick,
69    seq: u32,
70) -> Option<EntityId> {
71    let domain_key = blake3::derive_key("arkhe-forge-entity-id", world_seed);
72    for bump in 0..MAX_ID_DERIVE_RETRIES {
73        let mut h = blake3::Hasher::new_keyed(&domain_key);
74        h.update(&instance_id.get().to_be_bytes());
75        h.update(&type_code.0.to_be_bytes());
76        h.update(&tick.0.to_be_bytes());
77        // `seq` occupies the linear sequence domain; `bump` is a DISTINCT
78        // retry domain position. Hashing both (rather than folding `bump`
79        // into `seq` via wrapping_add, which let `seq=0,bump=1` collide with
80        // `seq=1,bump=0`) guarantees the zero-digest retry space cannot
81        // overlap the next caller's linear `seq` and produce a duplicate
82        // `EntityId` for two distinct `seq` values.
83        h.update(&seq.to_be_bytes());
84        h.update(&bump.to_be_bytes());
85        let out = h.finalize();
86        let digest = out.as_bytes();
87        let mut first_eight = [0u8; 8];
88        first_eight.copy_from_slice(&digest[..8]);
89        let raw = u64::from_be_bytes(first_eight);
90        if let Some(id) = EntityId::new(raw) {
91            return Some(id);
92        }
93    }
94    None
95}
96
97#[cfg(test)]
98#[allow(clippy::unwrap_used, clippy::expect_used)]
99mod lib_tests {
100    use super::*;
101
102    fn instance(v: u64) -> InstanceId {
103        InstanceId::new(v).unwrap()
104    }
105
106    #[test]
107    fn derive_entity_id_is_deterministic() {
108        let seed = [0x11u8; 32];
109        let iid = instance(1);
110        let a = derive_entity_id(&seed, iid, TypeCode(0x0001_0001), Tick(42), 0);
111        let b = derive_entity_id(&seed, iid, TypeCode(0x0001_0001), Tick(42), 0);
112        assert_eq!(a, b);
113        assert!(a.is_some());
114    }
115
116    #[test]
117    fn derive_entity_id_differs_across_type_code() {
118        let seed = [0x11u8; 32];
119        let iid = instance(1);
120        let a = derive_entity_id(&seed, iid, TypeCode(0x0001_0001), Tick(1), 0);
121        let b = derive_entity_id(&seed, iid, TypeCode(0x0001_0002), Tick(1), 0);
122        assert_ne!(a, b);
123    }
124
125    #[test]
126    fn derive_entity_id_differs_across_instance() {
127        let seed = [0x11u8; 32];
128        let a = derive_entity_id(&seed, instance(1), TypeCode(0x0001_0001), Tick(1), 0);
129        let b = derive_entity_id(&seed, instance(2), TypeCode(0x0001_0001), Tick(1), 0);
130        assert_ne!(a, b);
131    }
132
133    #[test]
134    fn derive_entity_id_differs_across_world_seed() {
135        let s1 = [0x01u8; 32];
136        let s2 = [0x02u8; 32];
137        let iid = instance(1);
138        let a = derive_entity_id(&s1, iid, TypeCode(0x0001_0001), Tick(1), 0);
139        let b = derive_entity_id(&s2, iid, TypeCode(0x0001_0001), Tick(1), 0);
140        assert_ne!(a, b);
141    }
142
143    #[test]
144    fn derive_entity_id_seq_changes_output() {
145        let seed = [0x11u8; 32];
146        let iid = instance(1);
147        let a = derive_entity_id(&seed, iid, TypeCode(0x0001_0001), Tick(1), 0);
148        let b = derive_entity_id(&seed, iid, TypeCode(0x0001_0001), Tick(1), 1);
149        assert_ne!(a, b);
150    }
151
152    /// Retry-domain non-overlap: the zero-digest retry hashes a separate
153    /// `bump` counter, so the retry space of `seq=k` cannot collide with the
154    /// linear id of any other `seq`. Exercising a contiguous seq range proves
155    /// the linear sequence stays collision-free; the previous `seq+bump` fold
156    /// admitted `(seq=0,bump=1)` == `(seq=1,bump=0)`.
157    #[test]
158    fn derive_entity_id_linear_seq_space_is_collision_free() {
159        use std::collections::HashSet;
160        let seed = [0x5Au8; 32];
161        let iid = instance(1);
162        let mut seen = HashSet::new();
163        for seq in 0u32..4096 {
164            let id = derive_entity_id(&seed, iid, TypeCode(0x0001_0001), Tick(7), seq)
165                .expect("non-zero digest within retry bound");
166            assert!(
167                seen.insert(id.get()),
168                "seq {seq} produced a duplicate EntityId — linear seq collision",
169            );
170        }
171    }
172}