harness-read 0.1.1

Read tool for AI agent harnesses — filesystem read with pagination, binary sniff, directory listing, attachment shape, NOT_FOUND fuzzy siblings
Documentation
use std::path::Path;

use crate::constants::FUZZY_SUGGESTION_LIMIT;

pub async fn suggest_siblings(missing_path: &Path) -> Vec<String> {
    let dir = match missing_path.parent() {
        Some(d) => d,
        None => return Vec::new(),
    };
    let base_os = missing_path.file_name().and_then(|s| s.to_str()).unwrap_or("");
    let base = base_os.to_ascii_lowercase();

    let mut read_dir = match tokio::fs::read_dir(dir).await {
        Ok(r) => r,
        Err(_) => return Vec::new(),
    };

    let mut scored: Vec<(String, i32)> = Vec::new();
    loop {
        let entry = match read_dir.next_entry().await {
            Ok(Some(e)) => e,
            _ => break,
        };
        let name = entry.file_name();
        let name = match name.to_str() {
            Some(s) => s.to_string(),
            None => continue,
        };
        let lower = name.to_ascii_lowercase();
        let score = similarity(&base, &lower);
        if score > 0 {
            let full = dir.join(&name).to_string_lossy().into_owned();
            scored.push((full, score));
        }
    }
    scored.sort_by(|a, b| b.1.cmp(&a.1));
    scored
        .into_iter()
        .take(FUZZY_SUGGESTION_LIMIT)
        .map(|(p, _)| p)
        .collect()
}

fn similarity(a: &str, b: &str) -> i32 {
    if a == b {
        return 1000;
    }
    if a.is_empty() || b.is_empty() {
        return 0;
    }
    if a.contains(b) || b.contains(a) {
        return 500;
    }
    let prefix = common_prefix(a, b);
    if prefix >= 3 {
        return 200 + prefix as i32;
    }
    if prefix >= 2 && (a.len() as i64 - b.len() as i64).abs() <= 2 {
        return 100 + prefix as i32;
    }
    if ext_of(a) == ext_of(b) && !ext_of(a).is_empty() {
        return 10;
    }
    0
}

fn common_prefix(a: &str, b: &str) -> usize {
    let mut a_iter = a.chars();
    let mut b_iter = b.chars();
    let mut n = 0usize;
    loop {
        match (a_iter.next(), b_iter.next()) {
            (Some(x), Some(y)) if x == y => n += 1,
            _ => break,
        }
    }
    n
}

fn ext_of(name: &str) -> &str {
    match name.rfind('.') {
        Some(i) if i > 0 => &name[i..],
        _ => "",
    }
}