axiomsync 1.0.1

Local retrieval runtime and CLI for AxiomSync.
Documentation
use crate::models::{MemoryCandidate, Message};

pub(crate) fn stable_text_key(text: &str) -> String {
    let normalized = text
        .to_lowercase()
        .chars()
        .map(|c| if c.is_ascii_alphanumeric() { c } else { ' ' })
        .collect::<String>()
        .split_whitespace()
        .collect::<Vec<_>>()
        .join(" ");

    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
    for byte in normalized.as_bytes() {
        hash ^= u64::from(*byte);
        hash = hash.wrapping_mul(0x1000_0000_01b3);
    }
    format!("{hash:016x}")[..12].to_string()
}

pub(crate) fn build_memory_key(category: &str, text: &str) -> String {
    let suffix = stable_text_key(text);
    match category {
        "profile" => "profile".to_string(),
        "preferences" => format!("pref-{suffix}"),
        "entities" => format!("entity-{suffix}"),
        "events" => format!("event-{suffix}"),
        "cases" => format!("case-{suffix}"),
        _ => format!("pattern-{suffix}"),
    }
}

pub(crate) fn normalize_memory_text(text: &str) -> String {
    text.split_whitespace().collect::<Vec<_>>().join(" ")
}

pub(crate) fn slugify(input: &str) -> String {
    let mut out = String::new();
    for c in input.chars() {
        if c.is_ascii_alphanumeric() {
            out.push(c.to_ascii_lowercase());
        } else if (c.is_whitespace() || c == '-' || c == '_') && !out.ends_with('-') {
            out.push('-');
        }
    }
    out = out.trim_matches('-').to_string();
    if out.is_empty() {
        "item".to_string()
    } else {
        out
    }
}

pub(crate) fn extract_memories_heuristically(messages: &[Message]) -> Vec<MemoryCandidate> {
    let mut out = Vec::new();
    for msg in messages {
        let lower = msg.text.to_lowercase();
        let is_user = msg.role == "user";
        let key_suffix = stable_text_key(&msg.text);

        if is_user && is_profile_message(&lower, &msg.text) {
            out.push(MemoryCandidate {
                category: "profile".to_string(),
                key: "profile".to_string(),
                text: msg.text.clone(),
                source_message_id: msg.id.clone(),
            });
        }

        if is_user && is_preference_message(&lower, &msg.text) {
            out.push(MemoryCandidate {
                category: "preferences".to_string(),
                key: format!("pref-{key_suffix}"),
                text: msg.text.clone(),
                source_message_id: msg.id.clone(),
            });
        }

        if is_user && is_entity_message(&lower, &msg.text) {
            out.push(MemoryCandidate {
                category: "entities".to_string(),
                key: format!("entity-{key_suffix}"),
                text: msg.text.clone(),
                source_message_id: msg.id.clone(),
            });
        }

        if is_event_message(&lower, &msg.text) {
            out.push(MemoryCandidate {
                category: "events".to_string(),
                key: format!("event-{key_suffix}"),
                text: msg.text.clone(),
                source_message_id: msg.id.clone(),
            });
        }

        if is_case_message(&lower, &msg.text) {
            out.push(MemoryCandidate {
                category: "cases".to_string(),
                key: format!("case-{key_suffix}"),
                text: msg.text.clone(),
                source_message_id: msg.id.clone(),
            });
        }

        if is_pattern_message(&lower, &msg.text) {
            out.push(MemoryCandidate {
                category: "patterns".to_string(),
                key: format!("pattern-{key_suffix}"),
                text: msg.text.clone(),
                source_message_id: msg.id.clone(),
            });
        }
    }
    out
}

fn contains_any(text: &str, patterns: &[&str]) -> bool {
    patterns.iter().any(|pattern| text.contains(pattern))
}

fn is_profile_message(lower: &str, original: &str) -> bool {
    contains_any(lower, &["my name is", "i am ", "call me "]) || original.contains("내 이름")
}

fn is_preference_message(lower: &str, original: &str) -> bool {
    contains_any(
        lower,
        &[
            "prefer",
            "preference",
            "avoid",
            "i like",
            "i dislike",
            "i don't like",
        ],
    ) || contains_any(original, &["선호", "피해", "싫어", "좋아"])
}

fn is_entity_message(lower: &str, original: &str) -> bool {
    contains_any(lower, &["project", "repository", "repo", "service", "team"])
        || original.contains("프로젝트")
}

fn is_event_message(lower: &str, original: &str) -> bool {
    contains_any(
        lower,
        &[
            "today",
            "yesterday",
            "tomorrow",
            "incident",
            "outage",
            "deploy",
            "deployed",
            "release",
            "released",
            "meeting",
            "deadline",
            "milestone",
            "happened",
            "occurred",
            "failed at",
            "rolled back",
        ],
    ) || contains_any(
        original,
        &["오늘", "어제", "내일", "발생", "배포", "릴리스", "회의"],
    )
}

fn is_case_message(lower: &str, original: &str) -> bool {
    contains_any(
        lower,
        &[
            "root cause",
            "rca",
            "postmortem",
            "fixed",
            "resolved",
            "workaround",
            "repro",
            "reproduced",
            "solution",
            "solved",
            "debugged",
            "troubleshoot",
            "investigation",
        ],
    ) || contains_any(original, &["원인", "해결", "재현", "대응"])
}

fn is_pattern_message(lower: &str, original: &str) -> bool {
    contains_any(
        lower,
        &[
            "always",
            "never",
            "whenever",
            "if we",
            "if you",
            "checklist",
            "playbook",
            "rule",
            "guideline",
            "best practice",
            "pattern",
            "must",
            "should always",
        ],
    ) || contains_any(original, &["항상", "절대", "반드시", "체크리스트", "원칙"])
}