use anyhow::{Context, Result};
use crate::args::{self, CheckFormat};
use crate::{
CORPUS_PERCENTILE_SKIP_REASON, new_code_skip_reason, notice_corpus_lens_absent,
vacuous_pass_notice, write_github_output,
};
#[allow(clippy::too_many_lines)]
pub(crate) fn run_check_cmd(args: &args::CheckArgs) -> Result<()> {
use codelore_lib::cli_api::Options;
use codelore_lib::cli_api::cache::default_cache_root;
use codelore_lib::cli_api::facts::FactsDb;
use codelore_lib::cli_api::quality_gates::Thresholds;
use codelore_lib::cli_api::quality_gates::ledger::{
GateRunRecord, append_gate_runs, format_history, now_utc_ts, read_gate_runs,
};
use codelore_lib::cli_api::quality_gates::ratchet::{
RatchetMetrics, RatchetOutcome, evaluate_ratchet, format_ratchet_outcome, read_snapshot,
snapshot_from_metrics, write_snapshot,
};
use codelore_lib::cli_api::repo::{GixRepo, Repo as _};
let cache_root = args.cache_dir.clone().unwrap_or_else(default_cache_root);
if args.history {
use std::io::Write as _;
let records = read_gate_runs(&cache_root, &args.repo).context("read gate-run ledger")?;
let mut out = std::io::stdout().lock();
write!(out, "{}", format_history(&records, 20)).context("write gate-run history")?;
return Ok(());
}
let thresholds = if let Some(path) = &args.thresholds_file {
Thresholds::from_path(path).context("load thresholds file")?
} else {
Thresholds::discover(&args.repo).context("discover thresholds file")?
};
if thresholds.is_empty() && !args.ratchet {
if !args.quiet {
eprintln!("{}", vacuous_pass_notice("check"));
}
write_github_output("result", "pass");
write_github_output("violations", "0");
if matches!(args.format, CheckFormat::Sarif) {
let repo = GixRepo::open(&args.repo).context("open repo")?;
let head_sha = repo.head_sha().context("get HEAD sha")?;
emit_check_sarif(
&args.repo,
&head_sha,
&[],
&std::collections::HashMap::new(),
)?;
}
return Ok(());
}
let resolved_defect_calibration = args.defect_calibration.clone().or_else(|| {
thresholds.calibration.defect_artifact.clone().map(|p| {
if p.is_absolute() {
p
} else {
args.repo.join(p)
}
})
});
let opts = Options {
repo_path: args.repo.clone(),
calibration: args.calibration.clone(),
defect_calibration: resolved_defect_calibration,
allow_foreign_calibration: args.allow_foreign_calibration,
temp_dir: args.temp_dir.clone(),
..Options::default()
};
opts.validate().context("validate options")?;
let repo = GixRepo::open(&args.repo).context("open repo")?;
let head_sha = repo.head_sha().context("get HEAD sha")?;
let db =
FactsDb::open_or_ingest_with_cache_root(&opts, &repo, &cache_root).context("ingest")?;
db.ensure_ingest_witnessed(&head_sha)?;
let shallow_checkout = repo.is_shallow();
if shallow_checkout {
let warning = "⚠ codelore check: shallow checkout detected (.git/shallow present) — \
history is truncated by fetch-depth, so the behavioral gates (hotspots, \
effort-exposure, new-code) evaluate only partial history. Re-run against full \
history (fetch-depth: 0) for an authoritative verdict.";
if matches!(args.format, CheckFormat::Sarif) {
eprintln!("{warning}");
} else {
println!("{warning}");
}
}
let ts = now_utc_ts();
let external_store = if thresholds.gates.max_findings_in_hot_files.is_some() {
codelore_lib::cli_api::external::ExternalStore::open_nonempty(&cache_root, &args.repo)
.context("open external store")?
} else {
None
};
let (mut violations, mut ledger_records, hotspot_count, code_health) = evaluate_all_gates(
&thresholds,
&db,
&repo,
&opts,
&head_sha,
&ts,
external_store.as_ref(),
)
.context("evaluate gates")?;
if !args.quiet {
emit_gate_notices(&ledger_records, shallow_checkout);
}
notice_corpus_lens_absent(&opts, args.quiet);
if args.ratchet {
let worst_health = if thresholds.gates.code_health_min.is_some() {
code_health
.iter()
.map(|r| r.score)
.fold(f64::INFINITY, f64::min)
} else {
f64::INFINITY
};
let red_effort_pct = ledger_records
.iter()
.find(|r| r.gate == "max_red_effort_pct")
.map(|r| r.value);
let dep_cycles = ledger_records
.iter()
.find(|r| r.gate == "max_dependency_cycles")
.map(|r| r.value);
let metrics = RatchetMetrics {
code_health_min_observed: if worst_health.is_infinite() {
None
} else {
Some(worst_health)
},
red_effort_pct_observed: red_effort_pct,
dependency_cycles_observed: dep_cycles,
};
match read_snapshot(&args.repo).context("read ratchet snapshot")? {
None => {
let snap = snapshot_from_metrics(&metrics);
write_snapshot(&args.repo, &snap).context("write ratchet snapshot")?;
let tracked: Vec<&str> = [
metrics
.code_health_min_observed
.map(|_| "code_health_min_observed"),
metrics
.red_effort_pct_observed
.map(|_| "red_effort_pct_observed"),
metrics
.dependency_cycles_observed
.map(|_| "dependency_cycles_observed"),
]
.into_iter()
.flatten()
.collect();
emit_ratchet_message(
args,
&format!(
"✅ ratchet initialized — tracking {} metric(s): {}. \
Configure max_red_effort_pct / max_dependency_cycles gates to ratchet \
effort and cycles. Commit `.codelore-ratchet.toml` to enable regression detection.\n",
tracked.len(),
if tracked.is_empty() {
"(none)".to_owned()
} else {
tracked.join(", ")
},
),
);
ledger_records.push(GateRunRecord {
ts: ts.clone(),
head_sha: head_sha.clone(),
gate: "ratchet".into(),
threshold: 0.0,
value: 0.0,
verdict: "initialized".into(),
mode: "ratchet".into(),
});
append_gate_runs(&cache_root, &args.repo, &ledger_records);
emit_check_sarif_when_requested(args, &db, &opts, &head_sha, &violations)?;
return Ok(());
}
Some(snap) => {
let outcome = evaluate_ratchet(&snap, &metrics);
emit_ratchet_message(args, &format_ratchet_outcome(&outcome));
let (verdict, ratchet_failed) = match &outcome {
RatchetOutcome::Improved { .. } => ("improved", false),
RatchetOutcome::Regressed { .. } => ("regressed", true),
};
ledger_records.push(GateRunRecord {
ts: ts.clone(),
head_sha: head_sha.clone(),
gate: "ratchet".into(),
threshold: 0.0,
value: 0.0,
verdict: verdict.into(),
mode: "ratchet".into(),
});
append_gate_runs(&cache_root, &args.repo, &ledger_records);
emit_check_sarif_when_requested(args, &db, &opts, &head_sha, &violations)?;
if ratchet_failed {
anyhow::bail!("ratchet: regression detected — see above");
}
let tightened = snapshot_from_metrics(&metrics);
write_snapshot(&args.repo, &tightened).context("tighten ratchet snapshot")?;
return Ok(());
}
}
}
append_gate_runs(&cache_root, &args.repo, &ledger_records);
violations.extend(crate::skipped_gate_violations(
&ledger_records,
thresholds.gates.fail_on_skipped,
));
emit_check_sarif_when_requested(args, &db, &opts, &head_sha, &violations)?;
let degraded_count = ledger_records
.iter()
.filter(|r| r.verdict == "degraded")
.count();
if violations.is_empty() {
if degraded_count > 0 {
let warning = format!(
"⚠ codelore check: WARNING — {degraded_count} gate(s) degraded (non-degraded gates pass)"
);
if matches!(args.format, CheckFormat::Sarif) {
eprintln!("{warning}");
} else {
println!("{warning}");
}
} else if matches!(args.format, CheckFormat::Text) {
println!("✅ codelore check: PASS ({hotspot_count} files evaluated)");
}
write_github_output("result", "pass");
write_github_output("violations", "0");
Ok(())
} else {
eprintln!(
"❌ codelore check: FAIL — {} violation(s)",
violations.len()
);
if !args.quiet && matches!(args.format, CheckFormat::Text) {
for v in &violations {
eprintln!(
" - {gate}: {path} — actual {actual} vs threshold {threshold}",
gate = v.gate,
path = v.path,
actual = v.actual,
threshold = v.threshold,
);
}
}
write_github_output("result", "fail");
write_github_output("violations", &violations.len().to_string());
if std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true")
&& matches!(args.format, CheckFormat::Text)
{
let mut stdout = std::io::stdout();
codelore_lib::cli_api::output::gha::write_gate_violations_gha(&violations, &mut stdout)
.context("emit gate annotations")?;
}
anyhow::bail!("{} gate violation(s) — see above", violations.len());
}
}
fn emit_gate_notices(
ledger_records: &[codelore_lib::cli_api::quality_gates::ledger::GateRunRecord],
shallow_checkout: bool,
) {
for r in ledger_records {
match (r.gate.as_str(), r.verdict.as_str()) {
("max_findings_in_hot_files", "skipped") => eprintln!(
" ⚠ max_findings_in_hot_files: skipped — run `codelore ingest-sarif` first"
),
("corpus_percentile_max", "skipped") => {
eprintln!(" ⚠ corpus_percentile_max: skipped — {CORPUS_PERCENTILE_SKIP_REASON}");
}
("hotspot_anchored_max", "skipped") => eprintln!(
" ⚠ hotspot_anchored_max: skipped — no anchored hotspot data (no calibration artifact active, or no analyzed file's language is covered by the corpus)"
),
("code_health_min", "degraded") => eprintln!(
" ⚠ code_health_min: degraded — health scan returned no rows on a non-empty repo"
),
("new_code", "skipped") => eprintln!(
" ⚠ new_code: skipped — {}",
new_code_skip_reason(r.threshold, shallow_checkout)
),
_ => {}
}
}
}
type GateGroupResult = (
Vec<codelore_lib::cli_api::quality_gates::GateViolation>,
Vec<codelore_lib::cli_api::quality_gates::ledger::GateRunRecord>,
);
fn make_rec(
gate: &str,
threshold: f64,
value: f64,
failed: bool,
ts: &str,
head_sha: &str,
) -> codelore_lib::cli_api::quality_gates::ledger::GateRunRecord {
use codelore_lib::cli_api::quality_gates::ledger::GateRunRecord;
GateRunRecord {
ts: ts.to_owned(),
head_sha: head_sha.to_owned(),
gate: gate.to_owned(),
threshold,
value,
verdict: if failed { "failed" } else { "passed" }.to_owned(),
mode: "check".to_owned(),
}
}
fn eval_hotspot_gates(
thresholds: &codelore_lib::cli_api::quality_gates::Thresholds,
db: &codelore_lib::cli_api::facts::FactsDb,
opts: &codelore_lib::cli_api::Options,
ts: &str,
head_sha: &str,
) -> Result<(
GateGroupResult,
Vec<codelore_lib::cli_api::analyses::hotspots::HotspotRow>,
)> {
use codelore_lib::cli_api::analyses::hotspots::run_hotspots_anchored;
use codelore_lib::cli_api::quality_gates::evaluate_full_tree;
let hotspots = run_hotspots_anchored(db, &opts.with_no_row_limit()).context("run hotspots")?;
let hs_violations = evaluate_full_tree(thresholds, &hotspots);
let g = &thresholds.gates;
let mut recs = Vec::new();
if let Some(max) = g.cognitive_max {
let failed = hs_violations.iter().any(|v| v.gate == "cognitive_max");
let value = hotspots
.iter()
.map(|r| r.cognitive)
.fold(f64::NAN, f64::max);
recs.push(make_rec(
"cognitive_max",
max,
if value.is_nan() { 0.0 } else { value },
failed,
ts,
head_sha,
));
}
if let Some(max) = g.hotspot_score_max {
let failed = hs_violations.iter().any(|v| v.gate == "hotspot_score_max");
let value = hotspots
.iter()
.map(|r| r.hotspot_score)
.fold(f64::NAN, f64::max);
recs.push(make_rec(
"hotspot_score_max",
max,
if value.is_nan() { 0.0 } else { value },
failed,
ts,
head_sha,
));
}
Ok(((hs_violations, recs), hotspots))
}
fn eval_code_health_gate(
thresholds: &codelore_lib::cli_api::quality_gates::Thresholds,
db: &codelore_lib::cli_api::facts::FactsDb,
repo: &impl codelore_lib::cli_api::repo::Repo,
opts: &codelore_lib::cli_api::Options,
ts: &str,
head_sha: &str,
) -> Result<(
GateGroupResult,
Vec<codelore_lib::cli_api::analyses::code_health::CodeHealthRow>,
)> {
use codelore_lib::cli_api::quality_gates::ledger::GateRunRecord;
use codelore_lib::cli_api::quality_gates::{GateViolation, evaluate_code_health_gate};
let code_health = codelore_lib::cli_api::analyses::code_health::run_code_health(
db,
&opts.with_no_row_limit(),
)
.context("run code-health")?;
let g = &thresholds.gates;
let Some(min) = g.code_health_min else {
return Ok(((Vec::new(), Vec::new()), code_health));
};
let ch_violations = evaluate_code_health_gate(thresholds, &code_health);
let degraded = code_health.is_empty()
&& codelore_lib::cli_api::quality_gates::head_has_scorable_source(repo, opts);
let worst = code_health
.iter()
.map(|r| r.score)
.fold(f64::INFINITY, f64::min);
let verdict = if degraded {
"degraded"
} else if ch_violations.is_empty() {
"passed"
} else {
"failed"
};
let rec = GateRunRecord {
ts: ts.to_owned(),
head_sha: head_sha.to_owned(),
gate: "code_health_min".into(),
threshold: min,
value: if worst.is_infinite() { 0.0 } else { worst },
verdict: verdict.to_owned(),
mode: "check".into(),
};
let mut violations = Vec::new();
if degraded && g.fail_on_degraded {
violations.push(GateViolation {
gate: "code_health_min".into(),
path: "(degraded)".into(),
actual: "no-data".into(),
threshold: format!("{min:.1}"),
});
} else {
violations.extend(ch_violations);
}
Ok(((violations, vec![rec]), code_health))
}
fn eval_arch_gates(
thresholds: &codelore_lib::cli_api::quality_gates::Thresholds,
db: &codelore_lib::cli_api::facts::FactsDb,
ts: &str,
head_sha: &str,
) -> Result<GateGroupResult> {
let (arch_v, measured) =
codelore_lib::cli_api::quality_gates::evaluate_architecture_gate_measured(thresholds, db)
.context("evaluate architecture gate")?;
let g = &thresholds.gates;
let mut recs = Vec::new();
if let (Some(max), Some(m)) = (g.max_dependency_cycles, measured) {
let failed = arch_v.iter().any(|v| v.gate == "max_dependency_cycles");
recs.push(make_rec(
"max_dependency_cycles",
f64::from(max),
f64::from(m.cycle_count),
failed,
ts,
head_sha,
));
}
if let (Some(max), Some(m)) = (g.max_propagation_cost, measured) {
let failed = arch_v.iter().any(|v| v.gate == "max_propagation_cost");
recs.push(make_rec(
"max_propagation_cost",
max,
m.propagation_cost,
failed,
ts,
head_sha,
));
}
Ok((arch_v, recs))
}
#[allow(clippy::type_complexity, clippy::too_many_lines)]
fn evaluate_all_gates(
thresholds: &codelore_lib::cli_api::quality_gates::Thresholds,
db: &codelore_lib::cli_api::facts::FactsDb,
repo: &impl codelore_lib::cli_api::repo::Repo,
opts: &codelore_lib::cli_api::Options,
head_sha: &str,
ts: &str,
external_store: Option<&codelore_lib::cli_api::external::ExternalStore>,
) -> Result<(
Vec<codelore_lib::cli_api::quality_gates::GateViolation>,
Vec<codelore_lib::cli_api::quality_gates::ledger::GateRunRecord>,
usize,
Vec<codelore_lib::cli_api::analyses::code_health::CodeHealthRow>,
)> {
use codelore_lib::cli_api::quality_gates::ledger::GateRunRecord;
let mut violations = Vec::new();
let mut recs = Vec::new();
let g = &thresholds.gates;
let ((hs_v, hs_r), hotspot_rows) = eval_hotspot_gates(thresholds, db, opts, ts, head_sha)?;
let hotspot_count = hotspot_rows.len();
violations.extend(hs_v);
recs.extend(hs_r);
let ((ch_v, ch_r), code_health) =
eval_code_health_gate(thresholds, db, repo, opts, ts, head_sha)?;
violations.extend(ch_v);
recs.extend(ch_r);
if g.disallow_clone_type_1 {
let clone_v = codelore_lib::cli_api::quality_gates::evaluate_clone_gate(thresholds, db)
.context("evaluate clone gate")?;
let count = clone_v
.first()
.and_then(|v| v.actual.parse::<f64>().ok())
.unwrap_or(0.0);
recs.push(make_rec(
"disallow_clone_type_1",
0.0,
count,
!clone_v.is_empty(),
ts,
head_sha,
));
violations.extend(clone_v);
}
let (arch_v, arch_r) = eval_arch_gates(thresholds, db, ts, head_sha)?;
violations.extend(arch_v);
recs.extend(arch_r);
if let Some(max) = g.max_red_effort_pct {
use codelore_lib::cli_api::analyses::effort_exposure;
let exempt = g.red_effort_exempt_improving;
let no_limit = opts.with_no_row_limit();
let rows = if exempt {
effort_exposure::run_effort_exposure_decomposed(db, repo, &no_limit, &code_health)
} else {
effort_exposure::run_effort_exposure_with_health(db, &no_limit, &code_health)
}
.context("run effort-exposure for gate")?;
let red = rows.iter().find(|r| r.band == "red");
let value = if exempt {
red.and_then(|r| r.churn_share_degrading_pct)
.or_else(|| red.map(|r| r.churn_share_pct))
.unwrap_or(0.0)
} else {
red.map_or(0.0, |r| r.churn_share_pct)
};
let effort_v = codelore_lib::cli_api::quality_gates::evaluate_effort_exposure_rows_exempt(
max, exempt, &rows,
);
recs.push(make_rec(
"max_red_effort_pct",
max,
value,
!effort_v.is_empty(),
ts,
head_sha,
));
violations.extend(effort_v);
}
if let Some(nc) = &thresholds.new_code {
use codelore_lib::cli_api::analyses::new_code;
let scope = new_code::run_new_code_scope(db, repo, opts, nc.window_days, &code_health)
.context("run new-code scope for gate")?;
if scope.window_start_present {
let nc_v = codelore_lib::cli_api::quality_gates::evaluate_new_code_rows(nc, &scope);
if let Some(floor) = nc.born_health_min {
let worst = scope
.born
.iter()
.map(|(_, s)| *s)
.fold(f64::INFINITY, f64::min);
recs.push(make_rec(
"born_health_min",
floor,
if worst.is_finite() { worst } else { 0.0 },
nc_v.iter().any(|v| v.gate == "born_health_min"),
ts,
head_sha,
));
}
if nc.touched_no_degradation {
let worst = scope
.touched
.iter()
.map(|(_, n)| *n)
.fold(f64::INFINITY, f64::min);
recs.push(make_rec(
"touched_no_degradation",
0.0,
if worst.is_finite() { worst } else { 0.0 },
nc_v.iter().any(|v| v.gate == "touched_no_degradation"),
ts,
head_sha,
));
}
violations.extend(nc_v);
} else {
recs.push(GateRunRecord {
ts: ts.to_owned(),
head_sha: head_sha.to_owned(),
gate: "new_code".into(),
threshold: f64::from(nc.window_days),
value: 0.0,
verdict: "skipped".into(),
mode: "check".into(),
});
}
}
if let Some(min) = g.code_familiarity_min {
let rows =
codelore_lib::cli_api::analyses::code_familiarity::run_code_familiarity(db, opts)
.context("run code-familiarity for gate")?;
let value = rows.first().map_or(0.0, |r| r.familiarity_pct);
let fam_v = codelore_lib::cli_api::quality_gates::evaluate_familiarity_rows(min, &rows);
recs.push(make_rec(
"code_familiarity_min",
min,
value,
!fam_v.is_empty(),
ts,
head_sha,
));
violations.extend(fam_v);
}
if let Some(threshold) = g.max_findings_in_hot_files {
match external_store {
None => {
recs.push(GateRunRecord {
ts: ts.to_owned(),
head_sha: head_sha.to_owned(),
gate: "max_findings_in_hot_files".into(),
threshold: f64::from(threshold),
value: 0.0,
verdict: "skipped".into(),
mode: "check".into(),
});
}
Some(store) => {
let overlap_rows = codelore_lib::cli_api::analyses::finding_hotspot_overlap::run_finding_hotspot_overlap_with(
store,
&hotspot_rows,
&code_health,
)
.context("run finding-hotspot-overlap for gate")?;
let act_now_count = overlap_rows
.iter()
.filter(|r| r.priority == "act-now")
.count();
let overlap_v = codelore_lib::cli_api::quality_gates::evaluate_finding_overlap_rows(
threshold,
&overlap_rows,
);
#[allow(clippy::cast_precision_loss)]
let act_now_f64 = act_now_count as f64;
recs.push(GateRunRecord {
ts: ts.to_owned(),
head_sha: head_sha.to_owned(),
gate: "max_findings_in_hot_files".into(),
threshold: f64::from(threshold),
value: act_now_f64,
verdict: if overlap_v.is_empty() {
"passed"
} else {
"failed"
}
.into(),
mode: "check".into(),
});
violations.extend(overlap_v);
}
}
}
if let Some(max) = g.corpus_percentile_max {
let has_calibration = code_health.iter().any(|r| r.corpus_percentile.is_some());
if has_calibration {
let corpus_v = codelore_lib::cli_api::quality_gates::evaluate_corpus_percentile_rows(
max,
&code_health,
);
let value = code_health
.iter()
.filter_map(|r| r.corpus_percentile)
.fold(0.0, f64::max);
recs.push(make_rec(
"corpus_percentile_max",
max,
value,
!corpus_v.is_empty(),
ts,
head_sha,
));
violations.extend(corpus_v);
} else {
recs.push(GateRunRecord {
ts: ts.to_owned(),
head_sha: head_sha.to_owned(),
gate: "corpus_percentile_max".into(),
threshold: max,
value: 0.0,
verdict: "skipped".into(),
mode: "check".into(),
});
}
}
if let Some(max) = g.hotspot_anchored_max {
let has_anchor = hotspot_rows
.iter()
.any(|r| r.hotspot_score_anchored.is_some());
if has_anchor {
let anchored_v = codelore_lib::cli_api::quality_gates::evaluate_hotspot_anchored_rows(
max,
&hotspot_rows,
);
let value = hotspot_rows
.iter()
.filter_map(|r| r.hotspot_score_anchored)
.fold(0.0, f64::max);
recs.push(make_rec(
"hotspot_anchored_max",
max,
value,
!anchored_v.is_empty(),
ts,
head_sha,
));
violations.extend(anchored_v);
} else {
recs.push(GateRunRecord {
ts: ts.to_owned(),
head_sha: head_sha.to_owned(),
gate: "hotspot_anchored_max".into(),
threshold: max,
value: 0.0,
verdict: "skipped".into(),
mode: "check".into(),
});
}
}
Ok((violations, recs, hotspot_count, code_health))
}
fn emit_ratchet_message(args: &args::CheckArgs, msg: &str) {
if matches!(args.format, CheckFormat::Sarif) {
eprint!("{msg}");
} else {
print!("{msg}");
}
}
fn emit_check_sarif_when_requested(
args: &args::CheckArgs,
db: &codelore_lib::cli_api::facts::FactsDb,
opts: &codelore_lib::cli_api::Options,
head_sha: &str,
violations: &[codelore_lib::cli_api::quality_gates::GateViolation],
) -> Result<()> {
use codelore_lib::cli_api::quality_gates::evidence::{EvidenceCommit, evidence_for_path};
use std::collections::HashMap;
if !matches!(args.format, CheckFormat::Sarif) {
return Ok(());
}
let mut evidence_map: HashMap<String, Vec<EvidenceCommit>> = HashMap::new();
let mut evidence_warned = false;
for v in violations {
if !codelore_lib::cli_api::quality_gates::evaluators::is_pseudo_path(&v.path) {
evidence_map.entry(v.path.clone()).or_insert_with(|| {
evidence_for_path(db, opts, &v.path, 5).unwrap_or_else(|e| {
if !evidence_warned {
evidence_warned = true;
eprintln!(
" ⚠ check: evidence lookup failed ({e}); SARIF results will be emitted without commit chains"
);
}
Vec::new()
})
});
}
}
emit_check_sarif(&args.repo, head_sha, violations, &evidence_map)
}
fn emit_check_sarif(
repo: &std::path::Path,
head_sha: &str,
violations: &[codelore_lib::cli_api::quality_gates::GateViolation],
evidence: &std::collections::HashMap<
String,
Vec<codelore_lib::cli_api::quality_gates::evidence::EvidenceCommit>,
>,
) -> Result<()> {
let repo_root = repo.canonicalize().unwrap_or_else(|_| repo.to_path_buf());
let mut stdout = std::io::stdout();
codelore_lib::cli_api::output::sarif::write_check_sarif(
violations,
evidence,
&repo_root,
head_sha,
&mut stdout,
)
.context("emit check SARIF")
}