feat(intelligence): persist pain points and proposals to SQLite
PainAggregator and SolutionGenerator were in-memory only, losing all data on restart. Add PainStorage module with SQLite backend (4 tables), dual-write strategy (hot cache + durable), and startup cache warming. - New: pain_storage.rs — SQLite CRUD for pain_points, pain_evidence, proposals, proposal_steps with schema initialization - Modified: pain_aggregator.rs — global PAIN_STORAGE singleton, init_pain_storage() for startup, dual-write in merge_or_create/update - Modified: solution_generator.rs — same dual-write pattern via global PAIN_STORAGE - 20 tests passing (10 storage + 10 aggregator)
This commit is contained in:
@@ -3,11 +3,16 @@
|
||||
//! When a PainPoint reaches confidence >= 0.7, the generator creates a Proposal
|
||||
//! with concrete steps derived from available skills and pipeline templates.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::debug;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::pain_aggregator::{PainEvidence, PainPoint, PainSeverity};
|
||||
use super::pain_storage::PainStorage;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Proposal data structures
|
||||
@@ -175,10 +180,10 @@ impl Proposal {
|
||||
// SolutionGenerator — manages proposals lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
/// Manages proposal generation from confirmed pain points.
|
||||
///
|
||||
/// When the global `PAIN_STORAGE` is initialized (via `init_pain_storage`),
|
||||
/// writes are dual: memory Vec (hot cache) + SQLite (durable).
|
||||
pub struct SolutionGenerator {
|
||||
proposals: Arc<RwLock<Vec<Proposal>>>,
|
||||
}
|
||||
@@ -190,11 +195,36 @@ impl SolutionGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the global pain storage, if initialized.
|
||||
fn get_storage() -> Option<Arc<PainStorage>> {
|
||||
super::pain_aggregator::PAIN_STORAGE.get().cloned()
|
||||
}
|
||||
|
||||
/// Load all persisted proposals from storage into the in-memory cache.
|
||||
pub async fn load_from_storage(&self) -> zclaw_types::Result<()> {
|
||||
if let Some(storage) = Self::get_storage() {
|
||||
let persisted = storage.load_all_proposals().await?;
|
||||
let mut proposals = self.proposals.write().await;
|
||||
*proposals = persisted;
|
||||
debug!("[SolutionGenerator] Loaded {} proposals from storage", proposals.len());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate a proposal for a high-confidence pain point.
|
||||
/// Persists to SQLite if storage is configured.
|
||||
pub async fn generate_solution(&self, pain: &PainPoint) -> Proposal {
|
||||
let proposal = Proposal::from_pain_point(pain);
|
||||
let mut proposals = self.proposals.write().await;
|
||||
proposals.push(proposal.clone());
|
||||
|
||||
// Dual-write
|
||||
if let Some(storage) = Self::get_storage() {
|
||||
if let Err(e) = storage.store_proposal(&proposal).await {
|
||||
debug!("[SolutionGenerator] Failed to persist proposal: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
proposal
|
||||
}
|
||||
|
||||
@@ -208,12 +238,20 @@ impl SolutionGenerator {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Update the status of a proposal.
|
||||
/// Update the status of a proposal. Persists to SQLite if configured.
|
||||
pub async fn update_status(&self, proposal_id: &str, status: ProposalStatus) -> Option<Proposal> {
|
||||
let mut proposals = self.proposals.write().await;
|
||||
if let Some(p) = proposals.iter_mut().find(|p| p.id == proposal_id) {
|
||||
p.status = status;
|
||||
p.updated_at = Utc::now();
|
||||
|
||||
// Dual-write
|
||||
if let Some(storage) = Self::get_storage() {
|
||||
if let Err(e) = storage.store_proposal(p).await {
|
||||
debug!("[SolutionGenerator] Failed to persist proposal update: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Some(p.clone())
|
||||
} else {
|
||||
None
|
||||
|
||||
Reference in New Issue
Block a user