remem-ai 0.6.11

Local-first coding agent memory for Claude Code and OpenAI Codex
Documentation
use std::collections::HashSet;

use anyhow::{Context, Result};

pub(super) fn parse_file_list(raw: Option<&str>, project: &str) -> Result<HashSet<String>> {
    let Some(raw) = raw.map(str::trim).filter(|value| !value.is_empty()) else {
        return Ok(HashSet::new());
    };
    let files = if raw.starts_with('[') {
        parse_json_file_array(raw)
            .with_context(|| "parse memory files for source-anchor staleness")?
    } else {
        raw.split([',', '\n'])
            .map(str::trim)
            .map(|value| value.trim_matches('"'))
            .filter(|value| !value.is_empty())
            .map(str::to_string)
            .collect()
    };
    Ok(files
        .into_iter()
        .filter_map(|file| normalize_file_path_for_project(&file, project))
        .collect())
}

pub(super) fn parse_json_file_array(raw: &str) -> Result<Vec<String>> {
    Ok(serde_json::from_str::<Vec<String>>(raw)?)
}

pub(super) fn file_path_overlaps(changed_file: &str, memory_file: &str, project: &str) -> bool {
    let Some(changed_file) = normalize_file_path_for_project(changed_file, project) else {
        return false;
    };
    changed_file == memory_file
        || changed_file
            .strip_prefix(memory_file)
            .is_some_and(|tail| tail.starts_with('/'))
        || memory_file
            .strip_prefix(&changed_file)
            .is_some_and(|tail| tail.starts_with('/'))
}

fn normalize_file_path(path: &str) -> Option<String> {
    let trimmed = path.trim().trim_start_matches("./").trim_matches('/');
    (!trimmed.is_empty()).then(|| trimmed.to_string())
}

fn normalize_file_path_for_project(path: &str, project: &str) -> Option<String> {
    let normalized = normalize_file_path(path)?;
    let Some(project) = normalize_file_path(project) else {
        return Some(normalized);
    };
    if normalized == project {
        return None;
    }
    normalized
        .strip_prefix(&format!("{project}/"))
        .map(str::to_string)
        .filter(|value| !value.is_empty())
        .or(Some(normalized))
}