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}