Skip to main content

kernel/event/
identity.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! Event identity: run, sequence, and time.
7
8use serde::{Deserialize, Serialize};
9
10use crate::error::{AxCode, AxError};
11
12/// Run identity; uuid v7 for humans, nil for the city itself. No
13/// generation here — the assembly layer (or a seeded simulator) mints.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct RunId(#[serde(deserialize_with = "read_run_id")] uuid::Uuid);
17
18impl RunId {
19    /// City-level records (genesis, tail truncation) carry the nil id;
20    /// real runs use uuid v7, whose timestamp bits never collide with nil.
21    pub const CITY: RunId = RunId(uuid::Uuid::nil());
22
23    pub fn from_bytes(bytes: [u8; 16]) -> Self {
24        RunId(uuid::Uuid::from_bytes(bytes))
25    }
26
27    pub fn parse(raw: &str) -> Result<Self, AxError> {
28        let reject = || {
29            let err = AxError::failure(AxCode::InvalidArgs, "parse run id", raw).with_recovery(
30                "use the hyphenated lower-case uuid this city writes, as \
31                 `0198f6a2-7c4a-7bbb-9d1e-000000000001`",
32            );
33            Err(err)
34        };
35        let parsed = match uuid::Uuid::parse_str(raw) {
36            Ok(parsed) => parsed,
37            Err(_) => return reject(),
38        };
39        // Canonical echo, as `kernel::locator` does: the value's own
40        // spelling is the only input accepted, which closes bare hex,
41        // braces, the `urn:uuid:` prefix and upper case in one rule.
42        if parsed.hyphenated().to_string() != raw {
43            return reject();
44        }
45        Ok(RunId(parsed))
46    }
47
48    pub fn as_bytes(&self) -> &[u8; 16] {
49        self.0.as_bytes()
50    }
51}
52
53/// Reads a run id through [`RunId::parse`], so an identity that arrives
54/// in a frame or a ledger line cannot be read in a spelling the
55/// constructor refuses. The derived `Deserialize` would otherwise ask
56/// `uuid::Uuid` directly, which takes four spellings to this type's one.
57///
58/// A binary format has no spelling: `uuid::Uuid` writes its sixteen bytes
59/// there, and a snapshot of the views is read back through the same
60/// format it was written in, so only a human-readable format is parsed.
61fn read_run_id<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<uuid::Uuid, D::Error> {
62    if !deserializer.is_human_readable() {
63        return uuid::Uuid::deserialize(deserializer);
64    }
65    let raw = String::deserialize(deserializer)?;
66    RunId::parse(&raw)
67        .map(|run| run.0)
68        .map_err(serde::de::Error::custom)
69}
70
71impl std::fmt::Display for RunId {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        self.0.hyphenated().fmt(f)
74    }
75}
76
77/// Event sequence number; contiguous from [`Seq::FIRST`], checked arithmetic.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
79#[serde(transparent)]
80#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
81pub struct Seq(u64);
82
83impl Seq {
84    /// The genesis line's number.
85    pub const FIRST: Seq = Seq(0);
86
87    pub fn new(value: u64) -> Self {
88        Seq(value)
89    }
90
91    pub fn value(&self) -> u64 {
92        self.0
93    }
94
95    pub fn next(self) -> Result<Seq, AxError> {
96        self.0.checked_add(1).map(Seq).ok_or_else(|| {
97            AxError::failure(AxCode::InvalidArgs, "advance seq", self.0.to_string())
98                .with_recovery("sequence space exhausted; this ledger cannot grow further")
99        })
100    }
101}
102
103/// UTC milliseconds as an integer (determinism rule 6). Always a
104/// parameter, never sampled inside kernel or memory.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
106#[serde(transparent)]
107#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
108pub struct TimeMs(u64);
109
110impl TimeMs {
111    pub fn new(value: u64) -> Self {
112        TimeMs(value)
113    }
114
115    pub fn value(&self) -> u64 {
116        self.0
117    }
118}
119
120#[cfg(test)]
121#[allow(
122    clippy::unwrap_used,
123    clippy::expect_used,
124    clippy::panic,
125    clippy::indexing_slicing,
126    reason = "test code"
127)]
128mod tests {
129    use super::*;
130    #[test]
131    fn seq_and_time_are_checked_integers() {
132        assert_eq!(Seq::FIRST.value(), 0);
133        assert_eq!(Seq::FIRST.next().unwrap().value(), 1);
134        assert!(Seq::new(u64::MAX).next().is_err());
135        assert_eq!(TimeMs::new(1234).value(), 1234);
136    }
137
138    #[test]
139    fn a_run_id_is_read_in_the_one_spelling_the_city_writes() {
140        assert_eq!(
141            RunId::CITY.to_string(),
142            "00000000-0000-0000-0000-000000000000"
143        );
144        let run = RunId::parse("0198f6a2-7c4a-7bbb-9d1e-000000000001").unwrap();
145        assert_eq!(run.to_string(), "0198f6a2-7c4a-7bbb-9d1e-000000000001");
146        for refused in [
147            "not-a-uuid",
148            "0198f6a27c4a7bbb9d1e000000000001",       // bare hex
149            "{0198f6a2-7c4a-7bbb-9d1e-000000000001}", // braced
150            "urn:uuid:0198f6a2-7c4a-7bbb-9d1e-000000000001", // urn
151            "0198F6A2-7C4A-7BBB-9D1E-000000000001",   // upper case
152        ] {
153            assert!(RunId::parse(refused).is_err(), "{refused}");
154        }
155    }
156
157    /// What arrives in a frame or a ledger line is read by the same
158    /// constructor the city writes with, so a spelling the city would
159    /// never write cannot arrive and be believed.
160    #[test]
161    fn serde_reads_a_run_id_through_its_sole_constructor() {
162        let written = "\"0198f6a2-7c4a-7bbb-9d1e-000000000001\"";
163        let run: RunId = serde_json::from_str(written).unwrap();
164        assert_eq!(serde_json::to_string(&run).unwrap(), written);
165        for refused in [
166            "\"0198f6a27c4a7bbb9d1e000000000001\"",
167            "\"{0198f6a2-7c4a-7bbb-9d1e-000000000001}\"",
168            "\"urn:uuid:0198f6a2-7c4a-7bbb-9d1e-000000000001\"",
169        ] {
170            assert!(serde_json::from_str::<RunId>(refused).is_err(), "{refused}");
171        }
172    }
173}