sdivi-core 0.2.16

Pure-compute facade for the Structural Divergence Indexer — WASM-compatible, no I/O
Documentation
//! [`normalize_and_hash`] — canonical blake3 fingerprint for pattern AST subtrees.

use crate::input::NormalizeNode;

/// Computes a canonical `blake3` fingerprint for a pattern AST subtree.
///
/// This is a re-exposed entry point for foreign extractors (e.g. the consumer
/// app or WASM callers) that need to produce fingerprints byte-identical to
/// those generated by the native Rust pipeline.
///
/// The input uses [`NormalizeNode`] from `sdivi-core::input`; internally the
/// node is forwarded to `sdivi_patterns::normalize::normalize_and_hash`.
///
/// ## Algorithm
///
/// Depth-first canonical walk.  For a node with kind `K` and ordered children
/// `[c1, c2, …]`, the `blake3` preimage is:
/// - `K` bytes + `0x00` separator + `(0x01 + child_digest_bytes)*`
///
/// **Empty `children`** produces the same digest as
/// `sdivi_patterns::fingerprint::fingerprint_node_kind(kind)`.
///
/// Returns a 64-character lowercase hex string.
///
/// # Examples
///
/// ```rust
/// use sdivi_core::compute::normalize::normalize_and_hash;
/// use sdivi_core::input::NormalizeNode;
///
/// let hex = normalize_and_hash("try_expression", &[]);
/// assert_eq!(hex.len(), 64);
/// ```
pub fn normalize_and_hash(kind: &str, children: &[NormalizeNode]) -> String {
    let sdivi_children: Vec<sdivi_patterns::normalize::NormalizeNode> =
        children.iter().map(to_sdivi_node).collect();
    sdivi_patterns::normalize::normalize_and_hash(kind, &sdivi_children)
}

fn to_sdivi_node(n: &NormalizeNode) -> sdivi_patterns::normalize::NormalizeNode {
    sdivi_patterns::normalize::NormalizeNode {
        kind: n.kind.clone(),
        children: n.children.iter().map(to_sdivi_node).collect(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use sdivi_patterns::fingerprint::fingerprint_node_kind;

    #[test]
    fn leaf_matches_fingerprint_node_kind() {
        let expected = fingerprint_node_kind("try_expression").to_hex();
        let actual = normalize_and_hash("try_expression", &[]);
        assert_eq!(actual, expected);
    }

    #[test]
    fn returns_64_char_hex() {
        let h = normalize_and_hash("function_definition", &[]);
        assert_eq!(h.len(), 64);
        assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
    }

    #[test]
    fn different_kinds_differ() {
        let a = normalize_and_hash("kind_a", &[]);
        let b = normalize_and_hash("kind_b", &[]);
        assert_ne!(a, b);
    }

    #[test]
    fn nested_differs_from_flat() {
        let child = NormalizeNode {
            kind: "child".to_string(),
            children: vec![],
        };
        let nested = normalize_and_hash("parent", &[child]);
        let flat = normalize_and_hash("parent", &[]);
        assert_ne!(nested, flat);
    }
}