use crate::*;
pub(crate) const CONTAINED: &[&str] = &["Db", "Net", "Exec", "Fs", "Ipc", "Clipboard"];
pub(crate) const AMBIENT: &[&str] = &["Log", "Clock", "Rand", "Env"];
pub(crate) fn cmd_blindspots(args: &[String]) -> i32 {
let want_json = args.iter().any(|a| a == "--json");
let pos: Vec<&String> = args.iter().filter(|a| *a != "--json").collect();
let Some(pre) = pos.first() else {
eprintln!("usage: candor-query blindspots <prefix> [--json]");
return 2;
};
let entries = match load_entries_loud(pre) {
Ok(v) => v,
Err(c) => return c,
};
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();
#[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() {
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,
}
println!("{}", serde_json::to_string(&Out { sources, total_unknown }).unwrap());
return 0;
}
if sources.is_empty() {
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 want_json = args.iter().any(|a| a == "--json");
let pos: Vec<&String> = args.iter().filter(|a| *a != "--json").collect();
let Some(cur_pre) = pos.first() else {
eprintln!("usage: candor-query containment <prefix> [baseline_prefix] [--json]");
return 2;
};
if glob_reports(cur_pre).is_empty() {
eprintln!("candor: no report files at prefix `{cur_pre}` — check the path.");
return 2;
}
let cur = load_fninfo(cur_pre);
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) = pos.get(1) {
let base = load_fninfo(base_pre);
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();
if want_json {
let out = serde_json::json!({ "leaks": leaks, "cleanups": cleanups });
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return if leaks.is_empty() { 0 } else { 1 };
}
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() {
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 out = serde_json::json!({ "contained": contained, "ambient": ambient });
println!("{}", serde_json::to_string_pretty(&out).unwrap());
return 0;
}
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 {
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
}