spool-memory 0.2.3

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
//! 对外 mutation / 查询 API。保持与拆分前同名、同签名,外部 `use crate::lifecycle_store::xxx` 不受影响。

use super::{
    LedgerEntry, LifecycleStore, ProposeMemoryRequest, RecordMemoryRequest, TransitionMetadata,
    internal::{transition_memory, write_record},
    projection::read_projection,
};
use crate::domain::{MemoryLedgerAction, MemoryLifecycleState, MemoryRecord, MemoryScope};

pub fn record_manual_memory(
    store: &LifecycleStore,
    request: RecordMemoryRequest,
) -> anyhow::Result<LedgerEntry> {
    let mut record = MemoryRecord::new_manual(
        request.title,
        request.summary,
        request.memory_type,
        request.scope,
        request.source_ref,
    );
    if let Some(project_id) = request.project_id {
        record = record.with_project_id(project_id);
    }
    if let Some(user_id) = request.user_id {
        record = record.with_user_id(user_id);
    }
    if let Some(sensitivity) = request.sensitivity {
        record = record.with_sensitivity(sensitivity);
    }
    record.entities = request.entities;
    record.tags = request.tags;
    record.triggers = request.triggers;
    record.related_files = request.related_files;
    record.related_records = request.related_records;
    record.supersedes = request.supersedes;
    record.applies_to = request.applies_to;
    record.valid_until = request.valid_until;
    backfill_applies_to(&mut record);
    write_record(store, record, request.metadata)
}

pub fn propose_ai_memory(
    store: &LifecycleStore,
    request: ProposeMemoryRequest,
) -> anyhow::Result<LedgerEntry> {
    let mut record = MemoryRecord::new_ai_proposal(
        request.title,
        request.summary,
        request.memory_type,
        request.scope,
        request.source_ref,
    );
    if let Some(project_id) = request.project_id {
        record = record.with_project_id(project_id);
    }
    if let Some(user_id) = request.user_id {
        record = record.with_user_id(user_id);
    }
    if let Some(sensitivity) = request.sensitivity {
        record = record.with_sensitivity(sensitivity);
    }
    record.entities = request.entities;
    record.tags = request.tags;
    record.triggers = request.triggers;
    record.related_files = request.related_files;
    record.related_records = request.related_records;
    record.supersedes = request.supersedes;
    record.applies_to = request.applies_to;
    record.valid_until = request.valid_until;
    backfill_applies_to(&mut record);
    write_record(store, record, request.metadata)
}

pub fn accept_memory(store: &LifecycleStore, record_id: &str) -> anyhow::Result<LedgerEntry> {
    accept_memory_with_metadata(store, record_id, TransitionMetadata::default())
}

pub fn accept_memory_with_metadata(
    store: &LifecycleStore,
    record_id: &str,
    metadata: TransitionMetadata,
) -> anyhow::Result<LedgerEntry> {
    transition_memory(store, record_id, MemoryLedgerAction::Accept, metadata)
}

pub fn promote_memory_to_canonical(
    store: &LifecycleStore,
    record_id: &str,
) -> anyhow::Result<LedgerEntry> {
    promote_memory_to_canonical_with_metadata(store, record_id, TransitionMetadata::default())
}

pub fn promote_memory_to_canonical_with_metadata(
    store: &LifecycleStore,
    record_id: &str,
    metadata: TransitionMetadata,
) -> anyhow::Result<LedgerEntry> {
    transition_memory(
        store,
        record_id,
        MemoryLedgerAction::PromoteToCanonical,
        metadata,
    )
}

pub fn archive_memory(store: &LifecycleStore, record_id: &str) -> anyhow::Result<LedgerEntry> {
    archive_memory_with_metadata(store, record_id, TransitionMetadata::default())
}

pub fn archive_memory_with_metadata(
    store: &LifecycleStore,
    record_id: &str,
    metadata: TransitionMetadata,
) -> anyhow::Result<LedgerEntry> {
    transition_memory(store, record_id, MemoryLedgerAction::Archive, metadata)
}

pub fn read_events_for_record(
    store: &LifecycleStore,
    record_id: &str,
) -> anyhow::Result<Vec<LedgerEntry>> {
    Ok(store
        .read_all()?
        .into_iter()
        .filter(|entry| entry.record_id == record_id)
        .collect())
}

pub fn project_latest_state(
    store: &LifecycleStore,
    record_id: &str,
) -> anyhow::Result<Option<MemoryRecord>> {
    Ok(read_projection(store)?
        .latest_by_record_id(record_id)
        .map(|entry| entry.record.clone()))
}

pub fn latest_state_entries(store: &LifecycleStore) -> anyhow::Result<Vec<LedgerEntry>> {
    Ok(read_projection(store)?.latest_entries().to_vec())
}

pub fn pending_review_entries(store: &LifecycleStore) -> anyhow::Result<Vec<LedgerEntry>> {
    Ok(read_projection(store)?.pending_review())
}

pub fn wakeup_ready_entries(store: &LifecycleStore) -> anyhow::Result<Vec<LedgerEntry>> {
    Ok(read_projection(store)?.wakeup_ready())
}

pub fn latest_state_by_scope(
    store: &LifecycleStore,
    scope: MemoryScope,
    scope_key: &str,
) -> anyhow::Result<Vec<LedgerEntry>> {
    Ok(read_projection(store)?.by_scope(scope, scope_key))
}

pub fn latest_state_by_state(
    store: &LifecycleStore,
    state: MemoryLifecycleState,
) -> anyhow::Result<Vec<LedgerEntry>> {
    Ok(read_projection(store)?.by_state(state))
}

pub fn review_queue_for_scope(
    store: &LifecycleStore,
    scope: MemoryScope,
    scope_key: &str,
) -> anyhow::Result<Vec<LedgerEntry>> {
    Ok(read_projection(store)?
        .pending_review()
        .into_iter()
        .filter(|entry| entry.record.scope == scope && entry.scope_key == scope_key)
        .collect())
}

pub fn wakeup_ready_for_scope(
    store: &LifecycleStore,
    scope: MemoryScope,
    scope_key: &str,
) -> anyhow::Result<Vec<LedgerEntry>> {
    Ok(read_projection(store)?
        .wakeup_ready()
        .into_iter()
        .filter(|entry| entry.record.scope == scope && entry.scope_key == scope_key)
        .collect())
}

pub fn lifecycle_query_plan() -> &'static str {
    "next query layer should expose latest-state reads by record_id/scope/state plus pending_review and wakeup_ready projections"
}

pub fn review_queue_plan() -> &'static str {
    "review queue should read projected latest state and allow optional scope/scope_key filtering"
}

fn backfill_applies_to(record: &mut MemoryRecord) {
    if record.scope == MemoryScope::Project
        && let Some(ref pid) = record.project_id
        && !record.applies_to.iter().any(|a| a == pid)
    {
        record.applies_to.push(pid.clone());
    }
}