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;
27pub mod v3_to_v4;
28
29pub use chain::ChainRunner;
30pub use v2_to_v3::V2ToV3Migrator;
31pub use v3_to_v4::V3ToV4Migrator;
32
33#[derive(Debug, Error)]
35pub enum MigrationError {
36 #[error("Migrator '{name}' ({from} -> {to}) failed: {reason}")]
38 StepFailed {
39 name: &'static str,
40 from: GenomeSchemaVersion,
41 to: GenomeSchemaVersion,
42 reason: String,
43 },
44
45 #[error("No migrator registered with from_version={from} (needed to reach v{target})")]
48 MissingMigrator {
49 from: GenomeSchemaVersion,
50 target: GenomeSchemaVersion,
51 },
52
53 #[error("Registry violates the contiguity invariant: {0}")]
57 InvalidRegistry(String),
58
59 #[error("Cannot migrate downward: genome is at v{from} but target is v{target}")]
62 DowngradeRefused {
63 from: GenomeSchemaVersion,
64 target: GenomeSchemaVersion,
65 },
66
67 #[error("Failed to detect genome schema version: {0}")]
69 DetectionFailed(String),
70}
71
72#[derive(Debug, Clone)]
78pub struct MigrationStepDiagnostics {
79 pub from_version: GenomeSchemaVersion,
80 pub to_version: GenomeSchemaVersion,
81 pub transformations: Vec<String>,
82 pub identifier_remaps: BTreeMap<String, String>,
87}
88
89impl MigrationStepDiagnostics {
90 pub fn new(from: GenomeSchemaVersion, to: GenomeSchemaVersion) -> Self {
91 Self {
92 from_version: from,
93 to_version: to,
94 transformations: Vec::new(),
95 identifier_remaps: BTreeMap::new(),
96 }
97 }
98
99 pub fn record(&mut self, msg: impl Into<String>) {
100 self.transformations.push(msg.into());
101 }
102
103 pub fn record_identifier_remap(
104 &mut self,
105 source: impl Into<String>,
106 destination: impl Into<String>,
107 ) {
108 self.identifier_remaps
109 .insert(source.into(), destination.into());
110 }
111}
112
113#[allow(clippy::wrong_self_convention)]
125pub trait Migrator: Send + Sync {
130 fn from_version(&self) -> GenomeSchemaVersion;
132
133 fn to_version(&self) -> GenomeSchemaVersion;
136
137 fn name(&self) -> &'static str;
140
141 fn migrate(&self, genome: &mut Value) -> Result<MigrationStepDiagnostics, MigrationError>;
144}
145
146#[derive(Debug, Clone)]
154pub struct ChainResult {
155 pub from_version: GenomeSchemaVersion,
156 pub to_version: GenomeSchemaVersion,
157 pub migrators_applied: Vec<&'static str>,
158 pub normalizers_applied: Vec<&'static str>,
159 pub per_step_diagnostics: Vec<MigrationStepDiagnostics>,
160 pub per_normalizer_diagnostics: Vec<NormalizationDiagnostics>,
161 pub advisory_warnings: Vec<String>,
162 pub blocking_errors: Vec<String>,
163}
164
165impl ChainResult {
166 pub fn is_blocking_clean(&self) -> bool {
168 self.blocking_errors.is_empty()
169 }
170}
171
172pub struct ChainRegistry {
181 migrators: BTreeMap<u32, Box<dyn Migrator>>,
182 normalizers: BTreeMap<u32, Box<dyn Normalizer>>,
183 validators: BTreeMap<u32, Box<dyn Validator>>,
184}
185
186impl ChainRegistry {
187 pub fn new() -> Self {
188 Self {
189 migrators: BTreeMap::new(),
190 normalizers: BTreeMap::new(),
191 validators: BTreeMap::new(),
192 }
193 }
194
195 pub fn register_migrator(&mut self, migrator: Box<dyn Migrator>) -> Result<(), MigrationError> {
198 let from = migrator.from_version();
199 let to = migrator.to_version();
200 if to.as_u32() != from.as_u32().saturating_add(1) {
201 return Err(MigrationError::InvalidRegistry(format!(
202 "migrator '{}' declares from={} to={}, expected to=from+1",
203 migrator.name(),
204 from,
205 to
206 )));
207 }
208 if self.migrators.contains_key(&from.as_u32()) {
209 return Err(MigrationError::InvalidRegistry(format!(
210 "duplicate migrator with from_version={from}"
211 )));
212 }
213 self.migrators.insert(from.as_u32(), migrator);
214 Ok(())
215 }
216
217 pub fn register_normalizer(
222 &mut self,
223 normalizer: Box<dyn Normalizer>,
224 ) -> Result<(), MigrationError> {
225 let v = normalizer.schema_version();
226 if self.normalizers.contains_key(&v.as_u32()) {
227 return Err(MigrationError::InvalidRegistry(format!(
228 "duplicate normalizer at schema_version={v}"
229 )));
230 }
231 self.normalizers.insert(v.as_u32(), normalizer);
232 Ok(())
233 }
234
235 pub fn register_validator(&mut self, validator: Box<dyn Validator>) {
239 let v = validator.schema_version().as_u32();
240 self.validators.insert(v, validator);
241 }
242
243 pub fn migrator_for(&self, from: GenomeSchemaVersion) -> Option<&dyn Migrator> {
245 self.migrators.get(&from.as_u32()).map(|b| b.as_ref())
246 }
247
248 pub fn normalizer_for(&self, version: GenomeSchemaVersion) -> Option<&dyn Normalizer> {
250 self.normalizers.get(&version.as_u32()).map(|b| b.as_ref())
251 }
252
253 pub fn validator_for(&self, version: GenomeSchemaVersion) -> Option<&dyn Validator> {
255 self.validators.get(&version.as_u32()).map(|b| b.as_ref())
256 }
257
258 pub fn run_validator(&self, version: GenomeSchemaVersion, genome: &Value) -> ValidationReport {
262 match self.validator_for(version) {
263 Some(v) => v.validate(genome),
264 None => ValidationReport::new(version),
265 }
266 }
267
268 pub fn migrator_count(&self) -> usize {
270 self.migrators.len()
271 }
272
273 pub fn normalizer_count(&self) -> usize {
275 self.normalizers.len()
276 }
277
278 pub fn validator_count(&self) -> usize {
280 self.validators.len()
281 }
282}
283
284impl Default for ChainRegistry {
285 fn default() -> Self {
286 Self::new()
287 }
288}
289
290#[cfg(test)]
295pub(super) mod test_support {
296 use super::*;
297 use serde_json::json;
298
299 pub struct SyntheticMigrator {
303 from: GenomeSchemaVersion,
304 to: GenomeSchemaVersion,
305 name: &'static str,
306 fail: bool,
307 }
308
309 impl SyntheticMigrator {
310 pub fn ok(from: u32, name: &'static str) -> Box<Self> {
311 Box::new(Self {
312 from: GenomeSchemaVersion(from),
313 to: GenomeSchemaVersion(from + 1),
314 name,
315 fail: false,
316 })
317 }
318
319 pub fn failing(from: u32, name: &'static str) -> Box<Self> {
320 Box::new(Self {
321 from: GenomeSchemaVersion(from),
322 to: GenomeSchemaVersion(from + 1),
323 name,
324 fail: true,
325 })
326 }
327 }
328
329 impl Migrator for SyntheticMigrator {
330 fn from_version(&self) -> GenomeSchemaVersion {
331 self.from
332 }
333
334 fn to_version(&self) -> GenomeSchemaVersion {
335 self.to
336 }
337
338 fn name(&self) -> &'static str {
339 self.name
340 }
341
342 fn migrate(&self, genome: &mut Value) -> Result<MigrationStepDiagnostics, MigrationError> {
343 if self.fail {
344 return Err(MigrationError::StepFailed {
345 name: self.name,
346 from: self.from,
347 to: self.to,
348 reason: "synthetic failure".to_string(),
349 });
350 }
351 let mut diag = MigrationStepDiagnostics::new(self.from, self.to);
352 let count = genome
353 .get("step_count")
354 .and_then(|v| v.as_u64())
355 .unwrap_or(0)
356 + 1;
357 genome
358 .as_object_mut()
359 .expect("test genome must be a JSON object")
360 .insert("step_count".to_string(), json!(count));
361 diag.record(format!("incremented step_count to {count}"));
362 Ok(diag)
363 }
364 }
365
366 pub fn make_ok(from: u32, name: &'static str) -> Box<dyn Migrator> {
367 SyntheticMigrator::ok(from, name)
368 }
369
370 pub fn make_failing(from: u32, name: &'static str) -> Box<dyn Migrator> {
371 SyntheticMigrator::failing(from, name)
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::test_support::SyntheticMigrator;
378 use super::*;
379 use serde_json::json;
380
381 #[test]
382 fn registry_accepts_a_well_formed_migrator() {
383 let mut reg = ChainRegistry::new();
384 reg.register_migrator(SyntheticMigrator::ok(2, "v2_to_v3"))
385 .unwrap();
386 assert_eq!(reg.migrator_count(), 1);
387 assert!(reg.migrator_for(GenomeSchemaVersion(2)).is_some());
388 assert!(reg.migrator_for(GenomeSchemaVersion(3)).is_none());
389 }
390
391 #[test]
392 fn registry_rejects_to_version_not_equal_to_from_plus_one() {
393 struct Skipping;
394 impl Migrator for Skipping {
395 fn from_version(&self) -> GenomeSchemaVersion {
396 GenomeSchemaVersion(2)
397 }
398 fn to_version(&self) -> GenomeSchemaVersion {
399 GenomeSchemaVersion(4)
400 }
401 fn name(&self) -> &'static str {
402 "skip"
403 }
404 fn migrate(
405 &self,
406 _genome: &mut Value,
407 ) -> Result<MigrationStepDiagnostics, MigrationError> {
408 unreachable!()
409 }
410 }
411 let mut reg = ChainRegistry::new();
412 let err = reg.register_migrator(Box::new(Skipping)).unwrap_err();
413 assert!(matches!(err, MigrationError::InvalidRegistry(_)));
414 }
415
416 #[test]
417 fn registry_rejects_duplicate_from_version() {
418 let mut reg = ChainRegistry::new();
419 reg.register_migrator(SyntheticMigrator::ok(2, "first"))
420 .unwrap();
421 let err = reg
422 .register_migrator(SyntheticMigrator::ok(2, "second"))
423 .unwrap_err();
424 assert!(matches!(err, MigrationError::InvalidRegistry(_)));
425 }
426
427 #[test]
428 fn migration_step_diagnostics_records_transformations() {
429 let mut diag =
430 MigrationStepDiagnostics::new(GenomeSchemaVersion(2), GenomeSchemaVersion(3));
431 diag.record("converted blueprint keys");
432 diag.record("renamed legacy fields");
433 assert_eq!(diag.transformations.len(), 2);
434 assert_eq!(diag.from_version, GenomeSchemaVersion(2));
435 assert_eq!(diag.to_version, GenomeSchemaVersion(3));
436 }
437
438 #[test]
439 fn run_validator_returns_empty_when_unregistered() {
440 let reg = ChainRegistry::new();
441 let report = reg.run_validator(GenomeSchemaVersion(3), &json!({}));
442 assert_eq!(report.schema_version, Some(GenomeSchemaVersion(3)));
443 assert!(report.is_clean());
444 }
445}