kimetsu-brain 0.5.2

Project + user-scope memory, hybrid retrieval (lexical + cosine), ambient context, secret redaction at ingest for kimetsu.
Documentation
use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};

use kimetsu_core::KimetsuResult;
use kimetsu_core::event::Event;
use kimetsu_core::ids::RunId;
use kimetsu_core::paths::ProjectPaths;

#[derive(Debug, Clone)]
pub struct RunPaths {
    pub run_dir: PathBuf,
    pub trace_jsonl: PathBuf,
    pub artifacts_dir: PathBuf,
    pub patch_plans_dir: PathBuf,
    pub final_report: PathBuf,
    pub run_log: PathBuf,
}

impl RunPaths {
    pub fn new(paths: &ProjectPaths, run_id: RunId) -> Self {
        let run_dir = paths.runs_dir.join(run_id.to_string());
        Self {
            trace_jsonl: run_dir.join("trace.jsonl"),
            artifacts_dir: run_dir.join("artifacts"),
            patch_plans_dir: run_dir.join("patch_plans"),
            final_report: run_dir.join("final_report.md"),
            run_log: run_dir.join("kimetsu.log"),
            run_dir,
        }
    }

    pub fn create_dirs(&self) -> KimetsuResult<()> {
        fs::create_dir_all(&self.artifacts_dir)?;
        fs::create_dir_all(&self.patch_plans_dir)?;
        Ok(())
    }
}

pub struct TraceWriter {
    file: File,
}

impl TraceWriter {
    pub fn create(paths: &ProjectPaths, run_id: RunId) -> KimetsuResult<(Self, RunPaths)> {
        let run_paths = RunPaths::new(paths, run_id);
        run_paths.create_dirs()?;
        let file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&run_paths.trace_jsonl)?;
        Ok((Self { file }, run_paths))
    }

    pub fn append(&mut self, event: &Event, fsync: bool) -> KimetsuResult<()> {
        serde_json::to_writer(&mut self.file, event)?;
        self.file.write_all(b"\n")?;
        self.file.flush()?;
        if fsync {
            self.file.sync_data()?;
        }
        Ok(())
    }
}

pub fn read_trace(trace_jsonl: &Path) -> KimetsuResult<Vec<Event>> {
    let file = File::open(trace_jsonl)?;
    let mut reader = BufReader::new(file);
    let mut events = Vec::new();
    let mut line = String::new();
    let mut line_number = 0usize;

    loop {
        line.clear();
        let bytes_read = reader.read_line(&mut line)?;
        if bytes_read == 0 {
            break;
        }
        line_number += 1;

        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }

        match serde_json::from_str::<Event>(trimmed) {
            Ok(event) => events.push(event),
            Err(err) => {
                if !line.ends_with('\n') {
                    eprintln!(
                        "warning: ignoring invalid trailing JSONL line in {}: {err}",
                        trace_jsonl.display()
                    );
                    break;
                }

                return Err(format!(
                    "invalid JSONL at {}:{}: {err}",
                    trace_jsonl.display(),
                    line_number
                )
                .into());
            }
        }
    }

    Ok(events)
}

pub fn discover_traces(paths: &ProjectPaths) -> KimetsuResult<Vec<PathBuf>> {
    if !paths.runs_dir.exists() {
        return Ok(Vec::new());
    }

    let mut traces = Vec::new();
    for entry in fs::read_dir(&paths.runs_dir)? {
        let entry = entry?;
        if !entry.file_type()?.is_dir() {
            continue;
        }

        let trace = entry.path().join("trace.jsonl");
        if trace.exists() {
            traces.push(trace);
        }
    }

    traces.sort();
    Ok(traces)
}

pub fn read_all_traces(paths: &ProjectPaths) -> KimetsuResult<Vec<Event>> {
    let mut events = Vec::new();
    for trace in discover_traces(paths)? {
        events.extend(read_trace(&trace)?);
    }

    events.sort_by(|left, right| {
        left.event_id
            .0
            .cmp(&right.event_id.0)
            .then_with(|| left.ts.cmp(&right.ts))
    });
    events.dedup_by_key(|event| event.event_id);
    Ok(events)
}