Skip to content

Commit bd85025

Browse files
committed
v0.6.0 - C9: retention classes
1 parent 09e97a7 commit bd85025

6 files changed

Lines changed: 423 additions & 52 deletions

File tree

daemon-rs/src/api_types.rs

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
use serde::Deserialize;
2+
use serde::{Deserialize, Serialize};
33

44
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
55
#[serde(rename_all = "lowercase")]
@@ -18,6 +18,111 @@ impl ExportFormat {
1818
}
1919
}
2020

21+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
22+
#[serde(rename_all = "lowercase")]
23+
pub enum RetentionClass {
24+
Durable,
25+
#[default]
26+
Operational,
27+
Audit,
28+
Ephemeral,
29+
}
30+
31+
impl RetentionClass {
32+
pub fn as_str(self) -> &'static str {
33+
match self {
34+
Self::Durable => "durable",
35+
Self::Operational => "operational",
36+
Self::Audit => "audit",
37+
Self::Ephemeral => "ephemeral",
38+
}
39+
}
40+
41+
pub fn parse(input: &str) -> Option<Self> {
42+
match input.trim().to_ascii_lowercase().as_str() {
43+
"durable" => Some(Self::Durable),
44+
"operational" => Some(Self::Operational),
45+
"audit" => Some(Self::Audit),
46+
"ephemeral" => Some(Self::Ephemeral),
47+
_ => None,
48+
}
49+
}
50+
51+
pub fn default_ttl_seconds(self) -> Option<i64> {
52+
match self {
53+
Self::Durable => None,
54+
Self::Operational => Some(90 * 24 * 60 * 60),
55+
Self::Audit => Some(365 * 24 * 60 * 60),
56+
Self::Ephemeral => Some(14 * 24 * 60 * 60),
57+
}
58+
}
59+
60+
pub fn from_entry_type(entry_type: &str) -> Option<Self> {
61+
match entry_type.trim().to_ascii_lowercase().as_str() {
62+
"decision" | "policy" | "rule" | "convention" | "contract" | "procedure"
63+
| "playbook" | "runbook" => Some(Self::Durable),
64+
"trace" | "security" | "rollback" | "permission" | "audit" => Some(Self::Audit),
65+
"chatter" | "scratch" | "transient" | "temporary" | "ephemeral" => {
66+
Some(Self::Ephemeral)
67+
}
68+
"observation" | "note" | "finding" | "fact" | "memory" | "focus_summary" => {
69+
Some(Self::Operational)
70+
}
71+
_ => None,
72+
}
73+
}
74+
75+
pub fn classify(
76+
explicit: Option<Self>,
77+
entry_type: &str,
78+
text: &str,
79+
context: Option<&str>,
80+
) -> Self {
81+
if let Some(explicit) = explicit {
82+
return explicit;
83+
}
84+
if let Some(mapped) = Self::from_entry_type(entry_type) {
85+
return mapped;
86+
}
87+
88+
let combined = match context {
89+
Some(context) if !context.trim().is_empty() => {
90+
format!("{} {}", text.trim(), context.trim()).to_ascii_lowercase()
91+
}
92+
_ => text.trim().to_ascii_lowercase(),
93+
};
94+
if [
95+
"architectural",
96+
"architecture",
97+
"convention",
98+
"always",
99+
"never",
100+
"api contract",
101+
"must ",
102+
"do not",
103+
]
104+
.iter()
105+
.any(|needle| combined.contains(needle))
106+
{
107+
return Self::Durable;
108+
}
109+
if ["rollback", "permission", "security event", "audit"]
110+
.iter()
111+
.any(|needle| combined.contains(needle))
112+
{
113+
return Self::Audit;
114+
}
115+
if ["throwaway", "temporary", "transient", "scratch"]
116+
.iter()
117+
.any(|needle| combined.contains(needle))
118+
{
119+
return Self::Ephemeral;
120+
}
121+
122+
Self::Operational
123+
}
124+
}
125+
21126
#[derive(Debug, Clone, Default, Deserialize)]
22127
pub struct StoreRequest {
23128
pub decision: Option<String>,
@@ -29,6 +134,7 @@ pub struct StoreRequest {
29134
pub confidence: Option<f64>,
30135
pub reasoning_depth: Option<String>,
31136
pub ttl_seconds: Option<i64>,
137+
pub retention_class: Option<RetentionClass>,
32138
}
33139

34140
#[derive(Debug, Clone, Deserialize)]
@@ -54,6 +160,7 @@ pub struct ImportMemory {
54160
pub observed_at: Option<String>,
55161
pub valid_from: Option<String>,
56162
pub valid_until: Option<String>,
163+
pub retention_class: Option<RetentionClass>,
57164
}
58165

59166
#[derive(Debug, Clone, Deserialize)]
@@ -72,6 +179,7 @@ pub struct ImportDecision {
72179
pub observed_at: Option<String>,
73180
pub valid_from: Option<String>,
74181
pub valid_until: Option<String>,
182+
pub retention_class: Option<RetentionClass>,
75183
}
76184

77185
#[derive(Debug, Clone)]

daemon-rs/src/db.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ pub fn configure(conn: &Connection) -> rusqlite::Result<()> {
150150

151151
type MigrationDef = (&'static str, &'static str);
152152

153-
const SCHEMA_MIGRATIONS: [MigrationDef; 15] = [
153+
const SCHEMA_MIGRATIONS: [MigrationDef; 16] = [
154154
("001_initial_schema", "initial_schema"),
155155
("002_aging_columns", "aging_columns"),
156156
("003_focus_table", "focus_table"),
@@ -166,6 +166,7 @@ const SCHEMA_MIGRATIONS: [MigrationDef; 15] = [
166166
("013", "embeddings_model_lookup_indexes"),
167167
("014", "temporal_semantics_fields"),
168168
("015", "boot_audits"),
169+
("016", "retention_classes"),
169170
];
170171

171172
/// Return ordered schema migration definitions.
@@ -623,6 +624,41 @@ fn apply_migration(conn: &Connection, version: &str) -> rusqlite::Result<()> {
623624
)?;
624625
Ok(())
625626
}
627+
"016" => {
628+
ensure_column(
629+
conn,
630+
"memories",
631+
"ALTER TABLE memories ADD COLUMN retention_class TEXT NOT NULL DEFAULT 'operational'",
632+
)?;
633+
ensure_column(
634+
conn,
635+
"decisions",
636+
"ALTER TABLE decisions ADD COLUMN retention_class TEXT NOT NULL DEFAULT 'operational'",
637+
)?;
638+
let _ = conn.execute(
639+
"UPDATE memories SET retention_class = 'operational'
640+
WHERE retention_class IS NULL
641+
OR retention_class = ''
642+
OR retention_class NOT IN ('durable', 'operational', 'audit', 'ephemeral')",
643+
[],
644+
);
645+
let _ = conn.execute(
646+
"UPDATE decisions SET retention_class = 'operational'
647+
WHERE retention_class IS NULL
648+
OR retention_class = ''
649+
OR retention_class NOT IN ('durable', 'operational', 'audit', 'ephemeral')",
650+
[],
651+
);
652+
conn.execute_batch(
653+
r#"
654+
CREATE INDEX IF NOT EXISTS idx_memories_retention_class
655+
ON memories(retention_class);
656+
CREATE INDEX IF NOT EXISTS idx_decisions_retention_class
657+
ON decisions(retention_class);
658+
"#,
659+
)?;
660+
Ok(())
661+
}
626662
other => Err(migration_error(format!(
627663
"unknown schema migration: {other}"
628664
))),
@@ -740,6 +776,8 @@ pub fn initialize_schema(conn: &Connection) -> rusqlite::Result<()> {
740776
confirmed_by TEXT,
741777
merged_count INTEGER DEFAULT 0,
742778
quality INTEGER DEFAULT 50,
779+
retention_class TEXT NOT NULL DEFAULT 'operational'
780+
CHECK (retention_class IN ('durable', 'operational', 'audit', 'ephemeral')),
743781
expires_at TEXT,
744782
observed_at TEXT,
745783
valid_from TEXT,
@@ -771,6 +809,8 @@ pub fn initialize_schema(conn: &Connection) -> rusqlite::Result<()> {
771809
confirmed_by TEXT,
772810
merged_count INTEGER DEFAULT 0,
773811
quality INTEGER DEFAULT 50,
812+
retention_class TEXT NOT NULL DEFAULT 'operational'
813+
CHECK (retention_class IN ('durable', 'operational', 'audit', 'ephemeral')),
774814
expires_at TEXT,
775815
observed_at TEXT,
776816
valid_from TEXT,
@@ -2504,6 +2544,7 @@ mod tests {
25042544
assert!(table_has_column(&conn, "memories", "source_model"));
25052545
assert!(table_has_column(&conn, "memories", "reasoning_depth"));
25062546
assert!(table_has_column(&conn, "memories", "trust_score"));
2547+
assert!(table_has_column(&conn, "memories", "retention_class"));
25072548
assert!(table_has_column(&conn, "memories", "observed_at"));
25082549
assert!(table_has_column(&conn, "memories", "valid_from"));
25092550
assert!(table_has_column(&conn, "memories", "valid_until"));
@@ -2514,6 +2555,7 @@ mod tests {
25142555
assert!(table_has_column(&conn, "decisions", "source_model"));
25152556
assert!(table_has_column(&conn, "decisions", "reasoning_depth"));
25162557
assert!(table_has_column(&conn, "decisions", "trust_score"));
2558+
assert!(table_has_column(&conn, "decisions", "retention_class"));
25172559
assert!(table_has_column(&conn, "decisions", "observed_at"));
25182560
assert!(table_has_column(&conn, "decisions", "valid_from"));
25192561
assert!(table_has_column(&conn, "decisions", "valid_until"));

0 commit comments

Comments
 (0)