use crate::*;
pub(crate) const CLASS_TOKENS: &str =
"reflect, dispatch, indirect, native, unresolved, setup (aliases: dynamic, *)";
pub(crate) fn parse_class_filter(
spec: &str,
) -> Result<std::collections::HashSet<candor_classify::policy::ReasonClass>, String> {
use candor_classify::policy::ReasonClass;
const ALL: [ReasonClass; 6] = [
ReasonClass::Reflect, ReasonClass::Dispatch, ReasonClass::Indirect,
ReasonClass::Native, ReasonClass::Unresolved, ReasonClass::Setup,
];
let mut out = std::collections::HashSet::new();
let mut star = false;
for t in spec.split(',') {
let t = t.trim();
if t.is_empty() {
continue;
}
if t == "*" {
star = true;
} else if t == "dynamic" {
out.extend(ReasonClass::dynamic_set());
} else if let Some(rc) = ReasonClass::from_token(t) {
out.insert(rc);
} else {
return Err(format!(
"candor-query: --class: unrecognised reason-class `{t}`\n \
accepted: {CLASS_TOKENS}\n \
a --class value that cannot be honoured is refused, not dropped: dropping it would \
narrow the filter and answer a question you did not ask, with a smaller number \
(SPEC §6.2 ⟨0.24⟩)"
));
}
}
if star {
return Ok(ALL.into_iter().collect());
}
Ok(out)
}
pub(crate) const CONTAINED: &[&str] = &["Db", "Net", "Llm", "Exec", "Fs", "Ipc", "Clipboard"];
pub(crate) const AMBIENT: &[&str] = &["Log", "Clock", "Rand", "Env"];
pub(crate) fn cmd_blindspots(args: &[String]) -> i32 {
let g = parse(args, Shape { verb_args: 0, sentinel: true, has_policy: false });
let want_json = g.want_json;
let Some(pre) = report_or_discover(&g) else {
eprintln!("candor: no report found (no --report and no .candor/ discovered) — scan the crate first.");
return 2;
};
let pre = pre.as_str();
let entries = match load_entries_loud(pre) {
Ok(v) => v,
Err(c) => return c,
};
let comp = crate::completeness::report_completeness(pre);
comp.warn_unreadable("blindspots");
let (bs_so_what, bs_tail) = (
"the Unknown source(s) below are only those inside the source candor read",
format!(
"An unread unit is a blind spot too, and it carries no `unknownWhy` to be listed under — \
so it is HERE and not below. {} Re-scan for the full picture.",
comp.gate_line()
),
);
let mut rev: HashMap<&str, Vec<&str>> = HashMap::new();
for e in &entries {
for c in &e.calls {
rev.entry(c.as_str()).or_default().push(e.func.as_str());
}
}
let total_unknown = entries.iter().filter(|e| e.inferred.iter().any(|x| x == "Unknown")).count();
let class_filter: Option<std::collections::HashSet<candor_classify::policy::ReasonClass>> =
match g.class.as_deref().map(parse_class_filter).transpose() {
Ok(v) => v,
Err(msg) => {
eprintln!("{msg}");
return 2;
}
};
let matches = |e: &candor_report::ReportEntry| -> bool {
use candor_classify::policy::ReasonClass;
match &class_filter {
None => true,
Some(set) => e.unknown_why.iter().any(|w| set.contains(&ReasonClass::classify(w))),
}
};
if g.stats {
use candor_classify::policy::ReasonClass;
const ORDER: [&str; 6] = ["reflect", "dispatch", "indirect", "native", "unresolved", "setup"];
let mut by_class: HashMap<&str, usize> = ORDER.iter().map(|c| (*c, 0usize)).collect();
let mut sources_n = 0usize;
for e in &entries {
if e.unknown_why.is_empty() || !matches(e) {
continue;
}
sources_n += 1;
let classes: HashSet<&str> = e.unknown_why.iter().map(|w| ReasonClass::classify(w).token()).collect();
for c in &classes {
*by_class.get_mut(c).unwrap() += 1;
}
}
if want_json {
let bc: serde_json::Map<String, serde_json::Value> =
ORDER.iter().map(|k| (k.to_string(), serde_json::json!(by_class[k]))).collect();
let mut out =
serde_json::json!({ "byClass": bc, "sources": sources_n, "totalUnknown": total_unknown });
comp.write_json(&mut out);
println!("{out}");
return 0;
}
comp.print_note(bs_so_what, &bs_tail);
if sources_n == 0 {
if comp.must_hedge() {
println!(
" no Unknown source inside what candor COULD SEE — but see the INCOMPLETE note \
above; this distribution is not a measure of the whole crate."
);
return 0;
}
println!(" no Unknown sources — nothing to classify (no direct-Unknown in this report).");
return 0;
}
println!(" {sources_n} Unknown source(s) by reason class (of {total_unknown} Unknown function(s)) — size the blind-spot cost before `deny E Unknown[…]`:");
let mut rows: Vec<(&str, usize)> = ORDER.iter().map(|k| (*k, by_class[k])).filter(|(_, v)| *v > 0).collect();
rows.sort_by_key(|r| std::cmp::Reverse(r.1)); for (k, v) in rows {
let hint = if k == "setup" { " ← fixable: the scan isn't configured, not a real blind spot" } else { "" };
println!(" {k:<12} {v:>4}{hint}");
}
return 0;
}
#[derive(Serialize)]
struct Source {
#[serde(rename = "fn")]
func: String,
why: Vec<String>,
reaches: usize,
affected: Vec<String>,
}
let mut sources: Vec<Source> = Vec::new();
for e in &entries {
if e.unknown_why.is_empty() || !matches(e) {
continue; }
let mut seen: HashSet<&str> = HashSet::new();
let mut q: VecDeque<&str> = VecDeque::new();
q.push_back(e.func.as_str());
seen.insert(e.func.as_str());
while let Some(cur) = q.pop_front() {
if let Some(callers) = rev.get(cur) {
for &caller in callers {
if seen.insert(caller) {
q.push_back(caller);
}
}
}
}
let mut affected: Vec<String> =
seen.iter().copied().filter(|n| *n != e.func.as_str()).map(String::from).collect();
affected.sort_unstable();
sources.push(Source { func: e.func.clone(), why: e.unknown_why.clone(), reaches: affected.len(), affected });
}
sources.sort_by(|a, b| b.reaches.cmp(&a.reaches).then_with(|| a.func.cmp(&b.func)));
if want_json {
#[derive(Serialize)]
struct Out {
sources: Vec<Source>,
#[serde(rename = "totalUnknown")]
total_unknown: usize,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
completeness: Option<crate::completeness::CompletenessFields>,
}
let out = Out { sources, total_unknown, completeness: comp.fields() };
println!("{}", serde_json::to_string(&out).unwrap());
return 0;
}
comp.print_note(bs_so_what, &bs_tail);
if sources.is_empty() {
if comp.must_hedge() {
println!(
" no Unknown source inside what candor COULD SEE — but see the INCOMPLETE note above; \
this is NOT \"no blind spots\"."
);
return 0;
}
println!(" no Unknown sources — every call resolved (or no Unknown in this report).");
return 0;
}
println!(
" {} Unknown source(s) explaining {} Unknown function(s) — the blind spots to declare, resolve, or accept:",
sources.len(), total_unknown
);
for s in &sources {
println!(" {:<52} reaches {:>4} {:?}", s.func, s.reaches, s.why);
}
0
}
pub(crate) fn cmd_containment(args: &[String]) -> i32 {
let g = parse(args, Shape { verb_args: 1, sentinel: false, has_policy: false });
let want_json = g.want_json;
let Some(cur_pre) = report_or_discover(&g) else {
eprintln!("candor: no report found (no --report and no .candor/ discovered) — scan the crate first.");
return 2;
};
let cur_pre = cur_pre.as_str();
let base_locator: Option<String> = g.positional.first().map(|b| resolve_locator(b));
let cur = match load_fninfo_loud(cur_pre, "") {
Ok(m) => m,
Err(c) => return c,
};
let mut comp = crate::completeness::report_completeness(cur_pre);
comp.warn_unreadable("containment");
let names: Vec<&String> = cur.keys().collect();
let pl = common_prefix_len(&names);
let mut by_eff: BTreeMap<&'static str, BTreeMap<String, usize>> = BTreeMap::new();
let known: Vec<&'static str> = CONTAINED.iter().chain(AMBIENT.iter()).copied().collect();
for (fname, info) in &cur {
let layer = layer_of(fname, pl);
for eff in &info.direct {
if let Some(k) = known.iter().find(|e| **e == eff.as_str()) {
*by_eff.entry(*k).or_default().entry(layer.clone()).or_default() += 1;
}
}
}
if let Some(base_pre) = base_locator.as_deref() {
let base = match load_fninfo_loud(base_pre, "baseline") {
Ok(m) => m,
Err(c) => return c,
};
comp.absorb(crate::completeness::report_completeness(base_pre));
comp.warn_unreadable("containment (baseline)");
let bnames: Vec<&String> = base.keys().collect();
let bpl = common_prefix_len(&bnames);
let mut base_layers: BTreeMap<&str, BTreeSet<String>> = BTreeMap::new();
for (fname, info) in &base {
let layer = layer_of(fname, bpl);
for eff in &info.direct {
if let Some(k) = CONTAINED.iter().find(|e| **e == eff.as_str()) {
base_layers.entry(*k).or_default().insert(layer.clone());
}
}
}
let mut leaks: Vec<String> = Vec::new();
let mut cleanups: Vec<String> = Vec::new();
for eff in CONTAINED {
let now: BTreeSet<String> =
by_eff.get(eff).map(|m| m.keys().cloned().collect()).unwrap_or_default();
let was = base_layers.get(eff).cloned().unwrap_or_default();
for l in now.difference(&was) {
leaks.push(format!("{eff} → {l}"));
}
for l in was.difference(&now) {
cleanups.push(format!("{eff} ⊘ {l}"));
}
}
leaks.sort();
cleanups.sort();
let (rt_so_what, rt_tail) = (
"the leak/cleanup lists below are a difference between two partially-read trees",
format!(
"A leak in one of those unread units is MISSING from this ratchet, and one that was \
always there but sat in an unread BASELINE unit would read as new. This verb's own \
EXIT CODE is unchanged — the rung adds a caveat, it does not refuse. {}",
comp.gate_line()
),
);
if want_json {
let mut out = serde_json::json!({ "leaks": leaks, "cleanups": cleanups });
comp.write_json(&mut out);
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return if leaks.is_empty() { 0 } else { 1 };
}
comp.print_note(rt_so_what, &rt_tail);
if !leaks.is_empty() {
println!("[AS-EFF-010] a boundary effect leaked into a layer it wasn't in:");
for l in &leaks {
println!(" {l}");
}
}
if !cleanups.is_empty() {
if !leaks.is_empty() {
println!();
}
println!("✓ improved — a boundary effect left a layer:");
for c in &cleanups {
println!(" {c}");
}
}
if leaks.is_empty() && cleanups.is_empty() {
println!("candor containment: unchanged vs {base_pre} (no leaks, no cleanups).");
} else if leaks.is_empty() {
if comp.must_hedge() {
println!("\ncandor containment: no regression IN WHAT CANDOR COULD SEE — see the INCOMPLETE note above");
} else {
println!("\ncandor containment: no regressions ✓");
}
}
if !leaks.is_empty() {
println!("\nfix: keep the call in its boundary layer, or refresh the baseline if intended.");
}
return if leaks.is_empty() { 0 } else { 1 };
}
let owner_of = |layers: &BTreeMap<String, usize>| -> (String, usize) {
layers.iter().max_by_key(|(_, n)| **n).map(|(k, n)| (k.clone(), *n)).unwrap()
};
if want_json {
let contained: Vec<serde_json::Value> = CONTAINED
.iter()
.filter_map(|eff| {
by_eff.get(eff).map(|layers| {
let tot: usize = layers.values().sum();
let (owner, on) = owner_of(layers);
serde_json::json!({
"effect": eff, "containmentPct": 100 * on / tot,
"layers": layers.len(), "owner": owner, "placement": layers,
})
})
})
.collect();
let ambient: BTreeMap<&str, usize> =
AMBIENT.iter().filter_map(|e| by_eff.get(e).map(|m| (*e, m.len()))).collect();
let mut out = serde_json::json!({ "contained": contained, "ambient": ambient });
comp.write_json(&mut out);
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return 0;
}
comp.print_note(
"the containment percentages below are computed over only the source candor read",
&format!(
"A boundary call in an unread unit is in NOBODY's layer here, so a 100% is a share of a \
partial denominator. {} Re-scan before ratcheting.",
comp.gate_line()
),
);
println!("candor containment — how well each boundary effect stays in one layer");
println!("(the signal is dispersion across layers, NOT effect count)\n");
println!(" {:<7} {:>9} {:>7} owner ← leaked into", "effect", "contained", "layers");
let mut any = false;
for eff in CONTAINED {
let Some(layers) = by_eff.get(eff) else { continue };
any = true;
let tot: usize = layers.values().sum();
let (owner, on) = owner_of(layers);
let mut others: Vec<(&String, &usize)> = layers.iter().filter(|(k, _)| **k != owner).collect();
others.sort_by(|a, b| b.1.cmp(a.1));
let leaks: String =
others.iter().map(|(k, v)| format!("{k}:{v}")).collect::<Vec<_>>().join(", ");
let tail = if leaks.is_empty() { String::new() } else { format!(" ← {leaks}") };
println!(" {eff:<7} {:>8}% {:>7} {owner} ({on}){tail}", 100 * on / tot, layers.len());
}
if !any {
if comp.must_hedge() {
println!(" (no boundary effect in what candor COULD SEE — see the INCOMPLETE note above)");
} else {
println!(" (no boundary effects in the report)");
}
}
let amb: String = AMBIENT
.iter()
.filter_map(|e| by_eff.get(e).map(|m| format!("{e} {}L", m.len())))
.collect::<Vec<_>>()
.join(", ");
if !amb.is_empty() {
println!("\n ambient (cross-cutting expected, not scored): {amb}");
}
println!(
"\n containment% = share of an effect's direct uses in its dominant layer; 100% = fully contained.\
\n ratchet a baseline: candor-query containment <prefix> <baseline_prefix> (exit 1 on a new leak)."
);
0
}