Skip to main content

spool/lifecycle_store/
api.rs

1//! 对外 mutation / 查询 API。保持与拆分前同名、同签名,外部 `use crate::lifecycle_store::xxx` 不受影响。
2
3use 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}