crtx-llm 0.1.1

Claude, Ollama, and replay adapters behind a shared trait.
Documentation
//! Regenerate `INDEX.toml` for a Cortex replay fixtures directory.
//!
//! Walks the directory non-recursively, BLAKE3-hashes each `*.json` fixture
//! (excluding `schema.json`), and writes an `INDEX.toml` listing each
//! fixture's filename and lowercase-hex hash. Existing `INDEX.toml` is
//! overwritten.
//!
//! ```text
//! Usage: sign-fixtures <fixtures-dir>
//! ```
//!
//! Lane 1.D / threat row T-RM-1: this is the only signer the v0 replay
//! adapter trusts. The output file is checked into git; protection is git
//! push permissions, not minisign (deferred to Phase 3.D / ADR 0010).

use std::env;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::ExitCode;

fn main() -> ExitCode {
    let mut args = env::args().skip(1);
    let Some(dir) = args.next() else {
        eprintln!("usage: sign-fixtures <fixtures-dir>");
        return ExitCode::from(2);
    };
    if args.next().is_some() {
        eprintln!("usage: sign-fixtures <fixtures-dir>");
        return ExitCode::from(2);
    }

    match run(PathBuf::from(dir)) {
        Ok(count) => {
            eprintln!("signed {count} fixture(s)");
            ExitCode::SUCCESS
        }
        Err(e) => {
            eprintln!("sign-fixtures: {e}");
            ExitCode::from(1)
        }
    }
}

fn run(dir: PathBuf) -> Result<usize, String> {
    let canonical =
        fs::canonicalize(&dir).map_err(|e| format!("canonicalize {}: {e}", dir.display()))?;

    let mut entries: Vec<(String, String)> = Vec::new();
    let read =
        fs::read_dir(&canonical).map_err(|e| format!("read_dir {}: {e}", canonical.display()))?;
    for dirent in read {
        let dirent = dirent.map_err(|e| format!("dirent: {e}"))?;
        let path = dirent.path();
        if !path.is_file() {
            continue;
        }
        let name = path
            .file_name()
            .and_then(|s| s.to_str())
            .ok_or_else(|| format!("non-utf8 filename in {}", canonical.display()))?
            .to_string();
        if name == "INDEX.toml" || name == "schema.json" {
            continue;
        }
        if !name.ends_with(".json") {
            continue;
        }
        let bytes = fs::read(&path).map_err(|e| format!("read {}: {e}", path.display()))?;
        let hex = blake3::hash(&bytes).to_hex().to_string();
        entries.push((name, hex));
    }
    entries.sort_by(|a, b| a.0.cmp(&b.0));

    let mut out = String::new();
    out.push_str("# AUTO-GENERATED by scripts/sign-fixtures.sh\n");
    out.push_str("# Lane 1.D / THREATS T-RM-1 — BLAKE3-only fixture index.\n");
    out.push_str(
        "# Edits MUST regenerate this file via the script; ReplayAdapter rejects mismatches.\n\n",
    );
    for (name, hex) in &entries {
        out.push_str(&format!(
            "[[fixture]]\npath = \"{name}\"\nblake3 = \"{hex}\"\n\n"
        ));
    }

    let index_path = canonical.join("INDEX.toml");
    let mut f = fs::File::create(&index_path)
        .map_err(|e| format!("create {}: {e}", index_path.display()))?;
    f.write_all(out.as_bytes())
        .map_err(|e| format!("write {}: {e}", index_path.display()))?;
    Ok(entries.len())
}