use std::io::Write;
use anyhow::{Context, Result};
use codelore_lib::cli_api::Options;
use codelore_lib::cli_api::analyses::delta_health::RiskClass;
use codelore_lib::cli_api::facts::FactsDb;
use codelore_lib::output::sarif::{SARIF_SCHEMA_URL, TOOL_INFO_URI};
use crate::diff::DiffOutput;
const DELTA_HEALTH_MAX_ROWS: usize = 20;
const DIFF_EVIDENCE_N: u32 = 3;
fn risk_str(c: Option<RiskClass>) -> &'static str {
c.map_or("\u{2205}", RiskClass::as_str)
}
pub fn emit(
out: &mut dyn Write,
output: &DiffOutput,
format: &str,
repo_root: &std::path::Path,
db_opts: Option<(&FactsDb, &Options)>,
narrative: Option<(String, String)>,
) -> Result<()> {
match format {
"text" => emit_text(out, output, narrative),
"json" => emit_json(out, output),
"markdown" => emit_markdown(out, output, narrative),
"sarif" => emit_sarif(out, output, repo_root, db_opts),
other => {
anyhow::bail!("unknown --format {other:?} for diff; valid: text, json, markdown, sarif")
}
}
}
#[allow(clippy::too_many_lines)] fn emit_text(
out: &mut dyn Write,
output: &DiffOutput,
narrative: Option<(String, String)>,
) -> Result<()> {
writeln!(
out,
"CodeLore diff — {} {} {} ({} files changed)",
&output.base_sha[..8.min(output.base_sha.len())],
if output.merge_base_used {
"...→"
} else {
".."
},
&output.head_sha[..8.min(output.head_sha.len())],
output.hotspots.pr_touched_existing.len()
+ output.coupling_absences.len()
+ output.clones.pr_touched_existing.len(),
)?;
writeln!(out)?;
if !output.gate_violations.is_empty() {
writeln!(
out,
"✗ {} quality-gate VIOLATION(s) (from [diff] section)",
output.gate_violations.len()
)?;
for v in &output.gate_violations {
writeln!(
out,
" {} actual={} threshold={}",
v.gate, v.actual, v.threshold
)?;
}
writeln!(out)?;
}
if let Some(reason) = &output.gate_skip_reason {
writeln!(out, "⚠ [diff] quality gate SKIPPED — {reason}")?;
writeln!(out)?;
}
if let Some(dh) = &output.delta_health {
match dh.ratio {
Some(ratio) => writeln!(
out,
"Delta health: {ratio:.1}/100 — {} ({} added, {} modified, {} removed, {} files skipped)",
dh.verdict,
dh.counts.added,
dh.counts.modified,
dh.counts.removed,
dh.counts.skipped
)?,
None => writeln!(
out,
"Delta health: {} (no analyzable code changed)",
dh.verdict
)?,
}
for f in dh.functions.iter().take(DELTA_HEALTH_MAX_ROWS) {
writeln!(
out,
" [{}] {}::{} {} \u{2192} {}{}",
f.outcome.as_str(),
f.path,
f.function,
risk_str(f.before),
risk_str(f.after),
if f.in_red_file { " (red file)" } else { "" },
)?;
}
if dh.functions.len() > DELTA_HEALTH_MAX_ROWS {
writeln!(
out,
" \u{2026} and {} more",
dh.functions.len() - DELTA_HEALTH_MAX_ROWS
)?;
}
writeln!(out)?;
}
if !output.hotspots.rank_entrants.is_empty() {
writeln!(
out,
"▶ {} NEW hotspot(s) entered the top-N (rank-entrant)",
output.hotspots.rank_entrants.len()
)?;
for h in &output.hotspots.rank_entrants {
writeln!(
out,
" {:<60} score={:.4} revs={} cognitive={:.1}",
h.path, h.hotspot_score, h.revisions, h.cognitive
)?;
}
writeln!(out)?;
}
if !output.hotspots.score_increased.is_empty() {
writeln!(
out,
"▶ {} hotspot(s) got WORSE (score increased)",
output.hotspots.score_increased.len()
)?;
for s in &output.hotspots.score_increased {
writeln!(
out,
" {:<60} {:.4} → {:.4} (+{:.4})",
s.path, s.base_score, s.head_score, s.delta
)?;
}
writeln!(out)?;
}
if !output.coupling_absences.is_empty() {
writeln!(
out,
"▶ {} historically-coupled file(s) NOT modified together",
output.coupling_absences.len()
)?;
for a in &output.coupling_absences {
writeln!(out, " touched: {}", a.touched_file)?;
writeln!(
out,
" expected: {} (historical {:.0}% coupling over {} shared commits, p={:.4})",
a.expected_partner, a.historical_coupling, a.historical_shared_revs, a.fisher_p,
)?;
}
writeln!(out)?;
}
if !output.clones.new_families.is_empty() {
writeln!(
out,
"▶ {} NEW clone family member(s) introduced",
output.clones.new_families.len()
)?;
for c in &output.clones.new_families {
writeln!(
out,
" group={} {}:{}:{} ({} nodes)",
c.clone_group_id, c.entity, c.function, c.start_line, c.node_count
)?;
}
writeln!(out)?;
}
if !output.hotspots.pr_touched_existing.is_empty() {
writeln!(
out,
"ℹ {} PR-touched file(s) are already in the top-N hotspot list",
output.hotspots.pr_touched_existing.len()
)?;
for h in &output.hotspots.pr_touched_existing {
writeln!(out, " {:<60} score={:.4}", h.path, h.hotspot_score)?;
}
}
if let Some((text, stamp)) = narrative {
writeln!(out)?;
writeln!(out, "── LLM narrative (advisory) ──")?;
writeln!(out, "{text}")?;
writeln!(out, "{stamp}")?;
}
Ok(())
}
fn emit_json(out: &mut dyn Write, output: &DiffOutput) -> Result<()> {
serde_json::to_writer_pretty(out, output).context("diff json")?;
Ok(())
}
#[allow(clippy::too_many_lines)] fn emit_markdown(
out: &mut dyn Write,
output: &DiffOutput,
narrative: Option<(String, String)>,
) -> Result<()> {
writeln!(out, "# CodeLore PR Analysis")?;
writeln!(out)?;
writeln!(
out,
"**Range:** `{}` {} `{}`{}",
&output.base_sha[..8.min(output.base_sha.len())],
if output.merge_base_used {
"...→"
} else {
".."
},
&output.head_sha[..8.min(output.head_sha.len())],
if output.merge_base_used {
" (merge-base resolved)"
} else {
""
}
)?;
writeln!(out)?;
if !output.gate_violations.is_empty() {
writeln!(
out,
"## ❌ Quality-gate violations ({})",
output.gate_violations.len()
)?;
writeln!(out)?;
writeln!(out, "| Gate | Actual | Threshold |")?;
writeln!(out, "|---|---:|---:|")?;
for v in &output.gate_violations {
writeln!(
out,
"| `{}` | `{}` | `{}` |",
codelore_lib::cli_api::output::markdown::escape_md_cell(&v.gate),
codelore_lib::cli_api::output::markdown::escape_md_cell(&v.actual),
codelore_lib::cli_api::output::markdown::escape_md_cell(&v.threshold),
)?;
}
writeln!(out)?;
}
if let Some(reason) = &output.gate_skip_reason {
writeln!(out, "## ⚠️ Quality gate SKIPPED")?;
writeln!(out)?;
writeln!(
out,
"{}",
codelore_lib::cli_api::output::markdown::escape_md_cell(reason)
)?;
writeln!(out)?;
}
if let Some(dh) = &output.delta_health {
writeln!(out, "## Delta health")?;
writeln!(out)?;
match dh.ratio {
Some(ratio) => writeln!(out, "**{ratio:.1}/100 — {}**", dh.verdict)?,
None => writeln!(out, "**{}** — no analyzable code changed", dh.verdict)?,
}
writeln!(out)?;
if !dh.functions.is_empty() {
writeln!(out, "| Function | Before | After | Outcome | Reasons |")?;
writeln!(out, "|---|---|---|---|---|")?;
for f in dh.functions.iter().take(DELTA_HEALTH_MAX_ROWS) {
writeln!(
out,
"| `{}::{}`{} | {} | {} | {} | {} |",
f.path,
f.function,
if f.in_red_file { " \u{1F534}" } else { "" },
risk_str(f.before),
risk_str(f.after),
f.outcome.as_str(),
codelore_lib::cli_api::output::markdown::escape_md_cell(&f.reasons.join("; ")),
)?;
}
if dh.functions.len() > DELTA_HEALTH_MAX_ROWS {
writeln!(out)?;
writeln!(
out,
"\u{2026} and {} more",
dh.functions.len() - DELTA_HEALTH_MAX_ROWS
)?;
}
}
writeln!(out)?;
}
if !output.hotspots.rank_entrants.is_empty() {
writeln!(
out,
"## ⚠️ New hotspots ({})",
output.hotspots.rank_entrants.len()
)?;
writeln!(out)?;
writeln!(
out,
"Files newly entering the top-{} hotspot list:",
output.hotspots.rank_entrants.len()
)?;
writeln!(out)?;
writeln!(out, "| File | Score | Revisions | Cognitive |")?;
writeln!(out, "|---|---:|---:|---:|")?;
for h in &output.hotspots.rank_entrants {
writeln!(
out,
"| `{}` | {:.4} | {} | {:.1} |",
codelore_lib::cli_api::output::markdown::escape_md_cell(&h.path),
h.hotspot_score,
h.revisions,
h.cognitive
)?;
}
writeln!(out)?;
}
if !output.hotspots.score_increased.is_empty() {
writeln!(
out,
"## 📈 Hotspots worsened ({})",
output.hotspots.score_increased.len()
)?;
writeln!(out)?;
writeln!(out, "| File | Base score | Head score | Δ |")?;
writeln!(out, "|---|---:|---:|---:|")?;
for s in &output.hotspots.score_increased {
writeln!(
out,
"| `{}` | {:.4} | {:.4} | +{:.4} |",
codelore_lib::cli_api::output::markdown::escape_md_cell(&s.path),
s.base_score,
s.head_score,
s.delta
)?;
}
writeln!(out)?;
}
if !output.coupling_absences.is_empty() {
writeln!(
out,
"## 🔗 Missing co-changes ({})",
output.coupling_absences.len()
)?;
writeln!(out)?;
writeln!(
out,
"Files in this PR historically change with another file that's NOT in the PR:"
)?;
writeln!(out)?;
writeln!(
out,
"| Touched | Expected partner | Historical coupling | Shared revs | Fisher p |"
)?;
writeln!(out, "|---|---|---:|---:|---:|")?;
for a in &output.coupling_absences {
writeln!(
out,
"| `{}` | `{}` | {:.1}% | {} | {:.4} |",
codelore_lib::cli_api::output::markdown::escape_md_cell(&a.touched_file),
codelore_lib::cli_api::output::markdown::escape_md_cell(&a.expected_partner),
a.historical_coupling,
a.historical_shared_revs,
a.fisher_p,
)?;
}
writeln!(out)?;
}
if !output.clones.new_families.is_empty() {
writeln!(
out,
"## 🌱 New clones ({})",
output.clones.new_families.len()
)?;
writeln!(out)?;
writeln!(out, "| Group | Entity | Function | Lines | Nodes |")?;
writeln!(out, "|---|---|---|---|---:|")?;
for c in &output.clones.new_families {
writeln!(
out,
"| {} | `{}` | `{}` | {}-{} | {} |",
c.clone_group_id,
codelore_lib::cli_api::output::markdown::escape_md_cell(&c.entity),
codelore_lib::cli_api::output::markdown::escape_md_cell(&c.function),
c.start_line,
c.end_line,
c.node_count
)?;
}
writeln!(out)?;
}
let has_narrative = narrative.is_some();
if let Some((text, stamp)) = narrative {
writeln!(out, "## LLM narrative (advisory)")?;
writeln!(out)?;
writeln!(out, "{text}")?;
writeln!(out)?;
writeln!(out, "_{stamp}_")?;
writeln!(out)?;
}
if !has_narrative
&& output.hotspots.rank_entrants.is_empty()
&& output.hotspots.score_increased.is_empty()
&& output.coupling_absences.is_empty()
&& output.clones.new_families.is_empty()
{
writeln!(out, "✅ No new behavioral findings.")?;
writeln!(out)?;
}
Ok(())
}
#[allow(clippy::too_many_lines)] fn emit_sarif(
out: &mut dyn Write,
output: &DiffOutput,
repo_root: &std::path::Path,
db_opts: Option<(&FactsDb, &Options)>,
) -> Result<()> {
use codelore_lib::cli_api::quality_gates::evidence::evidence_for_path;
use serde_json::json;
let evidence_warned = std::cell::Cell::new(false);
let evidence_for = |path: &str| -> Option<(serde_json::Value, serde_json::Value)> {
let (db, opts) = db_opts?;
let commits = match evidence_for_path(db, opts, path, DIFF_EVIDENCE_N) {
Ok(commits) => commits,
Err(e) => {
if !evidence_warned.replace(true) {
eprintln!(
" ⚠ diff: evidence lookup failed ({e}); results will be emitted without commit chains"
);
}
return None;
}
};
if commits.is_empty() {
return None;
}
let related: Vec<serde_json::Value> = commits
.iter()
.map(|ev| {
let message = format!("{} {} {} (churn={})", ev.date, ev.rev, ev.author, ev.churn);
codelore_lib::cli_api::output::sarif::evidence_location(path, &message, true)
})
.collect();
let (code_flows, related_locations) =
codelore_lib::cli_api::output::sarif::evidence_attachments(related);
Some((code_flows, related_locations))
};
let repo_root: String = repo_root
.canonicalize()
.unwrap_or_else(|_| repo_root.to_path_buf())
.to_string_lossy()
.into_owned();
let add_fingerprints = |result: &mut serde_json::Value, rule: &str, path: &str, disc: &str| {
use codelore_lib::cli_api::output::sarif::{diff_finding_hash, primary_location_line_hash};
let Some(fps) = result
.as_object_mut()
.map(|obj| {
obj.entry("partialFingerprints")
.or_insert_with(|| json!({}))
})
.and_then(serde_json::Value::as_object_mut)
else {
return;
};
fps.insert(
"diffFinding/v1".into(),
json!(diff_finding_hash(rule, path, disc)),
);
fps.insert(
"primaryLocationLineHash".into(),
json!(primary_location_line_hash(&repo_root, path)),
);
};
let mut hotspot_results: Vec<serde_json::Value> = Vec::new();
for h in &output.hotspots.rank_entrants {
let evidence = evidence_for(&h.path);
let mut result = json!({
"ruleId": "CODELORE-HOTSPOT",
"level": "warning",
"message": {
"text": format!(
"New hotspot in PR: '{}' (score={:.3}, revisions={}, cognitive={:.1})",
h.path, h.hotspot_score, h.revisions, h.cognitive
)
},
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": h.path },
"region": { "startLine": 1 }
}
}],
"properties": {
"security-severity": ((100.0 - h.cognitive_health) / 10.0).clamp(0.0, 10.0),
"codelore/diff-classification": "rank-entrant",
"codelore/score": h.hotspot_score,
"tags": ["behavioral", "hotspot", "pr-diff"]
}
});
add_fingerprints(&mut result, "CODELORE-HOTSPOT", &h.path, "rank-entrant");
if let Some((code_flows, related_locs)) = evidence {
result["codeFlows"] = code_flows;
result["relatedLocations"] = related_locs;
}
hotspot_results.push(result);
}
for s in &output.hotspots.score_increased {
let evidence = evidence_for(&s.path);
let mut result = json!({
"ruleId": "CODELORE-HOTSPOT",
"level": "warning",
"message": {
"text": format!(
"Hotspot worsened in PR: '{}' (score {:.3} → {:.3}, Δ +{:.3})",
s.path, s.base_score, s.head_score, s.delta
)
},
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": s.path },
"region": { "startLine": 1 }
}
}],
"properties": {
"codelore/diff-classification": "score-increase",
"codelore/base-score": s.base_score,
"codelore/head-score": s.head_score,
"codelore/score-delta": s.delta,
"tags": ["behavioral", "hotspot", "pr-diff"]
}
});
add_fingerprints(&mut result, "CODELORE-HOTSPOT", &s.path, "score-increase");
if let Some((code_flows, related_locs)) = evidence {
result["codeFlows"] = code_flows;
result["relatedLocations"] = related_locs;
}
hotspot_results.push(result);
}
for c in &output.clones.new_families {
let evidence = evidence_for(&c.entity);
let mut result = json!({
"ruleId": "CODELORE-CLONE",
"level": "note",
"message": {
"text": format!(
"New clone introduced in PR: group {} '{}' ({} nodes)",
c.clone_group_id, c.function, c.node_count
)
},
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": c.entity },
"region": { "startLine": c.start_line, "endLine": c.end_line }
}
}],
"properties": {
"codelore/diff-classification": "new-clone-family",
"codelore/clone-group-id": c.clone_group_id,
"tags": ["behavioral", "clone", "pr-diff"]
}
});
add_fingerprints(&mut result, "CODELORE-CLONE", &c.entity, &c.fingerprint);
if let Some((code_flows, related_locs)) = evidence {
result["codeFlows"] = code_flows;
result["relatedLocations"] = related_locs;
}
hotspot_results.push(result);
}
if let Some(dh) = output.delta_health.as_ref() {
use codelore_lib::cli_api::analyses::delta_health::Outcome;
use std::collections::HashSet;
let mut seen: HashSet<&str> = HashSet::new();
for f in &dh.functions {
if f.outcome != Outcome::Bad {
continue;
}
if seen.contains(f.path.as_str()) {
continue;
}
seen.insert(&f.path);
let evidence = evidence_for(&f.path);
let mut result = json!({
"ruleId": "CODELORE-DELTA-HEALTH",
"level": "warning",
"message": {
"text": format!(
"Function health degraded in PR: '{}' in '{}' \
(risk {} → {})",
f.function, f.path,
f.before.map_or("none", RiskClass::as_str),
f.after.map_or("none", RiskClass::as_str),
)
},
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": f.path },
"region": { "startLine": 1 }
}
}],
"properties": {
"codelore/diff-classification": "delta-health-degraded",
"codelore/function": f.function.clone(),
"tags": ["behavioral", "health", "pr-diff"]
}
});
add_fingerprints(
&mut result,
"CODELORE-DELTA-HEALTH",
&f.path,
"delta-health-degraded",
);
if let Some((code_flows, related_locs)) = evidence {
result["codeFlows"] = code_flows;
result["relatedLocations"] = related_locs;
}
hotspot_results.push(result);
}
}
for a in &output.coupling_absences {
let (lo, hi) = if a.touched_file <= a.expected_partner {
(&a.touched_file, &a.expected_partner)
} else {
(&a.expected_partner, &a.touched_file)
};
let mut result = json!({
"ruleId": "CODELORE-MISSING-COCHANGE",
"level": "note",
"message": {
"text": format!(
"PR modifies '{}' but historically '{}' co-changes with it \
(shared_revs={}, degree={:.1}%, fisher_p={:.4}). \
Verify the absence is intentional.",
a.touched_file,
a.expected_partner,
a.historical_shared_revs,
a.historical_coupling,
a.fisher_p,
)
},
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": a.touched_file },
"region": { "startLine": 1 }
}
}],
"partialFingerprints": {
"couplingPair/v1": format!("{lo}::{hi}")
},
"properties": {
"codelore/diff-classification": "missing-cochange",
"codelore/missing-partner": a.expected_partner.clone(),
"codelore/historical-coupling-pct": a.historical_coupling,
"codelore/historical-shared-revs": a.historical_shared_revs,
"codelore/fisher-p": a.fisher_p,
"tags": ["behavioral", "coupling", "absent-change-pattern", "pr-diff"]
}
});
add_fingerprints(
&mut result,
"CODELORE-MISSING-COCHANGE",
&a.touched_file,
&format!("{lo}::{hi}"),
);
hotspot_results.push(result);
}
let mut rules = vec![
json!({
"id": "CODELORE-HOTSPOT",
"shortDescription": { "text": "Behavioral hotspot diff" },
"properties": { "tags": ["behavioral", "hotspot", "pr-diff"] }
}),
json!({
"id": "CODELORE-CLONE",
"shortDescription": { "text": "New clone family in PR" },
"properties": { "tags": ["behavioral", "clone", "pr-diff"] }
}),
json!({
"id": "CODELORE-MISSING-COCHANGE",
"name": "MissingCoChange",
"shortDescription": { "text": "Coupled file omitted from PR" },
"fullDescription": { "text": "A file historically and significantly coupled (Fisher-significant) with the file modified in this PR was NOT modified. The 'absent change pattern' often indicates a forgotten test, migration script, or documentation update." },
"defaultConfiguration": { "level": "note" },
"properties": { "tags": ["behavioral", "coupling", "absent-change-pattern", "pr-diff"] }
}),
];
if output.delta_health.is_some() {
rules.push(json!({
"id": "CODELORE-DELTA-HEALTH",
"shortDescription": { "text": "Function health degraded in PR" },
"properties": { "tags": ["behavioral", "health", "pr-diff"] }
}));
}
let doc = json!({
"$schema": SARIF_SCHEMA_URL,
"version": "2.1.0",
"runs": [{
"automationDetails": { "id": "codelore/diff/run" },
"tool": {
"driver": {
"name": "codelore",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": TOOL_INFO_URI,
"rules": rules
}
},
"results": hotspot_results
}]
});
serde_json::to_writer_pretty(out, &doc).context("diff sarif")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diff::DiffOutput;
use codelore_lib::cli_api::analyses::coupling::CouplingAbsence;
#[test]
fn emit_sarif_includes_missing_cochange_rule_and_results() {
let output = DiffOutput {
base_sha: "deadbeef".into(),
head_sha: "cafef00d".into(),
merge_base_used: false,
coupling_absences: vec![CouplingAbsence {
touched_file: "src/auth/login.rs".into(),
expected_partner: "src/auth/session.rs".into(),
historical_coupling: 87.5,
fisher_p: 0.001,
historical_shared_revs: 12,
}],
..DiffOutput::default()
};
let mut buf = Vec::new();
let repo_root = std::path::Path::new("/repo");
emit_sarif(&mut buf, &output, repo_root, None).expect("emit_sarif");
let sarif: serde_json::Value = serde_json::from_slice(&buf).expect("valid SARIF JSON");
let rules = sarif["runs"][0]["tool"]["driver"]["rules"]
.as_array()
.expect("rules array present");
assert!(
rules.iter().any(|r| r["id"] == "CODELORE-MISSING-COCHANGE"),
"CODELORE-MISSING-COCHANGE rule must be declared, got: {rules:?}"
);
let results = sarif["runs"][0]["results"]
.as_array()
.expect("results array present");
let absence_results: Vec<_> = results
.iter()
.filter(|r| r["ruleId"] == "CODELORE-MISSING-COCHANGE")
.collect();
assert_eq!(
absence_results.len(),
1,
"exactly one CODELORE-MISSING-COCHANGE result expected for one absence"
);
let r = absence_results[0];
let msg = r["message"]["text"].as_str().expect("message present");
assert!(msg.contains("src/auth/login.rs"));
assert!(msg.contains("src/auth/session.rs"));
assert_eq!(
r["locations"][0]["physicalLocation"]["artifactLocation"]["uri"],
"src/auth/login.rs"
);
let fp = r["partialFingerprints"]["couplingPair/v1"]
.as_str()
.expect("fp present");
assert_eq!(fp, "src/auth/login.rs::src/auth/session.rs");
let diff_fp = r["partialFingerprints"]["diffFinding/v1"]
.as_str()
.expect("diffFinding/v1 present");
assert_eq!(
diff_fp,
codelore_lib::cli_api::output::sarif::diff_finding_hash(
"CODELORE-MISSING-COCHANGE",
"src/auth/login.rs",
"src/auth/login.rs::src/auth/session.rs",
)
);
let primary_fp = r["partialFingerprints"]["primaryLocationLineHash"]
.as_str()
.expect("primaryLocationLineHash present");
assert_eq!(
primary_fp,
codelore_lib::cli_api::output::sarif::primary_location_line_hash(
"/repo",
"src/auth/login.rs",
)
);
assert_eq!(
r["properties"]["codelore/diff-classification"],
"missing-cochange"
);
assert_eq!(
r["properties"]["codelore/missing-partner"],
"src/auth/session.rs"
);
assert_eq!(r["properties"]["codelore/historical-shared-revs"], 12);
}
#[test]
fn emit_sarif_zero_absences_zero_results() {
let output = DiffOutput::default();
let mut buf = Vec::new();
emit_sarif(&mut buf, &output, std::path::Path::new("/repo"), None).expect("emit_sarif");
let sarif: serde_json::Value = serde_json::from_slice(&buf).unwrap();
let results = sarif["runs"][0]["results"].as_array().unwrap();
assert_eq!(
results
.iter()
.filter(|r| r["ruleId"] == "CODELORE-MISSING-COCHANGE")
.count(),
0
);
}
}