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 pub identifier_remaps: BTreeMap<String, String>,
85}
86
87impl MigrationStepDiagnostics {
88 pub fn new(from: GenomeSchemaVersion, to: GenomeSchemaVersion) -> Self {
89 Self {
90 from_version: from,
91 to_version: to,
92 transformations: Vec::new(),
93 identifier_remaps: BTreeMap::new(),
94 }
95 }
96
97 pub fn record(&mut self, msg: impl Into<String>) {
98 self.transformations.push(msg.into());
99 }
100
101 pub fn record_identifier_remap(
102 &mut self,
103 source: impl Into<String>,
104 destination: impl Into<String>,
105 ) {
106 self.identifier_remaps
107 .insert(source.into(), destination.into());
108 }
109}
110
111#[allow(clippy::wrong_self_convention)]
123pub trait Migrator: Send + Sync {
128 fn from_version(&self) -> GenomeSchemaVersion;
130
131 fn to_version(&self) -> GenomeSchemaVersion;
134
135 fn name(&self) -> &'static str;
138
139 fn migrate(&self, genome: &mut Value) -> Result<MigrationStepDiagnostics, MigrationError>;
142}
143
144#[derive(Debug, Clone)]
152pub struct ChainResult {
153 pub from_version: GenomeSchemaVersion,
154 pub to_version: GenomeSchemaVersion,
155 pub migrators_applied: Vec<&'static str>,
156 pub normalizers_applied: Vec<&'static str>,
157 pub per_step_diagnostics: Vec<MigrationStepDiagnostics>,
158 pub per_normalizer_diagnostics: Vec<NormalizationDiagnostics>,
159 pub advisory_warnings: Vec<String>,
160 pub blocking_errors: Vec<String>,
161}
162
163impl ChainResult {
164 pub fn is_blocking_clean(&self) -> bool {
166 self.blocking_errors.is_empty()
167 }
168}
169
170pub struct ChainRegistry {
179 migrators: BTreeMap<u32, Box<dyn Migrator>>,
180 normalizers: BTreeMap<u32, Box<dyn Normalizer>>,
181 validators: BTreeMap<u32, Box<dyn Validator>>,
182}
183
184impl ChainRegistry {
185 pub fn new() -> Self {
186 Self {
187 migrators: BTreeMap::new(),
188 normalizers: BTreeMap::new(),
189 validators: BTreeMap::new(),
190 }
191 }
192
193 pub fn register_migrator(&mut self, migrator: Box<dyn Migrator>) -> Result<(), MigrationError> {
196 let from = migrator.from_version();
197 let to = migrator.to_version();
198 if to.as_u32() != from.as_u32().saturating_add(1) {
199 return Err(MigrationError::InvalidRegistry(format!(
200 "migrator '{}' declares from={} to={}, expected to=from+1",
201 migrator.name(),
202 from,
203 to
204 )));
205 }
206 if self.migrators.contains_key(&from.as_u32()) {
207 return Err(MigrationError::InvalidRegistry(format!(
208 "duplicate migrator with from_version={from}"
209 )));
210 }
211 self.migrators.insert(from.as_u32(), migrator);
212 Ok(())
213 }
214
215 pub fn register_normalizer(
220 &mut self,
221 normalizer: Box<dyn Normalizer>,
222 ) -> Result<(), MigrationError> {
223 let v = normalizer.schema_version();
224 if self.normalizers.contains_key(&v.as_u32()) {
225 return Err(MigrationError::InvalidRegistry(format!(
226 "duplicate normalizer at schema_version={v}"
227 )));
228 }
229 self.normalizers.insert(v.as_u32(), normalizer);
230 Ok(())
231 }
232
233 pub fn register_validator(&mut self, validator: Box<dyn Validator>) {
237 let v = validator.schema_version().as_u32();
238 self.validators.insert(v, validator);
239 }
240
241 pub fn migrator_for(&self, from: GenomeSchemaVersion) -> Option<&dyn Migrator> {
243 self.migrators.get(&from.as_u32()).map(|b| b.as_ref())
244 }
245
246 pub fn normalizer_for(&self, version: GenomeSchemaVersion) -> Option<&dyn Normalizer> {
248 self.normalizers.get(&version.as_u32()).map(|b| b.as_ref())
249 }
250
251 pub fn validator_for(&self, version: GenomeSchemaVersion) -> Option<&dyn Validator> {
253 self.validators.get(&version.as_u32()).map(|b| b.as_ref())
254 }
255
256 pub fn run_validator(&self, version: GenomeSchemaVersion, genome: &Value) -> ValidationReport {
260 match self.validator_for(version) {
261 Some(v) => v.validate(genome),
262 None => ValidationReport::new(version),
263 }
264 }
265
266 pub fn migrator_count(&self) -> usize {
268 self.migrators.len()
269 }
270
271 pub fn normalizer_count(&self) -> usize {
273 self.normalizers.len()
274 }
275
276 pub fn validator_count(&self) -> usize {
278 self.validators.len()
279 }
280}
281
282impl Default for ChainRegistry {
283 fn default() -> Self {
284 Self::new()
285 }
286}
287
288#[cfg(test)]
293pub(super) mod test_support {
294 use super::*;
295 use serde_json::json;
296
297 pub struct SyntheticMigrator {
301 from: GenomeSchemaVersion,
302 to: GenomeSchemaVersion,
303 name: &'static str,
304 fail: bool,
305 }
306
307 impl SyntheticMigrator {
308 pub fn ok(from: u32, name: &'static str) -> Box<Self> {
309 Box::new(Self {
310 from: GenomeSchemaVersion(from),
311 to: GenomeSchemaVersion(from + 1),
312 name,
313 fail: false,
314 })
315 }
316
317 pub fn failing(from: u32, name: &'static str) -> Box<Self> {
318 Box::new(Self {
319 from: GenomeSchemaVersion(from),
320 to: GenomeSchemaVersion(from + 1),
321 name,
322 fail: true,
323 })
324 }
325 }
326
327 impl Migrator for SyntheticMigrator {
328 fn from_version(&self) -> GenomeSchemaVersion {
329 self.from
330 }
331
332 fn to_version(&self) -> GenomeSchemaVersion {
333 self.to
334 }
335
336 fn name(&self) -> &'static str {
337 self.name
338 }
339
340 fn migrate(&self, genome: &mut Value) -> Result<MigrationStepDiagnostics, MigrationError> {
341 if self.fail {
342 return Err(MigrationError::StepFailed {
343 name: self.name,
344 from: self.from,
345 to: self.to,
346 reason: "synthetic failure".to_string(),
347 });
348 }
349 let mut diag = MigrationStepDiagnostics::new(self.from, self.to);
350 let count = genome
351 .get("step_count")
352 .and_then(|v| v.as_u64())
353 .unwrap_or(0)
354 + 1;
355 genome
356 .as_object_mut()
357 .expect("test genome must be a JSON object")
358 .insert("step_count".to_string(), json!(count));
359 diag.record(format!("incremented step_count to {count}"));
360 Ok(diag)
361 }
362 }
363
364 pub fn make_ok(from: u32, name: &'static str) -> Box<dyn Migrator> {
365 SyntheticMigrator::ok(from, name)
366 }
367
368 pub fn make_failing(from: u32, name: &'static str) -> Box<dyn Migrator> {
369 SyntheticMigrator::failing(from, name)
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::test_support::SyntheticMigrator;
376 use super::*;
377 use serde_json::json;
378
379 #[test]
380 fn registry_accepts_a_well_formed_migrator() {
381 let mut reg = ChainRegistry::new();
382 reg.register_migrator(SyntheticMigrator::ok(2, "v2_to_v3"))
383 .unwrap();
384 assert_eq!(reg.migrator_count(), 1);
385 assert!(reg.migrator_for(GenomeSchemaVersion(2)).is_some());
386 assert!(reg.migrator_for(GenomeSchemaVersion(3)).is_none());
387 }
388
389 #[test]
390 fn registry_rejects_to_version_not_equal_to_from_plus_one() {
391 struct Skipping;
392 impl Migrator for Skipping {
393 fn from_version(&self) -> GenomeSchemaVersion {
394 GenomeSchemaVersion(2)
395 }
396 fn to_version(&self) -> GenomeSchemaVersion {
397 GenomeSchemaVersion(4)
398 }
399 fn name(&self) -> &'static str {
400 "skip"
401 }
402 fn migrate(
403 &self,
404 _genome: &mut Value,
405 ) -> Result<MigrationStepDiagnostics, MigrationError> {
406 unreachable!()
407 }
408 }
409 let mut reg = ChainRegistry::new();
410 let err = reg.register_migrator(Box::new(Skipping)).unwrap_err();
411 assert!(matches!(err, MigrationError::InvalidRegistry(_)));
412 }
413
414 #[test]
415 fn registry_rejects_duplicate_from_version() {
416 let mut reg = ChainRegistry::new();
417 reg.register_migrator(SyntheticMigrator::ok(2, "first"))
418 .unwrap();
419 let err = reg
420 .register_migrator(SyntheticMigrator::ok(2, "second"))
421 .unwrap_err();
422 assert!(matches!(err, MigrationError::InvalidRegistry(_)));
423 }
424
425 #[test]
426 fn migration_step_diagnostics_records_transformations() {
427 let mut diag =
428 MigrationStepDiagnostics::new(GenomeSchemaVersion(2), GenomeSchemaVersion(3));
429 diag.record("converted blueprint keys");
430 diag.record("renamed legacy fields");
431 assert_eq!(diag.transformations.len(), 2);
432 assert_eq!(diag.from_version, GenomeSchemaVersion(2));
433 assert_eq!(diag.to_version, GenomeSchemaVersion(3));
434 }
435
436 #[test]
437 fn run_validator_returns_empty_when_unregistered() {
438 let reg = ChainRegistry::new();
439 let report = reg.run_validator(GenomeSchemaVersion(3), &json!({}));
440 assert_eq!(report.schema_version, Some(GenomeSchemaVersion(3)));
441 assert!(report.is_clean());
442 }
443}