cordance-cli 0.1.2

Cordance CLI — installs the `cordance` binary. The umbrella package `cordance` re-exports this entry; either install command works.
Documentation
//! `cordance explain <rule>` — show source anchors for a rule.
//!
//! Reads `.cordance/evidence-map.json` (produced by `cordance pack`) and
//! filters entries whose `rule_id` or `text` matches the query substring.
//! When the map is missing, returns a descriptive error directing the user
//! to run `cordance pack` first.

use anyhow::{anyhow, Result};
use camino::Utf8PathBuf;
use cordance_core::evidence::EvidenceMap;

/// Run `cordance explain <rule>` against the evidence map at `target`.
///
/// # Errors
///
/// Returns `Err` if the evidence map does not exist, cannot be read, or
/// fails to parse.
pub fn run(rule: &str, target: &Utf8PathBuf) -> Result<()> {
    let map_path = target.join(".cordance/evidence-map.json");
    if !map_path.exists() {
        return Err(anyhow!(
            "No evidence map at {map_path}. Run 'cordance pack' first."
        ));
    }
    let raw = std::fs::read_to_string(&map_path)?;
    let map: EvidenceMap = serde_json::from_str(&raw)?;

    // Match by rule ID first, then by text substring.
    let matches: Vec<_> = map
        .rules
        .iter()
        .filter(|e| e.rule_id.contains(rule) || e.text.contains(rule))
        .collect();

    if matches.is_empty() {
        println!("No rule found matching '{rule}'");
        return Ok(());
    }

    for entry in matches {
        println!("Rule: {}", entry.rule_id);
        println!("Text: {}", entry.text);
        println!("Sources:");
        for source in &entry.sources {
            println!("  - {} ({})", source.path, source.kind);
        }
        println!();
    }
    Ok(())
}