spool/lifecycle_store/
api.rs1use super::{
4 LedgerEntry, LifecycleStore, ProposeMemoryRequest, RecordMemoryRequest, TransitionMetadata,
5 internal::{transition_memory, write_record},
6 projection::read_projection,
7};
8use crate::domain::{MemoryLedgerAction, MemoryLifecycleState, MemoryRecord, MemoryScope};
9
10pub fn record_manual_memory(
11 store: &LifecycleStore,
12 request: RecordMemoryRequest,
13) -> anyhow::Result<LedgerEntry> {
14 let mut record = MemoryRecord::new_manual(
15 request.title,
16 request.summary,
17 request.memory_type,
18 request.scope,
19 request.source_ref,
20 );
21 if let Some(project_id) = request.project_id {
22 record = record.with_project_id(project_id);
23 }
24 if let Some(user_id) = request.user_id {
25 record = record.with_user_id(user_id);
26 }
27 if let Some(sensitivity) = request.sensitivity {
28 record = record.with_sensitivity(sensitivity);
29 }
30 record.entities = request.entities;
31 record.tags = request.tags;
32 record.triggers = request.triggers;
33 record.related_files = request.related_files;
34 record.related_records = request.related_records;
35 record.supersedes = request.supersedes;
36 record.applies_to = request.applies_to;
37 record.valid_until = request.valid_until;
38 backfill_applies_to(&mut record);
39 write_record(store, record, request.metadata)
40}
41
42pub fn propose_ai_memory(
43 store: &LifecycleStore,
44 request: ProposeMemoryRequest,
45) -> anyhow::Result<LedgerEntry> {
46 let mut record = MemoryRecord::new_ai_proposal(
47 request.title,
48 request.summary,
49 request.memory_type,
50 request.scope,
51 request.source_ref,
52 );
53 if let Some(project_id) = request.project_id {
54 record = record.with_project_id(project_id);
55 }
56 if let Some(user_id) = request.user_id {
57 record = record.with_user_id(user_id);
58 }
59 if let Some(sensitivity) = request.sensitivity {
60 record = record.with_sensitivity(sensitivity);
61 }
62 record.entities = request.entities;
63 record.tags = request.tags;
64 record.triggers = request.triggers;
65 record.related_files = request.related_files;
66 record.related_records = request.related_records;
67 record.supersedes = request.supersedes;
68 record.applies_to = request.applies_to;
69 record.valid_until = request.valid_until;
70 backfill_applies_to(&mut record);
71 write_record(store, record, request.metadata)
72}
73
74pub fn accept_memory(store: &LifecycleStore, record_id: &str) -> anyhow::Result<LedgerEntry> {
75 accept_memory_with_metadata(store, record_id, TransitionMetadata::default())
76}
77
78pub fn accept_memory_with_metadata(
79 store: &LifecycleStore,
80 record_id: &str,
81 metadata: TransitionMetadata,
82) -> anyhow::Result<LedgerEntry> {
83 transition_memory(store, record_id, MemoryLedgerAction::Accept, metadata)
84}
85
86pub fn promote_memory_to_canonical(
87 store: &LifecycleStore,
88 record_id: &str,
89) -> anyhow::Result<LedgerEntry> {
90 promote_memory_to_canonical_with_metadata(store, record_id, TransitionMetadata::default())
91}
92
93pub fn promote_memory_to_canonical_with_metadata(
94 store: &LifecycleStore,
95 record_id: &str,
96 metadata: TransitionMetadata,
97) -> anyhow::Result<LedgerEntry> {
98 transition_memory(
99 store,
100 record_id,
101 MemoryLedgerAction::PromoteToCanonical,
102 metadata,
103 )
104}
105
106pub fn archive_memory(store: &LifecycleStore, record_id: &str) -> anyhow::Result<LedgerEntry> {
107 archive_memory_with_metadata(store, record_id, TransitionMetadata::default())
108}
109
110pub fn archive_memory_with_metadata(
111 store: &LifecycleStore,
112 record_id: &str,
113 metadata: TransitionMetadata,
114) -> anyhow::Result<LedgerEntry> {
115 transition_memory(store, record_id, MemoryLedgerAction::Archive, metadata)
116}
117
118pub fn read_events_for_record(
119 store: &LifecycleStore,
120 record_id: &str,
121) -> anyhow::Result<Vec<LedgerEntry>> {
122 Ok(store
123 .read_all()?
124 .into_iter()
125 .filter(|entry| entry.record_id == record_id)
126 .collect())
127}
128
129pub fn project_latest_state(
130 store: &LifecycleStore,
131 record_id: &str,
132) -> anyhow::Result<Option<MemoryRecord>> {
133 Ok(read_projection(store)?
134 .latest_by_record_id(record_id)
135 .map(|entry| entry.record.clone()))
136}
137
138pub fn latest_state_entries(store: &LifecycleStore) -> anyhow::Result<Vec<LedgerEntry>> {
139 Ok(read_projection(store)?.latest_entries().to_vec())
140}
141
142pub fn pending_review_entries(store: &LifecycleStore) -> anyhow::Result<Vec<LedgerEntry>> {
143 Ok(read_projection(store)?.pending_review())
144}
145
146pub fn wakeup_ready_entries(store: &LifecycleStore) -> anyhow::Result<Vec<LedgerEntry>> {
147 Ok(read_projection(store)?.wakeup_ready())
148}
149
150pub fn latest_state_by_scope(
151 store: &LifecycleStore,
152 scope: MemoryScope,
153 scope_key: &str,
154) -> anyhow::Result<Vec<LedgerEntry>> {
155 Ok(read_projection(store)?.by_scope(scope, scope_key))
156}
157
158pub fn latest_state_by_state(
159 store: &LifecycleStore,
160 state: MemoryLifecycleState,
161) -> anyhow::Result<Vec<LedgerEntry>> {
162 Ok(read_projection(store)?.by_state(state))
163}
164
165pub fn review_queue_for_scope(
166 store: &LifecycleStore,
167 scope: MemoryScope,
168 scope_key: &str,
169) -> anyhow::Result<Vec<LedgerEntry>> {
170 Ok(read_projection(store)?
171 .pending_review()
172 .into_iter()
173 .filter(|entry| entry.record.scope == scope && entry.scope_key == scope_key)
174 .collect())
175}
176
177pub fn wakeup_ready_for_scope(
178 store: &LifecycleStore,
179 scope: MemoryScope,
180 scope_key: &str,
181) -> anyhow::Result<Vec<LedgerEntry>> {
182 Ok(read_projection(store)?
183 .wakeup_ready()
184 .into_iter()
185 .filter(|entry| entry.record.scope == scope && entry.scope_key == scope_key)
186 .collect())
187}
188
189pub fn lifecycle_query_plan() -> &'static str {
190 "next query layer should expose latest-state reads by record_id/scope/state plus pending_review and wakeup_ready projections"
191}
192
193pub fn review_queue_plan() -> &'static str {
194 "review queue should read projected latest state and allow optional scope/scope_key filtering"
195}
196
197fn backfill_applies_to(record: &mut MemoryRecord) {
198 if record.scope == MemoryScope::Project
199 && let Some(ref pid) = record.project_id
200 && !record.applies_to.iter().any(|a| a == pid)
201 {
202 record.applies_to.push(pid.clone());
203 }
204}