Skip to main content

ai_memory/storage/
migration_meta.rs

1// Copyright 2026 AlphaOne LLC
2// SPDX-License-Identifier: Apache-2.0
3
4//! ARCH-8 (FX-C4-batch2, 2026-05-26) — per-migration metadata matrix.
5//!
6//! The v0.7.0 substrate ships a 50-step migration ladder
7//! (v2 → v51) without a documented "reversible? data-loss-risk?
8//! idempotent?" matrix. An operator who needs to roll back a v0.7.0
9//! daemon to v0.6.4 currently has no on-rails option — restore from
10//! backup is the only fallback. The matrix below makes the
11//! reversibility / data-loss-risk / idempotency contract per
12//! migration explicit so `ai-memory migrate --plan` can read it,
13//! release notes can quote it, and a CI test can assert every
14//! ladder step has a populated entry.
15//!
16//! Adding a migration: extend [`MIGRATION_LADDER`] in lockstep with
17//! the `migrate_v<N>` arm in `migrations.rs`. The compile-time
18//! `arch_8_*` tests in this module catch ladder/matrix drift.
19
20/// Whether reverting this migration on a populated DB destroys
21/// caller-visible rows / columns / data.
22///
23/// `None` = pure additive change (no `DROP`, no `ALTER ... DROP`,
24/// no destructive `UPDATE`). Safe to revert by un-bumping the
25/// schema-version row.
26///
27/// `Column` = drops a column or table; reverting means losing the
28/// data that lived there. Operators rolling back must export+import.
29///
30/// `Table` = drops an entire table or applies a destructive
31/// large-scale rewrite. Highest data-loss tier.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum DataLossRisk {
34    None,
35    Column,
36    Table,
37}
38
39/// Per-migration metadata record.
40#[derive(Debug, Clone, Copy)]
41pub struct MigrationMeta {
42    /// Target schema version this migration produces (i.e. the
43    /// `CURRENT_SCHEMA_VERSION` value reached AFTER it runs).
44    pub version: i64,
45    /// Short human-readable name. Convention: SCREAMING_SNAKE
46    /// summarising the schema delta (e.g. `ADD_TIER`,
47    /// `FEDERATION_NONCES`).
48    pub name: &'static str,
49    /// `true` when re-running the migration against an already-
50    /// at-target DB is a no-op (uses `CREATE ... IF NOT EXISTS`,
51    /// `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` shapes, or
52    /// per-row UPDATE WHERE-clauses that match nothing on a
53    /// second pass).
54    pub idempotent: bool,
55    /// `true` when the migration can be reverted purely by lowering
56    /// the `schema_version` row (no data was destroyed, no table
57    /// was dropped, no `UPDATE` clobbered a column the prior
58    /// schema needed).
59    pub reversible: bool,
60    /// Data-loss class on revert. See [`DataLossRisk`].
61    pub data_loss_risk: DataLossRisk,
62}
63
64/// Canonical migration matrix. Every sqlite `if version < N` arm in
65/// `migrations.rs::apply_migrations` MUST have a corresponding entry
66/// here. The `arch_8_*` tests assert (a) coverage (every ladder
67/// step has a meta row), (b) monotonicity (versions strictly
68/// increasing).
69///
70/// Entry rationale: every v0.7.x migration that ADDED a column or
71/// CREATE'd a new table without dropping or rewriting existing data
72/// is `reversible = true, data_loss_risk = None`. The handful of
73/// migrations that dropped columns or rewrote rows are flagged
74/// `reversible = false` with the appropriate data-loss tier.
75pub const MIGRATION_LADDER: &[MigrationMeta] = &[
76    // v2: add confidence + auto-tag scaffolding columns.
77    MigrationMeta {
78        version: 2,
79        name: "ADD_CONFIDENCE_DEFAULT",
80        idempotent: true,
81        reversible: true,
82        data_loss_risk: DataLossRisk::None,
83    },
84    MigrationMeta {
85        version: 3,
86        name: "ADD_TIER",
87        idempotent: true,
88        reversible: true,
89        data_loss_risk: DataLossRisk::None,
90    },
91    MigrationMeta {
92        version: 4,
93        name: "ADD_AGENT_ID_INDEX",
94        idempotent: true,
95        reversible: true,
96        data_loss_risk: DataLossRisk::None,
97    },
98    MigrationMeta {
99        version: 5,
100        name: "ADD_FTS5",
101        idempotent: true,
102        reversible: true,
103        data_loss_risk: DataLossRisk::None,
104    },
105    MigrationMeta {
106        version: 6,
107        name: "ADD_HNSW_EMBEDDINGS",
108        idempotent: true,
109        reversible: true,
110        data_loss_risk: DataLossRisk::None,
111    },
112    MigrationMeta {
113        version: 7,
114        name: "ADD_LINKS_TABLE",
115        idempotent: true,
116        reversible: true,
117        data_loss_risk: DataLossRisk::None,
118    },
119    MigrationMeta {
120        version: 8,
121        name: "ADD_ARCHIVE_TABLE",
122        idempotent: true,
123        reversible: true,
124        data_loss_risk: DataLossRisk::None,
125    },
126    MigrationMeta {
127        version: 9,
128        name: "ADD_NAMESPACE_META",
129        idempotent: true,
130        reversible: true,
131        data_loss_risk: DataLossRisk::None,
132    },
133    MigrationMeta {
134        version: 10,
135        name: "ADD_PERMISSIONS_RULES",
136        idempotent: true,
137        reversible: true,
138        data_loss_risk: DataLossRisk::None,
139    },
140    MigrationMeta {
141        version: 11,
142        name: "ADD_SIGNED_EVENTS",
143        idempotent: true,
144        reversible: true,
145        data_loss_risk: DataLossRisk::None,
146    },
147    MigrationMeta {
148        version: 12,
149        name: "ADD_AGENTS_REGISTRATION",
150        idempotent: true,
151        reversible: true,
152        data_loss_risk: DataLossRisk::None,
153    },
154    MigrationMeta {
155        version: 13,
156        name: "ADD_LINKS_ATTESTATION",
157        idempotent: true,
158        reversible: true,
159        data_loss_risk: DataLossRisk::None,
160    },
161    MigrationMeta {
162        version: 14,
163        name: "ADD_FED_NONCES_INDEX",
164        idempotent: true,
165        reversible: true,
166        data_loss_risk: DataLossRisk::None,
167    },
168    MigrationMeta {
169        version: 15,
170        name: "ADD_HOOKS_CONFIG",
171        idempotent: true,
172        reversible: true,
173        data_loss_risk: DataLossRisk::None,
174    },
175    MigrationMeta {
176        version: 16,
177        name: "ADD_PENDING_ACTIONS",
178        idempotent: true,
179        reversible: true,
180        data_loss_risk: DataLossRisk::None,
181    },
182    MigrationMeta {
183        version: 17,
184        name: "ADD_TRANSCRIPTS",
185        idempotent: true,
186        reversible: true,
187        data_loss_risk: DataLossRisk::None,
188    },
189    MigrationMeta {
190        version: 18,
191        name: "ADD_OBSERVATIONS",
192        idempotent: true,
193        reversible: true,
194        data_loss_risk: DataLossRisk::None,
195    },
196    MigrationMeta {
197        version: 19,
198        name: "ADD_HOOK_SUBSCRIBERS",
199        idempotent: true,
200        reversible: true,
201        data_loss_risk: DataLossRisk::None,
202    },
203    MigrationMeta {
204        version: 20,
205        name: "ADD_AUDIT_INDEX",
206        idempotent: true,
207        reversible: true,
208        data_loss_risk: DataLossRisk::None,
209    },
210    MigrationMeta {
211        version: 21,
212        name: "ADD_AGENT_QUOTAS",
213        idempotent: true,
214        reversible: true,
215        data_loss_risk: DataLossRisk::None,
216    },
217    MigrationMeta {
218        version: 22,
219        name: "ADD_GOVERNANCE_RULES",
220        idempotent: true,
221        reversible: true,
222        data_loss_risk: DataLossRisk::None,
223    },
224    MigrationMeta {
225        version: 23,
226        name: "ADD_KG_TRAVERSAL_CACHE",
227        idempotent: true,
228        reversible: true,
229        data_loss_risk: DataLossRisk::None,
230    },
231    MigrationMeta {
232        version: 24,
233        name: "ADD_FED_PEERS",
234        idempotent: true,
235        reversible: true,
236        data_loss_risk: DataLossRisk::None,
237    },
238    MigrationMeta {
239        version: 25,
240        name: "ADD_INDEX_REFLECTS_ON",
241        idempotent: true,
242        reversible: true,
243        data_loss_risk: DataLossRisk::None,
244    },
245    MigrationMeta {
246        version: 26,
247        name: "ADD_SKILL_REGISTRY",
248        idempotent: true,
249        reversible: true,
250        data_loss_risk: DataLossRisk::None,
251    },
252    MigrationMeta {
253        version: 27,
254        name: "ADD_PERSONA_REGISTRY",
255        idempotent: true,
256        reversible: true,
257        data_loss_risk: DataLossRisk::None,
258    },
259    MigrationMeta {
260        version: 28,
261        name: "ADD_OFFLOAD_REGISTRY",
262        idempotent: true,
263        reversible: true,
264        data_loss_risk: DataLossRisk::None,
265    },
266    MigrationMeta {
267        version: 29,
268        name: "ADD_REFLECTION_DEPTH",
269        idempotent: true,
270        reversible: true,
271        data_loss_risk: DataLossRisk::None,
272    },
273    MigrationMeta {
274        version: 30,
275        name: "ADD_KG_CYCLE_CHECK",
276        idempotent: true,
277        reversible: true,
278        data_loss_risk: DataLossRisk::None,
279    },
280    MigrationMeta {
281        version: 31,
282        name: "ADD_FORENSIC_SINK",
283        idempotent: true,
284        reversible: true,
285        data_loss_risk: DataLossRisk::None,
286    },
287    MigrationMeta {
288        version: 32,
289        name: "ADD_CONFIDENCE_CALIBRATION",
290        idempotent: true,
291        reversible: true,
292        data_loss_risk: DataLossRisk::None,
293    },
294    MigrationMeta {
295        version: 33,
296        name: "ADD_CONSOLIDATION_LEDGER",
297        idempotent: true,
298        reversible: true,
299        data_loss_risk: DataLossRisk::None,
300    },
301    MigrationMeta {
302        version: 34,
303        name: "BACKFILL_SIGNED_CHAIN",
304        idempotent: true,
305        reversible: false,
306        data_loss_risk: DataLossRisk::None,
307    },
308    MigrationMeta {
309        version: 35,
310        name: "ADD_KG_AGE_PROJECTION",
311        idempotent: true,
312        reversible: true,
313        data_loss_risk: DataLossRisk::None,
314    },
315    MigrationMeta {
316        version: 36,
317        name: "ADD_ATOMISATION_SCHEMA",
318        idempotent: true,
319        reversible: true,
320        data_loss_risk: DataLossRisk::None,
321    },
322    MigrationMeta {
323        version: 37,
324        name: "ADD_MEMORY_KIND",
325        idempotent: true,
326        reversible: true,
327        data_loss_risk: DataLossRisk::None,
328    },
329    MigrationMeta {
330        version: 38,
331        name: "ADD_ENTITY_REGISTRY",
332        idempotent: true,
333        reversible: true,
334        data_loss_risk: DataLossRisk::None,
335    },
336    MigrationMeta {
337        version: 39,
338        name: "ADD_FORM4_PROVENANCE",
339        idempotent: true,
340        reversible: true,
341        data_loss_risk: DataLossRisk::None,
342    },
343    MigrationMeta {
344        version: 40,
345        name: "ADD_FORM5_CONFIDENCE_SOURCE",
346        idempotent: true,
347        reversible: true,
348        data_loss_risk: DataLossRisk::None,
349    },
350    MigrationMeta {
351        version: 41,
352        name: "ADD_PERSONA_VERSION",
353        idempotent: true,
354        reversible: true,
355        data_loss_risk: DataLossRisk::None,
356    },
357    MigrationMeta {
358        version: 42,
359        name: "ADD_HOOK_DLQ",
360        idempotent: true,
361        reversible: true,
362        data_loss_risk: DataLossRisk::None,
363    },
364    MigrationMeta {
365        version: 43,
366        name: "ADD_RECURSIVE_LEARNING_LEDGER",
367        idempotent: true,
368        reversible: true,
369        data_loss_risk: DataLossRisk::None,
370    },
371    MigrationMeta {
372        version: 44,
373        name: "ADD_BATMAN_VOCABULARY_COLUMNS",
374        idempotent: true,
375        reversible: true,
376        data_loss_risk: DataLossRisk::None,
377    },
378    MigrationMeta {
379        version: 45,
380        name: "ADD_VERSION_OPTIMISTIC_CONCURRENCY",
381        idempotent: true,
382        reversible: true,
383        data_loss_risk: DataLossRisk::None,
384    },
385    MigrationMeta {
386        version: 46,
387        name: "ADD_SHARE_LINKS",
388        idempotent: true,
389        reversible: true,
390        data_loss_risk: DataLossRisk::None,
391    },
392    MigrationMeta {
393        version: 47,
394        name: "ADD_MENTIONED_ENTITY_ID",
395        idempotent: true,
396        reversible: true,
397        data_loss_risk: DataLossRisk::None,
398    },
399    MigrationMeta {
400        version: 48,
401        name: "ADD_FEDERATION_PUSH_DLQ",
402        idempotent: true,
403        reversible: true,
404        data_loss_risk: DataLossRisk::None,
405    },
406    MigrationMeta {
407        version: 49,
408        name: "BACKFILL_ARCHIVED_MEMORIES_COLUMNS",
409        idempotent: true,
410        reversible: true,
411        data_loss_risk: DataLossRisk::None,
412    },
413    MigrationMeta {
414        version: 50,
415        name: "EXPAND_AGENT_QUOTAS_PK",
416        idempotent: true,
417        // The PK widening is reversible (rebuild original table)
418        // but rebuild loses the namespace component on backfill.
419        reversible: false,
420        data_loss_risk: DataLossRisk::Column,
421    },
422    MigrationMeta {
423        version: 51,
424        name: "ADD_FEDERATION_NONCES",
425        idempotent: true,
426        reversible: true,
427        data_loss_risk: DataLossRisk::None,
428    },
429    MigrationMeta {
430        version: 52,
431        name: "ADD_TRANSCRIPT_LINE_DEDUP",
432        idempotent: true,
433        reversible: true,
434        data_loss_risk: DataLossRisk::None,
435    },
436    // v0.7.0 #1418 + #1419 — sqlite scopes the `memories_au` FTS5
437    // trigger from `AFTER UPDATE ON memories` to `AFTER UPDATE OF
438    // title, content, tags`. Reversible (DROP TRIGGER + recreate at
439    // the prior form). Idempotent via the `IF NOT EXISTS` guard on
440    // the recreate. No data loss — the trigger only governs how
441    // FTS5 stays in sync with row updates. Postgres is a no-op
442    // (no equivalent FTS5 trigger surface; same lockstep precedent
443    // as v51 federation_nonce_cache).
444    MigrationMeta {
445        version: 53,
446        name: "SCOPE_MEMORIES_AU_TRIGGER_TO_FTS_COLUMNS",
447        idempotent: true,
448        reversible: true,
449        data_loss_risk: DataLossRisk::None,
450    },
451    // v0.7.0 #1466 — one-shot backfill of tier-default expiry on legacy
452    // immortal rows (mid/short rows persisted with a NULL `expires_at`
453    // before the write-path chokepoint fix landed). Both backends apply
454    // it: sqlite via a parameterised UPDATE arm, postgres via the
455    // `migrate_v54` twin — the interval is derived from
456    // `Tier::default_ttl_secs()` (no literal). Idempotent (only NULL-
457    // expiry rows match; a second pass updates nothing). NOT reversible:
458    // the original NULL set is unrecoverable once stamped, but no column
459    // or table is dropped, so the data-loss class is `None`.
460    MigrationMeta {
461        version: crate::storage::migrations::current_schema_version(),
462        name: "BACKFILL_NULL_EXPIRY_TIER_DEFAULT",
463        idempotent: true,
464        reversible: false,
465        data_loss_risk: DataLossRisk::None,
466    },
467];
468
469/// Look up the metadata for a target schema version.
470#[must_use]
471pub fn meta_for(version: i64) -> Option<&'static MigrationMeta> {
472    MIGRATION_LADDER.iter().find(|m| m.version == version)
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    #[test]
480    fn arch_8_ladder_versions_strictly_monotonic() {
481        let mut prev = 1_i64;
482        for meta in MIGRATION_LADDER {
483            assert!(
484                meta.version > prev,
485                "ARCH-8: migration ladder is not strictly monotonic at version {}; prev={prev}",
486                meta.version,
487            );
488            prev = meta.version;
489        }
490    }
491
492    #[test]
493    fn arch_8_ladder_terminates_at_current_schema_version() {
494        let last = MIGRATION_LADDER
495            .last()
496            .expect("MIGRATION_LADDER is non-empty")
497            .version;
498        let current = crate::storage::current_schema_version_for_tests();
499        assert_eq!(
500            last, current,
501            "ARCH-8: MIGRATION_LADDER tail = {last}, but CURRENT_SCHEMA_VERSION = {current}; \
502             when bumping the ladder add a meta row in lockstep.",
503        );
504    }
505
506    #[test]
507    fn arch_8_every_meta_row_has_a_non_empty_name() {
508        for meta in MIGRATION_LADDER {
509            assert!(
510                !meta.name.is_empty(),
511                "ARCH-8: migration v{} has an empty `name`",
512                meta.version,
513            );
514        }
515    }
516
517    #[test]
518    fn arch_8_meta_for_round_trip() {
519        // meta_for(<known>) returns Some; meta_for(<unknown>) returns None.
520        assert!(meta_for(2).is_some());
521        assert!(meta_for(51).is_some());
522        assert!(meta_for(9999).is_none());
523        assert!(meta_for(0).is_none());
524    }
525}