batpak 0.3.0

Event sourcing with causal graphs and policy gates. Sync API, zero async.
Documentation
use crate::event::StoredEvent;
use crate::store::Store;

pub(crate) fn walk_ancestors_by_hash(
    store: &Store,
    event_id: u128,
    limit: usize,
) -> Vec<StoredEvent<serde_json::Value>> {
    let mut results = Vec::new();
    let mut current_id = Some(event_id);
    while let Some(id) = current_id {
        if results.len() >= limit {
            break;
        }
        if let Some(entry) = store.index.get_by_id(id) {
            if let Ok(stored) = store.reader.read_entry(&entry.disk_pos) {
                results.push(stored);
            }
            let prev = entry.hash_chain.prev_hash;
            if prev == [0_u8; 32] {
                break;
            }
            current_id = store
                .index
                .stream(entry.coord.entity())
                .iter()
                .find(|candidate| candidate.hash_chain.event_hash == prev)
                .map(|candidate| candidate.event_id);
        } else {
            break;
        }
    }
    results
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::coordinate::Coordinate;
    use crate::event::EventKind;
    use crate::store::{Store, StoreConfig, SyncConfig};
    use tempfile::TempDir;

    fn test_store() -> (Store, TempDir) {
        let dir = TempDir::new().expect("temp dir");
        let config = StoreConfig {
            data_dir: dir.path().to_path_buf(),
            segment_max_bytes: 4096,
            sync: SyncConfig {
                every_n_events: 1,
                ..SyncConfig::default()
            },
            ..StoreConfig::new("")
        };
        let store = Store::open(config).expect("open store");
        (store, dir)
    }

    fn seeded_chain(store: &Store, entity: &str) -> Vec<u128> {
        let coord = Coordinate::new(entity, "scope:test").expect("coord");
        let kind = EventKind::custom(0xF, 1);
        (0..4)
            .map(|step| {
                store
                    .append(&coord, kind, &serde_json::json!({ "step": step }))
                    .expect("append")
                    .event_id
            })
            .collect()
    }

    #[test]
    fn hash_helper_returns_exact_chain_in_reverse_order() {
        let (store, _dir) = test_store();
        let ids = seeded_chain(&store, "entity:hash-helper");

        let actual: Vec<_> = walk_ancestors_by_hash(&store, *ids.last().expect("last"), 8)
            .into_iter()
            .map(|stored| stored.event.event_id())
            .collect();
        let expected: Vec<_> = ids.iter().rev().copied().collect();

        assert_eq!(
            actual,
            expected,
            "PROPERTY: hash-based ancestor traversal must return the exact chain in reverse append order."
        );
    }

    #[test]
    fn hash_helper_honors_zero_limit_and_unknown_anchor() {
        let (store, _dir) = test_store();
        let ids = seeded_chain(&store, "entity:hash-zero");

        assert!(
            walk_ancestors_by_hash(&store, *ids.last().expect("last"), 0).is_empty(),
            "PROPERTY: hash-based ancestor traversal with limit=0 must return an empty vector."
        );
        assert!(
            walk_ancestors_by_hash(&store, 0xDEAD_BEEF, 4).is_empty(),
            "PROPERTY: hash-based ancestor traversal must return empty for an unknown anchor event."
        );
    }
}