#![allow(dead_code)]
use anyhow::{Context, Result};
use chrono::Utc;
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pattern {
pub id: String,
pub user_id: String,
pub project: String,
pub slug: String,
pub title: String,
pub description: String,
pub reason: String,
pub file_paths: Vec<String>,
pub usage_count: i64,
pub last_used_at: Option<String>,
pub evolution_log: Vec<EvolutionEntry>,
pub source_signal_ids: Vec<String>,
pub source_cluster_signature: Option<i64>,
pub status: String,
pub metadata: serde_json::Value,
pub created_at: String,
pub updated_at: String,
pub synced_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvolutionEntry {
pub at: String,
pub kind: String,
pub summary: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_advice_id: Option<String>,
}
#[derive(Debug, Clone)]
pub struct PatternInput {
pub user_id: String,
pub project: String,
pub slug: String,
pub title: String,
pub description: String,
pub reason: String,
pub file_paths: Vec<String>,
pub source_signal_ids: Vec<String>,
pub source_cluster_signature: Option<i64>,
pub metadata: serde_json::Value,
}
pub fn insert(conn: &Connection, input: PatternInput) -> Result<Pattern> {
let id = super::uuid_like();
let now = Utc::now().to_rfc3339();
let initial_log = vec![EvolutionEntry {
at: now.clone(),
kind: "created".into(),
summary: format!(
"초기 생성 — 신호 {}개에서 도출",
input.source_signal_ids.len()
),
source_advice_id: None,
}];
conn.execute(
r#"INSERT INTO patterns
(id, user_id, project, slug, title, description, reason,
file_paths, evolution_log, source_signal_ids,
source_cluster_signature, status, metadata, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10,
?11, 'active', ?12, ?13, ?13)"#,
params![
id,
input.user_id,
input.project,
input.slug,
input.title,
input.description,
input.reason,
serde_json::to_string(&input.file_paths)?,
serde_json::to_string(&initial_log)?,
serde_json::to_string(&input.source_signal_ids)?,
input.source_cluster_signature,
input.metadata.to_string(),
now,
],
)
.context("insert pattern")?;
get(conn, &input.user_id, &id)?.context("pattern missing after insert")
}
pub fn get(conn: &Connection, user_id: &str, id: &str) -> Result<Option<Pattern>> {
let mut stmt = conn.prepare(SELECT_ALL_FROM_HARNESSES)?;
Ok(stmt.query_row(params![user_id, id], row_to_pattern).ok())
}
pub fn get_by_slug(
conn: &Connection,
user_id: &str,
project: &str,
slug: &str,
) -> Result<Option<Pattern>> {
let mut stmt = conn.prepare(
r#"SELECT id, user_id, project, slug, title, description, reason,
file_paths, usage_count, last_used_at, evolution_log,
source_signal_ids, source_cluster_signature, status, metadata,
created_at, updated_at, synced_at
FROM patterns
WHERE user_id = ?1 AND project = ?2 AND slug = ?3"#,
)?;
Ok(stmt
.query_row(params![user_id, project, slug], row_to_pattern)
.ok())
}
pub fn list(conn: &Connection, user_id: &str, project: Option<&str>) -> Result<Vec<Pattern>> {
if let Some(p) = project {
let mut stmt = conn.prepare(
r#"SELECT id, user_id, project, slug, title, description, reason,
file_paths, usage_count, last_used_at, evolution_log,
source_signal_ids, source_cluster_signature, status, metadata,
created_at, updated_at, synced_at
FROM patterns
WHERE user_id = ?1 AND project = ?2 AND status = 'active'
ORDER BY last_used_at DESC NULLS LAST, created_at DESC"#,
)?;
let rows: Vec<Pattern> = stmt
.query_map(params![user_id, p], row_to_pattern)?
.filter_map(|r| r.ok())
.collect();
Ok(rows)
} else {
let mut stmt = conn.prepare(
r#"SELECT id, user_id, project, slug, title, description, reason,
file_paths, usage_count, last_used_at, evolution_log,
source_signal_ids, source_cluster_signature, status, metadata,
created_at, updated_at, synced_at
FROM patterns
WHERE user_id = ?1 AND status = 'active'
ORDER BY last_used_at DESC NULLS LAST, created_at DESC"#,
)?;
let rows: Vec<Pattern> = stmt
.query_map(params![user_id], row_to_pattern)?
.filter_map(|r| r.ok())
.collect();
Ok(rows)
}
}
pub fn list_by_signature(conn: &Connection, user_id: &str, signature: i64) -> Result<Vec<Pattern>> {
let mut stmt = conn.prepare(
r#"SELECT id, user_id, project, slug, title, description, reason,
file_paths, usage_count, last_used_at, evolution_log,
source_signal_ids, source_cluster_signature, status, metadata,
created_at, updated_at, synced_at
FROM patterns
WHERE user_id = ?1 AND source_cluster_signature = ?2 AND status = 'active'
ORDER BY usage_count DESC"#,
)?;
let rows: Vec<Pattern> = stmt
.query_map(params![user_id, signature], row_to_pattern)?
.filter_map(|r| r.ok())
.collect();
Ok(rows)
}
pub fn record_use(conn: &Connection, id: &str) -> Result<()> {
let now = Utc::now().to_rfc3339();
conn.execute(
r#"UPDATE patterns
SET usage_count = usage_count + 1,
last_used_at = ?1,
updated_at = ?1,
synced_at = NULL
WHERE id = ?2"#,
params![now, id],
)?;
Ok(())
}
pub fn append_evolution(conn: &Connection, id: &str, entry: EvolutionEntry) -> Result<()> {
let now = Utc::now().to_rfc3339();
let mut stmt = conn.prepare("SELECT evolution_log FROM patterns WHERE id = ?1")?;
let log_str: String = stmt
.query_row(params![id], |r| r.get(0))
.unwrap_or_else(|_| "[]".into());
let mut log: Vec<EvolutionEntry> = serde_json::from_str(&log_str).unwrap_or_default();
log.push(entry);
conn.execute(
r#"UPDATE patterns
SET evolution_log = ?1, updated_at = ?2, synced_at = NULL
WHERE id = ?3"#,
params![serde_json::to_string(&log)?, now, id],
)?;
Ok(())
}
pub fn set_status(conn: &Connection, id: &str, status: &str) -> Result<()> {
let now = Utc::now().to_rfc3339();
conn.execute(
r#"UPDATE patterns
SET status = ?1, updated_at = ?2, synced_at = NULL
WHERE id = ?3"#,
params![status, now, id],
)?;
Ok(())
}
pub fn list_unsynced(conn: &Connection, limit: usize) -> Result<Vec<Pattern>> {
let mut stmt = conn.prepare(
r#"SELECT id, user_id, project, slug, title, description, reason,
file_paths, usage_count, last_used_at, evolution_log,
source_signal_ids, source_cluster_signature, status, metadata,
created_at, updated_at, synced_at
FROM patterns
WHERE synced_at IS NULL OR updated_at > synced_at
ORDER BY updated_at ASC
LIMIT ?1"#,
)?;
let rows: Vec<Pattern> = stmt
.query_map(params![limit as i64], row_to_pattern)?
.filter_map(|r| r.ok())
.collect();
Ok(rows)
}
pub fn mark_synced(conn: &Connection, ids: &[&str], when: &str) -> Result<()> {
if ids.is_empty() {
return Ok(());
}
let placeholders = ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
let sql = format!(
"UPDATE patterns SET synced_at = ? WHERE id IN ({})",
placeholders
);
let mut stmt = conn.prepare(&sql)?;
let mut binds: Vec<rusqlite::types::Value> = Vec::with_capacity(ids.len() + 1);
binds.push(when.to_string().into());
for id in ids {
binds.push((*id).to_string().into());
}
stmt.execute(rusqlite::params_from_iter(binds.iter()))?;
Ok(())
}
const SELECT_ALL_FROM_HARNESSES: &str = r#"
SELECT id, user_id, project, slug, title, description, reason,
file_paths, usage_count, last_used_at, evolution_log,
source_signal_ids, source_cluster_signature, status, metadata,
created_at, updated_at, synced_at
FROM patterns
WHERE user_id = ?1 AND id = ?2
"#;
fn row_to_pattern(row: &rusqlite::Row<'_>) -> rusqlite::Result<Pattern> {
let file_paths_str: String = row.get(7)?;
let evolution_log_str: String = row.get(10)?;
let source_ids_str: String = row.get(11)?;
let metadata_str: String = row.get(14)?;
Ok(Pattern {
id: row.get(0)?,
user_id: row.get(1)?,
project: row.get(2)?,
slug: row.get(3)?,
title: row.get(4)?,
description: row.get(5)?,
reason: row.get(6)?,
file_paths: serde_json::from_str(&file_paths_str).unwrap_or_default(),
usage_count: row.get(8)?,
last_used_at: row.get(9)?,
evolution_log: serde_json::from_str(&evolution_log_str).unwrap_or_default(),
source_signal_ids: serde_json::from_str(&source_ids_str).unwrap_or_default(),
source_cluster_signature: row.get(12)?,
status: row.get(13)?,
metadata: serde_json::from_str(&metadata_str).unwrap_or_else(|_| serde_json::json!({})),
created_at: row.get(15)?,
updated_at: row.get(16)?,
synced_at: row.get(17)?,
})
}