cordance-cli 0.1.2

Cordance CLI โ€” installs the `cordance` binary. The umbrella package `cordance` re-exports this entry; either install command works.
Documentation
//! Doctrine tier: `cordance_doctrine_topics`, `cordance_doctrine_lookup`.
//!
//! **No file body content is ever returned** โ€” only topic names and resolved
//! paths. The adversarial review (ยง4, F1) treats relayed doctrine prose as
//! an injection vector; the safe surface is the index, not the markdown.

use camino::Utf8PathBuf;
use cordance_doctrine::DoctrineEntry;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

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

#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
pub struct TopicsParams {
    #[serde(default)]
    pub target: Option<String>,
}

#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct TopicsOutput {
    pub schema: String,
    pub repo: String,
    pub commit: Option<String>,
    pub principles: Vec<TopicSummary>,
    pub patterns: Vec<TopicSummary>,
    pub checklists: Vec<TopicSummary>,
    pub tooling: Vec<TopicSummary>,
}

#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct TopicSummary {
    pub topic: String,
    #[schemars(with = "String")]
    pub path: Utf8PathBuf,
    pub sha256: String,
    pub bytes: u64,
}

#[derive(Clone, Debug, Deserialize, JsonSchema)]
pub struct LookupParams {
    #[serde(default)]
    pub target: Option<String>,
    /// Topic name (filename stem) or substring of one.
    pub topic: String,
}

#[derive(Clone, Debug, Serialize, JsonSchema)]
pub struct LookupOutput {
    pub schema: String,
    pub repo: String,
    pub commit: Option<String>,
    pub matches: Vec<TopicSummary>,
}

// Returning Result is intentional for symmetry with the other tool entry
// points; doctrine loading currently never fails (it falls back to an empty
// index) but future paths may surface here.
#[allow(clippy::unnecessary_wraps)]
pub fn topics(target: &Utf8PathBuf, cfg: &Config) -> McpToolResult<TopicsOutput> {
    let root = cfg.doctrine_root(target);
    let idx = cordance_doctrine::load_doctrine_or_default(&root);
    Ok(TopicsOutput {
        schema: "cordance-doctrine-topics.v1".to_string(),
        repo: idx.repo,
        commit: idx.commit,
        principles: idx.principles.iter().map(summarise).collect(),
        patterns: idx.patterns.iter().map(summarise).collect(),
        checklists: idx.checklists.iter().map(summarise).collect(),
        tooling: idx.tooling.iter().map(summarise).collect(),
    })
}

pub fn lookup(target: &Utf8PathBuf, cfg: &Config, topic: &str) -> McpToolResult<LookupOutput> {
    if topic.is_empty() {
        return Err(McpToolError::InvalidArgument(
            "topic must not be empty".into(),
        ));
    }
    let root = cfg.doctrine_root(target);
    let idx = cordance_doctrine::load_doctrine_or_default(&root);
    let matches: Vec<TopicSummary> = idx
        .principles
        .iter()
        .chain(idx.patterns.iter())
        .chain(idx.checklists.iter())
        .chain(idx.tooling.iter())
        .filter(|e| e.topic.contains(topic) || topic.contains(&e.topic))
        .map(summarise)
        .collect();
    Ok(LookupOutput {
        schema: "cordance-doctrine-lookup.v1".to_string(),
        repo: idx.repo,
        commit: idx.commit,
        matches,
    })
}

fn summarise(e: &DoctrineEntry) -> TopicSummary {
    TopicSummary {
        topic: e.topic.clone(),
        path: e.path.clone(),
        sha256: e.sha256.clone(),
        bytes: e.bytes,
    }
}