1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
use serde::{Deserialize, Serialize};
// =============================================================================
// KVASIR IDENTITY -- Platform-wide unique identifier (crosscrate.md Finding #2)
// =============================================================================
/// Platform-wide unique identifier used by every CVKG graph layer.
///
/// # Why this exists
/// The crosscrate audit (Finding #2) identified that each crate maintained its own
/// incompatible `NodeId(u64)` newtype, causing type-level friction whenever two
/// layers needed to reference the same object (e.g., VDOM ↔ Scene sync).
///
/// # Contract
/// - Every `KvasirId` produced by [`KvasirId::new`] is globally unique within
/// a single process lifetime (backed by a monotonic atomic counter).
/// - IDs are sequential and cache-friendly in `HashMap` / `BTreeMap` keys.
/// - `KvasirId(0)` is **reserved as the null/invalid sentinel** — never returned
/// by `new()`.
/// - `Serialize`/`Deserialize` round-trips through the inner `u64`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub struct KvasirId(pub u64);
impl KvasirId {
/// The null sentinel value. Never allocated by [`KvasirId::new`].
pub const NULL: KvasirId = KvasirId(0);
/// Allocate a new process-unique `KvasirId`.
///
/// Uses a relaxed atomic increment — order does not matter because IDs
/// only need to be distinct, not sequentially ordered relative to other
/// memory operations.
pub fn new() -> Self {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(1);
KvasirId(COUNTER.fetch_add(1, Ordering::Relaxed))
}
/// Returns `true` if this is the null sentinel value.
pub fn is_null(self) -> bool {
self.0 == 0
}
}
impl std::fmt::Display for KvasirId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "KvasirId({})", self.0)
}
}
/// Lossless conversion from a raw `u64` into a `KvasirId`.
///
/// # Why this exists
/// The crosscrate audit (Phase 1 of the implementation plan) unifies identity
/// across `cvkg-scene`, `cvkg-vdom`, and `cvkg-flow` by making each crate's
/// `NodeId` a type alias for `KvasirId`. Existing call sites that constructed
/// `NodeId(some_u64)` need a way to migrate without touching every literal.
///
/// # Contract
/// - `u64` -> `KvasirId` is infallible (any `u64` is a valid id; 0 maps to NULL).
/// - `KvasirId` -> `u64` is infallible (trivially the inner value).
///
/// # Note
/// Allocating ids should still go through `KvasirId::new()` so that the
/// atomic counter is respected. `From<u64>` is for *existing* ids that came
/// from serialized data or stable test fixtures.
impl From<u64> for KvasirId {
fn from(value: u64) -> Self {
KvasirId(value)
}
}
impl From<KvasirId> for u64 {
fn from(id: KvasirId) -> Self {
id.0
}
}