helena 0.1.0

Core types and component interfaces for helena, a latent data-to-waveform generation platform.
Documentation
use super::*;

const MINIMAL_CONFIG: &str = r#"
project = "p"
experiment_name = "e"
sample_rate = 8
clip_duration_seconds = 0.5
[source_data]
type = "tabular"
path = "m.json"
[target_audio]
path = "audio"
[data_encoder]
type = "vector"
latent_dim = 2
[audio_codec]
type = "pcm"
latent_type = "continuous"
[conditional_generator]
type = "mlp"
[training]
batch_size = 1
learning_rate = 0.001
max_steps = 1
"#;

fn recipe_fp(cfg: &RawExperimentConfig) -> RecipeFingerprint {
    let spec = cfg.clone().resolve().expect("resolve");
    RecipeFingerprint::of(&spec.recipe()).expect("recipe fingerprint")
}

fn config_fp(cfg: &RawExperimentConfig) -> ConfigFingerprint {
    ConfigFingerprint::of(cfg).expect("config fingerprint")
}

#[test]
fn recipe_fingerprint_ignores_generation_but_tracks_recipe_fields() {
    let base = RawExperimentConfig::from_toml_str(MINIMAL_CONFIG).unwrap();

    // Editing inference-only generation.* leaves the recipe fingerprint
    // unchanged, so a checkpoint stays loadable across sampling settings (the
    // GEN-14 contract)...
    let mut tuned = base.clone();
    tuned.generation.guidance_scale = 9.0;
    tuned.generation.temperature = 0.5;
    tuned.generation.num_candidates = 4;
    assert_eq!(recipe_fp(&base), recipe_fp(&tuned));
    // ...while the full provenance fingerprint *does* change (the artifact must
    // reflect the knobs).
    assert_ne!(config_fp(&base), config_fp(&tuned));

    // A training-determining field still moves the recipe fingerprint.
    let mut rerecipe = base.clone();
    rerecipe.training.learning_rate = 0.5;
    assert_ne!(recipe_fp(&base), recipe_fp(&rerecipe));
}

#[test]
fn fingerprints_are_stable_across_load_save_cycles() {
    // Re-parsing the same document yields the same recipe and config
    // fingerprints — the byte-stability the recipe projection is built on.
    let a = RawExperimentConfig::from_toml_str(MINIMAL_CONFIG).unwrap();
    let b = RawExperimentConfig::from_toml_str(MINIMAL_CONFIG).unwrap();
    assert_eq!(recipe_fp(&a), recipe_fp(&b));
    assert_eq!(config_fp(&a), config_fp(&b));

    // A JSON round-trip of the raw document preserves both fingerprints.
    let json = a.to_json_string().unwrap();
    let reparsed = RawExperimentConfig::from_json_str(&json).unwrap();
    assert_eq!(recipe_fp(&a), recipe_fp(&reparsed));
    assert_eq!(config_fp(&a), config_fp(&reparsed));
}

#[test]
fn fingerprints_display_as_sha256() {
    let cfg = RawExperimentConfig::from_toml_str(MINIMAL_CONFIG).unwrap();
    assert!(config_fp(&cfg).to_string().starts_with("sha256:"));
    assert!(recipe_fp(&cfg).to_string().starts_with("sha256:"));
}

#[test]
fn spec_convenience_methods_match_standalone_constructors() {
    // A spec holder can mint both provenance stamps without juggling the raw
    // document, and gets the same fingerprints the standalone constructors do.
    let cfg = RawExperimentConfig::from_toml_str(MINIMAL_CONFIG).unwrap();
    let spec = cfg.clone().resolve().unwrap();
    assert_eq!(spec.config_fingerprint().unwrap(), config_fp(&cfg));
    assert_eq!(spec.recipe_fingerprint().unwrap(), recipe_fp(&cfg));
}