fn run_check(repo_root: &Path, args: CheckArgs) -> Result<i32> {
if args.details && !(1..=10_000).contains(&args.limit) {
return usage_error("--limit must be between 1 and 10000");
}
let (loaded, _) = report_or_missing(repo_root, args.report.as_deref())?;
let incomplete_records = loaded
.get("files")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter(|record| crate::report_ops::inventory_record_has_incomplete_evidence(record))
.count();
if incomplete_records > 0 && !args.allow_incomplete_evidence {
return Err(ClassifiedError::new(
ErrorKind::Contract,
"incomplete_evidence",
format!("report contains {incomplete_records} incomplete selected inventory record(s); rerun analysis with complete inputs or pass --allow-incomplete-evidence"),
)
.at("/files")
.with_details(json!({"incomplete_record_count": incomplete_records}))
.into());
}
let loaded_config = loaded
.get("config")
.cloned()
.unwrap_or_else(config::default_config);
let context_band = args
.fail_on_context_band
.map(ContextBand::as_str)
.or_else(|| {
loaded_config
.pointer("/check/fail_on_context_band")
.and_then(Value::as_str)
})
.unwrap_or("critical");
let slop_band = args
.fail_on_slop_band
.map(SlopBand::as_str)
.or_else(|| {
loaded_config
.pointer("/check/fail_on_slop_band")
.and_then(Value::as_str)
})
.unwrap_or("critical");
let failures = failing_records_in(
&loaded,
Some(context_band),
Some(slop_band),
args.include_folders,
);
if !matches!(args.format, CheckFormat::Text) {
match args.format {
CheckFormat::Json => {
let mut payload = json!({
"schema_version": 1,
"command": "check",
"report": {"schema_version": loaded.get("schema_version"), "analyzer": loaded.get("analyzer"), "repo": loaded.get("repo"), "scope": loaded.get("scope")},
"boundary": {"context_band": context_band, "slop_band": slop_band},
"passed": failures.is_empty(),
"finding_count": failures.len(),
"details_included": args.details,
"gate_scope": if args.include_folders { "files_and_folders" } else { "files" },
});
if args.details {
let findings = failures
.iter()
.skip(args.offset)
.take(args.limit)
.cloned()
.collect::<Vec<_>>();
payload["findings"] = json!(findings);
payload["collection"] = json!({
"total": failures.len(),
"offset": args.offset,
"limit": args.limit,
"returned": payload["findings"].as_array().map(Vec::len).unwrap_or_default(),
"truncated": args.offset.saturating_add(args.limit) < failures.len(),
});
}
print_text(&render_json(&payload)?);
}
CheckFormat::Github => {
for failure in &failures {
let path = crate::text::github_property_escape(
failure
.get("path")
.and_then(Value::as_str)
.unwrap_or_default(),
);
println!(
"::error file={}::Git Slop context={} slop={} score={}",
path,
failure
.get("context_band")
.and_then(Value::as_str)
.unwrap_or_default(),
failure
.get("slop_band")
.and_then(Value::as_str)
.unwrap_or_default(),
failure.get("slop_score").unwrap_or(&Value::Null)
);
}
}
CheckFormat::Text => {}
}
return Ok(if failures.is_empty() { 0 } else { 1 });
}
if failures.is_empty() {
println!(
"Check passed: no file records met or exceeded context={context_band} or slop={slop_band}."
);
return Ok(0);
}
println!(
"Check failed: {} file records met or exceeded context={context_band} or slop={slop_band}.",
failures.len()
);
for failure in failures.iter().take(10) {
println!(
"- {} (slop={}, context={}, slop_score={})",
safe_terminal(
failure
.get("path")
.and_then(Value::as_str)
.unwrap_or_default(),
),
failure
.get("slop_band")
.and_then(Value::as_str)
.unwrap_or_default(),
failure
.get("context_band")
.and_then(Value::as_str)
.unwrap_or_default(),
failure
.get("slop_score")
.map(ToString::to_string)
.unwrap_or_else(|| "null".to_string()),
);
}
Ok(1)
}
fn bounded_compare_output(
payload: &Value,
detail: CompareDetail,
top: usize,
offset: usize,
limit: usize,
) -> Result<Value> {
if limit == 0 {
anyhow::bail!("--limit must be greater than zero");
}
let mut bounded = payload.clone();
let cap = match detail {
CompareDetail::Summary => 0,
CompareDetail::Top => top,
CompareDetail::Full => limit,
};
let start = if matches!(detail, CompareDetail::Full) {
offset
} else {
0
};
let mut pagination = serde_json::Map::new();
for key in [
"file_deltas",
"folder_deltas",
"queue_movement",
"overlay_deltas",
"regressions",
] {
let values = payload
.get(key)
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let returned = values
.iter()
.skip(start)
.take(cap)
.cloned()
.collect::<Vec<_>>();
pagination.insert(
key.to_string(),
json!({
"total": values.len(),
"offset": start,
"limit": cap,
"returned": returned.len(),
"has_more": start.saturating_add(returned.len()) < values.len()
}),
);
bounded[key] = json!(returned);
}
bounded["detail"] = json!(match detail {
CompareDetail::Summary => "summary",
CompareDetail::Top => "top",
CompareDetail::Full => "full",
});
bounded["pagination"] = Value::Object(pagination);
Ok(bounded)
}
fn render_compare_ndjson(payload: &Value) -> Result<String> {
let mut lines = vec![render_json(&json!({
"record_type": "summary",
"schema_version": payload.get("schema_version"),
"stream": {
"schema": "schemas/compare-ndjson-1.json",
"record_types": ["summary", "file_delta", "folder_delta", "queue_movement", "overlay_delta", "regression"]
},
"summary": payload.get("summary"),
"pagination": payload.get("pagination"),
"baseline_status": payload.get("baseline_status")
}))?];
for (key, record_type) in [
("file_deltas", "file_delta"),
("folder_deltas", "folder_delta"),
("queue_movement", "queue_movement"),
("overlay_deltas", "overlay_delta"),
("regressions", "regression"),
] {
for record in payload
.get(key)
.and_then(Value::as_array)
.into_iter()
.flatten()
{
lines.push(render_json(
&json!({"record_type": record_type, "record": record}),
)?);
}
}
Ok(lines.join("\n"))
}