Skip to main content

feagi_evolutionary/genome/
artifact.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Encoding boundary for external `.genome` artifacts.
5//!
6//! Genome artifact encoding is independent from genome schema versioning. The
7//! codec converts bytes to and from a schema-bearing [`serde_json::Value`];
8//! migration and validation remain the responsibility of the genome chain.
9
10use crate::{types::EvoError, EvoResult};
11use serde_json::Value;
12
13/// Required extension for external genome artifacts.
14pub const GENOME_ARTIFACT_EXTENSION: &str = "genome";
15
16/// Media type for the current JSON genome artifact encoding.
17pub const GENOME_ARTIFACT_MEDIA_TYPE: &str = "application/vnd.feagi.genome+json";
18
19/// Supported external artifact encoding.
20///
21/// This enum identifies representation only. It does not version genome
22/// schemas and must never be used to dispatch schema migration.
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum GenomeArtifactEncoding {
25    /// UTF-8 JSON containing a versioned genome document.
26    Json,
27}
28
29/// Codec contract for external genome artifact bytes.
30pub trait GenomeArtifactCodec: Send + Sync {
31    /// Return the representation handled by this codec.
32    fn encoding(&self) -> GenomeArtifactEncoding;
33
34    /// Return the media type emitted for this representation.
35    fn media_type(&self) -> &'static str;
36
37    /// Decode artifact bytes into a schema-bearing genome document.
38    fn decode(&self, artifact: &[u8]) -> EvoResult<Value>;
39
40    /// Encode a schema-bearing genome document as artifact bytes.
41    fn encode(&self, genome: &Value) -> EvoResult<Vec<u8>>;
42}
43
44/// Current UTF-8 JSON artifact codec.
45#[derive(Clone, Copy, Debug, Default)]
46pub struct JsonGenomeArtifactCodec;
47
48impl GenomeArtifactCodec for JsonGenomeArtifactCodec {
49    fn encoding(&self) -> GenomeArtifactEncoding {
50        GenomeArtifactEncoding::Json
51    }
52
53    fn media_type(&self) -> &'static str {
54        GENOME_ARTIFACT_MEDIA_TYPE
55    }
56
57    fn decode(&self, artifact: &[u8]) -> EvoResult<Value> {
58        serde_json::from_slice(artifact)
59            .map_err(|error| EvoError::InvalidGenome(format!("Failed to parse JSON: {error}")))
60    }
61
62    fn encode(&self, genome: &Value) -> EvoResult<Vec<u8>> {
63        serde_json::to_vec(genome).map_err(EvoError::from)
64    }
65}
66
67/// Decode bytes using the current explicitly selected artifact encoding.
68pub fn decode_genome_artifact(artifact: &[u8]) -> EvoResult<Value> {
69    JsonGenomeArtifactCodec.decode(artifact)
70}
71
72/// Encode a genome using the current explicitly selected artifact encoding.
73pub fn encode_genome_artifact(genome: &Value) -> EvoResult<Vec<u8>> {
74    JsonGenomeArtifactCodec.encode(genome)
75}
76
77/// Validate that an external artifact filename uses `.genome`.
78pub fn validate_genome_artifact_file_name(file_name: &str) -> EvoResult<()> {
79    let leaf_name = file_name.rsplit(['/', '\\']).next().unwrap_or(file_name);
80    let valid_extension = leaf_name.rsplit_once('.').is_some_and(|(stem, extension)| {
81        !stem.is_empty() && extension.eq_ignore_ascii_case(GENOME_ARTIFACT_EXTENSION)
82    });
83
84    if valid_extension {
85        Ok(())
86    } else {
87        Err(EvoError::InvalidGenome(
88            "Genome files must use the .genome extension".to_string(),
89        ))
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use serde_json::json;
97
98    #[test]
99    fn json_codec_round_trips_without_changing_schema_version() {
100        let genome = json!({
101            "genome_schema_version": 3,
102            "version": "3.0",
103            "blueprint": {}
104        });
105
106        let encoded = encode_genome_artifact(&genome).expect("JSON encoding should succeed");
107        let decoded = decode_genome_artifact(&encoded).expect("JSON decoding should succeed");
108
109        assert_eq!(decoded, genome);
110        assert_eq!(decoded["genome_schema_version"], 3);
111    }
112
113    #[test]
114    fn json_codec_rejects_malformed_artifact_bytes() {
115        let result = decode_genome_artifact(b"not-json");
116
117        assert!(result.is_err());
118    }
119
120    #[test]
121    fn artifact_filename_contract_is_platform_independent() {
122        assert!(validate_genome_artifact_file_name("brain.genome").is_ok());
123        assert!(validate_genome_artifact_file_name("brain.GENOME").is_ok());
124        assert!(validate_genome_artifact_file_name(r"C:\brains\brain.genome").is_ok());
125        assert!(validate_genome_artifact_file_name("brain.json").is_err());
126        assert!(validate_genome_artifact_file_name(".genome").is_err());
127    }
128
129    #[test]
130    fn artifact_decode_precedes_existing_schema_migration_chain() {
131        let artifact = br#"{
132            "genome_id": "artifact-test",
133            "genome_title": "Artifact test",
134            "genome_description": "Codec and schema integration",
135            "version": "2.0",
136            "blueprint": {},
137            "brain_regions": {},
138            "neuron_morphologies": {},
139            "physiology": {"simulation_timestep": 0.025, "max_age": 1},
140            "stats": {
141                "innate_cortical_area_count": 0,
142                "innate_neuron_count": 0,
143                "innate_synapse_count": 0
144            },
145            "signatures": {"genome": "0", "blueprint": "0", "physiology": "0"},
146            "timestamp": 0.0
147        }"#;
148
149        let decoded = decode_genome_artifact(artifact).expect("artifact should decode");
150        let (migrated, report) = crate::genome::migrate_genome_value_to_current(decoded)
151            .expect("existing schema chain should migrate decoded genome");
152
153        assert_eq!(report.from_version.as_u32(), 2);
154        assert_eq!(report.to_version.as_u32(), 3);
155        assert_eq!(migrated["genome_schema_version"], 3);
156    }
157}