ijima_core/memory.rs
1// Copyright (C) 2026 Industrial Algebra
2// SPDX-License-Identifier: Apache-2.0
3
4//! Memory palace domain types — the curated long-term memory surface.
5//!
6//! These types are import-compatible with the pi-mempalace schema (see
7//! `docs/HANDOFF.md` §3 for the live SQLite schema). The store
8//! implementation lives in `ijima-server`; this module defines only the
9//! pure domain model so it can be shared by the server, miner, and
10//! client crates.
11
12use crate::harness::Harness;
13use crate::provenance::{AuthorityScope, InstanceId};
14
15/// A newtype for the stable, opaque identifier of a stored memory.
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[cfg_attr(feature = "serde", serde(transparent))]
19pub struct MemoryId(pub String);
20
21/// A curated memory palace entry: the refined-metal output of either an
22/// explicit save or a mining pass.
23///
24/// Provenance (`harness`, `session_id`, `source`) is mandatory so that
25/// any entry can be traced back to the conversation that produced it.
26#[derive(Debug, Clone, PartialEq)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub struct Memory {
29 /// Stable, opaque identifier (e.g. `mem_<ulid>`).
30 pub id: MemoryId,
31 /// The curated content text.
32 pub content: String,
33 /// Project namespace (defaults to `"general"`).
34 pub project: String,
35 /// Topic within the project.
36 pub topic: String,
37 /// Provenance: how this entry was created — explicit save, auto-capture,
38 /// or a mined extraction.
39 pub source: MemorySource,
40 /// Provenance: which harness wrote this entry.
41 pub harness: Harness,
42 /// Provenance: the originating session, when known.
43 pub session_id: Option<String>,
44 /// Provenance: the instance that authored this entry (ADR
45 /// provenance-tier). Defaults to the local instance for 0.1.0;
46 /// federation (Phase 5) stamps the origin instance.
47 #[cfg_attr(feature = "serde", serde(default))]
48 pub origin: InstanceId,
49 /// Provenance: the authority scope (source-of-truth) for this entry's
50 /// domain (ADR provenance-tier). Defaults to local; drives Phase 5
51 /// conflict resolution.
52 #[cfg_attr(feature = "serde", serde(default))]
53 pub authority: AuthorityScope,
54 /// Importance score (0.0–1.0). Used for wake-up ranking
55 /// (top-N by importance × recency). Defaults to 0.5, matching
56 /// pi-mempalace.
57 #[cfg_attr(feature = "serde", serde(default = "default_importance"))]
58 pub importance: f32,
59 /// Creation timestamp. v0: Unix epoch seconds as a string (monotonic
60 /// for DESC ordering). Future: ISO-8601 when a time crate lands.
61 #[cfg_attr(feature = "serde", serde(default))]
62 pub created_at: String,
63}
64
65#[cfg(feature = "serde")]
66fn default_importance() -> f32 {
67 0.5
68}
69
70/// How a [`Memory`] entered the palace.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73pub enum MemorySource {
74 /// An operator or harness explicitly saved it.
75 Explicit,
76 /// An auto-capture hook wrote it.
77 AutoCapture,
78 /// The miner extracted it from a session transcript.
79 Mined,
80 /// Doctrine: curated, Git-versioned, PR-reviewed memory mirrored from
81 /// the repository seed pack. Never written directly by agents — the
82 /// highest-trust, lowest-write-rate tier.
83 Doctrine,
84}
85
86impl MemorySource {
87 /// The Schubert trust grade (codimension) of this tier — the
88 /// quantitative trust axis. **Higher = more trusted.** Phase 5 egress
89 /// filters turn this into intersection arithmetic ("does this
90 /// content's grade fit the link's trust budget?"); Phase 4 keys
91 /// doctrine-health on it.
92 ///
93 /// Grades fit comfortably inside Gr(4,8)'s 4×4 Schubert box:
94 ///
95 /// | Tier | `trust_grade` |
96 /// |---|---|
97 /// | [`AutoCapture`](MemorySource::AutoCapture) | 1 (ambient, unverified) |
98 /// | [`Mined`](MemorySource::Mined) | 2 (model-extracted, review-eligible) |
99 /// | [`Explicit`](MemorySource::Explicit) | 3 (a human deliberately saved it) |
100 /// | [`Doctrine`](MemorySource::Doctrine) | 4 (Git-versioned, PR-reviewed, curated) |
101 pub fn trust_grade(self) -> u64 {
102 match self {
103 MemorySource::AutoCapture => 1,
104 MemorySource::Mined => 2,
105 MemorySource::Explicit => 3,
106 MemorySource::Doctrine => 4,
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn memory_round_trips_its_fields() {
117 let m = Memory {
118 id: MemoryId("mem_01".into()),
119 content: "Decided to use DeepSeek for extraction.".into(),
120 project: "ijima".into(),
121 topic: "mining".into(),
122 source: MemorySource::Mined,
123 harness: Harness::Pi,
124 session_id: Some("sess_7".into()),
125 origin: InstanceId::local(),
126 authority: AuthorityScope::local(),
127 importance: 0.8,
128 created_at: "123".into(),
129 };
130 assert_eq!(m.id.0, "mem_01");
131 assert_eq!(m.source, MemorySource::Mined);
132 assert_eq!(m.harness, Harness::Pi);
133 assert_eq!(m.importance, 0.8);
134 assert_eq!(m.created_at, "123");
135 assert_eq!(m.session_id.as_deref(), Some("sess_7"));
136 }
137
138 #[test]
139 fn doctrine_is_distinct_from_explicit() {
140 // Doctrine is the curated, Git-versioned tier — it must not be
141 // confused with an operator's explicit save.
142 assert!(matches!(MemorySource::Doctrine, MemorySource::Doctrine));
143 assert_ne!(MemorySource::Doctrine, MemorySource::Explicit);
144 assert_ne!(MemorySource::Doctrine, MemorySource::Mined);
145 }
146
147 #[test]
148 fn trust_grade_monotone_in_trust() {
149 // Higher tier ⇒ higher grade (more trusted). Drives Phase 5 egress
150 // intersection arithmetic and Phase 4 doctrine-health.
151 assert_eq!(MemorySource::AutoCapture.trust_grade(), 1);
152 assert_eq!(MemorySource::Mined.trust_grade(), 2);
153 assert_eq!(MemorySource::Explicit.trust_grade(), 3);
154 assert_eq!(MemorySource::Doctrine.trust_grade(), 4);
155 assert!(
156 MemorySource::AutoCapture.trust_grade() < MemorySource::Mined.trust_grade()
157 && MemorySource::Mined.trust_grade() < MemorySource::Explicit.trust_grade()
158 && MemorySource::Explicit.trust_grade() < MemorySource::Doctrine.trust_grade()
159 );
160 }
161}