Skip to main content

mimir_core/
symbol.rs

1//! `SymbolId`, `ScopedSymbolId`, `SymbolKind` — the symbol-identity
2//! primitives from `docs/concepts/symbol-identity-semantics.md` §§ 3–4.
3
4use std::fmt;
5
6use crate::workspace::WorkspaceId;
7
8/// A workspace-local symbol identifier.
9///
10/// Monotonic `u64` counter allocated by the librarian (see
11/// `symbol-identity-semantics.md` § 5.1). `SymbolId(42)` in workspace A
12/// and `SymbolId(42)` in workspace B are **distinct symbols** in
13/// distinct tables — never cross-workspace equal.
14///
15/// # Examples
16///
17/// ```
18/// use mimir_core::SymbolId;
19///
20/// let a = SymbolId::new(42);
21/// assert_eq!(a.as_u64(), 42);
22/// ```
23#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub struct SymbolId(u64);
25
26impl SymbolId {
27    /// Construct a [`SymbolId`] from its raw `u64` value.
28    #[must_use]
29    pub const fn new(raw: u64) -> Self {
30        Self(raw)
31    }
32
33    /// The underlying numeric ID.
34    #[must_use]
35    pub const fn as_u64(self) -> u64 {
36        self.0
37    }
38}
39
40impl fmt::Display for SymbolId {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        write!(f, "#{}", self.0)
43    }
44}
45
46/// A fully-qualified symbol reference carrying both a workspace and a
47/// local ID.
48///
49/// Surfaced at workspace boundaries — cross-workspace reads, decoder
50/// output, audit logs — where the workspace component must be explicit.
51/// Within a workspace the librarian uses bare `SymbolId` internally.
52/// See `symbol-identity-semantics.md` § 3.2.
53#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
54pub struct ScopedSymbolId {
55    /// The workspace that owns the underlying symbol.
56    pub workspace: WorkspaceId,
57    /// The workspace-local symbol identifier.
58    pub local: SymbolId,
59}
60
61impl ScopedSymbolId {
62    /// Construct a [`ScopedSymbolId`].
63    #[must_use]
64    pub const fn new(workspace: WorkspaceId, local: SymbolId) -> Self {
65        Self { workspace, local }
66    }
67}
68
69impl fmt::Display for ScopedSymbolId {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "{}@{}", self.local, self.workspace)
72    }
73}
74
75/// Kind of a symbol in the Mimir taxonomy.
76///
77/// Twelve kinds, matching `symbol-identity-semantics.md` § 4. The enum
78/// is `#[non_exhaustive]` so additions do not break callers on semver
79/// minor bumps (see `PRINCIPLES.md` § 10).
80#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
81#[non_exhaustive]
82pub enum SymbolKind {
83    /// An actor — profile subject, observer, reporter, rule-maker.
84    Agent,
85    /// A static document with a citation pointer.
86    Document,
87    /// An authoritative programmatic source (package manifest, DNS,
88    /// filesystem metadata).
89    Registry,
90    /// A live third-party API.
91    Service,
92    /// A policy-making source distinct from an Agent (dual-kind symbols
93    /// are not permitted in v1).
94    Policy,
95    /// A memory ID (used in `derived_from`, `event_id`, `rule_id`).
96    Memory,
97    /// A registered inference-method tag for an [`crate::Inferential`]
98    /// memory.
99    InferenceMethod,
100    /// A scope tag — Procedural rule applicability, named ephemeral
101    /// scopes.
102    Scope,
103    /// A predicate name in an `s-p-o` tuple.
104    Predicate,
105    /// An event-type tag for an [`crate::Episodic`] memory.
106    EventType,
107    /// A workspace identifier referenced as a symbol.
108    Workspace,
109    /// A typed-value bareword not belonging to any registry (catch-all
110    /// for enum-ish values).
111    Literal,
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use ulid::Ulid;
118
119    #[test]
120    fn symbol_id_roundtrip() {
121        for raw in [0_u64, 1, 42, u64::MAX] {
122            assert_eq!(SymbolId::new(raw).as_u64(), raw);
123        }
124    }
125
126    #[test]
127    fn scoped_symbol_distinguishes_workspaces() {
128        let ws_a = WorkspaceId::from_ulid(Ulid::from_parts(1, 1));
129        let ws_b = WorkspaceId::from_ulid(Ulid::from_parts(2, 2));
130        let a = ScopedSymbolId::new(ws_a, SymbolId::new(42));
131        let b = ScopedSymbolId::new(ws_b, SymbolId::new(42));
132        assert_ne!(a, b);
133    }
134
135    #[test]
136    fn scoped_symbol_equal_when_workspace_and_local_match() {
137        let ws = WorkspaceId::from_ulid(Ulid::from_parts(7, 7));
138        let a = ScopedSymbolId::new(ws, SymbolId::new(99));
139        let b = ScopedSymbolId::new(ws, SymbolId::new(99));
140        assert_eq!(a, b);
141    }
142
143    #[test]
144    fn symbol_kind_is_nonexhaustive_pattern() {
145        // Compile-time check: exhaustive matches over #[non_exhaustive] enums
146        // from the defining crate are allowed, so this test only asserts we
147        // can still use the enum in a match block.
148        let kind = SymbolKind::Agent;
149        let label = match kind {
150            SymbolKind::Agent => "agent",
151            SymbolKind::Document => "document",
152            SymbolKind::Registry => "registry",
153            SymbolKind::Service => "service",
154            SymbolKind::Policy => "policy",
155            SymbolKind::Memory => "memory",
156            SymbolKind::InferenceMethod => "inference_method",
157            SymbolKind::Scope => "scope",
158            SymbolKind::Predicate => "predicate",
159            SymbolKind::EventType => "event_type",
160            SymbolKind::Workspace => "workspace",
161            SymbolKind::Literal => "literal",
162        };
163        assert_eq!(label, "agent");
164    }
165}