Skip to main content

remem/memory/
types.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4pub enum MemoryType {
5    Decision,
6    Discovery,
7    Bugfix,
8    Architecture,
9    Lesson,
10    Preference,
11    Procedure,
12    SessionActivity,
13}
14
15impl MemoryType {
16    pub const ALL: [Self; 8] = [
17        Self::Decision,
18        Self::Discovery,
19        Self::Bugfix,
20        Self::Architecture,
21        Self::Lesson,
22        Self::Preference,
23        Self::Procedure,
24        Self::SessionActivity,
25    ];
26
27    pub const fn as_str(self) -> &'static str {
28        match self {
29            Self::Decision => "decision",
30            Self::Discovery => "discovery",
31            Self::Bugfix => "bugfix",
32            Self::Architecture => "architecture",
33            Self::Lesson => "lesson",
34            Self::Preference => "preference",
35            Self::Procedure => "procedure",
36            Self::SessionActivity => "session_activity",
37        }
38    }
39
40    pub const fn label(self) -> &'static str {
41        match self {
42            Self::Decision => "Decisions",
43            Self::Discovery => "Discoveries",
44            Self::Bugfix => "Bug Fixes",
45            Self::Architecture => "Architecture",
46            Self::Lesson => "Lessons",
47            Self::Preference => "Preferences",
48            Self::Procedure => "Procedures",
49            Self::SessionActivity => "Sessions",
50        }
51    }
52
53    pub const fn index_order(self) -> Option<usize> {
54        match self {
55            Self::Decision => Some(0),
56            Self::Bugfix => Some(1),
57            Self::Architecture => Some(2),
58            Self::Discovery => Some(3),
59            Self::Procedure => Some(4),
60            Self::SessionActivity => Some(5),
61            Self::Lesson | Self::Preference => None,
62        }
63    }
64
65    pub const fn is_indexed(self) -> bool {
66        matches!(
67            self,
68            Self::Decision
69                | Self::Bugfix
70                | Self::Architecture
71                | Self::Discovery
72                | Self::Procedure
73                | Self::SessionActivity
74        )
75    }
76
77    pub const fn is_core(self) -> bool {
78        matches!(
79            self,
80            Self::Bugfix | Self::Architecture | Self::Decision | Self::Discovery
81        )
82    }
83
84    pub const fn weight(self) -> f64 {
85        match self {
86            Self::Bugfix => 3.0,
87            Self::Architecture => 2.6,
88            Self::Decision => 2.2,
89            Self::Discovery => 1.8,
90            Self::Lesson | Self::Preference | Self::Procedure | Self::SessionActivity => 0.0,
91        }
92    }
93
94    pub const fn auto_promote(self) -> bool {
95        matches!(
96            self,
97            Self::Architecture | Self::Bugfix | Self::Decision | Self::Discovery
98        )
99    }
100
101    pub fn parse(value: &str) -> Option<Self> {
102        Self::ALL
103            .iter()
104            .copied()
105            .find(|memory_type| memory_type.as_str() == value)
106    }
107
108    /// Map a raw observation_type (the legal observation vocabulary lives in
109    /// `crate::db::models::OBSERVATION_TYPES`: bugfix/feature/refactor/discovery/
110    /// decision/change) onto the candidate `MemoryType` it can support.
111    ///
112    /// The candidate vocabulary and the observation vocabulary are different
113    /// word lists, so a raw string-equality comparison between them is wrong:
114    /// `architecture` is a valid candidate type but never a valid observation
115    /// type, so an architecture candidate could never be matched and could
116    /// never auto-promote. `feature`/`refactor`/`change` observations all
117    /// describe project discoveries, so they collapse onto `Discovery`.
118    pub fn from_observation_type(observation_type: &str) -> Option<Self> {
119        match observation_type.trim().to_ascii_lowercase().as_str() {
120            "bugfix" => Some(Self::Bugfix),
121            "decision" => Some(Self::Decision),
122            "discovery" | "feature" | "refactor" | "change" => Some(Self::Discovery),
123            _ => None,
124        }
125    }
126
127    /// Whether an observation of `observation_type` can serve as supporting
128    /// evidence for a candidate of `self`. Auto-promotable candidate types are
129    /// matched to their observation equivalent; `Architecture` candidates have
130    /// no observation equivalent, so they accept `Discovery`-class evidence
131    /// (the closest project-knowledge observation class).
132    pub fn supports_observation_type(self, observation_type: &str) -> bool {
133        match Self::from_observation_type(observation_type) {
134            Some(mapped) => {
135                mapped == self || (self == Self::Architecture && mapped == Self::Discovery)
136            }
137            None => false,
138        }
139    }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct Memory {
144    pub id: i64,
145    pub session_id: Option<String>,
146    pub project: String,
147    pub topic_key: Option<String>,
148    pub title: String,
149    pub text: String,
150    pub memory_type: String,
151    pub files: Option<String>,
152    pub created_at_epoch: i64,
153    pub updated_at_epoch: i64,
154    pub status: String,
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub branch: Option<String>,
157    #[serde(default = "default_scope")]
158    pub scope: String,
159}
160
161fn default_scope() -> String {
162    "project".to_string()
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct Event {
167    pub id: i64,
168    pub session_id: String,
169    pub project: String,
170    pub event_type: String,
171    pub summary: String,
172    pub detail: Option<String>,
173    pub files: Option<String>,
174    pub exit_code: Option<i32>,
175    pub created_at_epoch: i64,
176}
177
178pub const MEMORY_TYPES: &[&str] = &[
179    MemoryType::Decision.as_str(),
180    MemoryType::Discovery.as_str(),
181    MemoryType::Bugfix.as_str(),
182    MemoryType::Architecture.as_str(),
183    MemoryType::Lesson.as_str(),
184    MemoryType::Preference.as_str(),
185    MemoryType::Procedure.as_str(),
186    MemoryType::SessionActivity.as_str(),
187];
188
189pub const MEMORY_COLS: &str = "id, session_id, project, topic_key, title, content, memory_type, \
190                              files, created_at_epoch, updated_at_epoch, status, branch, scope";
191
192pub fn memory_status_filter_sql(column: &str, include_inactive: bool) -> String {
193    if include_inactive {
194        format!("{column} IN ('active', 'stale', 'archived')")
195    } else {
196        format!("{column} = 'active'")
197    }
198}
199
200pub fn memory_current_filter_sql(
201    status_column: &str,
202    expires_column: &str,
203    include_inactive: bool,
204) -> String {
205    if include_inactive {
206        memory_status_filter_sql(status_column, true)
207    } else {
208        format!(
209            "{status_column} = 'active' AND \
210             ({expires_column} IS NULL OR {expires_column} > CAST(strftime('%s', 'now') AS INTEGER))"
211        )
212    }
213}
214
215pub fn memory_state_key_current_filter_sql(table_alias: &str) -> String {
216    let table_alias = table_alias.trim();
217    let qualifier = if table_alias.is_empty() {
218        String::new()
219    } else {
220        format!("{table_alias}.")
221    };
222    format!(
223        "({qualifier}state_key_id IS NULL OR NOT EXISTS (
224             SELECT 1 FROM memory_state_keys sk
225             WHERE sk.id = {qualifier}state_key_id
226               AND sk.current_memory_id IS NOT NULL
227               AND sk.current_memory_id <> {qualifier}id
228         ))"
229    )
230}
231
232pub fn memory_not_superseded_filter_sql(table_alias: &str) -> String {
233    let table_alias = table_alias.trim();
234    let qualifier = if table_alias.is_empty() {
235        String::new()
236    } else {
237        format!("{table_alias}.")
238    };
239    format!(
240        "NOT EXISTS (
241             SELECT 1 FROM memory_edges supersede_edge
242             WHERE supersede_edge.edge_type = 'supersedes'
243               AND supersede_edge.from_memory_id = {qualifier}id
244         )"
245    )
246}
247
248pub fn map_memory_row_pub(row: &rusqlite::Row) -> rusqlite::Result<Memory> {
249    map_memory_row(row)
250}
251
252pub(super) fn map_memory_row(row: &rusqlite::Row) -> rusqlite::Result<Memory> {
253    Ok(Memory {
254        id: row.get(0)?,
255        session_id: row.get(1)?,
256        project: row.get(2)?,
257        topic_key: row.get(3)?,
258        title: row.get(4)?,
259        text: row.get(5)?,
260        memory_type: row.get(6)?,
261        files: row.get(7)?,
262        created_at_epoch: row.get(8)?,
263        updated_at_epoch: row.get(9)?,
264        status: row.get(10)?,
265        branch: row.get(11)?,
266        scope: row
267            .get::<_, Option<String>>(12)?
268            .unwrap_or_else(|| "project".to_string()),
269    })
270}
271
272pub(super) fn map_event_row(row: &rusqlite::Row) -> rusqlite::Result<Event> {
273    Ok(Event {
274        id: row.get(0)?,
275        session_id: row.get(1)?,
276        project: row.get(2)?,
277        event_type: row.get(3)?,
278        summary: row.get(4)?,
279        detail: row.get(5)?,
280        files: row.get(6)?,
281        exit_code: row.get(7)?,
282        created_at_epoch: row.get(8)?,
283    })
284}
285
286#[cfg(test)]
287mod tests {
288    use super::{MemoryType, MEMORY_TYPES};
289
290    #[test]
291    fn memory_types_are_derived_from_canonical_enum_order() {
292        let canonical = MemoryType::ALL
293            .iter()
294            .copied()
295            .map(MemoryType::as_str)
296            .collect::<Vec<_>>();
297
298        assert_eq!(MEMORY_TYPES, canonical.as_slice());
299    }
300
301    #[test]
302    fn architecture_candidate_accepts_discovery_class_observations() {
303        // architecture is a valid candidate type but never a valid observation
304        // type; it must accept discovery-class observation evidence.
305        assert!(MemoryType::Architecture.supports_observation_type("discovery"));
306        assert!(MemoryType::Architecture.supports_observation_type("feature"));
307        assert!(MemoryType::Architecture.supports_observation_type("refactor"));
308        assert!(MemoryType::Architecture.supports_observation_type("change"));
309        // but not unrelated classes
310        assert!(!MemoryType::Architecture.supports_observation_type("bugfix"));
311        assert!(!MemoryType::Architecture.supports_observation_type("decision"));
312    }
313
314    #[test]
315    fn auto_promote_types_match_their_observation_equivalents() {
316        assert!(MemoryType::Bugfix.supports_observation_type("bugfix"));
317        assert!(MemoryType::Decision.supports_observation_type("decision"));
318        assert!(MemoryType::Discovery.supports_observation_type("discovery"));
319        // feature/refactor/change all collapse onto discovery
320        assert!(MemoryType::Discovery.supports_observation_type("feature"));
321        assert!(MemoryType::Discovery.supports_observation_type("refactor"));
322        assert!(MemoryType::Discovery.supports_observation_type("change"));
323        // mismatches stay false
324        assert!(!MemoryType::Bugfix.supports_observation_type("decision"));
325        assert!(!MemoryType::Decision.supports_observation_type("discovery"));
326        // unknown observation type maps to nothing
327        assert!(MemoryType::from_observation_type("architecture").is_none());
328        assert!(MemoryType::from_observation_type("nonsense").is_none());
329    }
330
331    #[test]
332    fn procedure_has_context_metadata() {
333        let memory_type = MemoryType::Procedure;
334
335        assert_eq!(memory_type.as_str(), "procedure");
336        assert_eq!(memory_type.label(), "Procedures");
337        assert_eq!(memory_type.index_order(), Some(4));
338        assert!(memory_type.is_indexed());
339        assert!(!memory_type.is_core());
340        assert_eq!(memory_type.weight(), 0.0);
341        assert!(!memory_type.auto_promote());
342    }
343}
344
345#[cfg(test)]
346pub mod tests_helper {
347    use rusqlite::Connection;
348
349    pub fn setup_memory_schema(conn: &Connection) {
350        conn.execute_batch(
351            "CREATE TABLE memories (
352                id INTEGER PRIMARY KEY,
353                session_id TEXT,
354                project TEXT NOT NULL,
355                topic_key TEXT,
356                title TEXT NOT NULL,
357                content TEXT NOT NULL,
358                memory_type TEXT NOT NULL,
359                files TEXT,
360                search_context TEXT,
361                created_at_epoch INTEGER NOT NULL,
362                updated_at_epoch INTEGER NOT NULL,
363                reference_time_epoch INTEGER,
364                status TEXT NOT NULL DEFAULT 'active',
365                branch TEXT,
366                scope TEXT DEFAULT 'project',
367                last_accessed_epoch INTEGER,
368                access_count INTEGER NOT NULL DEFAULT 0,
369                source_project TEXT,
370                target_project TEXT,
371                owner_scope TEXT,
372                owner_key TEXT,
373                topic_domain TEXT,
374                routing_confidence REAL,
375                routing_reason TEXT,
376                context_class TEXT,
377                expires_at_epoch INTEGER,
378                valid_from_epoch INTEGER,
379                valid_to_epoch INTEGER,
380                state_key_id INTEGER,
381                version INTEGER NOT NULL DEFAULT 1,
382                source_candidate_id INTEGER,
383                evidence_event_ids TEXT,
384                confidence REAL,
385                source_trust_class TEXT NOT NULL DEFAULT 'local_tool_output',
386                acknowledged_pattern_id TEXT,
387                acknowledged_pattern_version INTEGER,
388                acknowledged_at_epoch INTEGER
389            );
390            CREATE TABLE memory_state_keys (
391                id INTEGER PRIMARY KEY,
392                owner_scope TEXT NOT NULL,
393                owner_key TEXT NOT NULL,
394                memory_type TEXT NOT NULL,
395                state_key TEXT NOT NULL,
396                state_label TEXT,
397                state_status TEXT NOT NULL DEFAULT 'active',
398                current_memory_id INTEGER,
399                created_at_epoch INTEGER NOT NULL,
400                updated_at_epoch INTEGER NOT NULL,
401                UNIQUE(owner_scope, owner_key, memory_type, state_key)
402            );
403            CREATE TABLE memory_candidates (
404                id INTEGER PRIMARY KEY,
405                project_id INTEGER,
406                scope TEXT,
407                memory_type TEXT,
408                topic_key TEXT,
409                text TEXT,
410                evidence_event_ids TEXT,
411                confidence REAL,
412                risk_class TEXT,
413                review_status TEXT NOT NULL DEFAULT 'pending_review',
414                created_at_epoch INTEGER,
415                updated_at_epoch INTEGER
416            );
417            CREATE TABLE captured_events (
418                id INTEGER PRIMARY KEY
419            );
420            CREATE TABLE memory_embeddings (
421                memory_id INTEGER NOT NULL,
422                embedding BLOB NOT NULL,
423                dimensions INTEGER NOT NULL,
424                model TEXT NOT NULL,
425                content_hash TEXT NOT NULL,
426                updated_at_epoch INTEGER NOT NULL,
427                PRIMARY KEY(memory_id, model, dimensions),
428                FOREIGN KEY(memory_id) REFERENCES memories(id) ON DELETE CASCADE
429            );
430            CREATE INDEX idx_memory_embeddings_model
431                ON memory_embeddings(model, updated_at_epoch);
432            CREATE INDEX idx_memory_embeddings_profile_memory_id
433                ON memory_embeddings(model, dimensions, memory_id);
434            CREATE TABLE context_injection_items (
435                id INTEGER PRIMARY KEY,
436                injection_run_id TEXT NOT NULL,
437                host TEXT NOT NULL,
438                project TEXT NOT NULL,
439                session_id TEXT,
440                injection_key TEXT NOT NULL,
441                hook_source TEXT,
442                context_hash TEXT,
443                output_mode TEXT NOT NULL,
444                decision TEXT NOT NULL,
445                item_kind TEXT NOT NULL,
446                item_id INTEGER,
447                memory_id INTEGER,
448                channel TEXT NOT NULL,
449                score REAL,
450                render_order INTEGER,
451                status TEXT NOT NULL,
452                drop_reason TEXT,
453                title TEXT,
454                provenance TEXT,
455                staleness TEXT,
456                injected_at_epoch INTEGER NOT NULL
457            );
458            CREATE TABLE memory_poisoning_injection_drops (
459                id INTEGER PRIMARY KEY AUTOINCREMENT,
460                memory_id INTEGER NOT NULL,
461                pattern_id TEXT NOT NULL,
462                pattern_version INTEGER NOT NULL,
463                source_trust_class TEXT NOT NULL DEFAULT 'local_tool_output',
464                source_project TEXT,
465                title TEXT,
466                created_at_epoch INTEGER NOT NULL
467            );
468            CREATE INDEX idx_memory_poisoning_drops_created
469                ON memory_poisoning_injection_drops(created_at_epoch DESC, id DESC);
470            CREATE INDEX idx_memory_poisoning_drops_pattern
471                ON memory_poisoning_injection_drops(pattern_id, pattern_version, created_at_epoch DESC);
472            CREATE TABLE memory_citation_events (
473                id INTEGER PRIMARY KEY,
474                host TEXT NOT NULL,
475                project TEXT NOT NULL,
476                session_id TEXT NOT NULL,
477                source TEXT NOT NULL,
478                message_hash TEXT NOT NULL,
479                citation_line_present INTEGER NOT NULL DEFAULT 0,
480                parsed_count INTEGER NOT NULL DEFAULT 0,
481                matched_count INTEGER NOT NULL DEFAULT 0,
482                inserted_count INTEGER NOT NULL DEFAULT 0,
483                status TEXT NOT NULL,
484                created_at_epoch INTEGER NOT NULL,
485                UNIQUE(host, project, session_id, source, message_hash)
486            );
487            CREATE TABLE memory_usage_events (
488                id INTEGER PRIMARY KEY,
489                citation_event_id INTEGER NOT NULL,
490                host TEXT NOT NULL,
491                project TEXT NOT NULL,
492                session_id TEXT NOT NULL,
493                source TEXT NOT NULL,
494                message_hash TEXT NOT NULL,
495                memory_id INTEGER NOT NULL,
496                context_injection_item_id INTEGER,
497                created_at_epoch INTEGER NOT NULL,
498                UNIQUE(host, project, session_id, source, message_hash, memory_id)
499            );
500            CREATE TABLE ai_usage_events (
501                id INTEGER PRIMARY KEY,
502                model TEXT,
503                estimated_cost_usd REAL NOT NULL DEFAULT 0.0,
504                pricing_source TEXT NOT NULL DEFAULT 'remem_static'
505            );
506            CREATE TABLE memory_operation_log (
507                id INTEGER PRIMARY KEY,
508                operation TEXT NOT NULL,
509                planner_version TEXT NOT NULL,
510                actor TEXT NOT NULL,
511                source TEXT NOT NULL,
512                owner_scope TEXT,
513                owner_key TEXT,
514                memory_type TEXT,
515                state_key TEXT,
516                input_topic_key TEXT,
517                source_candidate_id INTEGER,
518                result_memory_id INTEGER,
519                superseded_ids TEXT NOT NULL DEFAULT '[]',
520                conflicting_ids TEXT NOT NULL DEFAULT '[]',
521                noop_reason TEXT,
522                defer_reason TEXT,
523                confidence REAL,
524                reason TEXT,
525                created_at_epoch INTEGER NOT NULL
526            );
527            CREATE INDEX idx_memory_operation_log_state
528                ON memory_operation_log(owner_scope, owner_key, memory_type, state_key, created_at_epoch);
529            CREATE TABLE memory_edges (
530                id INTEGER PRIMARY KEY,
531                edge_type TEXT NOT NULL,
532                from_memory_id INTEGER,
533                to_memory_id INTEGER,
534                state_key_id INTEGER,
535                source_candidate_id INTEGER,
536                evidence_event_ids TEXT,
537                source_operation_id INTEGER,
538                confidence REAL,
539                reason TEXT,
540                created_at_epoch INTEGER NOT NULL,
541                FOREIGN KEY(from_memory_id) REFERENCES memories(id),
542                FOREIGN KEY(to_memory_id) REFERENCES memories(id),
543                FOREIGN KEY(state_key_id) REFERENCES memory_state_keys(id),
544                FOREIGN KEY(source_candidate_id) REFERENCES memory_candidates(id),
545                FOREIGN KEY(source_operation_id) REFERENCES memory_operation_log(id)
546            );
547            CREATE INDEX idx_memory_edges_from
548                ON memory_edges(from_memory_id, edge_type);
549            CREATE INDEX idx_memory_edges_to
550                ON memory_edges(to_memory_id, edge_type);
551            CREATE INDEX idx_memory_edges_state
552                ON memory_edges(state_key_id, edge_type, created_at_epoch);
553            CREATE TABLE dream_cluster_decisions (
554                id INTEGER PRIMARY KEY AUTOINCREMENT,
555                project TEXT NOT NULL,
556                memory_type TEXT NOT NULL,
557                cluster_signature TEXT NOT NULL,
558                decision TEXT NOT NULL CHECK(decision IN ('merged', 'no_merge', 'defer', 'failed')),
559                reason TEXT,
560                member_ids_json TEXT NOT NULL,
561                cluster_size INTEGER NOT NULL,
562                next_review_epoch INTEGER,
563                source_memory_id INTEGER,
564                source_operation_id INTEGER,
565                created_at_epoch INTEGER NOT NULL,
566                updated_at_epoch INTEGER NOT NULL,
567                last_seen_epoch INTEGER NOT NULL,
568                UNIQUE(project, memory_type, cluster_signature),
569                FOREIGN KEY(source_memory_id) REFERENCES memories(id),
570                FOREIGN KEY(source_operation_id) REFERENCES memory_operation_log(id)
571            );
572            CREATE INDEX idx_dream_cluster_decisions_review
573                ON dream_cluster_decisions(project, decision, next_review_epoch);
574            CREATE INDEX idx_dream_cluster_decisions_signature
575                ON dream_cluster_decisions(project, memory_type, cluster_signature);
576            CREATE TABLE events (
577                id INTEGER PRIMARY KEY,
578                session_id TEXT NOT NULL,
579                project TEXT NOT NULL,
580                event_type TEXT NOT NULL,
581                summary TEXT NOT NULL,
582                detail TEXT,
583                files TEXT,
584                exit_code INTEGER,
585                created_at_epoch INTEGER NOT NULL,
586                retention_class TEXT NOT NULL DEFAULT 'audit'
587                    CHECK (retention_class IN ('ephemeral', 'audit'))
588            );
589            CREATE TABLE IF NOT EXISTS entities (
590                id INTEGER PRIMARY KEY,
591                canonical_name TEXT NOT NULL COLLATE NOCASE,
592                entity_type TEXT,
593                mention_count INTEGER DEFAULT 1,
594                created_at_epoch INTEGER NOT NULL DEFAULT 0,
595                UNIQUE(canonical_name)
596            );
597            CREATE TABLE IF NOT EXISTS memory_entities (
598                memory_id INTEGER NOT NULL,
599                entity_id INTEGER NOT NULL,
600                PRIMARY KEY(memory_id, entity_id)
601            );
602            CREATE TABLE IF NOT EXISTS memory_lessons (
603                memory_id INTEGER PRIMARY KEY,
604                confidence REAL NOT NULL DEFAULT 0.7,
605                reinforcement_count INTEGER NOT NULL DEFAULT 1,
606                source_evidence TEXT,
607                last_reinforced_at_epoch INTEGER NOT NULL,
608                stale_after_epoch INTEGER,
609                outcome_kind TEXT NOT NULL DEFAULT 'unknown'
610                    CHECK (outcome_kind IN ('unknown', 'success', 'failure', 'recovery', 'correction', 'revert')),
611                success_count INTEGER NOT NULL DEFAULT 0 CHECK (success_count >= 0),
612                failure_count INTEGER NOT NULL DEFAULT 0 CHECK (failure_count >= 0),
613                recovery_count INTEGER NOT NULL DEFAULT 0 CHECK (recovery_count >= 0),
614                correction_count INTEGER NOT NULL DEFAULT 0 CHECK (correction_count >= 0),
615                revert_count INTEGER NOT NULL DEFAULT 0 CHECK (revert_count >= 0)
616            );
617            CREATE TABLE IF NOT EXISTS memory_suppressions (
618                id INTEGER PRIMARY KEY,
619                owner_scope TEXT,
620                owner_key TEXT,
621                target_kind TEXT NOT NULL,
622                target_id INTEGER,
623                target_value TEXT,
624                reason TEXT NOT NULL,
625                actor TEXT NOT NULL,
626                status TEXT NOT NULL,
627                created_at_epoch INTEGER NOT NULL,
628                updated_at_epoch INTEGER NOT NULL
629            );
630            CREATE TABLE IF NOT EXISTS memory_feedback (
631                id INTEGER PRIMARY KEY,
632                target_kind TEXT NOT NULL,
633                target_id INTEGER,
634                target_value TEXT,
635                feedback TEXT NOT NULL,
636                source TEXT NOT NULL,
637                context_injection_item_id INTEGER,
638                session_id TEXT,
639                project TEXT,
640                reason TEXT,
641                created_at_epoch INTEGER NOT NULL
642            );",
643        )
644        .unwrap();
645        conn.execute_batch(include_str!("../migrations/v020_memory_fts_all_status.sql"))
646            .unwrap();
647        // v072 owns the enrichment identity columns and compatibility
648        // singleton. v083 adds the bounded-work state machine and replaces the
649        // canonical memories_au trigger. Running both real migrations keeps
650        // this fixture byte-identical to the migrated schema for those objects.
651        conn.execute_batch(include_str!(
652            "../migrations/v072_memory_retrieval_enrichment.sql"
653        ))
654        .unwrap();
655        conn.execute_batch(include_str!(
656            "../migrations/v083_retrieval_enrichment_budget.sql"
657        ))
658        .unwrap();
659        conn.execute_batch(include_str!(
660            "../migrations/v086_memory_activation_boundary.sql"
661        ))
662        .unwrap();
663        conn.execute_batch(include_str!(
664            "../migrations/v087_activation_result_trust.sql"
665        ))
666        .unwrap();
667        conn.execute_batch(include_str!(
668            "../migrations/v088_activation_legacy_trust.sql"
669        ))
670        .unwrap();
671        conn.execute_batch(include_str!(
672            "../migrations/v089_supplemental_local_copy_receipt.sql"
673        ))
674        .unwrap();
675        conn.execute_batch(include_str!("../migrations/v090_scope_cleanup_receipt.sql"))
676            .unwrap();
677    }
678}