use crate::grammar::{parse, report_or_discover, Shape};
use crate::load::load_entries;
use candor_classify::policy::{rule_and_upgrade, unverified_hole_rule, PolicyRule};
use candor_report::ReportEntry;
pub(crate) fn cmd_unverified(args: &[String]) -> i32 {
let g = parse(args, Shape { verb_args: 0, sentinel: true, has_policy: true });
let Some(prefix) = report_or_discover(&g) else {
eprintln!("candor: no report found (no --report and no .candor/ discovered) — scan the crate first.");
return 2;
};
let prefix = &prefix;
let want_json = g.want_json;
let strict = g.strict;
let policy_path = g.policy.clone().or_else(|| std::env::var("CANDOR_POLICY").ok());
let Some(pp) = policy_path else {
eprintln!("candor unverified: a policy is required (the check is relative to your pure/deny layers).");
return 2;
};
let parsed = match crate::policy::load_policy_as_the_gate_does("unverified", &pp) {
Ok(p) => p,
Err(code) => return code,
};
let rules = &parsed.rules;
let entries = load_entries(prefix);
if entries.is_empty() {
eprintln!("candor unverified: no report for `{prefix}` — scan the crate first.");
return 2;
}
struct Hole<'a> {
func: &'a ReportEntry,
rule: &'a PolicyRule,
}
let class_filter = match g.class.as_deref().map(crate::containment::parse_class_filter).transpose() {
Ok(v) => v,
Err(msg) => {
eprintln!("{msg}");
return 2;
}
};
let want: Option<std::collections::BTreeSet<&str>> = class_filter
.as_ref()
.map(|set| set.iter().map(|c| c.token()).collect());
let sig = crate::gate::report_signature(&entries);
let reason_acc = &sig.reason_classes;
let class_matches = |e: &ReportEntry| -> bool {
match &want {
Some(w) => candor_classify::policy::reason_class_matches(reason_acc.get(&e.func), w),
None => true, }
};
let no_classes: Vec<String> = Vec::new();
let holes: Vec<Hole> = entries
.iter()
.filter_map(|e| {
let nets = if e.net_class.is_empty() { &no_classes } else { &e.net_class };
unverified_hole_rule(&e.func, &e.inferred, reason_acc.get(&e.func), nets, rules)
.filter(|_| class_matches(e))
.map(|rule| Hole { func: e, rule })
})
.collect();
let unanswered = crate::gate::unanswerable_pairs(&parsed, &sig);
let comp = crate::completeness::report_completeness(prefix);
comp.warn_unreadable("unverified");
if want_json {
let mut items: Vec<_> = holes
.iter()
.map(|h| {
let (rule, upgrade) = rule_and_upgrade(h.rule);
serde_json::json!({
"fn": h.func.func,
"rule": rule,
"unknownWhy": h.func.unknown_why,
"upgrade": upgrade,
})
})
.collect();
items.extend(unanswered.iter().map(|u| {
serde_json::json!({ "fn": u.func, "rule": u.rule, "why": u.why })
}));
let out = serde_json::json!({
"ok": items.is_empty(),
"unverified": items,
"unevaluated": unevaluated_json(&unanswered),
});
let mut out = out;
if unanswered.is_empty() {
out.as_object_mut().unwrap().remove("unevaluated");
}
if comp.incomplete() || !unanswered.is_empty() {
out.as_object_mut().unwrap().remove("ok");
comp.write_json(&mut out);
}
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return unverified_exit(strict, !holes.is_empty(), !unanswered.is_empty(), comp.incomplete());
}
comp.print_note(
"the functions named below are only those candor could see",
"A function in one of those is ABSENT from the report, so it cannot be named here at all. \
`gate --report` exits 2 over these bytes. Re-scan for a complete answer.",
);
if holes.is_empty() && unanswered.is_empty() {
if comp.incomplete() {
println!(
"candor unverified: nothing candor COULD SEE is an unverified hole — but see the \
INCOMPLETE note above; this is NOT the provably-clean all-clear."
);
return unverified_exit(strict, false, false, true);
}
println!("candor unverified: every function in a pure/deny layer is PROVABLY clean (no Unknown holes) ✓");
return 0;
}
if !holes.is_empty() {
println!(
"candor unverified — {} function(s) PASS their policy but aren't PROVABLY clean:\n",
holes.len()
);
}
let mut upgrades: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for h in &holes {
let (rule, upgrade) = rule_and_upgrade(h.rule);
upgrades.insert(upgrade.clone());
println!(" `{}` (in `{rule}`)", h.func.func);
let why = if h.func.unknown_why.is_empty() {
"an unresolvable call".to_string()
} else {
h.func.unknown_why.join(", ")
};
println!(" is Unknown ({why}) — candor can't confirm it's free of the forbidden effect(s);");
println!(" the Unknown could hide the very effect the rule forbids (e.g. a fn/closure-injected port).");
println!(" → make it provable: add `{upgrade}`");
println!();
}
if !unanswered.is_empty() {
println!(
"candor unverified — {} function(s) the GATE COULD NOT JUDGE over this report (`candor-query \
gate --report` refuses on them, SPEC §3.1):\n",
unanswered.len()
);
for u in &unanswered {
println!(" `{}` (in `{}`)", u.func, u.rule);
println!(" {}", u.why);
println!();
}
}
if !holes.is_empty() {
if comp.incomplete() {
println!(
" The gate does NOT pass over this report — it declares unanalyzed unit(s) (above) and \
`gate --report` exits 2. Once the scan is complete, to REQUIRE provable purity add:"
);
} else {
let scope = if unanswered.is_empty() { "" } else { " on these" };
println!(" The gate still PASSES{scope} — this is advisory. To REQUIRE provable purity, add:");
}
for u in &upgrades {
println!(" {u}");
}
}
unverified_exit(strict, !holes.is_empty(), !unanswered.is_empty(), comp.incomplete())
}
fn unevaluated_json(unanswered: &[crate::gate::Unanswerable]) -> Vec<serde_json::Value> {
let mut seen: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
unanswered
.iter()
.filter(|u| seen.insert(u.rule.as_str()))
.map(|u| serde_json::json!({ "rule": u.rule, "why": u.why }))
.collect()
}
fn unverified_exit(strict: bool, any_holes: bool, any_unanswered: bool, incomplete: bool) -> i32 {
match (strict, any_unanswered || incomplete, any_holes) {
(true, true, _) => 2,
(true, false, true) => 1,
_ => 0,
}
}