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}