cordance-cli 0.1.2

Cordance CLI — installs the `cordance` binary. The umbrella package `cordance` re-exports this entry; either install command works.
Documentation
//! Evidence tier: `cordance_evidence_lookup`.
//!
//! Filters the evidence map by `rule_id` (substring). The map is computed
//! deterministically from the current pack, so the result is reproducible
//! across calls when no input has changed.

use camino::Utf8PathBuf;
use cordance_core::evidence::{EvidenceEntry, EvidenceMap, EvidenceSource};
use cordance_emit::TargetEmitter;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::config::Config;
use crate::mcp::error::{McpToolError, McpToolResult};

#[derive(Clone, Debug, Deserialize, JsonSchema)]
pub struct EvidenceLookupParams {
    #[serde(default)]
    pub target: Option<String>,
    /// Substring matched against `EvidenceEntry::rule_id` (case-sensitive).
    pub rule_id: String,
}

#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct EvidenceLookupOutput {
    pub schema: String,
    pub matches: Vec<EvidenceEntrySummary>,
}

#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct EvidenceEntrySummary {
    pub rule_id: String,
    pub text: String,
    pub sources: Vec<EvidenceSourceSummary>,
}

#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct EvidenceSourceSummary {
    #[schemars(with = "String")]
    pub path: Utf8PathBuf,
    pub kind: String,
    pub line_range: Option<(u32, u32)>,
}

pub fn lookup(
    target: &Utf8PathBuf,
    cfg: &Config,
    rule_id: &str,
) -> McpToolResult<EvidenceLookupOutput> {
    if rule_id.is_empty() {
        return Err(McpToolError::InvalidArgument(
            "rule_id must not be empty".into(),
        ));
    }
    let mut pack = super::pack::build_pack(target, cfg)?;

    // The evidence map emitter consumes `pack.advise.findings`, so make sure
    // the report is populated before rendering. `build_pack` already runs
    // advise but the emitter reads `pack.advise` so let's double-check.
    if pack.advise.findings.is_empty() {
        if let Ok(report) = cordance_advise::run_all(&pack) {
            pack.advise = report;
        }
    }

    let emitter = cordance_emit::evidence_map::EvidenceMapEmitter;
    let rendered = emitter
        .render(&pack)
        .map_err(|e| McpToolError::internal_redacted("evidence map render", e))?;
    let (_, bytes) = rendered
        .first()
        .ok_or_else(|| McpToolError::Internal("evidence map emitter produced no output".into()))?
        .clone();
    let map: EvidenceMap = serde_json::from_slice(&bytes)
        .map_err(|e| McpToolError::internal_redacted("evidence map parse", e))?;
    let matches: Vec<EvidenceEntrySummary> = map
        .rules
        .iter()
        .filter(|e| e.rule_id.contains(rule_id))
        .map(summarise_entry)
        .collect();
    Ok(EvidenceLookupOutput {
        schema: "cordance-evidence-lookup.v1".to_string(),
        matches,
    })
}

fn summarise_entry(e: &EvidenceEntry) -> EvidenceEntrySummary {
    EvidenceEntrySummary {
        rule_id: e.rule_id.clone(),
        text: e.text.clone(),
        sources: e.sources.iter().map(summarise_source).collect(),
    }
}

fn summarise_source(s: &EvidenceSource) -> EvidenceSourceSummary {
    EvidenceSourceSummary {
        path: s.path.clone(),
        kind: s.kind.clone(),
        line_range: s.line_range,
    }
}