Skip to main content

ijima_core/
provenance.rs

1// Copyright (C) 2026 Industrial Algebra
2// SPDX-License-Identifier: Apache-2.0
3
4//! Provenance newtypes — the identity/authority stamp every [`Memory`]
5//! carries.
6//!
7//! See `docs/adr/provenance-tier-model.md`. These are the substrate for
8//! Phase 4 (context-poisoning source-tracing) and Phase 5 (federation
9//! provenance + authority-scoped conflict resolution). For 0.1.0
10//! (single instance) they default to the local instance; the fields are
11//! present so the schema doesn't churn when federation lands.
12
13/// The stable identifier of the Ijima instance that authored a record.
14///
15/// Typed newtype (IA convention) so an instance id is never confused with
16/// a free string, a [`crate::NamespaceId`], or an
17/// [`AuthorityScope`](self::AuthorityScope).
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20#[cfg_attr(feature = "serde", serde(transparent))]
21pub struct InstanceId(pub String);
22
23impl InstanceId {
24    /// The local instance id — the author of every record in a
25    /// single-instance 0.1.0 deployment.
26    ///
27    /// This is a pure constant (`"local"`) in the domain crate; the
28    /// `IJIMA_INSTANCE_ID` override is a Phase 5 concern, applied at the
29    /// server boundary once instances actually matter. Keeping core
30    /// process-environment-free preserves the "pure, backend-free" contract.
31    pub fn local() -> Self {
32        Self("local".to_string())
33    }
34}
35
36impl Default for InstanceId {
37    fn default() -> Self {
38        Self::local()
39    }
40}
41
42/// The scope (instance + namespace/project) that is **source-of-truth** for
43/// a record — the authority a receiver defers to on conflict (Phase 5).
44///
45/// For 0.1.0 it defaults to the local instance's scope; not exercised until
46/// federation, but present so the schema is forward-compatible.
47#[derive(Debug, Clone, PartialEq, Eq, Hash)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49#[cfg_attr(feature = "serde", serde(transparent))]
50pub struct AuthorityScope(pub String);
51
52impl AuthorityScope {
53    /// The local authority scope — the source-of-truth for a record in a
54    /// single-instance 0.1.0 deployment.
55    pub fn local() -> Self {
56        Self(InstanceId::local().0)
57    }
58}
59
60impl Default for AuthorityScope {
61    fn default() -> Self {
62        Self::local()
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn local_instance_id_is_the_constant_local() {
72        // Pure domain default — no env coupling (the IJIMA_INSTANCE_ID
73        // override is a Phase 5 server-boundary concern).
74        assert_eq!(InstanceId::local().0, "local");
75        assert_eq!(InstanceId::default().0, "local");
76    }
77
78    #[test]
79    fn local_authority_scope_mirrors_local_instance() {
80        assert_eq!(AuthorityScope::local().0, "local");
81        assert_eq!(AuthorityScope::default().0, InstanceId::local().0);
82    }
83
84    #[test]
85    fn instance_id_is_type_distinct_from_authority_scope() {
86        // Both wrap String but are distinct newtypes — cannot be
87        // confused at the type level (IA newtype convention).
88        let _: InstanceId = InstanceId("prod-hub".into());
89        let _: AuthorityScope = AuthorityScope("prod-hub/ijima".into());
90        assert_ne!(InstanceId("x".into()), InstanceId("y".into()));
91    }
92}