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();
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);
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"));
}
}