kcode-k1-kmap-format 0.1.0

Durable Kmap domain invariants and versioned binary formats
Documentation
use kcode_k1_kmap_format::*;
use kcode_k1_transaction_id::TxId;

fn id(value: u8) -> NodeId {
    NodeId([value; 12])
}

fn specs(count: usize, tier: ConnectionTier) -> Vec<ConnectionSpec> {
    (0..count)
        .map(|value| ConnectionSpec {
            target: id(value as u8),
            tier,
        })
        .collect()
}

#[test]
fn identity_weight_and_mass_decay_are_exact() {
    let node = id(7);
    let transaction: TxId = node.into();
    assert_eq!(NodeId::from(transaction), node);
    assert_eq!(Weight::initial(), Weight::new(1.0, 3.0).unwrap());
    for (value, mass) in [(0.0, 3.0), (0.25, 0.2)] {
        let aged = 3.0 * 2.0_f64.powf(-mass / 30.0);
        let expected = Weight {
            value: (aged + value * mass) / (aged + mass),
            mass: aged + mass,
        };
        let actual = Weight::initial().update(value, mass).unwrap().unwrap();
        assert!((actual.value - expected.value).abs() < 1e-12);
        assert!((actual.mass - expected.mass).abs() < 1e-12);
    }
    assert_eq!(
        Weight::new(0.0, 3.0).unwrap().update(0.0, 0.2).unwrap(),
        None
    );
}

#[test]
fn node_and_actions_roundtrip_with_callback_owned_create_identity() {
    let node = Node::from_specs(
        "title",
        "hint",
        "narrative",
        vec![ConnectionSpec {
            target: id(2),
            tier: ConnectionTier::Navigation,
        }],
    )
    .unwrap();
    assert_eq!(Node::decode(&node.encode().unwrap()).unwrap(), node);
    let actions = [
        KmapAction::create_node("title", "hint", "narrative", Vec::new()).unwrap(),
        KmapAction::update_node(id(1), Some("new".into()), None, None, Vec::new()).unwrap(),
        KmapAction::apply_measurements(vec![
            ConnectionMeasurement::new(id(1), id(2), 1.0, 3.0).unwrap(),
        ])
        .unwrap(),
    ];
    for action in actions {
        assert_eq!(
            KmapAction::decode(&action.encode().unwrap()).unwrap(),
            action
        );
    }
    assert!(matches!(
        KmapAction::create_node("", "", "", Vec::new()).unwrap(),
        KmapAction::CreateNode { .. }
    ));
}

#[test]
fn wire_rejects_empty_version_malformed_trailing_and_invalid_values() {
    for bytes in [Vec::new(), vec![2], vec![FORMAT_VERSION, 255]] {
        assert!(Node::decode(&bytes).is_err());
        assert!(KmapAction::decode(&bytes).is_err());
    }
    let mut trailing = Node::new("", "", "", Vec::new()).unwrap().encode().unwrap();
    trailing.push(0);
    assert!(Node::decode(&trailing).is_err());
    let invalid = Node {
        title: String::new(),
        navigation_hint: String::new(),
        narrative: String::new(),
        connections: vec![Connection {
            target: id(1),
            tier: ConnectionTier::Automated,
            weight: Weight {
                value: f64::NAN,
                mass: 1.0,
            },
        }],
    };
    let mut bytes = vec![FORMAT_VERSION];
    bytes.extend(postcard::to_stdvec(&invalid).unwrap());
    assert!(Node::decode(&bytes).is_err());
}

#[test]
fn target_uniqueness_and_navigation_limit_are_enforced() {
    let duplicate = vec![
        ConnectionSpec {
            target: id(1),
            tier: ConnectionTier::Navigation,
        },
        ConnectionSpec {
            target: id(1),
            tier: ConnectionTier::Automated,
        },
    ];
    assert!(Node::from_specs("", "", "", duplicate.clone()).is_err());
    assert!(KmapAction::create_node("", "", "", duplicate).is_err());
    assert!(Node::from_specs("", "", "", specs(12, ConnectionTier::Navigation)).is_ok());
    assert!(Node::from_specs("", "", "", specs(13, ConnectionTier::Navigation)).is_err());
    assert!(Node::from_specs("", "", "", specs(100, ConnectionTier::Automated)).is_ok());
    assert!(
        KmapAction::update_node(
            id(0),
            None,
            None,
            None,
            vec![
                ConnectionChange::Remove(id(1)),
                ConnectionChange::Set(ConnectionSpec {
                    target: id(1),
                    tier: ConnectionTier::Navigation,
                }),
            ],
        )
        .is_err()
    );
}

#[test]
fn changes_preserve_weight_and_measurements_are_atomic() {
    let preserved = Weight::new(0.4, 7.0).unwrap();
    let mut node = Node::new(
        "",
        "",
        "",
        vec![Connection {
            target: id(2),
            tier: ConnectionTier::Automated,
            weight: preserved,
        }],
    )
    .unwrap();
    node.apply_connection_changes(&[ConnectionChange::Set(ConnectionSpec {
        target: id(2),
        tier: ConnectionTier::Navigation,
    })])
    .unwrap();
    assert_eq!(node.connections[0].weight, preserved);
    assert_eq!(node.connections[0].tier, ConnectionTier::Navigation);
    assert_eq!(
        node.apply_measurement(id(2), 0.5, 1.0).unwrap(),
        MeasurementOutcome::Updated
    );
    assert_eq!(
        node.apply_measurement(id(9), 0.5, 1.0).unwrap(),
        MeasurementOutcome::Absent
    );
    let before = node.clone();
    assert!(node.apply_measurement(id(2), f64::NAN, 1.0).is_err());
    assert_eq!(node, before);
    let mut weak = Node::new(
        "",
        "",
        "",
        vec![Connection {
            target: id(3),
            tier: ConnectionTier::Automated,
            weight: Weight::new(0.0, 3.0).unwrap(),
        }],
    )
    .unwrap();
    assert_eq!(
        weak.apply_measurement(id(3), 0.0, 0.2).unwrap(),
        MeasurementOutcome::Pruned
    );
}

#[test]
fn invalid_values_and_masses_are_rejected() {
    for value in [f64::NAN, f64::INFINITY, -0.1, 1.1] {
        assert!(Weight::new(value, 1.0).is_err());
        assert!(ConnectionMeasurement::new(id(1), id(2), value, 1.0).is_err());
    }
    for mass in [f64::NAN, f64::INFINITY, 0.0, -0.1] {
        assert!(Weight::new(0.5, mass).is_err());
        assert!(ConnectionMeasurement::new(id(1), id(2), 0.5, mass).is_err());
    }
}