feagi_evolutionary/genome/migration/
mod.rs1use std::collections::BTreeMap;
17
18use serde_json::Value;
19use thiserror::Error;
20
21use crate::genome::normalizers::{NormalizationDiagnostics, Normalizer};
22use crate::genome::schema::GenomeSchemaVersion;
23use crate::genome::validators::{ValidationReport, Validator};
24
25pub mod chain;
26pub mod v2_to_v3;
27
28pub use chain::ChainRunner;
29pub use v2_to_v3::V2ToV3Migrator;
30
31#[derive(Debug, Error)]
33pub enum MigrationError {
34 #[error("Migrator '{name}' ({from} -> {to}) failed: {reason}")]
36 StepFailed {
37 name: &'static str,
38 from: GenomeSchemaVersion,
39 to: GenomeSchemaVersion,
40 reason: String,
41 },
42
43 #[error("No migrator registered with from_version={from} (needed to reach v{target})")]
46 MissingMigrator {
47 from: GenomeSchemaVersion,
48 target: GenomeSchemaVersion,
49 },
50
51 #[error("Registry violates the contiguity invariant: {0}")]
55 InvalidRegistry(String),
56
57 #[error("Cannot migrate downward: genome is at v{from} but target is v{target}")]
60 DowngradeRefused {
61 from: GenomeSchemaVersion,
62 target: GenomeSchemaVersion,
63 },
64
65 #[error("Failed to detect genome schema version: {0}")]
67 DetectionFailed(String),
68}
69
70#[derive(Debug, Clone)]
76pub struct MigrationStepDiagnostics {
77 pub from_version: GenomeSchemaVersion,
78 pub to_version: GenomeSchemaVersion,
79 pub transformations: Vec<String>,
80}
81
82impl MigrationStepDiagnostics {
83 pub fn new(from: GenomeSchemaVersion, to: GenomeSchemaVersion) -> Self {
84 Self {
85 from_version: from,
86 to_version: to,
87 transformations: Vec::new(),
88 }
89 }
90
91 pub fn record(&mut self, msg: impl Into<String>) {
92 self.transformations.push(msg.into());
93 }
94}
95
96#[allow(clippy::wrong_self_convention)]
108pub trait Migrator: Send + Sync {
113 fn from_version(&self) -> GenomeSchemaVersion;
115
116 fn to_version(&self) -> GenomeSchemaVersion;
119
120 fn name(&self) -> &'static str;
123
124 fn migrate(&self, genome: &mut Value) -> Result<MigrationStepDiagnostics, MigrationError>;
127}
128
129#[derive(Debug, Clone)]
137pub struct ChainResult {
138 pub from_version: GenomeSchemaVersion,
139 pub to_version: GenomeSchemaVersion,
140 pub migrators_applied: Vec<&'static str>,
141 pub normalizers_applied: Vec<&'static str>,
142 pub per_step_diagnostics: Vec<MigrationStepDiagnostics>,
143 pub per_normalizer_diagnostics: Vec<NormalizationDiagnostics>,
144 pub advisory_warnings: Vec<String>,
145 pub blocking_errors: Vec<String>,
146}
147
148impl ChainResult {
149 pub fn is_blocking_clean(&self) -> bool {
151 self.blocking_errors.is_empty()
152 }
153}
154
155pub struct ChainRegistry {
164 migrators: BTreeMap<u32, Box<dyn Migrator>>,
165 normalizers: BTreeMap<u32, Box<dyn Normalizer>>,
166 validators: BTreeMap<u32, Box<dyn Validator>>,
167}
168
169impl ChainRegistry {
170 pub fn new() -> Self {
171 Self {
172 migrators: BTreeMap::new(),
173 normalizers: BTreeMap::new(),
174 validators: BTreeMap::new(),
175 }
176 }
177
178 pub fn register_migrator(&mut self, migrator: Box<dyn Migrator>) -> Result<(), MigrationError> {
181 let from = migrator.from_version();
182 let to = migrator.to_version();
183 if to.as_u32() != from.as_u32().saturating_add(1) {
184 return Err(MigrationError::InvalidRegistry(format!(
185 "migrator '{}' declares from={} to={}, expected to=from+1",
186 migrator.name(),
187 from,
188 to
189 )));
190 }
191 if self.migrators.contains_key(&from.as_u32()) {
192 return Err(MigrationError::InvalidRegistry(format!(
193 "duplicate migrator with from_version={from}"
194 )));
195 }
196 self.migrators.insert(from.as_u32(), migrator);
197 Ok(())
198 }
199
200 pub fn register_normalizer(
205 &mut self,
206 normalizer: Box<dyn Normalizer>,
207 ) -> Result<(), MigrationError> {
208 let v = normalizer.schema_version();
209 if self.normalizers.contains_key(&v.as_u32()) {
210 return Err(MigrationError::InvalidRegistry(format!(
211 "duplicate normalizer at schema_version={v}"
212 )));
213 }
214 self.normalizers.insert(v.as_u32(), normalizer);
215 Ok(())
216 }
217
218 pub fn register_validator(&mut self, validator: Box<dyn Validator>) {
222 let v = validator.schema_version().as_u32();
223 self.validators.insert(v, validator);
224 }
225
226 pub fn migrator_for(&self, from: GenomeSchemaVersion) -> Option<&dyn Migrator> {
228 self.migrators.get(&from.as_u32()).map(|b| b.as_ref())
229 }
230
231 pub fn normalizer_for(&self, version: GenomeSchemaVersion) -> Option<&dyn Normalizer> {
233 self.normalizers.get(&version.as_u32()).map(|b| b.as_ref())
234 }
235
236 pub fn validator_for(&self, version: GenomeSchemaVersion) -> Option<&dyn Validator> {
238 self.validators.get(&version.as_u32()).map(|b| b.as_ref())
239 }
240
241 pub fn run_validator(&self, version: GenomeSchemaVersion, genome: &Value) -> ValidationReport {
245 match self.validator_for(version) {
246 Some(v) => v.validate(genome),
247 None => ValidationReport::new(version),
248 }
249 }
250
251 pub fn migrator_count(&self) -> usize {
253 self.migrators.len()
254 }
255
256 pub fn normalizer_count(&self) -> usize {
258 self.normalizers.len()
259 }
260
261 pub fn validator_count(&self) -> usize {
263 self.validators.len()
264 }
265}
266
267impl Default for ChainRegistry {
268 fn default() -> Self {
269 Self::new()
270 }
271}
272
273#[cfg(test)]
278pub(super) mod test_support {
279 use super::*;
280 use serde_json::json;
281
282 pub struct SyntheticMigrator {
286 from: GenomeSchemaVersion,
287 to: GenomeSchemaVersion,
288 name: &'static str,
289 fail: bool,
290 }
291
292 impl SyntheticMigrator {
293 pub fn ok(from: u32, name: &'static str) -> Box<Self> {
294 Box::new(Self {
295 from: GenomeSchemaVersion(from),
296 to: GenomeSchemaVersion(from + 1),
297 name,
298 fail: false,
299 })
300 }
301
302 pub fn failing(from: u32, name: &'static str) -> Box<Self> {
303 Box::new(Self {
304 from: GenomeSchemaVersion(from),
305 to: GenomeSchemaVersion(from + 1),
306 name,
307 fail: true,
308 })
309 }
310 }
311
312 impl Migrator for SyntheticMigrator {
313 fn from_version(&self) -> GenomeSchemaVersion {
314 self.from
315 }
316
317 fn to_version(&self) -> GenomeSchemaVersion {
318 self.to
319 }
320
321 fn name(&self) -> &'static str {
322 self.name
323 }
324
325 fn migrate(&self, genome: &mut Value) -> Result<MigrationStepDiagnostics, MigrationError> {
326 if self.fail {
327 return Err(MigrationError::StepFailed {
328 name: self.name,
329 from: self.from,
330 to: self.to,
331 reason: "synthetic failure".to_string(),
332 });
333 }
334 let mut diag = MigrationStepDiagnostics::new(self.from, self.to);
335 let count = genome
336 .get("step_count")
337 .and_then(|v| v.as_u64())
338 .unwrap_or(0)
339 + 1;
340 genome
341 .as_object_mut()
342 .expect("test genome must be a JSON object")
343 .insert("step_count".to_string(), json!(count));
344 diag.record(format!("incremented step_count to {count}"));
345 Ok(diag)
346 }
347 }
348
349 pub fn make_ok(from: u32, name: &'static str) -> Box<dyn Migrator> {
350 SyntheticMigrator::ok(from, name)
351 }
352
353 pub fn make_failing(from: u32, name: &'static str) -> Box<dyn Migrator> {
354 SyntheticMigrator::failing(from, name)
355 }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::test_support::SyntheticMigrator;
361 use super::*;
362 use serde_json::json;
363
364 #[test]
365 fn registry_accepts_a_well_formed_migrator() {
366 let mut reg = ChainRegistry::new();
367 reg.register_migrator(SyntheticMigrator::ok(2, "v2_to_v3"))
368 .unwrap();
369 assert_eq!(reg.migrator_count(), 1);
370 assert!(reg.migrator_for(GenomeSchemaVersion(2)).is_some());
371 assert!(reg.migrator_for(GenomeSchemaVersion(3)).is_none());
372 }
373
374 #[test]
375 fn registry_rejects_to_version_not_equal_to_from_plus_one() {
376 struct Skipping;
377 impl Migrator for Skipping {
378 fn from_version(&self) -> GenomeSchemaVersion {
379 GenomeSchemaVersion(2)
380 }
381 fn to_version(&self) -> GenomeSchemaVersion {
382 GenomeSchemaVersion(4)
383 }
384 fn name(&self) -> &'static str {
385 "skip"
386 }
387 fn migrate(
388 &self,
389 _genome: &mut Value,
390 ) -> Result<MigrationStepDiagnostics, MigrationError> {
391 unreachable!()
392 }
393 }
394 let mut reg = ChainRegistry::new();
395 let err = reg.register_migrator(Box::new(Skipping)).unwrap_err();
396 assert!(matches!(err, MigrationError::InvalidRegistry(_)));
397 }
398
399 #[test]
400 fn registry_rejects_duplicate_from_version() {
401 let mut reg = ChainRegistry::new();
402 reg.register_migrator(SyntheticMigrator::ok(2, "first"))
403 .unwrap();
404 let err = reg
405 .register_migrator(SyntheticMigrator::ok(2, "second"))
406 .unwrap_err();
407 assert!(matches!(err, MigrationError::InvalidRegistry(_)));
408 }
409
410 #[test]
411 fn migration_step_diagnostics_records_transformations() {
412 let mut diag =
413 MigrationStepDiagnostics::new(GenomeSchemaVersion(2), GenomeSchemaVersion(3));
414 diag.record("converted blueprint keys");
415 diag.record("renamed legacy fields");
416 assert_eq!(diag.transformations.len(), 2);
417 assert_eq!(diag.from_version, GenomeSchemaVersion(2));
418 assert_eq!(diag.to_version, GenomeSchemaVersion(3));
419 }
420
421 #[test]
422 fn run_validator_returns_empty_when_unregistered() {
423 let reg = ChainRegistry::new();
424 let report = reg.run_validator(GenomeSchemaVersion(3), &json!({}));
425 assert_eq!(report.schema_version, Some(GenomeSchemaVersion(3)));
426 assert!(report.is_clean());
427 }
428}