cordance-emit 0.1.1

Cordance target emitters: AGENTS.md, CLAUDE.md, .cursor/rules, .codex, axiom harness-target.
Documentation
//! Evidence map emitter. Writes `.cordance/evidence-map.json` — the
//! deterministic map from rule/output ID to the doctrine, ADR, schema, or
//! scan anchor that produced it.
//!
//! This emitter is registered last in the pack pipeline so it can observe
//! the outputs collected by every other emitter.

use camino::Utf8PathBuf;
use cordance_core::evidence::{EvidenceEntry, EvidenceMap, EvidenceSource, OutputEvidence};
use cordance_core::pack::CordancePack;

use crate::{EmitError, TargetEmitter};

pub struct EvidenceMapEmitter;

impl TargetEmitter for EvidenceMapEmitter {
    fn name(&self) -> &'static str {
        "cordance-evidence-map"
    }

    fn render(&self, pack: &CordancePack) -> Result<Vec<(Utf8PathBuf, Vec<u8>)>, EmitError> {
        let mut rules: Vec<EvidenceEntry> = pack
            .advise
            .findings
            .iter()
            .map(|f| EvidenceEntry {
                rule_id: f.id.clone(),
                text: f.summary.clone(),
                sources: vec![EvidenceSource {
                    path: f.doctrine_anchor.clone(),
                    line_range: None,
                    kind: "doctrine".into(),
                }],
            })
            .collect();

        // Also record doctrine pin sources so `cordance explain doctrine-pin:...`
        // can show the pinned commit and source file.
        for pin in &pack.doctrine_pins {
            rules.push(EvidenceEntry {
                rule_id: format!("doctrine-pin:{}", pin.repo),
                text: format!("Doctrine pin: {} @ {}", pin.repo, pin.commit),
                sources: vec![EvidenceSource {
                    path: pin.source_path.clone(),
                    line_range: None,
                    kind: "doctrine".into(),
                }],
            });
        }

        let outputs: Vec<OutputEvidence> = pack
            .outputs
            .iter()
            .map(|o| OutputEvidence {
                path: o.path.clone(),
                sha256: o.sha256.clone(),
                source_ids: o.source_anchors.clone(),
            })
            .collect();

        let map = EvidenceMap {
            schema: cordance_core::schema::CORDANCE_EVIDENCE_MAP_V1.into(),
            pack_id: pack.source_lock.pack_id.clone(),
            rules,
            outputs,
        };

        let bytes = serde_json::to_vec_pretty(&map)?;
        Ok(vec![(".cordance/evidence-map.json".into(), bytes)])
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use cordance_core::advise::{AdviseFinding, AdviseReport, Severity};
    use cordance_core::lock::SourceLock;
    use cordance_core::pack::{DoctrinePin, PackTargets, ProjectIdentity};

    fn fixture_pack() -> CordancePack {
        CordancePack {
            schema: cordance_core::schema::CORDANCE_PACK_V1.into(),
            project: ProjectIdentity {
                name: "fixture".into(),
                repo_root: ".".into(),
                kind: "rust-workspace".into(),
                host_os: "linux".into(),
                axiom_pin: None,
            },
            sources: vec![],
            doctrine_pins: vec![DoctrinePin {
                repo: "0ryant/engineering-doctrine".into(),
                commit: "deadbeef".into(),
                source_path: "../engineering-doctrine/doctrine/SEMANTIC_INDEX.md".into(),
            }],
            targets: PackTargets::default(),
            outputs: vec![],
            source_lock: SourceLock::empty(),
            advise: AdviseReport {
                schema: cordance_core::schema::CORDANCE_ADVISE_REPORT_V1.into(),
                findings: vec![AdviseFinding {
                    id: "R-build-1".into(),
                    severity: Severity::Warning,
                    summary: "No local task runner detected.".into(),
                    doctrine_anchor: "doctrine/principles/build.md".into(),
                    project_paths: vec![],
                    remediation: "Add a Justfile or Makefile.".into(),
                }],
            },
            residual_risk: vec![],
        }
    }

    #[test]
    fn emitter_writes_to_expected_path() {
        let pack = fixture_pack();
        let out = EvidenceMapEmitter.render(&pack).expect("render");
        assert_eq!(out.len(), 1);
        assert_eq!(out[0].0.as_str(), ".cordance/evidence-map.json");
    }

    #[test]
    fn emitter_carries_advise_findings_and_doctrine_pins() {
        let pack = fixture_pack();
        let out = EvidenceMapEmitter.render(&pack).expect("render");
        let body = std::str::from_utf8(&out[0].1).expect("utf-8");
        let map: EvidenceMap = serde_json::from_str(body).expect("de");
        assert_eq!(map.schema, cordance_core::schema::CORDANCE_EVIDENCE_MAP_V1);
        // 1 advise finding + 1 doctrine pin
        assert_eq!(map.rules.len(), 2);
        assert!(map.rules.iter().any(|r| r.rule_id == "R-build-1"));
        assert!(map
            .rules
            .iter()
            .any(|r| r.rule_id == "doctrine-pin:0ryant/engineering-doctrine"));
    }
}